diff --git a/.android.env.example b/.android.env.example index aceaabefa76..93b4a8c334e 100644 --- a/.android.env.example +++ b/.android.env.example @@ -1,3 +1,16 @@ export MM_FOX_CODE="EXAMPLE_FOX_CODE" export MM_BRANCH_KEY_TEST= export MM_BRANCH_KEY_LIVE= +export METAMASK_BUILD_TYPE= +# Firebase +export FCM_CONFIG_API_KEY= +export FCM_CONFIG_AUTH_DOMAIN= +export FCM_CONFIG_PROJECT_ID= +export FCM_CONFIG_STORAGE_BUCKET= +export FCM_CONFIG_MESSAGING_SENDER_ID= +export FCM_CONFIG_APP_ID= +export GOOGLE_SERVICES_B64= +#Notifications Feature Announcements +export FEATURES_ANNOUNCEMENTS_ACCESS_TOKEN= +export FEATURES_ANNOUNCEMENTS_SPACE_ID= + diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index f18ba45e82f..31ee91b2191 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -48,6 +48,18 @@ body: label: Error messages or log output description: Please copy and paste any relevant error messages or log output. This will be automatically formatted, so there is no need for backticks. render: shell + - type: dropdown + id: stage + attributes: + label: Detection stage + description: At what stage was the bug detected? + options: + - In production (default) + - In beta + - During release testing + - On the development branch + validations: + required: true - type: input id: version attributes: diff --git a/.github/guidelines/LABELING_GUIDELINES.md b/.github/guidelines/LABELING_GUIDELINES.md index 4d3077b5128..ee49fad0639 100644 --- a/.github/guidelines/LABELING_GUIDELINES.md +++ b/.github/guidelines/LABELING_GUIDELINES.md @@ -16,11 +16,13 @@ It's essential to ensure that PRs have the appropriate labels before they are co ### Mandatory QA labels: Every PR shall include one the QA labels below: - **needs-qa**: If the PR includes a new features, complex testing steps, or large refactors, this label must be added to indicated PR requires a full manual QA prior being merged and added to a release. -- **No QA/E2E only**: If the PR does not require any manual QA effort, this label must be added. However, prior to merging, you must ensure end-to-end test runs in Bitrise are successful. + - **Spot check on release build**: If PR does not require feature QA but needs non-automated verification, this label must be added. Furthermore, when that label is added, you must provide test scenarios in the description section, as well as add screenshots, and or recordings of what was tested. -Once PR has been tested by QA (only if the PR was labeled with `needs-qa`): +To merge your PR one of the following QA labels are required: - **QA Passed**: If the PR was labeled with `needs-qa`, this label must be added once QA has signed off +- **No QA Needed**: If the PR does not require any QA effort. This label should only be used in case you are updating a README or other files that does not impact the building or runtime of the application. +- **Run E2E Smoke**: This label will kick-off E2E testing and trigger a check to make sure the E2E tests pass. ### Optional labels: - **regression-develop**: This label can manually be added to a bug report issue at the time of its creation if the bug is present on the development branch, i.e., `main`, but is not yet released in production. diff --git a/.github/scripts/check-pr-has-required-labels.ts b/.github/scripts/check-pr-has-required-labels.ts index fadb2856ac2..bc91e6f8ac7 100644 --- a/.github/scripts/check-pr-has-required-labels.ts +++ b/.github/scripts/check-pr-has-required-labels.ts @@ -50,6 +50,7 @@ async function main(): Promise { 'DO-NOT-MERGE', ]; let hasTeamLabel = false; + let hasQALabel = false; // Check pull request has at least required QA label and team label for (const label of pullRequestLabels) { @@ -57,22 +58,30 @@ async function main(): Promise { console.log(`PR contains a team label as expected: ${label}`); hasTeamLabel = true; } + if (label.includes('Run Smoke E2E') || label.includes('No QA Needed') || label.includes('QA Passed') ) { + console.log(`PR contains a QA label as expected: ${label}`); + hasQALabel = true; + } if (preventMergeLabels.includes(label)) { core.setFailed( `PR cannot be merged because it still contains this label: ${label}`, ); process.exit(1); } - if (hasTeamLabel) { + if (hasTeamLabel && hasQALabel) { return; } } - // Otherwise, throw an arror to prevent from merging + // Otherwise, throw an error to prevent from merging let errorMessage = ''; if (!hasTeamLabel) { errorMessage += 'No team labels found on the PR. '; } + + if (!hasQALabel) { + errorMessage += 'No \'Run E2E Smoke\' or \'No QA Needed\' or \'QA Passed\' label. '; + } errorMessage += `Please make sure the PR is appropriately labeled before merging it.\n\nSee labeling guidelines for more detail: https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md`; core.setFailed(errorMessage); process.exit(1); diff --git a/.github/scripts/check-template-and-add-labels.ts b/.github/scripts/check-template-and-add-labels.ts index 99d72694764..b8fd1c4bff0 100644 --- a/.github/scripts/check-template-and-add-labels.ts +++ b/.github/scripts/check-template-and-add-labels.ts @@ -19,6 +19,13 @@ import { import { TemplateType, templates } from './shared/template'; import { retrievePullRequest } from './shared/pull-request'; +enum RegressionStage { + Development, + Testing, + Beta, + Production +} + const knownBots = ["metamaskbot", "dependabot", "github-actions", "sentry-io"]; main().catch((error: Error): void => { @@ -105,23 +112,9 @@ async function main(): Promise { invalidIssueTemplateLabel, ); - // Extract release version from bug report issue body (if existing) - const releaseVersion = extractReleaseVersionFromBugReportIssueBody( - labelable.body, - ); + // Add regression label to the bug report issue + addRegressionLabelToIssue(octokit, labelable); - // Add regression prod label to the bug report issue if release version was found in issue body - if(isReleaseCandidateIssue(labelable)) { - console.log( - `Issue ${labelable?.number} is not a production issue. Regression prod label is not needed.`, - ); - } else if (releaseVersion) { - await addRegressionProdLabelToIssue(octokit, releaseVersion, labelable); - } else { - console.log( - `No release version was found in body of bug report issue ${labelable?.number}.`, - ); - } } else { const errorMessage = "Issue body does not match any of expected templates ('general-issue.yml' or 'bug-report.yml').\n\nMake sure issue's body includes all section titles.\n\nSections titles are listed here: https://github.com/MetaMask/metamask-mobile/blob/main/.github/scripts/shared/template.ts#L14-L37"; @@ -200,6 +193,28 @@ function extractTemplateTypeFromBody(body: string): TemplateType { return TemplateType.None; } +// This helper function extracts regression stage (Development, Testing, Production) from bug report issue's body. +function extractRegressionStageFromBugReportIssueBody( + body: string, +): RegressionStage | undefined { + const detectionStageRegex = /### Detection stage\s*\n\s*(.*)/i; + const match = body.match(detectionStageRegex); + const extractedAnswer = match ? match[1].trim() : undefined; + + switch (extractedAnswer) { + case 'On the development branch': + return RegressionStage.Development; + case 'During release testing': + return RegressionStage.Testing; + case 'In beta': + return RegressionStage.Beta; + case 'In production (default)': + return RegressionStage.Production; + default: + return undefined; + } +} + // This helper function extracts release version from bug report issue's body. function extractReleaseVersionFromBugReportIssueBody( body: string, @@ -220,49 +235,54 @@ function extractReleaseVersionFromBugReportIssueBody( return version; } -// This function adds the correct "regression-prod-x.y.z" label to the issue, and removes other ones -async function addRegressionProdLabelToIssue( +// This function adds the correct regression label to the issue, and removes other ones +async function addRegressionLabelToIssue( octokit: InstanceType, - releaseVersion: string, issue: Labelable, ): Promise { - // Craft regression prod label to add - const regressionProdLabel: Label = { - name: `regression-prod-${releaseVersion}`, - color: '5319E7', // violet - description: `Regression bug that was found in production in release ${releaseVersion}`, - }; - - let regressionProdLabelFound: boolean = false; - const regressionProdLabelsToBeRemoved: { + // Extract regression stage from bug report issue body (if existing) + const regressionStage = extractRegressionStageFromBugReportIssueBody( + issue.body, + ); + + // Extract release version from bug report issue body (if existing) + const releaseVersion = extractReleaseVersionFromBugReportIssueBody( + issue.body, + ); + + // Craft regression label to add + const regressionLabel: Label = craftRegressionLabel(regressionStage, releaseVersion); + + let regressionLabelFound: boolean = false; + const regressionLabelsToBeRemoved: { id: string; name: string; }[] = []; // Loop over issue's labels, to see if regression labels are either missing, or to be removed issue?.labels?.forEach((label) => { - if (label?.name === regressionProdLabel.name) { - regressionProdLabelFound = true; - } else if (label?.name?.startsWith('regression-prod-')) { - regressionProdLabelsToBeRemoved.push(label); + if (label?.name === regressionLabel.name) { + regressionLabelFound = true; + } else if (label?.name?.startsWith('regression-')) { + regressionLabelsToBeRemoved.push(label); } }); // Add regression prod label to the issue if missing - if (regressionProdLabelFound) { + if (regressionLabelFound) { console.log( - `Issue ${issue?.number} already has ${regressionProdLabel.name} label.`, + `Issue ${issue?.number} already has ${regressionLabel.name} label.`, ); } else { console.log( - `Add ${regressionProdLabel.name} label to issue ${issue?.number}.`, + `Add ${regressionLabel.name} label to issue ${issue?.number}.`, ); - await addLabelToLabelable(octokit, issue, regressionProdLabel); + await addLabelToLabelable(octokit, issue, regressionLabel); } // Remove other regression prod label from the issue await Promise.all( - regressionProdLabelsToBeRemoved.map((label) => { + regressionLabelsToBeRemoved.map((label) => { removeLabelFromLabelable(octokit, issue, label?.id); }), ); @@ -294,9 +314,42 @@ async function userBelongsToMetaMaskOrg( return Boolean(userBelongsToMetaMaskOrgResult?.user?.organization?.id); } -// This function checks if issue is a release candidate (RC) issue, discovered during release regression testing phase. If so, it means it is not a production issue. -function isReleaseCandidateIssue( - issue: Labelable, -): boolean { - return Boolean(issue.labels.find(label => label.name.startsWith('regression-RC'))); +// This function crafts appropriate label, corresponding to regression stage and release version. +function craftRegressionLabel(regressionStage: RegressionStage | undefined, releaseVersion: string | undefined): Label { + switch (regressionStage) { + case RegressionStage.Development: + return { + name: `regression-develop`, + color: '5319E7', // violet + description: `Regression bug that was found on development branch, but not yet present in production`, + }; + + case RegressionStage.Testing: + return { + name: `regression-RC-${releaseVersion || '*'}`, + color: '744C11', // orange + description: releaseVersion ? `Regression bug that was found in release candidate (RC) for release ${releaseVersion}` : `TODO: Unknown release version. Please replace with correct 'regression-RC-x.y.z' label, where 'x.y.z' is the number of the release where bug was found.`, + }; + + case RegressionStage.Beta: + return { + name: `regression-beta-${releaseVersion || '*'}`, + color: 'D94A83', // pink + description: releaseVersion ? `Regression bug that was found in beta in release ${releaseVersion}` : `TODO: Unknown release version. Please replace with correct 'regression-beta-x.y.z' label, where 'x.y.z' is the number of the release where bug was found.`, + }; + + case RegressionStage.Production: + return { + name: `regression-prod-${releaseVersion || '*'}`, + color: '5319E7', // violet + description: releaseVersion ? `Regression bug that was found in production in release ${releaseVersion}` : `TODO: Unknown release version. Please replace with correct 'regression-prod-x.y.z' label, where 'x.y.z' is the number of the release where bug was found.`, + }; + + default: + return { + name: `regression-*`, + color: 'EDEDED', // grey + description: `TODO: Unknown regression stage. Please replace with correct regression label: 'regression-develop', 'regression-RC-x.y.z', or 'regression-prod-x.y.z' label, where 'x.y.z' is the number of the release where bug was found.`, + }; + } } diff --git a/.github/workflows/create-release-pr.yml b/.github/workflows/create-release-pr.yml index 87609ee4f96..58c8c4152d2 100644 --- a/.github/workflows/create-release-pr.yml +++ b/.github/workflows/create-release-pr.yml @@ -12,7 +12,9 @@ on: version-number: description: 'A natural version number. eg: 862' required: true - + previous-version-tag: + description: 'Previous release version tag. eg: v7.7.0' + required: true jobs: create-release-pr: runs-on: ubuntu-latest @@ -34,20 +36,21 @@ jobs: # The workaround is to use a personal access token (BUG_REPORT_TOKEN) instead of # the default GITHUB_TOKEN for the checkout action. token: ${{ secrets.BUG_REPORT_TOKEN }} - - name: Get Node.js version - id: nvm - run: echo "NODE_VERSION=$(cat .nvmrc)" >> "$GITHUB_OUTPUT" - - uses: actions/setup-node@v3 + - name: Set up Node.js + uses: actions/setup-node@v3 with: - node-version: ${{ steps.nvm.outputs.NODE_VERSION }} + node-version-file: '.nvmrc' + cache: yarn + - name: Install dependencies + run: yarn --immutable - name: Set Versions id: set-versions shell: bash - run: SEMVER_VERSION=${{ github.event.inputs.semver-version }} VERSION_NUMBER=${{ github.event.inputs.version-number }} yarn create-release + run: SEMVER_VERSION=${{ github.event.inputs.semver-version }} VERSION_NUMBER=${{ github.event.inputs.version-number }} yarn set-version - name: Create Release PR id: create-release-pr shell: bash env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - ./scripts/create-release-pr.sh ${{ github.event.inputs.semver-version }} + ./scripts/create-release-pr.sh ${{ github.event.inputs.previous-version-tag }} ${{ github.event.inputs.semver-version }} \ No newline at end of file diff --git a/.github/workflows/run-bitrise-e2e-check.yml b/.github/workflows/run-bitrise-e2e-check.yml index 23340257179..23274a35f5e 100644 --- a/.github/workflows/run-bitrise-e2e-check.yml +++ b/.github/workflows/run-bitrise-e2e-check.yml @@ -22,9 +22,10 @@ jobs: - uses: actions/checkout@v3 - name: Determine whether this PR is from a fork id: is-fork - run: echo "IS_FORK=$(gh pr view --json isCrossRepository --jq '.isCrossRepository' "${{ github.event.number }}" )" >> "$GITHUB_OUTPUT" + run: echo "IS_FORK=$(gh pr view --json isCrossRepository --jq '.isCrossRepository' "${PR_NUMBER}" )" >> "$GITHUB_OUTPUT" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.number || github.event.issue.number }} run-bitrise-e2e-check: needs: is-fork-pull-request diff --git a/.github/workflows/update-attributions.yml b/.github/workflows/update-attributions.yml new file mode 100644 index 00000000000..101270ac270 --- /dev/null +++ b/.github/workflows/update-attributions.yml @@ -0,0 +1,197 @@ +name: Update Attributions + +on: + issue_comment: + types: created + +jobs: + is-fork-pull-request: + name: Determine whether this issue comment was on a pull request from a fork + if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, '@metamaskbot update-attributions') }} + runs-on: ubuntu-latest + outputs: + IS_FORK: ${{ steps.is-fork.outputs.IS_FORK }} + steps: + - uses: actions/checkout@v4 + - name: Determine whether this PR is from a fork + id: is-fork + run: echo "IS_FORK=$(gh pr view --json isCrossRepository --jq '.isCrossRepository' "${PR_NUMBER}" )" >> "$GITHUB_OUTPUT" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.issue.number }} + + react-to-comment: + name: React to the comment + runs-on: ubuntu-latest + needs: is-fork-pull-request + # Early exit if this is a fork, since later steps are skipped for forks + if: ${{ needs.is-fork-pull-request.outputs.IS_FORK == 'false' }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: React to the comment + run: | + gh api \ + --method POST \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "/repos/${REPO}/issues/comments/${COMMENT_ID}/reactions" \ + -f content='+1' + env: + COMMENT_ID: ${{ github.event.comment.id }} + GITHUB_TOKEN: ${{ secrets.ACTIONS_WRITE_TOKEN }} + REPO: ${{ github.repository }} + + prepare: + name: Prepare dependencies + runs-on: ubuntu-latest + needs: is-fork-pull-request + # Early exit if this is a fork, since later steps are skipped for forks + if: ${{ needs.is-fork-pull-request.outputs.IS_FORK == 'false' }} + outputs: + COMMIT_SHA: ${{ steps.commit-sha.outputs.COMMIT_SHA }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Checkout pull request + run: gh pr checkout "${PR_NUMBER}" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.issue.number }} + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'yarn' + - name: Install Yarn dependencies + run: yarn --immutable + - name: Get commit SHA + id: commit-sha + run: echo "COMMIT_SHA=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" + + update-attributions: + name: Update Attributions + runs-on: ubuntu-latest + needs: + - prepare + - is-fork-pull-request + # Early exit if this is a fork, since later steps are skipped for forks + if: ${{ needs.is-fork-pull-request.outputs.IS_FORK == 'false' }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Checkout pull request + run: gh pr checkout "${PR_NUMBER}" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.issue.number }} + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'yarn' + - name: Install dependencies from cache + run: yarn --immutable --immutable-cache + - name: Generate Attributions + run: yarn build:attribution + - name: Cache attributions file + uses: actions/cache/save@v3 + with: + path: attribution.txt + key: cache-build-${{ needs.prepare.outputs.COMMIT_SHA }} + + commit-updated-attributions: + name: Commit the updated Attributions + runs-on: ubuntu-latest + needs: + - prepare + - is-fork-pull-request + - update-attributions + if: ${{ needs.is-fork-pull-request.outputs.IS_FORK == 'false' }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + # Use PAT to ensure that the commit later can trigger status check workflows + token: ${{ secrets.GITHUB_TOKEN }} + - name: Checkout pull request + run: gh pr checkout "${PR_NUMBER}" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.issue.number }} + - name: Get commit SHA + id: commit-sha + run: echo "COMMIT_SHA=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" + - name: Restore attributions file + uses: actions/cache/restore@v3 + with: + path: attribution.txt + key: cache-build-${{ needs.prepare.outputs.COMMIT_SHA }} + fail-on-cache-miss: true + - name: Check whether there are attributions changes + id: attributions-changes + run: | + if git diff --exit-code + then + echo "HAS_CHANGES=false" >> "$GITHUB_OUTPUT" + else + echo "HAS_CHANGES=true" >> "$GITHUB_OUTPUT" + fi + - name: Commit the updated attributions + if: steps.attributions-changes.outputs.HAS_CHANGES == 'true' + run: | + git config --global user.name 'MetaMask Bot' + git config --global user.email 'metamaskbot@users.noreply.github.com' + git commit -am "Update Attributions" + git push + env: + GITHUB_TOKEN: ${{ secrets.ACTIONS_WRITE_TOKEN }} + - name: Post comment + run: | + if [[ $HAS_CHANGES == 'true' ]] + then + gh pr comment "${PR_NUMBER}" --body 'Attributions updated' + else + gh pr comment "${PR_NUMBER}" --body 'No attributions changes' + fi + env: + HAS_CHANGES: ${{ steps.attributions-changes.outputs.HAS_CHANGES }} + GITHUB_TOKEN: ${{ secrets.ACTIONS_WRITE_TOKEN }} + PR_NUMBER: ${{ github.event.issue.number }} + + check-status: + name: Check whether the attributions update succeeded + runs-on: ubuntu-latest + needs: + - commit-updated-attributions + - is-fork-pull-request + # Early exit if this is a fork, since later steps are skipped for forks + if: ${{ needs.is-fork-pull-request.outputs.IS_FORK == 'false' }} + outputs: + PASSED: ${{ steps.set-output.outputs.PASSED }} + steps: + - name: Set PASSED output + id: set-output + run: echo "PASSED=true" >> "$GITHUB_OUTPUT" + + failure-comment: + name: Comment about the attributions update failure + if: ${{ always() && needs.is-fork-pull-request.outputs.IS_FORK == 'false' }} + runs-on: ubuntu-latest + needs: + - is-fork-pull-request + - check-status + steps: + - uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + - name: Post comment if the update failed + run: | + passed="${{ needs.check-status.outputs.PASSED }}" + if [[ $passed != "true" ]]; then + gh pr comment "${PR_NUMBER}" --body "Attributions update failed. You can [review the logs or retry the attributions update here](${ACTION_RUN_URL})" + fi + env: + GITHUB_TOKEN: ${{ secrets.ACTIONS_WRITE_TOKEN }} + PR_NUMBER: ${{ github.event.issue.number }} + ACTION_RUN_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}' diff --git a/.gitignore b/.gitignore index f4c8af6ff83..d0ee5c4774a 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,8 @@ android/app/google-services.json node_modules/ npm-debug.log yarn-error.log +.yalc +yalc.lock # buck buck-out/ diff --git a/.ios.env.example b/.ios.env.example index bd49b067660..05aadc9b359 100644 --- a/.ios.env.example +++ b/.ios.env.example @@ -1,3 +1,14 @@ MM_FOX_CODE = EXAMPLE_FOX_CODE MM_BRANCH_KEY_TEST = MM_BRANCH_KEY_LIVE = +# Firebase +FCM_CONFIG_API_KEY= +FCM_CONFIG_AUTH_DOMAIN= +FCM_CONFIG_PROJECT_ID= +FCM_CONFIG_STORAGE_BUCKET= +FCM_CONFIG_MESSAGING_SENDER_ID= +FCM_CONFIG_APP_ID= +GOOGLE_SERVICES_B64= +#Notifications Feature Announcements +FEATURES_ANNOUNCEMENTS_ACCESS_TOKEN= +FEATURES_ANNOUNCEMENTS_SPACE_ID= diff --git a/.js.env.example b/.js.env.example index 4648ae1b482..c3674e10de1 100644 --- a/.js.env.example +++ b/.js.env.example @@ -45,7 +45,6 @@ export WALLET_CONNECT_PROJECT_ID="" # CDN for blockaid files export BLOCKAID_FILE_CDN="" export BLOCKAID_PUBLIC_KEY="" -export MM_BLOCKAID_UI_ENABLED="" # Default PORT for metro export WATCHER_PORT=8081 @@ -73,3 +72,14 @@ export SECURITY_ALERTS_API_URL="http://localhost:3000" # Temporary mechanism to enable security alerts API prior to release. export SECURITY_ALERTS_API_ENABLED="true" +# Firebase +export FCM_CONFIG_API_KEY="" +export FCM_CONFIG_AUTH_DOMAIN="" +export FCM_CONFIG_PROJECT_ID="" +export FCM_CONFIG_STORAGE_BUCKET="" +export FCM_CONFIG_MESSAGING_SENDER_ID="" +export FCM_CONFIG_APP_ID="" +export GOOGLE_SERVICES_B64="" +#Notifications Feature Announcements +export FEATURES_ANNOUNCEMENTS_ACCESS_TOKEN= +export FEATURES_ANNOUNCEMENTS_SPACE_ID= diff --git a/.storybook/storybook.requires.js b/.storybook/storybook.requires.js index 8c929c9fe7d..4c8963175cc 100644 --- a/.storybook/storybook.requires.js +++ b/.storybook/storybook.requires.js @@ -117,6 +117,8 @@ const getStories = () => { './app/components/UI/Name/Name.stories.tsx': require('../app/components/UI/Name/Name.stories.tsx'), "./app/components/UI/SimulationDetails/SimulationDetails.stories.tsx": require("../app/components/UI/SimulationDetails/SimulationDetails.stories.tsx"), "./app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.stories.tsx": require("../app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.stories.tsx"), + './app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.stories': require('../app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.stories.tsx'), + './app/component-library/components-temp/Price/PercentageChange/PercentageChange.stories': require('../app/component-library/components-temp/Price/PercentageChange/PercentageChange.stories.tsx'), }; }; diff --git a/CHANGELOG.md b/CHANGELOG.md index 94dc5e5e7cf..6115fbf3b01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,176 +2,182 @@ ## Current Main Branch -## 7.24.4 - Jun 25, 2024 -### Fixed -- [10064](https://github.com/MetaMask/metamask-mobile/pull/10064) fix: Always mark the STX Opt In modal as seen -- [10088](https://github.com/MetaMask/metamask-mobile/pull/10088) fix: Swap with unapproved token -- [10099](https://github.com/MetaMask/metamask-mobile/pull/10099) fix: stx on ramps missing origin - -## 7.24.3 - Jun 19, 2024 -### Fixed -- [#10045](https://github.com/MetaMask/metamask-mobile/pull/10045): fix: Update ppom package to 1.4.8 (#10041) - -## 7.24.2 - Jun 13, 2024 +## 7.27.0 - Jul 5, 2024 ### Added -- [#9687](https://github.com/MetaMask/metamask-mobile/pull/9687): feat: adds "data collection for marketing" toggles +- [#7759](https://github.com/MetaMask/metamask-mobile/pull/7759): feat: upgrade react-native-webview (#7759) +- [#10000](https://github.com/MetaMask/metamask-mobile/pull/10000): feat: support security alerts API (#10000) +- [#10039](https://github.com/MetaMask/metamask-mobile/pull/10039): feat: enable transaction simulations (#10039) +- [#10085](https://github.com/MetaMask/metamask-mobile/pull/10085): feat: Integrate Firebase libraries and initial config to enable Push Notifications FCM. (#10085) +- [#9724](https://github.com/MetaMask/metamask-mobile/pull/9724): feat: remove `selectIdentities` in favour of `selectInternalAccounts` (#9724) +- [#9356](https://github.com/MetaMask/metamask-mobile/pull/9356): feat: add api spec test infrastructure (#9356) +- [#10144](https://github.com/MetaMask/metamask-mobile/pull/10144): feat: add increase decrease token percentage (#10144) +- [#10189](https://github.com/MetaMask/metamask-mobile/pull/10189): feat: Revamp Snap connection screen (#10189) +- [#10121](https://github.com/MetaMask/metamask-mobile/pull/10121): feat: enables snaps feature flag on mobile (#10121) +- [#10040](https://github.com/MetaMask/metamask-mobile/pull/10040): feat: edit networks UI redesign (#10040) +- [#10120](https://github.com/MetaMask/metamask-mobile/pull/10120): feat: add i18n to for the add/edit network ui re-design (#10120) +- [#9961](https://github.com/MetaMask/metamask-mobile/pull/9961): feat: adding search to the network bottomsheet as part of the networks management UI redesign (#9961) +- [#10112](https://github.com/MetaMask/metamask-mobile/pull/10112): feat: Add the ""is_smart_transaction"" prop for the ""Swap Started"" event (#10112) +- [#10086](https://github.com/MetaMask/metamask-mobile/pull/10086): feat: Add team label to pr (#10086) -### Fixed -- [#9905](https://github.com/MetaMask/metamask-mobile/pull/9905): fix: remove metametrics redundant calls and improve compliance +### Changed +- [#10153](https://github.com/MetaMask/metamask-mobile/pull/10153): chore: Migrate AsyncStorage to mmkv (#10153) +- [#10071](https://github.com/MetaMask/metamask-mobile/pull/10071): chore: New Crowdin translations by Github Action (#10071) +- [#9441](https://github.com/MetaMask/metamask-mobile/pull/9441): chore: Convert initial background state to typed background state (#9441) +- [#10219](https://github.com/MetaMask/metamask-mobile/pull/10219): test: address flaky smoke e2e (#10219) +- [#10167](https://github.com/MetaMask/metamask-mobile/pull/10167): chore: added the requirement for PRs to contain a QA label (#10167) +- [#10015](https://github.com/MetaMask/metamask-mobile/pull/10015): test: add connect to Portfolio (#10015) +- [#10134](https://github.com/MetaMask/metamask-mobile/pull/10134): test: disable privacy policy toast and whats new modal using fixtures (#10134) +- [#10128](https://github.com/MetaMask/metamask-mobile/pull/10128): refactor: update bitrise e2e check to not run on forks (#10128) +- [#10092](https://github.com/MetaMask/metamask-mobile/pull/10092): chore: merge release 7.24.3 tag in 7.26.0 (#10092) +- [#10070](https://github.com/MetaMask/metamask-mobile/pull/10070): chore: update attribution (#10070) +- [#9779](https://github.com/MetaMask/metamask-mobile/pull/9779): chore: Update accounts controller v14 (#9779) +- [#10055](https://github.com/MetaMask/metamask-mobile/pull/10055): test: Fix detox test run inconsistencies (#10055) +- [#9777](https://github.com/MetaMask/metamask-mobile/pull/9777): chore: New Crowdin translations by Github Action (#9777) +- [#9960](https://github.com/MetaMask/metamask-mobile/pull/9960): chore: add typescript fitness function (#9960) +- [#10048](https://github.com/MetaMask/metamask-mobile/pull/10048): chore: Chore/9924 - Enable `@typescript-eslint/no-explicit-any` as error rule (#10048) +- [#9652](https://github.com/MetaMask/metamask-mobile/pull/9652): chore(ramp): upgrade sdk to 1.28.1 (#9652) +- [#9969](https://github.com/MetaMask/metamask-mobile/pull/9969): test: add E2E for increase allowance (#9969) +- [#9811](https://github.com/MetaMask/metamask-mobile/pull/9811): chore: Add Notification devs to codeowners file (#9811) +- [#9936](https://github.com/MetaMask/metamask-mobile/pull/9936): chore: restore bot workflow to update attributions (#9936) +- [#10067](https://github.com/MetaMask/metamask-mobile/pull/10067): chore: add smokeAssets e2e tag (#10067) +- [#10213](https://github.com/MetaMask/metamask-mobile/pull/10213): chore: reorder accounts in ETH_REQUESTACCOUNTS response to prioritize selectedAddress in the 'AndroidService' (#10213) +- [#9914](https://github.com/MetaMask/metamask-mobile/pull/9914): chore: fix the incorrect URL displayed during transaction confirmation (#9914) +- [#10063](https://github.com/MetaMask/metamask-mobile/pull/10063): chore: Refactor Snaps integration (#10063) -## 7.24.1 - Jun 13, 2024 ### Fixed -- [#9943](https://github.com/MetaMask/metamask-mobile/pull/9943): fix: Remove duplicate accounts (#9943) -- [#10006](https://github.com/MetaMask/metamask-mobile/pull/10006): fix: Fix order of accounts (#10006) -- [#10004](https://github.com/MetaMask/metamask-mobile/pull/10004): fix: Synchronize account names (#10004) -- [#9974](https://github.com/MetaMask/metamask-mobile/pull/9974): fix: Fix freeze on buy and sell flow (#9974) -- [#9980](https://github.com/MetaMask/metamask-mobile/pull/9980): fix: Fix initialization crash / login error "Engine does not exist (#9980) +- [#10168](https://github.com/MetaMask/metamask-mobile/pull/10168): fix: convert Sentry messages to log (#10168) +- [#9918](https://github.com/MetaMask/metamask-mobile/pull/9918): fix: hide amount in simulations for testnets if its opt out (#9918) +- [#10164](https://github.com/MetaMask/metamask-mobile/pull/10164): fix: Stop running fencing logic on `node_modules` (#10164) +- [#10146](https://github.com/MetaMask/metamask-mobile/pull/10146): fix: add API Spec Tests section in testing.md (#10146) +- [#10122](https://github.com/MetaMask/metamask-mobile/pull/10122): fix: updated changelog for 7.24.4 (#10122) +- [#10034](https://github.com/MetaMask/metamask-mobile/pull/10034): fix: Auto capitalize letter to none to have the same behaviour as the password field on create new wallet flow (#10034) +- [#10233](https://github.com/MetaMask/metamask-mobile/pull/10233): fix: copy changes in blockaid settings (#10233) +- [#10190](https://github.com/MetaMask/metamask-mobile/pull/10190): fix: untranslated error when speed up transaction (#10190) +- [#10227](https://github.com/MetaMask/metamask-mobile/pull/10227): fix: add edge case of having falsy address for `useTokenListEntries` (#10227) +- [#10163](https://github.com/MetaMask/metamask-mobile/pull/10163): fix: add tokenList iconUrl to `IdentIcon` component (#10163) +- [#10192](https://github.com/MetaMask/metamask-mobile/pull/10192): fix: blockaid validations for deeplink transactions (#10192) +- [#10142](https://github.com/MetaMask/metamask-mobile/pull/10142): fix: memoize token list (#10142) +- [#10049](https://github.com/MetaMask/metamask-mobile/pull/10049): fix: fix stuck after `nonce too low` error (#10049) +- [#9795](https://github.com/MetaMask/metamask-mobile/pull/9795): fix: improve message display for typed sign messages (#9795) +- [#10247](https://github.com/MetaMask/metamask-mobile/pull/10247): fix: add support for svg uris without viewbox (#10247) +- [#9972](https://github.com/MetaMask/metamask-mobile/pull/9972): fix(devDeps): ganache@^7.7.7->^7.9.2 (#9972) +- [#10127](https://github.com/MetaMask/metamask-mobile/pull/10127): fix: swaps android swap allowance error (#10127) +- [#10088](https://github.com/MetaMask/metamask-mobile/pull/10088): fix: Swap with unapproved token (#10088) +- [#10133](https://github.com/MetaMask/metamask-mobile/pull/10133): fix: Fix BaseControllerV1 state rehydration (#10133) +- [#10151](https://github.com/MetaMask/metamask-mobile/pull/10151): fix: fix checksum address (#10151) +- [#10135](https://github.com/MetaMask/metamask-mobile/pull/10135): fix: fix checksum address for balance check (#10135) +- [#9857](https://github.com/MetaMask/metamask-mobile/pull/9857): fix: create nft auto detection modal and remove nft polling logic (#9857) +- [#9843](https://github.com/MetaMask/metamask-mobile/pull/9843): fix: deeplink is not opening the site in the inapp-browser when the wallet is locked (#9843) +- [#10064](https://github.com/MetaMask/metamask-mobile/pull/10064): fix: Always mark the STX Opt In modal as seen (#10064) + +## 7.26.1 - Jul 17, 2024 +### Fixed +- [#9724](https://github.com/MetaMask/metamask-mobile/pull/9724): feat: remove selectIdentities in favour of selectInternalAccounts -## 7.24.0 - Jun 11, 2024 +## 7.26.0 - Jul 5, 2024 ### Added -- [#9767](https://github.com/MetaMask/metamask-mobile/pull/9767): feat: push Privacy policy date to 2024 Jun 18 12:00 UTC (#9767) -- [#9707](https://github.com/MetaMask/metamask-mobile/pull/9707): feat: adds strings for the opt in toggles (#9707) -- [#9661](https://github.com/MetaMask/metamask-mobile/pull/9661): feat: smart-tx opt in modal 2 (#9661) -- [#9448](https://github.com/MetaMask/metamask-mobile/pull/9448): feat: smart tx small views (#9448) -- [#9442](https://github.com/MetaMask/metamask-mobile/pull/9442): feat: smart-tx-small-logic (#9442) -- [#9204](https://github.com/MetaMask/metamask-mobile/pull/9204): feat: Add new privacy policy alert toast (#9204) -- [#9651](https://github.com/MetaMask/metamask-mobile/pull/9651): feat: Upgrade signature Controller to v14 (#9651) -- [#9394](https://github.com/MetaMask/metamask-mobile/pull/9394): feat: Network & Gas & Assets & Utils controllers update (#9394) -- [#9625](https://github.com/MetaMask/metamask-mobile/pull/9625): feat: Update signature controller v13 (#9625) -- [#9129](https://github.com/MetaMask/metamask-mobile/pull/9129): feat: setting to show fiat values on testnets (#9129) -- [#9740](https://github.com/MetaMask/metamask-mobile/pull/9740): feat: Update checkbox to be able to override checkbox style (#9740) -- [#9346](https://github.com/MetaMask/metamask-mobile/pull/9346): feat: notifications LIST screen UI - [9 of 10] (#9346) -- [#9572](https://github.com/MetaMask/metamask-mobile/pull/9572): feat: notifications onboarding wizard - [8 of 10] (#9572) +- [#9937](https://github.com/MetaMask/metamask-mobile/pull/9937): feat: modification of the network bottom sheet to use the new UI redesign by adding the popular network section as additional network (#9937) +- [#9856](https://github.com/MetaMask/metamask-mobile/pull/9856): feat: new attribution github workflow (#9856) +- [#9768](https://github.com/MetaMask/metamask-mobile/pull/9768): feat: add MetaMetrics delete on Wallet delete (#9768) +- [#9785](https://github.com/MetaMask/metamask-mobile/pull/9785): feat: Log the validity of the keyringController in the top 3 Migrations that appear in Sentry (#9785) +- [#9885](https://github.com/MetaMask/metamask-mobile/pull/9885): feat: Feat/9492 add unsupported method and legacy method middlewares (#9885) +- [#9743](https://github.com/MetaMask/metamask-mobile/pull/9743): feat: Metrics/1803 emit error viewed event (#9743) +- [#9888](https://github.com/MetaMask/metamask-mobile/pull/9888): feat: add set approve for all screen (#9888) +- [#9794](https://github.com/MetaMask/metamask-mobile/pull/9794): feat: add increase allowance screen (#9794) +- [#9828](https://github.com/MetaMask/metamask-mobile/pull/9828): feat: enable transaction simulations (#9828) +- [#9648](https://github.com/MetaMask/metamask-mobile/pull/9648): feat: add transaction simulations preference (#9648) +- [#9783](https://github.com/MetaMask/metamask-mobile/pull/9783): feat: add transaction simulation metrics (#9783) +- [#9793](https://github.com/MetaMask/metamask-mobile/pull/9793): feat: add fiat support to simulations (#9793) +- [#9410](https://github.com/MetaMask/metamask-mobile/pull/9410): feat: add SimulationDetails component (#9410) +- [#9070](https://github.com/MetaMask/metamask-mobile/pull/9070): feat: remove selectSelectedAddress in favour of selectSelectedInternalAccount (#9070) +- [#9845](https://github.com/MetaMask/metamask-mobile/pull/9845): feat: updated design-tokens to v4 (#9845) +- [#9653](https://github.com/MetaMask/metamask-mobile/pull/9653): feat: added design tokens eslint rules to mobile (#9653) +- [#9473](https://github.com/MetaMask/metamask-mobile/pull/9473): feat: notifications details screen (#9473) ### Changed -- [#9612](https://github.com/MetaMask/metamask-mobile/pull/9612): revert: feat(swaps): enable Base for swaps (#9286) (#9612) -- [#9735](https://github.com/MetaMask/metamask-mobile/pull/9735): chore: add swaps team to swaps domain folder (#9735) -- [#9683](https://github.com/MetaMask/metamask-mobile/pull/9683): refactor: the network name and image utils into selectors (#9683) -- [#9639](https://github.com/MetaMask/metamask-mobile/pull/9639): chore: New Crowdin translations by Github Action (#9639) -- [#9725](https://github.com/MetaMask/metamask-mobile/pull/9725): chore: make test code dev env only (#9725) -- [#9574](https://github.com/MetaMask/metamask-mobile/pull/9574): chore: update confirmations codeowners (#9574) -- [#9663](https://github.com/MetaMask/metamask-mobile/pull/9663): chore: update tx controller v13 patch notes (#9663) -- [#9629](https://github.com/MetaMask/metamask-mobile/pull/9629): chore: apply string changes from #9565 (#9629) -- [#9753](https://github.com/MetaMask/metamask-mobile/pull/9753): chore: Chore/optimize Wallet screen re-renders (#9753) -- [#9771](https://github.com/MetaMask/metamask-mobile/pull/9771): chore: align main and flask build numbers (#9771) -- [#9751](https://github.com/MetaMask/metamask-mobile/pull/9751): test: disable flakey test (#9751) -- [#9708](https://github.com/MetaMask/metamask-mobile/pull/9708): test: add send to saved contact e2e test (#9708) -- [#9690](https://github.com/MetaMask/metamask-mobile/pull/9690): test: Address App launch times failure test (#9690) -- [#9694](https://github.com/MetaMask/metamask-mobile/pull/9694): test: send flow page object refactor (#9694) -- [#9658](https://github.com/MetaMask/metamask-mobile/pull/9658): chore: Upgrade react-native-svg to 15.3 (#9658) -- [#9657](https://github.com/MetaMask/metamask-mobile/pull/9657): chore: Update boost checksum (#9657) -- [#9609](https://github.com/MetaMask/metamask-mobile/pull/9609): chore: add deprecated tag to the websiteicon component (#9609) -- [#9619](https://github.com/MetaMask/metamask-mobile/pull/9619): chore(deps): remove unused react-native-v8 (#9619) -- [#9599](https://github.com/MetaMask/metamask-mobile/pull/9599): chore: update Encryptor CODEOWNER to Accounts Team (#9599) -- [#9734](https://github.com/MetaMask/metamask-mobile/pull/9734): test: Added swap ERC20->ETH test case (#9734) -- [#9712](https://github.com/MetaMask/metamask-mobile/pull/9712): chore: remove unnecessary patch imports and change the patch branch name (#9712) +- [#9884](https://github.com/MetaMask/metamask-mobile/pull/9884): chore(pr template + readme): add link to contributor docs (#9884) +- [#9863](https://github.com/MetaMask/metamask-mobile/pull/9863): chore: Revert ""fix: swaps quote nan to bnjs (#9848)"" (#9863) +- [#9915](https://github.com/MetaMask/metamask-mobile/pull/9915): test: 1454 refactor modal pages batch 4 (#9915) +- [#9998](https://github.com/MetaMask/metamask-mobile/pull/9998): test: marketing optin date trigger testing. (#9998) +- [#9941](https://github.com/MetaMask/metamask-mobile/pull/9941): chore: Revert ""chore: fitness quality gate to only allow TS & TSX files in app directory"" (#9941) +- [#9913](https://github.com/MetaMask/metamask-mobile/pull/9913): chore: Update signature controller to v16 (#9913) +- [#9723](https://github.com/MetaMask/metamask-mobile/pull/9723): chore: fitness quality gate to only allow TS & TSX files in app directory (#9723) +- [#9926](https://github.com/MetaMask/metamask-mobile/pull/9926): chore: Resolve braces package to address audit issue (#9926) +- [#9814](https://github.com/MetaMask/metamask-mobile/pull/9814): chore: Update Preferences Controller v^11 (#9814) +- [#9714](https://github.com/MetaMask/metamask-mobile/pull/9714): test: import tokens detected (#9714) +- [#9693](https://github.com/MetaMask/metamask-mobile/pull/9693): chore: update gas fee controller to 15.1.2 (#9693) +- [#9868](https://github.com/MetaMask/metamask-mobile/pull/9868): chore: add source to setup script (#9868) +- [#9886](https://github.com/MetaMask/metamask-mobile/pull/9886): test: Update Browserstack url as old link deprecated (#9886) +- [#9865](https://github.com/MetaMask/metamask-mobile/pull/9865): chore: swap view crash fetching quotes (#9865) +- [#9852](https://github.com/MetaMask/metamask-mobile/pull/9852): test: fix Assertion only working on IOS (#9852) +- [#9838](https://github.com/MetaMask/metamask-mobile/pull/9838): test: E2e Regression failure fix (#9838) +- [#9805](https://github.com/MetaMask/metamask-mobile/pull/9805): chore: Upgrade address-book-controller (#9805) +- [#9809](https://github.com/MetaMask/metamask-mobile/pull/9809): chore: merge 7.23.0 tag (#9809) +- [#9952](https://github.com/MetaMask/metamask-mobile/pull/9952): chore: update code owners (#9952) +- [#9790](https://github.com/MetaMask/metamask-mobile/pull/9790): chore(ci): update @rhysd/actionlint to 1.7.1 (#9790) +- [#9545](https://github.com/MetaMask/metamask-mobile/pull/9545): chore: Fix CocoaPods install on Linux (#9545) +- [#9883](https://github.com/MetaMask/metamask-mobile/pull/9883): chore: Update ppom package to 1.4.7 (#9883) +- [#9866](https://github.com/MetaMask/metamask-mobile/pull/9866): chore: commit changes to project.pgxproj caused by known issue in xcode 15 (#9866) +- [#9986](https://github.com/MetaMask/metamask-mobile/pull/9986): test: fix `TransactionReview` snapshots (#9986) +- [#9965](https://github.com/MetaMask/metamask-mobile/pull/9965): test: comment out flaky test from `encryption-with-key` (#9965) +- [#9964](https://github.com/MetaMask/metamask-mobile/pull/9964): test: fix snapshots from `AesCryptoTestForm` (#9964) +- [#9898](https://github.com/MetaMask/metamask-mobile/pull/9898): test: AES module E2E tests (#9898) +- [#9949](https://github.com/MetaMask/metamask-mobile/pull/9949): chore: add SmokeAccounts E2E tag (#9949) +- [#9942](https://github.com/MetaMask/metamask-mobile/pull/9942): refactor: updated cellbase to allow size changes (#9942) +- [#9922](https://github.com/MetaMask/metamask-mobile/pull/9922): refactor: replace secondary colors with warning colors (#9922) +- [#9899](https://github.com/MetaMask/metamask-mobile/pull/9899): chore: align ButtonIcons with design (#9899) +- [#9875](https://github.com/MetaMask/metamask-mobile/pull/9875): refactor: update brandColors to be imported from design system (#9875) +- [#9718](https://github.com/MetaMask/metamask-mobile/pull/9718): chore: upgrade snaps-controller and adapts its usage (#9718) +- [#9920](https://github.com/MetaMask/metamask-mobile/pull/9920): chore: remove update-attributions.yml (#9920) +- [#9570](https://github.com/MetaMask/metamask-mobile/pull/9570): chore: Update `@metamask/keyring-controller` to v16 (#9570) +- [#9234](https://github.com/MetaMask/metamask-mobile/pull/9234): chore: update the 'CODEOWNERS' file to include directories relevant to the 'sdk-devs' team (#9234) ### Fixed -- [#9301](https://github.com/MetaMask/metamask-mobile/pull/9301): fix: flaky tests issues template skip 2 (#9301) -- [#9774](https://github.com/MetaMask/metamask-mobile/pull/9774): fix: unit tests (#9774) -- [#9706](https://github.com/MetaMask/metamask-mobile/pull/9706): fix: QR scanner crash when user dismisses camera view in web view (#9706) -- [#9787](https://github.com/MetaMask/metamask-mobile/pull/9787): fix: revert use of sponge not available in actions Ubuntu image (#9787) -- [#9780](https://github.com/MetaMask/metamask-mobile/pull/9780): fix: make version setting script work with both main and flask (#9780) -- [#9721](https://github.com/MetaMask/metamask-mobile/pull/9721): fix: App slower when changing account and switching network (#9721) -- [#9775](https://github.com/MetaMask/metamask-mobile/pull/9775): fix: Update boost checksum (#9775) -- [#9772](https://github.com/MetaMask/metamask-mobile/pull/9772): fix: Fix/ruby install (#9772) -- [#9773](https://github.com/MetaMask/metamask-mobile/pull/9773): fix: e2e builds (#9773) -- [#9742](https://github.com/MetaMask/metamask-mobile/pull/9742): fix: Selecting custom ethereum mainnet on fresh install (#9742) -- [#9733](https://github.com/MetaMask/metamask-mobile/pull/9733): fix: Fix/9662 account section disappears (#9733) -- [#9699](https://github.com/MetaMask/metamask-mobile/pull/9699): fix: Move permission middleware to be later than rpc method middleware (#9699) -- [#9680](https://github.com/MetaMask/metamask-mobile/pull/9680): fix: Increase waiting for swaps and switch account on E2E (#9680) -- [#9656](https://github.com/MetaMask/metamask-mobile/pull/9656): fix: Fix/1723 broken sign verification (#9656) -- [#9630](https://github.com/MetaMask/metamask-mobile/pull/9630): fix: Fix/rn svg pod files (#9630) -- [#9614](https://github.com/MetaMask/metamask-mobile/pull/9614): fix: migrations key numbers (#9614) -- [#9611](https://github.com/MetaMask/metamask-mobile/pull/9611): fix: Fix/9345 bitrise cache failure (#9611) -- [#9696](https://github.com/MetaMask/metamask-mobile/pull/9696): fix(devDeps): @lavamoat/allow-scripts@^2.3.1->^3.0.4 (#9696) -- [#9685](https://github.com/MetaMask/metamask-mobile/pull/9685): fix(deps): @metamask/eth-sig-util@^4.0.1->^7.0.2 (#9685) -- [#9682](https://github.com/MetaMask/metamask-mobile/pull/9682): fix: update base nickname (#9682) -- [#9686](https://github.com/MetaMask/metamask-mobile/pull/9686): fix: update sepolia nickname (#9686) -- [#9764](https://github.com/MetaMask/metamask-mobile/pull/9764): fix: selectSelectedInternalAccount: Account with ID not found (#9764) -- [#9568](https://github.com/MetaMask/metamask-mobile/pull/9568): fix: 9559 issue android json parse (#9568) -- [#9616](https://github.com/MetaMask/metamask-mobile/pull/9616): fix: fix the issue 9560 which QR code accounts has been reappeared aftter user `remove wallets` (#9616) - -## 7.23.0 - May 10, 2024 -### Added -- [#9595](https://github.com/MetaMask/metamask-mobile/pull/9595): feat: Upgrade to react-native-svg to 15.2.0 (#9595) -- [#9305](https://github.com/MetaMask/metamask-mobile/pull/9305): feat: Update SignatureController v6.1.3 + LoggingController v2.0.0 (#9305) -- [#9546](https://github.com/MetaMask/metamask-mobile/pull/9546): feat: fix logs for 1709 (#9546) -- [#9504](https://github.com/MetaMask/metamask-mobile/pull/9504): feat: Log the validity of keyringController in EngineService for initialisation and update (#9504) -- [#9288](https://github.com/MetaMask/metamask-mobile/pull/9288): feat: Update assets controller to v^18 (#9288) -- [#9286](https://github.com/MetaMask/metamask-mobile/pull/9286): feat(swaps): enable Base for swaps (#9286) -- [#9495](https://github.com/MetaMask/metamask-mobile/pull/9495): feat: url bar no longer shown in the tab thumnail list view (#9495) -- [#9474](https://github.com/MetaMask/metamask-mobile/pull/9474): feat: add localizations for the browser tab (#9474) -- [#9435](https://github.com/MetaMask/metamask-mobile/pull/9435): feat: sdk async persistence and protocol upgrade (#9435) -- [#9119](https://github.com/MetaMask/metamask-mobile/pull/9119): feat(ramp): add activation keys labels and DS components (#9119) -- [#9372](https://github.com/MetaMask/metamask-mobile/pull/9372): feat: Add `useTokenListName` hook for `Name` component (#9372) -- [#9407](https://github.com/MetaMask/metamask-mobile/pull/9407): feat: Add `useFirstPartyContractName` hook (#9407) -- [#9379](https://github.com/MetaMask/metamask-mobile/pull/9379): feat: create basic Name component for simulations (#9379) -- [#9547](https://github.com/MetaMask/metamask-mobile/pull/9547): feat: OS to Reservoir migration (#9547) -- [#9431](https://github.com/MetaMask/metamask-mobile/pull/9431): feat: add palm to popular network (#9431) -- [#9508](https://github.com/MetaMask/metamask-mobile/pull/9508): feat: bump controllers related accounts logic (#9508) -- [#8827](https://github.com/MetaMask/metamask-mobile/pull/8827): feat: preinstalled mobile snaps (#8827) -- [#9392](https://github.com/MetaMask/metamask-mobile/pull/9392): feat: notifications onboarding wizard (#9392) -- [#9450](https://github.com/MetaMask/metamask-mobile/pull/9450): feat: added TagBase component (#9450) -- [#9401](https://github.com/MetaMask/metamask-mobile/pull/9401): feat: updated styling in badgenetwork (#9401) - -### Changed -- [#9571](https://github.com/MetaMask/metamask-mobile/pull/9571): chore: revert feat: notifications onboarding wizard (#9392) (#9571) -- [#9610](https://github.com/MetaMask/metamask-mobile/pull/9610): test: Fix flakiness caused by the notification permission dialog which caused confirmation test flakiness (#9610) -- [#9577](https://github.com/MetaMask/metamask-mobile/pull/9577): chore: Chore/1742 remove vault recreation log in (#9577) -- [#9576](https://github.com/MetaMask/metamask-mobile/pull/9576): chore: Update migrations with Fatal Errors (#9576) -- [#9529](https://github.com/MetaMask/metamask-mobile/pull/9529): test: Refactor browser and testdapp page objects (#9529) -- [#9231](https://github.com/MetaMask/metamask-mobile/pull/9231): chore: ensure gemfile versions (#9231) -- [#9502](https://github.com/MetaMask/metamask-mobile/pull/9502): test: add edit custom mainnet scenario (#9502) -- [#9088](https://github.com/MetaMask/metamask-mobile/pull/9088): chore: Update TransactionController to v13 and ApprovalController to v3.5.2 (#9088) -- [#9430](https://github.com/MetaMask/metamask-mobile/pull/9430): test: Add timeouts to Detox builds/tests worflows (#9430) -- [#9264](https://github.com/MetaMask/metamask-mobile/pull/9264): chore: Force appium drivers to use @xmldom/xmldom@0.7.13 (#9264) -- [#9501](https://github.com/MetaMask/metamask-mobile/pull/9501): "chore: Revert ""chore: Remove notify step from pr_e2e_smoke_pipeline"" (#9501)" -- [#9500](https://github.com/MetaMask/metamask-mobile/pull/9500): chore: Remove notify step from pr_e2e_smoke_pipeline (#9500) -- [#9460](https://github.com/MetaMask/metamask-mobile/pull/9460): "chore: Revert ""chore(iyarc): remove resolved audit advisory (#9455)"" (#9460)" -- [#9420](https://github.com/MetaMask/metamask-mobile/pull/9420): test: move NFT import test to quarantine folder (#9420) -- [#9413](https://github.com/MetaMask/metamask-mobile/pull/9413): test: fix flakey E2E tests (#9413) -- [#9415](https://github.com/MetaMask/metamask-mobile/pull/9415): refactor(ramp): transform aggregator network chain id to string (#9415) -- [#8138](https://github.com/MetaMask/metamask-mobile/pull/8138): test(ramp): add useActivationKeys hook test (#8138) -- [#9011](https://github.com/MetaMask/metamask-mobile/pull/9011): refactor(ramp): use statusDescription in order details (#9011) -- [#9203](https://github.com/MetaMask/metamask-mobile/pull/9203): refactor(encryptor): align Encryptor methods to match @metamask/browser-passworder (#9203) -- [#9503](https://github.com/MetaMask/metamask-mobile/pull/9503): chore: upgrade nodejs to v20 LTS (#9503) -- [#9371](https://github.com/MetaMask/metamask-mobile/pull/9371): chore: remove unecessary conditional (#9371) -- [#9484](https://github.com/MetaMask/metamask-mobile/pull/9484): chore(devDeps): @actions/github@^5.1.1->^6.0.0 (#9484) -- [#9454](https://github.com/MetaMask/metamask-mobile/pull/9454): chore: Update Jest to v29 (#9454) -- [#9475](https://github.com/MetaMask/metamask-mobile/pull/9475): chore: Fix handling of generated ppom files (#9475) -- [#9388](https://github.com/MetaMask/metamask-mobile/pull/9388): chore: initial *.metafi and *.metaswap URL migrations (#9388) -- [#9455](https://github.com/MetaMask/metamask-mobile/pull/9455): chore(iyarc): remove resolved audit advisory (#9455) -- [#8557](https://github.com/MetaMask/metamask-mobile/pull/8557): chore: remove unused @metamask/oss-attribution-generator (#8557) +- [#9903](https://github.com/MetaMask/metamask-mobile/pull/9903): fix: upgrade test failure fix for 7.24.0 release branch (#9903) +- [#9844](https://github.com/MetaMask/metamask-mobile/pull/9844): fix: confirmations failing ci tests (#9844) +- [#9831](https://github.com/MetaMask/metamask-mobile/pull/9831): fix: Fix audit ci (#9831) +- [#9893](https://github.com/MetaMask/metamask-mobile/pull/9893): fix: contributor docs link is not correct in pr template (#9893) +- [#9847](https://github.com/MetaMask/metamask-mobile/pull/9847): Fix/release testing issues (#9847) +- [#9946](https://github.com/MetaMask/metamask-mobile/pull/9946): fix: Update help center URLs (#9946) +- [#9848](https://github.com/MetaMask/metamask-mobile/pull/9848): fix: swaps quote nan to bnjs (#9848) +- [#9781](https://github.com/MetaMask/metamask-mobile/pull/9781): fix: Update Basic Functionality settings description.json (#9781) +- [#9763](https://github.com/MetaMask/metamask-mobile/pull/9763): "fix: JS ""pseudo protocol"" works when pasted on mobile browser (#9763)" +- [#9993](https://github.com/MetaMask/metamask-mobile/pull/9993): fix: Fix/re order internal accounts (#9993) +- [#9991](https://github.com/MetaMask/metamask-mobile/pull/9991): fix: fixed snapshots (#9991) +- [#9905](https://github.com/MetaMask/metamask-mobile/pull/9905): "fix: ""data collection for marketing"" from PR #9687 (#9905)" +- [#9980](https://github.com/MetaMask/metamask-mobile/pull/9980): fix: add migration to fix engine does not exist and (#9980) +- [#9982](https://github.com/MetaMask/metamask-mobile/pull/9982): fix: migration 43 stringify to type of (#9982) +- [#9894](https://github.com/MetaMask/metamask-mobile/pull/9894): fix: Update bitrise.yml with correct Browserstack url (#9894) +- [#9887](https://github.com/MetaMask/metamask-mobile/pull/9887): fix: Update Browserstack url as old link deprecated (#9887) +- [#9869](https://github.com/MetaMask/metamask-mobile/pull/9869): fix: Cherry pick of e2e fixes from main (#9869) +- [#9855](https://github.com/MetaMask/metamask-mobile/pull/9855): fix: Undefined balance when fetching from chain (#9855) +- [#9812](https://github.com/MetaMask/metamask-mobile/pull/9812): fix: main token balance not updating when switching accounts (#9812) +- [#9674](https://github.com/MetaMask/metamask-mobile/pull/9674): fix: update Delete MetaMetrics Data copy to 30 days (#9674) +- [#9819](https://github.com/MetaMask/metamask-mobile/pull/9819): fix: Add .e2e as part of the setup script (#9819) +- [#9791](https://github.com/MetaMask/metamask-mobile/pull/9791): fix: Disable segment in E2E mode (#9791) +- [#9934](https://github.com/MetaMask/metamask-mobile/pull/9934): fix: flakey CI asdf node version (#9934) +- [#9584](https://github.com/MetaMask/metamask-mobile/pull/9584): fix: @metamask/swaps-controller v6 -> v9 (#9584) +- [#9867](https://github.com/MetaMask/metamask-mobile/pull/9867): fix: only remove SES from exception if exception exists (#9867) +- [#9870](https://github.com/MetaMask/metamask-mobile/pull/9870): fix: yarn deduplicate release/7.24.0 to fix dupe in #9864 (#9870) +- [#9842](https://github.com/MetaMask/metamask-mobile/pull/9842): fix: error validating wallet connect signature with security provider (#9842) +- [#9999](https://github.com/MetaMask/metamask-mobile/pull/9999): fix: new locales (#9999) +- [#9826](https://github.com/MetaMask/metamask-mobile/pull/9826): fix: add migration for linea goerli (#9826) +- [#9876](https://github.com/MetaMask/metamask-mobile/pull/9876): fix: update patch for updateNftMetadata fct (#9876) +- [#9759](https://github.com/MetaMask/metamask-mobile/pull/9759): fix: remove unecessary calls to third party apis (#9759) +- [#9746](https://github.com/MetaMask/metamask-mobile/pull/9746): fix: render images of networks removed from popularNetwork list (#9746) +- [#9970](https://github.com/MetaMask/metamask-mobile/pull/9970): fix: error 'Invalid character in NaN' while gas editing (#9970) +- [#9902](https://github.com/MetaMask/metamask-mobile/pull/9902): fix: Update PPOM controller to fix handling of HTTP status codes (#9902) +- [#9943](https://github.com/MetaMask/metamask-mobile/pull/9943): fix: Duplicate accounts (#9943) +- [#9974](https://github.com/MetaMask/metamask-mobile/pull/9974): fix(ramp): memoize asset before passing it to balance hook (#9968) (#9974) +- [#9882](https://github.com/MetaMask/metamask-mobile/pull/9882): fix: edit account name screen display incorrect account name (#9882) +- [#9891](https://github.com/MetaMask/metamask-mobile/pull/9891): fix: bug report template - remove reference to recordit (#9891) +- [#9755](https://github.com/MetaMask/metamask-mobile/pull/9755): fix: display the DApp URL in connect screen for MetaMask IOS-SDK (#9755) +## 7.24.4 - Jun 25, 2024 ### Fixed -- [#9525](https://github.com/MetaMask/metamask-mobile/pull/9525): fix: fix regression tests (#9525) -- [#9411](https://github.com/MetaMask/metamask-mobile/pull/9411): fix: refactor mechanism for sending analytics events (#9411) -- [#9575](https://github.com/MetaMask/metamask-mobile/pull/9575): fix: Add missing wallet_addEthereumChain to unrestricted list (#9575) -- [#9521](https://github.com/MetaMask/metamask-mobile/pull/9521): fix: Fix/1723 add permission middleware (#9521) -- [#9514](https://github.com/MetaMask/metamask-mobile/pull/9514): fix: Opensea Popup (#9514) -- [#9412](https://github.com/MetaMask/metamask-mobile/pull/9412): fix: revert siwe and apg resolution (#9412) -- [#9597](https://github.com/MetaMask/metamask-mobile/pull/9597): fix: permission-controller to include minor updates (#9597) -- [#9527](https://github.com/MetaMask/metamask-mobile/pull/9527): fix: upgrade permission-controller to 8.0.0 (#9527) -- [#9538](https://github.com/MetaMask/metamask-mobile/pull/9538): fix: cp url fix (#9538) -- [#9489](https://github.com/MetaMask/metamask-mobile/pull/9489): fix: adjust UI and Cancel ability on Basic Functionality flow (#9489) -- [#9498](https://github.com/MetaMask/metamask-mobile/pull/9498): fix(translations): Trying to account for updated translations in main for crowdin (#9498) -- [#9494](https://github.com/MetaMask/metamask-mobile/pull/9494): fix(translations): Attempting to reduce additional translations (#9494) -- [#9569](https://github.com/MetaMask/metamask-mobile/pull/9569): fix: deeplink invalid error when importing privateKey via qr (#9569) -- [#9347](https://github.com/MetaMask/metamask-mobile/pull/9347): fix: documentation link in storybook.md (#9347) -- [#9456](https://github.com/MetaMask/metamask-mobile/pull/9456): fix: fix sentry error when adding network (#9456) -- [#9177](https://github.com/MetaMask/metamask-mobile/pull/9177): fix: add symbol check on network add custom form (#9177) -- [#9566](https://github.com/MetaMask/metamask-mobile/pull/9566): fix: Fix Engine context types (#9566) -- [#9453](https://github.com/MetaMask/metamask-mobile/pull/9453): fix: include blockaid parameters in metrics of send flow (#9453) -- [#9340](https://github.com/MetaMask/metamask-mobile/pull/9340): fix: update blockaid setting section and align with extension (#9340) -- [#9539](https://github.com/MetaMask/metamask-mobile/pull/9539): fix: disable notifee badges (#9539) -- [#9436](https://github.com/MetaMask/metamask-mobile/pull/9436): fix: notification settings state bug (#9436) -- [#9543](https://github.com/MetaMask/metamask-mobile/pull/9543): fix: IPHONEOS_DEPLOYMENT_TARGET: 11 -> 12 (#9543) -- [#9523](https://github.com/MetaMask/metamask-mobile/pull/9523): fix: migrate from git-reffed react-native-search-api to @metamask/react-native-search-api (#9523) -- [#9522](https://github.com/MetaMask/metamask-mobile/pull/9522): fix: migrate from patched react-native-actionsheet to @metamask/react-native-actionsheet (#9522) -- [#9483](https://github.com/MetaMask/metamask-mobile/pull/9483): fix: migrate from patched @exodus/react-native-payments to @metamask/react-native-payments (#9483) -- [#9482](https://github.com/MetaMask/metamask-mobile/pull/9482): fix: remove resolution react-native-svg-asset-plugin/sharp@^0.30.5 (#9482) -- [#9143](https://github.com/MetaMask/metamask-mobile/pull/9143): fix: Yarn 1.22.22 (#9143) +- [10064](https://github.com/MetaMask/metamask-mobile/pull/10064) fix: Always mark the STX Opt In modal as seen +- [10088](https://github.com/MetaMask/metamask-mobile/pull/10088) fix: Swap with unapproved token +- [10099](https://github.com/MetaMask/metamask-mobile/pull/10099) fix: stx on ramps missing origin ## 7.24.3 - Jun 19, 2024 ### Fixed diff --git a/README.md b/README.md index a0a5c9acce8..64eff4aa1df 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,22 @@ git clone git@github.com:MetaMask/metamask-mobile.git && \ cd metamask-mobile ``` +**Firebase Messaging Setup** + +Before running the app, keep in mind that MetaMask uses FCM (Firebase Cloud Message) to empower communications. Based on this, would be preferable that you provide your own Firebase project config file and update your `google-services.json` file in the `android/app` directory as well your .env files (ios.env, js.env, android.env), adding GOOGLE_SERVICES_B64 variable depending on the environment you are running the app (ios/android). + +ATTENTION: In case you don't provide your own Firebase project config file, you can make usage of a mock file at `android/app/google-services-example.json`, following the steps below from the root of the project: + +```bash +base64 -i ./android/app/google-services-example.json +``` + +Copy the result to your clipboard and paste it in the GOOGLE_SERVICES_B64 variable in the .env file you are running the app. + +In case of any doubt, please follow the instructions in the link below to get your Firebase project config file. + +[Firebase Project Quickstart](https://firebaseopensource.com/projects/firebase/quickstart-js/messaging/readme/#getting_started) + **Install dependencies** ```bash diff --git a/android/app/build.gradle b/android/app/build.gradle index b9d2c2f58e5..faf096bcbd6 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,6 +1,7 @@ apply plugin: "com.android.application" apply plugin: "com.facebook.react" apply plugin: "io.sentry.android.gradle" +apply plugin: 'com.google.gms.google-services' import com.android.build.OutputFile @@ -181,8 +182,8 @@ android { applicationId "io.metamask" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 1354 - versionName "7.24.4" + versionCode 1364 + versionName "7.27.0" testBuildType System.getProperty('testBuildType', 'debug') missingDimensionStrategy 'react-native-camera', 'general' testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/android/app/google-services-example.json b/android/app/google-services-example.json new file mode 100644 index 00000000000..56d9bd48e3c --- /dev/null +++ b/android/app/google-services-example.json @@ -0,0 +1,22 @@ +{ + "project_info": { + "project_id": "mockproject-1234", + "project_number": "123456789000", + "name": "FirebaseQuickstarts", + "firebase_url": "https://mockproject-1234.firebaseio.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:123456789000:android:f1bf012572b04063", + "client_id": "android:com.google.samples.quickstart.admobexample", + "client_type": 1, + "android_client_info": { + "package_name": "com.google.samples.quickstart.admobexample", + "certificate_hash": [] + } + }, + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index 54950802185..497382a7533 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -24,6 +24,7 @@ buildscript { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" classpath("com.facebook.react:react-native-gradle-plugin") classpath("io.sentry:sentry-android-gradle-plugin:4.2.0") + classpath("com.google.gms:google-services:4.4.2") } allprojects { repositories { diff --git a/android/gradle.properties b/android/gradle.properties index f06abec2465..75e96754502 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -48,4 +48,3 @@ hermesEnabled=true # TODO: explain following config options android.disableResourceValidation=true android.enableDexingArtifactTransform.desugaring=false -AsyncStorage_db_size_in_MB=10 diff --git a/app/__mocks__/htmlMock.ts b/app/__mocks__/htmlMock.ts new file mode 100644 index 00000000000..d0d5234756b --- /dev/null +++ b/app/__mocks__/htmlMock.ts @@ -0,0 +1,3 @@ +//@ts-expect-error TS issue: Cannot find module '../../node_modules/@metamask/... +// eslint-disable-next-line import/prefer-default-export +export { default as html } from '../../node_modules/@metamask/snaps-execution-environments/dist/browserify/webview/index.html'; diff --git a/app/actions/notification/constants/index.ts b/app/actions/notification/constants/index.ts new file mode 100644 index 00000000000..f84e48a5142 --- /dev/null +++ b/app/actions/notification/constants/index.ts @@ -0,0 +1,23 @@ +export enum notificationsErrors { + PERFORM_SIGN_IN = 'Error while trying to sign in', + PERFORM_SIGN_OUT = 'Error while trying to sign out', + ENABLE_PROFILE_SYNCING = 'Error while trying to enable profile syncing', + DISABLE_PROFILE_SYNCING = 'Error while trying to disable profile syncing', + ENABLE_PUSH_NOTIFICATIONS = 'Error while trying to enable push notifications', + DISABLE_PUSH_NOTIFICATIONS = 'Error while trying to disable push notifications', + CHECK_ACCOUNTS_PRESENCE = 'Error while trying to check accounts presence', + DELETE_ON_CHAIN_TRIGGERS_BY_ACCOUNT = 'Error while trying to delete on chain triggers by account', + UPDATE_ON_CHAIN_TRIGGERS_BY_ACCOUNT = 'Error while trying to update on chain triggers by account', + SET_FEATURE_ANNOUNCEMENTS_ENABLED = 'Error while trying to set feature announcements enabled', + SET_SNAP_NOTIFICATIONS_ENABLED = 'Error while trying to set snap notifications enabled', + SET_METAMASK_NOTIFICATIONS_FEATURE_SEEN = 'Error while trying to set metamask notifications feature seen', + FETCH_AND_UPDATE_METAMASK_NOTIFICATIONS = 'Error while trying to fetch and update metamask notifications', + MARK_METAMASK_NOTIFICATIONS_AS_READ = 'Error while trying to mark metamask notifications as read', + DELETE_NOTIFICATION_STATUS = 'Error while trying to delete notification', + SET_PARTICIPATE_IN_META_METRICS = 'Error while trying to set participate in meta metrics', + UPDATE_TRIGGER_PUSH_NOTIFICATIONS = 'Error while trying to update trigger push notifications', + ENABLE_NOTIFICATIONS_SERVICES = 'Error while trying to enable notifications services', + DISABLE_NOTIFICATIONS_SERVICES = 'Error while trying to disable notifications services', +} + +export default notificationsErrors; diff --git a/app/actions/notification/helpers/index.ts b/app/actions/notification/helpers/index.ts new file mode 100644 index 00000000000..f1483a50cc9 --- /dev/null +++ b/app/actions/notification/helpers/index.ts @@ -0,0 +1,155 @@ +import { getErrorMessage } from '@metamask/utils'; + +import { notificationsErrors } from '../constants'; +import Engine from '../../../core/Engine'; +import { Notification } from '../../../util/notifications'; + +const { + AuthenticationController, + UserStorageController, + NotificationServicesController, +} = Engine.context; + +type MarkAsReadNotificationsParam = Pick< + Notification, + 'id' | 'type' | 'isRead' +>[]; + +export const signIn = async () => { + try { + const accessToken = await AuthenticationController.performSignIn(); + if (!accessToken) { + return getErrorMessage(notificationsErrors.PERFORM_SIGN_IN); + } + + const profile = await AuthenticationController.getSessionProfile(); + if (!profile) { + return getErrorMessage(notificationsErrors.PERFORM_SIGN_IN); + } + } catch (error) { + return getErrorMessage(error); + } +}; + +export const signOut = async () => { + try { + await AuthenticationController.performSignOut(); + } catch (error) { + return getErrorMessage(error); + } +}; + +export const enableProfileSyncing = async () => { + try { + await UserStorageController.enableProfileSyncing(); + } catch (error) { + return getErrorMessage(error); + } +}; + +export const disableProfileSyncing = async () => { + try { + await NotificationServicesController.disableNotificationServices(); + await UserStorageController.disableProfileSyncing(); + } catch (error) { + return getErrorMessage(error); + } +}; + +export const enableNotificationServices = async () => { + try { + await NotificationServicesController.enableMetamaskNotifications(); + } catch (error) { + return getErrorMessage(error); + } +}; + +export const disableNotificationServices = async () => { + try { + await NotificationServicesController.disableNotificationServices(); + } catch (error) { + return getErrorMessage(error); + } +}; + +export const checkAccountsPresence = async (accounts: string[]) => { + try { + const { presence } = + await NotificationServicesController.checkAccountsPresence(accounts); + if (!presence) { + return getErrorMessage(notificationsErrors.CHECK_ACCOUNTS_PRESENCE); + } + } catch (error) { + return getErrorMessage(error); + } +}; + +export const deleteOnChainTriggersByAccount = async (accounts: string[]) => { + try { + const { userStorage } = + await NotificationServicesController.deleteOnChainTriggersByAccount( + accounts, + ); + if (!userStorage) { + return getErrorMessage( + notificationsErrors.DELETE_ON_CHAIN_TRIGGERS_BY_ACCOUNT, + ); + } + } catch (error) { + return getErrorMessage(error); + } +}; + +export const updateOnChainTriggersByAccount = async (accounts: string[]) => { + try { + const { userStorage } = + await NotificationServicesController.updateOnChainTriggersByAccount( + accounts, + ); + if (!userStorage) { + return getErrorMessage( + notificationsErrors.UPDATE_ON_CHAIN_TRIGGERS_BY_ACCOUNT, + ); + } + } catch (error) { + return getErrorMessage(error); + } +}; + +export const setFeatureAnnouncementsEnabled = async ( + featureAnnouncementsEnabled: boolean, +) => { + try { + await NotificationServicesController.setFeatureAnnouncementsEnabled( + featureAnnouncementsEnabled, + ); + } catch (error) { + return getErrorMessage(error); + } +}; + +export const fetchAndUpdateMetamaskNotifications = async () => { + try { + const metamaskNotifications = + await NotificationServicesController.fetchAndUpdateMetamaskNotifications(); + if (!metamaskNotifications) { + return getErrorMessage( + notificationsErrors.FETCH_AND_UPDATE_METAMASK_NOTIFICATIONS, + ); + } + } catch (error) { + return getErrorMessage(error); + } +}; + +export const markMetamaskNotificationsAsRead = async ( + notifications: MarkAsReadNotificationsParam, +) => { + try { + await NotificationServicesController.markMetamaskNotificationsAsRead( + notifications, + ); + } catch (error) { + return getErrorMessage(error); + } +}; diff --git a/app/actions/notification/index.js b/app/actions/notification/index.js index 9924fdd33d9..14916e22502 100644 --- a/app/actions/notification/index.js +++ b/app/actions/notification/index.js @@ -1,12 +1,17 @@ +/** + * This file contains all the actions related to the in app (old/v1) notification system. + */ +import { ACTIONS } from '../../reducers/notification'; + export function hideCurrentNotification() { return { - type: 'HIDE_CURRENT_NOTIFICATION', + type: ACTIONS.HIDE_CURRENT_NOTIFICATION, }; } export function hideNotificationById(id) { return { - type: 'HIDE_NOTIFICATION_BY_ID', + type: ACTIONS.HIDE_NOTIFICATION_BY_ID, id, }; } @@ -17,7 +22,7 @@ export function modifyOrShowTransactionNotificationById({ status, }) { return { - type: 'MODIFY_OR_SHOW_TRANSACTION_NOTIFICATION', + type: ACTIONS.MODIFY_OR_SHOW_TRANSACTION_NOTIFICATION, autodismiss, transaction, status, @@ -31,7 +36,7 @@ export function modifyOrShowSimpleNotificationById({ status, }) { return { - type: 'MODIFY_OR_SHOW_SIMPLE_NOTIFICATION', + type: ACTIONS.MODIFY_OR_SHOW_SIMPLE_NOTIFICATION, autodismiss, title, description, @@ -41,7 +46,7 @@ export function modifyOrShowSimpleNotificationById({ export function replaceNotificationById(notification) { return { - type: 'REPLACE_NOTIFICATION_BY_ID', + type: ACTIONS.REPLACE_NOTIFICATION_BY_ID, notification, id: notification.id, }; @@ -49,14 +54,14 @@ export function replaceNotificationById(notification) { export function removeNotificationById(id) { return { - type: 'REMOVE_NOTIFICATION_BY_ID', + type: ACTIONS.REMOVE_NOTIFICATION_BY_ID, id, }; } export function removeCurrentNotification() { return { - type: 'REMOVE_CURRENT_NOTIFICATION', + type: ACTIONS.REMOVE_CURRENT_NOTIFICATION, }; } @@ -69,7 +74,7 @@ export function showSimpleNotification({ }) { return { id, - type: 'SHOW_SIMPLE_NOTIFICATION', + type: ACTIONS.SHOW_SIMPLE_NOTIFICATION, autodismiss, title, description, @@ -83,7 +88,7 @@ export function showTransactionNotification({ status, }) { return { - type: 'SHOW_TRANSACTION_NOTIFICATION', + type: ACTIONS.SHOW_TRANSACTION_NOTIFICATION, autodismiss, transaction, status, @@ -92,13 +97,6 @@ export function showTransactionNotification({ export function removeNotVisibleNotifications() { return { - type: 'REMOVE_NOT_VISIBLE_NOTIFICATIONS', - }; -} - -export function updateNotificationStatus(notificationsSettings) { - return { - type: 'UPDATE_NOTIFICATION_STATUS', - notificationsSettings, + type: ACTIONS.REMOVE_NOT_VISIBLE_NOTIFICATIONS, }; } diff --git a/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.stories.tsx b/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.stories.tsx new file mode 100644 index 00000000000..d9706d5e2e6 --- /dev/null +++ b/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.stories.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import AggregatedPercentage from './AggregatedPercentage'; +import { createStore } from 'redux'; +import initialBackgroundState from '../../../../util/test/initial-background-state.json'; + +const mockInitialState = { + wizard: { + step: 1, + }, + engine: { + backgroundState: initialBackgroundState, + }, +}; + +const rootReducer = (state = mockInitialState) => state; +const store = createStore(rootReducer); + +export default { + title: 'Component Library / AggregatedPercentage', + component: AggregatedPercentage, + decorators: [ + (Story) => ( + + + + ), + ], +}; + +const Template = (args) => ; + +export const Default = Template.bind({}); +Default.args = { + ethFiat: 1000, + tokenFiat: 500, + tokenFiat1dAgo: 950, + ethFiat1dAgo: 450, +}; + +export const NegativePercentageChange = Template.bind({}); +NegativePercentageChange.args = { + ethFiat: 900, + tokenFiat: 400, + tokenFiat1dAgo: 950, + ethFiat1dAgo: 1000, +}; + +export const PositivePercentageChange = Template.bind({}); +PositivePercentageChange.args = { + ethFiat: 1100, + tokenFiat: 600, + tokenFiat1dAgo: 500, + ethFiat1dAgo: 1000, +}; + +export const MixedPercentageChange = Template.bind({}); +MixedPercentageChange.args = { + ethFiat: 1050, + tokenFiat: 450, + tokenFiat1dAgo: 500, + ethFiat1dAgo: 1000, +}; diff --git a/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.styles.ts b/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.styles.ts new file mode 100644 index 00000000000..4852ce1d684 --- /dev/null +++ b/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.styles.ts @@ -0,0 +1,20 @@ +// Third party dependencies. +import { StyleSheet } from 'react-native'; + +/** + * Style sheet function for AggregatedPercentage component. + * + * @param params Style sheet params. + * @param params.theme App theme from ThemeContext. + * @param params.vars Inputs that the style sheet depends on. + * @returns StyleSheet object. + */ +const styleSheet = () => + StyleSheet.create({ + wrapper: { + flexDirection: 'row', + alignItems: 'center', + }, + }); + +export default styleSheet; diff --git a/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.test.tsx b/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.test.tsx new file mode 100644 index 00000000000..affec361321 --- /dev/null +++ b/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.test.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import { render } from '@testing-library/react-native'; +import AggregatedPercentage from './AggregatedPercentage'; +import { mockTheme } from '../../../../util/theme'; +import { useSelector } from 'react-redux'; +import { selectCurrentCurrency } from '../../../../selectors/currencyRateController'; + +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useSelector: jest.fn(), +})); +describe('AggregatedPercentage', () => { + beforeEach(() => { + (useSelector as jest.Mock).mockImplementation((selector) => { + if (selector === selectCurrentCurrency) return 'USD'; + }); + }); + afterEach(() => { + (useSelector as jest.Mock).mockClear(); + }); + it('should render correctly', () => { + const { toJSON } = render( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('renders positive percentage change correctly', () => { + const { getByText } = render( + , + ); + + expect(getByText('(+25.00%)')).toBeTruthy(); + expect(getByText('+100 USD')).toBeTruthy(); + + expect(getByText('(+25.00%)').props.style).toMatchObject({ + color: mockTheme.colors.success.default, + }); + }); + + it('renders negative percentage change correctly', () => { + const { getByText } = render( + , + ); + + expect(getByText('(-30.00%)')).toBeTruthy(); + expect(getByText('-150 USD')).toBeTruthy(); + + expect(getByText('(-30.00%)').props.style).toMatchObject({ + color: mockTheme.colors.error.default, + }); + }); +}); diff --git a/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.tsx b/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.tsx new file mode 100644 index 00000000000..3e0907f937b --- /dev/null +++ b/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import Text, { + TextColor, + TextVariant, +} from '../../../../component-library/components/Texts/Text'; +import { View } from 'react-native'; +import { renderFiat } from '../../../../util/number'; +import { useSelector } from 'react-redux'; +import { selectCurrentCurrency } from '../../../../selectors/currencyRateController'; +import styleSheet from './AggregatedPercentage.styles'; +import { useStyles } from '../../../hooks'; + +const isValidAmount = (amount: number | null | undefined): boolean => + amount !== null && amount !== undefined && !Number.isNaN(amount); + +const AggregatedPercentage = ({ + ethFiat, + tokenFiat, + tokenFiat1dAgo, + ethFiat1dAgo, +}: { + ethFiat: number; + tokenFiat: number; + tokenFiat1dAgo: number; + ethFiat1dAgo: number; +}) => { + const { styles } = useStyles(styleSheet, {}); + + const currentCurrency = useSelector(selectCurrentCurrency); + const DECIMALS_TO_SHOW = 2; + + const totalBalance = ethFiat + tokenFiat; + const totalBalance1dAgo = ethFiat1dAgo + tokenFiat1dAgo; + + const amountChange = totalBalance - totalBalance1dAgo; + + const percentageChange = + ((totalBalance - totalBalance1dAgo) / totalBalance1dAgo) * 100 || 0; + + let percentageTextColor = TextColor.Default; + + if (percentageChange === 0) { + percentageTextColor = TextColor.Default; + } else if (percentageChange > 0) { + percentageTextColor = TextColor.Success; + } else { + percentageTextColor = TextColor.Error; + } + + const formattedPercentage = isValidAmount(percentageChange) + ? `(${(percentageChange as number) >= 0 ? '+' : ''}${( + percentageChange as number + ).toFixed(2)}%)` + : ''; + + const formattedValuePrice = isValidAmount(amountChange) + ? `${(amountChange as number) >= 0 ? '+' : ''}${renderFiat( + amountChange, + currentCurrency, + DECIMALS_TO_SHOW, + )} ` + : ''; + + return ( + + + {formattedValuePrice} + + + {formattedPercentage} + + + ); +}; + +export default AggregatedPercentage; diff --git a/app/component-library/components-temp/Price/AggregatedPercentage/__snapshots__/AggregatedPercentage.test.tsx.snap b/app/component-library/components-temp/Price/AggregatedPercentage/__snapshots__/AggregatedPercentage.test.tsx.snap new file mode 100644 index 00000000000..7eea6a3bd9f --- /dev/null +++ b/app/component-library/components-temp/Price/AggregatedPercentage/__snapshots__/AggregatedPercentage.test.tsx.snap @@ -0,0 +1,43 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AggregatedPercentage should render correctly 1`] = ` + + + +20 USD + + + (+11.11%) + + +`; diff --git a/app/component-library/components-temp/Price/AggregatedPercentage/index.ts b/app/component-library/components-temp/Price/AggregatedPercentage/index.ts new file mode 100644 index 00000000000..3e7965d02fa --- /dev/null +++ b/app/component-library/components-temp/Price/AggregatedPercentage/index.ts @@ -0,0 +1 @@ +export { default } from './AggregatedPercentage'; diff --git a/app/component-library/components-temp/Price/PercentageChange/PercentageChange.stories.tsx b/app/component-library/components-temp/Price/PercentageChange/PercentageChange.stories.tsx new file mode 100644 index 00000000000..59612dcf9ba --- /dev/null +++ b/app/component-library/components-temp/Price/PercentageChange/PercentageChange.stories.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import PercentageChange from './PercentageChange'; +import { createStore } from 'redux'; +import initialBackgroundState from '../../../../util/test/initial-background-state.json'; + +const mockInitialState = { + wizard: { + step: 1, + }, + engine: { + backgroundState: initialBackgroundState, + }, +}; + +const rootReducer = (state = mockInitialState) => state; +const store = createStore(rootReducer); + +export default { + title: 'Component Library / PercentageChange', + component: PercentageChange, + decorators: [ + (Story) => ( + + + + ), + ], +}; + +const Template = (args) => ; + +export const Default = Template.bind({}); +Default.args = { + value: 0, +}; + +export const PositiveChange = Template.bind({}); +PositiveChange.args = { + value: 5.5, +}; + +export const NegativeChange = Template.bind({}); +NegativeChange.args = { + value: -3.75, +}; + +export const NoChange = Template.bind({}); +NoChange.args = { + value: 0, +}; + +export const InvalidValue = Template.bind({}); +InvalidValue.args = { + value: null, +}; diff --git a/app/component-library/components-temp/Price/PercentageChange/PercentageChange.test.tsx b/app/component-library/components-temp/Price/PercentageChange/PercentageChange.test.tsx new file mode 100644 index 00000000000..f01d3e7606f --- /dev/null +++ b/app/component-library/components-temp/Price/PercentageChange/PercentageChange.test.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { render } from '@testing-library/react-native'; +import PercentageChange from './PercentageChange'; +import { mockTheme } from '../../../../util/theme'; + +describe('PercentageChange', () => { + it('should render correctly', () => { + const { toJSON } = render(); + expect(toJSON()).toMatchSnapshot(); + }); + it('displays a positive value correctly', () => { + const { getByText } = render(); + const positiveText = getByText('+5.50%'); + expect(positiveText).toBeTruthy(); + expect(positiveText.props.style).toMatchObject({ + color: mockTheme.colors.success.default, + }); + }); + + it('displays a negative value correctly', () => { + const { getByText } = render(); + const negativeText = getByText('-3.25%'); + expect(negativeText).toBeTruthy(); + expect(negativeText.props.style).toMatchObject({ + color: mockTheme.colors.error.default, + }); + }); + + it('handles null value correctly', () => { + const { queryByText } = render(); + expect(queryByText(/\+/)).toBeNull(); + expect(queryByText(/-/)).toBeNull(); + }); + + it('handles undefined value correctly', () => { + const { queryByText } = render(); + expect(queryByText(/\+/)).toBeNull(); + expect(queryByText(/-/)).toBeNull(); + }); +}); diff --git a/app/component-library/components-temp/Price/PercentageChange/PercentageChange.tsx b/app/component-library/components-temp/Price/PercentageChange/PercentageChange.tsx new file mode 100644 index 00000000000..206b905fc92 --- /dev/null +++ b/app/component-library/components-temp/Price/PercentageChange/PercentageChange.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import Text, { + TextColor, + TextVariant, +} from '../../../../component-library/components/Texts/Text'; +import { View } from 'react-native'; + +const PercentageChange = ({ value }: { value: number | null | undefined }) => { + const percentageColorText = + value && value >= 0 ? TextColor.Success : TextColor.Error; + + const isValidAmount = (amount: number | null | undefined): boolean => + amount !== null && amount !== undefined && !Number.isNaN(amount); + + const formattedValue = isValidAmount(value) + ? `${(value as number) >= 0 ? '+' : ''}${(value as number).toFixed(2)}%` + : ''; + + return ( + + + {formattedValue} + + + ); +}; + +export default PercentageChange; diff --git a/app/component-library/components-temp/Price/PercentageChange/__snapshots__/PercentageChange.test.tsx.snap b/app/component-library/components-temp/Price/PercentageChange/__snapshots__/PercentageChange.test.tsx.snap new file mode 100644 index 00000000000..571dec70ebc --- /dev/null +++ b/app/component-library/components-temp/Price/PercentageChange/__snapshots__/PercentageChange.test.tsx.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PercentageChange should render correctly 1`] = ` + + + +5.50% + + +`; diff --git a/app/component-library/components-temp/Price/PercentageChange/index.ts b/app/component-library/components-temp/Price/PercentageChange/index.ts new file mode 100644 index 00000000000..6a3f076bb2a --- /dev/null +++ b/app/component-library/components-temp/Price/PercentageChange/index.ts @@ -0,0 +1 @@ +export { default } from './PercentageChange'; diff --git a/app/component-library/components/Badges/Badge/__snapshots__/Badge.test.tsx.snap b/app/component-library/components/Badges/Badge/__snapshots__/Badge.test.tsx.snap index 9d09c20a3ed..d7ae4963929 100644 --- a/app/component-library/components/Badges/Badge/__snapshots__/Badge.test.tsx.snap +++ b/app/component-library/components/Badges/Badge/__snapshots__/Badge.test.tsx.snap @@ -9,6 +9,7 @@ exports[`Badge should render badge network given the badge network variant 1`] = }, } } + isScaled={true} name="Ethereum" testID="badge-badgenetwork" /> diff --git a/app/component-library/components/Badges/Badge/variants/BadgeNetwork/BadgeNetwork.constants.ts b/app/component-library/components/Badges/Badge/variants/BadgeNetwork/BadgeNetwork.constants.ts index 411b01a7475..2eb4cf0af34 100644 --- a/app/component-library/components/Badges/Badge/variants/BadgeNetwork/BadgeNetwork.constants.ts +++ b/app/component-library/components/Badges/Badge/variants/BadgeNetwork/BadgeNetwork.constants.ts @@ -11,7 +11,7 @@ import { import { BadgeNetworkProps } from './BadgeNetwork.types'; // Test IDs -export const BADGE_NETWORK_TEST_ID = 'badge-network'; +export const BADGENETWORK_TEST_ID = 'badgenetwork'; // Defaults export const DEFAULT_BADGENETWORK_NETWORKICON_SIZE = AvatarSize.Md; @@ -22,4 +22,5 @@ const SAMPLE_BADGENETWORK_IMAGESOURCE = SAMPLE_AVATARNETWORK_IMAGESOURCE_LOCAL; export const SAMPLE_BADGENETWORK_PROPS: BadgeNetworkProps = { name: SAMPLE_BADGENETWORK_NAME, imageSource: SAMPLE_BADGENETWORK_IMAGESOURCE, + isScaled: true, }; diff --git a/app/component-library/components/Badges/Badge/variants/BadgeNetwork/BadgeNetwork.stories.tsx b/app/component-library/components/Badges/Badge/variants/BadgeNetwork/BadgeNetwork.stories.tsx index 938692c7fbe..7a4640e9fca 100644 --- a/app/component-library/components/Badges/Badge/variants/BadgeNetwork/BadgeNetwork.stories.tsx +++ b/app/component-library/components/Badges/Badge/variants/BadgeNetwork/BadgeNetwork.stories.tsx @@ -1,37 +1,52 @@ /* eslint-disable react-native/no-inline-styles */ -/* eslint-disable react/display-name */ +// Third party dependencies import React from 'react'; import { View } from 'react-native'; +import { Meta, Story } from '@storybook/react-native'; // Internal dependencies. -import { default as BadgeNetworkComponent } from './BadgeNetwork'; -import { SAMPLE_BADGENETWORK_PROPS } from './BadgeNetwork.constants'; +import BadgeNetwork from './BadgeNetwork'; import { BadgeNetworkProps } from './BadgeNetwork.types'; +import { SAMPLE_BADGENETWORK_PROPS } from './BadgeNetwork.constants'; -const BadgeNetworkMeta = { - title: 'Component Library / Badges', - component: BadgeNetworkComponent, +export default { + title: 'Component Library / Badges / BadgeNetwork', + component: BadgeNetwork, argTypes: { - name: { - control: { type: 'text' }, - defaultValue: SAMPLE_BADGENETWORK_PROPS.name, - }, + name: { control: 'text' }, + isScaled: { control: 'boolean' }, }, +} as Meta; + +const customRender = (args: BadgeNetworkProps) => ( + + + +); + +const Template: Story = (args) => customRender(args); + +export const Default = Template.bind({}); +Default.args = { + ...SAMPLE_BADGENETWORK_PROPS, +}; + +export const Scaled = Template.bind({}); +Scaled.args = { + ...SAMPLE_BADGENETWORK_PROPS, + isScaled: true, }; -export default BadgeNetworkMeta; -export const BadgeNetwork = { - render: (args: JSX.IntrinsicAttributes & BadgeNetworkProps) => ( - - - - ), +export const NotScaled = Template.bind({}); +NotScaled.args = { + ...SAMPLE_BADGENETWORK_PROPS, + isScaled: false, }; diff --git a/app/component-library/components/Badges/Badge/variants/BadgeNetwork/BadgeNetwork.styles.ts b/app/component-library/components/Badges/Badge/variants/BadgeNetwork/BadgeNetwork.styles.ts index 792dc19a2c2..f33723fa951 100644 --- a/app/component-library/components/Badges/Badge/variants/BadgeNetwork/BadgeNetwork.styles.ts +++ b/app/component-library/components/Badges/Badge/variants/BadgeNetwork/BadgeNetwork.styles.ts @@ -3,10 +3,10 @@ import { StyleSheet, ViewStyle } from 'react-native'; // External dependencies. import { Theme } from '../../../../../../util/theme/models'; -import { AvatarSize } from '../../../../Avatars/Avatar'; // Internal dependencies. import { BadgeNetworkStyleSheetVars } from './BadgeNetwork.types'; +import getScaledStyles from './BadgeNetwork.utils'; /** * Style sheet function for BadgeNetwork component. @@ -21,53 +21,43 @@ const styleSheet = (params: { vars: BadgeNetworkStyleSheetVars; }) => { const { theme, vars } = params; - const { style, containerSize, size } = vars; - /** - * Design Requirements: - * - The Network Badge needs to be 1/2 the height of its content, up to 1/2 the height - * of the largest AvatarSize. - * - It needs to have a 1px stroke on a 16px badge. - * (Current) Solution: - * - Use invisible base wrapper and set height to 50% to get the 1/2 height measurement. - * - Scale content to a scale ratio based on the container size's height. - * - Set borderWidth to scale with given Network Icon size (always given with default). - */ - const badgeToContentScaleRatio = 0.5; - const borderWidthRatio = 1 / 16; - const borderWidth = Number(size) * borderWidthRatio; - const currentAvatarSizes = Object.values(AvatarSize).map((avatarSize) => - Number(avatarSize), - ); - const currentSmallestAvatarSize = Math.min(...currentAvatarSizes); - const currentLargestAvatarSize = Math.max(...currentAvatarSizes); - let scaleRatio = 1; + const { style, containerSize, size, isScaled } = vars; + let opacity = 0; if (containerSize) { - scaleRatio = containerSize.height / Number(size); // This is so that the BadgeNetwork won't be visible until a containerSize is known opacity = 1; } - return StyleSheet.create({ - base: { - minHeight: currentSmallestAvatarSize * badgeToContentScaleRatio, - maxHeight: currentLargestAvatarSize * badgeToContentScaleRatio, - height: `${(badgeToContentScaleRatio * 100).toString()}%`, - aspectRatio: 1, + let baseStyles = {}; + let networkIconStyles = {}; + + if (isScaled) { + const scaledStyles = getScaledStyles(Number(size), containerSize); + baseStyles = { alignItems: 'center', justifyContent: 'center', + minHeight: scaledStyles.minHeight, + maxHeight: scaledStyles.maxHeight, + height: scaledStyles.height, + aspectRatio: 1, opacity, - }, + }; + networkIconStyles = { + transform: [{ scale: scaledStyles.scaleRatio }], + borderWidth: scaledStyles.borderWidth, + borderColor: theme.colors.background.default, + ...theme.shadows.size.xs, + }; + } + + return StyleSheet.create({ + base: baseStyles, networkIcon: Object.assign( + isScaled ? networkIconStyles : {}, { - /** - * This is to make sure scale the Network Icon. - * If the BadgeNetwork needs to have style changes specifically with dimensions, - * set transform to [{scale: 1}] first - */ - transform: [{ scale: scaleRatio }], - borderWidth, + borderWidth: Number(size) * (1 / 16), borderColor: theme.colors.background.default, ...theme.shadows.size.xs, } as ViewStyle, diff --git a/app/component-library/components/Badges/Badge/variants/BadgeNetwork/BadgeNetwork.test.tsx b/app/component-library/components/Badges/Badge/variants/BadgeNetwork/BadgeNetwork.test.tsx index 581146add77..5580ba4e2ed 100644 --- a/app/component-library/components/Badges/Badge/variants/BadgeNetwork/BadgeNetwork.test.tsx +++ b/app/component-library/components/Badges/Badge/variants/BadgeNetwork/BadgeNetwork.test.tsx @@ -1,41 +1,82 @@ -// Third party dependencies. +// Third party dependencies import React from 'react'; -import { shallow } from 'enzyme'; +import { View } from 'react-native'; +import { render } from '@testing-library/react-native'; -// External dependencies. -import { - SAMPLE_AVATARNETWORK_NAME, - SAMPLE_AVATARNETWORK_IMAGESOURCE_LOCAL, -} from '../../../../Avatars/Avatar/variants/AvatarNetwork/AvatarNetwork.constants'; +// External dependencies +import { AVATARNETWORK_IMAGE_TESTID } from '../../../../Avatars/Avatar/variants/AvatarNetwork/AvatarNetwork.constants'; +import { AvatarSize } from '../../../../Avatars/Avatar'; -// Internal dependencies. +// Internal dependencies import BadgeNetwork from './BadgeNetwork'; -import { BADGE_NETWORK_TEST_ID } from './BadgeNetwork.constants'; +import { + BADGENETWORK_TEST_ID, + SAMPLE_BADGENETWORK_PROPS, +} from './BadgeNetwork.constants'; +import getScaledStyles from './BadgeNetwork.utils'; + +describe('BadgeNetwork', () => { + const renderComponent = (props = {}) => + render(); + + it('should render BadgeNetwork', () => { + const { toJSON, queryByTestId } = renderComponent(); + expect(toJSON()).toMatchSnapshot(); + expect(queryByTestId(BADGENETWORK_TEST_ID)).not.toBe(null); + }); -describe('BadgeNetwork - snapshots', () => { - it('should render badge network correctly', () => { - const wrapper = shallow( - , + it('should render with correct image source', () => { + const { getByTestId } = renderComponent(); + const imgElement = getByTestId(AVATARNETWORK_IMAGE_TESTID); + expect(imgElement.props.source).toEqual( + SAMPLE_BADGENETWORK_PROPS.imageSource, ); - expect(wrapper).toMatchSnapshot(); }); -}); -describe('BadgeNetwork', () => { - it('should render badge network with the given content', () => { - const wrapper = shallow( - , + it('should apply scaled style only when isScaled is true', () => { + const containerSize = { + height: 32, + width: 32, + }; + const sampleSize = AvatarSize.Md; + const { getByTestId } = render( + + + , + ); + const scaledStyled = getScaledStyles(Number(sampleSize), containerSize); + const badgeNetworkElement = getByTestId(BADGENETWORK_TEST_ID); + expect(badgeNetworkElement.props.style.minHeight).toBe( + scaledStyled.minHeight, ); + expect(badgeNetworkElement.props.style.maxHeight).toBe( + scaledStyled.maxHeight, + ); + expect(badgeNetworkElement.props.style.height).toBe(scaledStyled.height); + }); - const contentElement = wrapper.findWhere( - (node) => node.prop('testID') === BADGE_NETWORK_TEST_ID, + it('should not apply scaled style when isScaled is false', () => { + const containerSize = { + height: 32, + width: 32, + }; + const sampleSize = AvatarSize.Md; + const { getByTestId } = render( + + + , ); - expect(contentElement.exists()).toBe(true); + const badgeNetworkElement = getByTestId(BADGENETWORK_TEST_ID); + expect(badgeNetworkElement.props.style.minHeight).not.toBeDefined(); + expect(badgeNetworkElement.props.style.maxHeight).not.toBeDefined(); + expect(badgeNetworkElement.props.style.height).not.toBeDefined(); }); }); diff --git a/app/component-library/components/Badges/Badge/variants/BadgeNetwork/BadgeNetwork.tsx b/app/component-library/components/Badges/Badge/variants/BadgeNetwork/BadgeNetwork.tsx index 9b2342d5818..52cf608b7fd 100644 --- a/app/component-library/components/Badges/Badge/variants/BadgeNetwork/BadgeNetwork.tsx +++ b/app/component-library/components/Badges/Badge/variants/BadgeNetwork/BadgeNetwork.tsx @@ -12,7 +12,7 @@ import Avatar, { AvatarVariant } from '../../../../Avatars/Avatar'; import { BadgeNetworkProps } from './BadgeNetwork.types'; import styleSheet from './BadgeNetwork.styles'; import { - BADGE_NETWORK_TEST_ID, + BADGENETWORK_TEST_ID, DEFAULT_BADGENETWORK_NETWORKICON_SIZE, } from './BadgeNetwork.constants'; @@ -21,6 +21,7 @@ const BadgeNetwork = ({ name, imageSource, size = DEFAULT_BADGENETWORK_NETWORKICON_SIZE, + isScaled = true, }: BadgeNetworkProps) => { const { size: containerSize, onLayout: onLayoutContainerSize } = useComponentSize(); @@ -28,11 +29,12 @@ const BadgeNetwork = ({ style, containerSize, size, + isScaled, }); return ( , - AvatarNetworkProps {} + AvatarNetworkProps { + /** + * Optional prop to control whether the Badge should be scaled to the content. + * @default true + */ + isScaled?: boolean; +} /** * Style sheet BadgeNetwork parameters. */ export type BadgeNetworkStyleSheetVars = Pick< BadgeNetworkProps, - 'style' | 'size' + 'style' | 'size' | 'isScaled' > & { containerSize: { width: number; height: number } | null; }; diff --git a/app/component-library/components/Badges/Badge/variants/BadgeNetwork/BadgeNetwork.utils.ts b/app/component-library/components/Badges/Badge/variants/BadgeNetwork/BadgeNetwork.utils.ts new file mode 100644 index 00000000000..54adbd320d6 --- /dev/null +++ b/app/component-library/components/Badges/Badge/variants/BadgeNetwork/BadgeNetwork.utils.ts @@ -0,0 +1,42 @@ +import { AvatarSize } from '../../../../Avatars/Avatar'; + +const getScaledStyles = ( + size: number, + containerSize: { width: number; height: number } | null, +) => { + /** + * Design Requirements for Scaled BadgeNetwork: + * - The Network Badge needs to be 1/2 the height of its content, up to 1/2 the height + * of the largest AvatarSize. + * - It needs to have a 1px stroke on a 16px badge. + * (Current) Solution: + * - Use invisible base wrapper and set height to 50% to get the 1/2 height measurement. + * - Scale content to a scale ratio based on the container size's height. + * - Set borderWidth to scale with given Network Icon size (always given with default). + */ + const badgeToContentScaleRatio = 0.5; + const borderWidthRatio = 1 / 16; + const borderWidth = Number(size) * borderWidthRatio; + + const currentAvatarSizes = Object.values(AvatarSize).map((avatarSize) => + Number(avatarSize), + ); + const smallestAvatarSize = Math.min(...currentAvatarSizes); + const largestAvatarSize = Math.max(...currentAvatarSizes); + + let scaleRatio = 1; + + if (containerSize) { + scaleRatio = containerSize.height / Number(size); + } + + return { + minHeight: smallestAvatarSize * badgeToContentScaleRatio, + maxHeight: largestAvatarSize * badgeToContentScaleRatio, + height: `${(badgeToContentScaleRatio * 100).toString()}%`, + scaleRatio, + borderWidth, + }; +}; + +export default getScaledStyles; diff --git a/app/component-library/components/Badges/Badge/variants/BadgeNetwork/README.md b/app/component-library/components/Badges/Badge/variants/BadgeNetwork/README.md index 39b019216cb..c0a1303c251 100644 --- a/app/component-library/components/Badges/Badge/variants/BadgeNetwork/README.md +++ b/app/component-library/components/Badges/Badge/variants/BadgeNetwork/README.md @@ -21,8 +21,16 @@ Optional prop for name of the network. Optional prop to control the image source of the network from either a local or remote source. | TYPE | REQUIRED | -| :-------------------------------------------------------------------- | :------------------------------------------------------ | -| [ImageSourcePropType](https://reactnative.dev/docs/image#imagesource) | Yes | | +| :-------------------------------------------------------------------- | :------------------------------------------------------ | --- | +| [ImageSourcePropType](https://reactnative.dev/docs/image#imagesource) | Yes | | + +### `isScaled` + +Optional prop to configure the checked state. + +| TYPE | REQUIRED | DEFAULT | +| :-------------------------------------------------- | :------------------------------------------------------ | :----------------------------------------------------- | +| boolean | No | false | ## Usage @@ -31,5 +39,5 @@ Optional prop to control the image source of the network from either a local or variant={BadgeVariant.Network} name={NETWORK_NAME} imageSource={NETWORK_IMAGE_SOURCE} -/>; +/> ``` diff --git a/app/component-library/components/Badges/Badge/variants/BadgeNetwork/__snapshots__/BadgeNetwork.test.tsx.snap b/app/component-library/components/Badges/Badge/variants/BadgeNetwork/__snapshots__/BadgeNetwork.test.tsx.snap index dac86cbb2b9..0b9cce8ca0d 100644 --- a/app/component-library/components/Badges/Badge/variants/BadgeNetwork/__snapshots__/BadgeNetwork.test.tsx.snap +++ b/app/component-library/components/Badges/Badge/variants/BadgeNetwork/__snapshots__/BadgeNetwork.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`BadgeNetwork - snapshots should render badge network correctly 1`] = ` - - - + > + + + `; diff --git a/app/component-library/components/Badges/BadgeWrapper/__snapshots__/BadgeWrapper.test.tsx.snap b/app/component-library/components/Badges/BadgeWrapper/__snapshots__/BadgeWrapper.test.tsx.snap index f618c0d96ee..4aefce564c0 100644 --- a/app/component-library/components/Badges/BadgeWrapper/__snapshots__/BadgeWrapper.test.tsx.snap +++ b/app/component-library/components/Badges/BadgeWrapper/__snapshots__/BadgeWrapper.test.tsx.snap @@ -60,6 +60,7 @@ exports[`BadgeWrapper should render BadgeWrapper correctly 1`] = ` }, } } + isScaled={true} name="Ethereum" variant="network" /> diff --git a/app/component-library/components/Modals/ModalMandatory/ModalMandatory.tsx b/app/component-library/components/Modals/ModalMandatory/ModalMandatory.tsx index d4b38eda8c7..1ac25e15493 100644 --- a/app/component-library/components/Modals/ModalMandatory/ModalMandatory.tsx +++ b/app/component-library/components/Modals/ModalMandatory/ModalMandatory.tsx @@ -8,7 +8,7 @@ import { TouchableOpacity, View, } from 'react-native'; -import { WebView } from 'react-native-webview'; +import { WebView } from '@metamask/react-native-webview'; // External dependencies. import ButtonPrimary from '../../Buttons/Button/variants/ButtonPrimary'; diff --git a/app/component-library/components/Navigation/TabBar/TabBar.stories.tsx b/app/component-library/components/Navigation/TabBar/TabBar.stories.tsx index 50389c64ea7..55874e85b0a 100644 --- a/app/component-library/components/Navigation/TabBar/TabBar.stories.tsx +++ b/app/component-library/components/Navigation/TabBar/TabBar.stories.tsx @@ -7,7 +7,7 @@ import { createStore } from 'redux'; import { Provider } from 'react-redux'; // External dependencies. -import initialBackgroundState from '../../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../../util/test/initial-root-state'; // Internal dependencies. import { default as TabBarComponent } from './TabBar'; @@ -18,7 +18,7 @@ const mockInitialState = { step: 1, }, engine: { - backgroundState: initialBackgroundState, + backgroundState, }, }; const rootReducer = (state = mockInitialState) => state; diff --git a/app/component-library/components/Navigation/TabBar/TabBar.test.tsx b/app/component-library/components/Navigation/TabBar/TabBar.test.tsx index e009d59d2bf..0d9ee3d09f9 100644 --- a/app/component-library/components/Navigation/TabBar/TabBar.test.tsx +++ b/app/component-library/components/Navigation/TabBar/TabBar.test.tsx @@ -5,7 +5,7 @@ import { ParamListBase, TabNavigationState } from '@react-navigation/native'; // External dependencies import renderWithProvider from '../../../../util/test/renderWithProvider'; -import initialBackgroundState from '../../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../../util/test/initial-root-state'; // Internal dependencies import TabBar from './TabBar'; @@ -22,7 +22,7 @@ const mockInitialState = { step: 1, }, engine: { - backgroundState: initialBackgroundState, + backgroundState, }, }; diff --git a/app/component-library/components/Pickers/PickerAccount/PickerAccount.tsx b/app/component-library/components/Pickers/PickerAccount/PickerAccount.tsx index 2f6ee293579..29fdaffa08f 100644 --- a/app/component-library/components/Pickers/PickerAccount/PickerAccount.tsx +++ b/app/component-library/components/Pickers/PickerAccount/PickerAccount.tsx @@ -2,7 +2,7 @@ // Third party dependencies. import React, { forwardRef } from 'react'; -import { Platform, TouchableOpacity, View } from 'react-native'; +import { TouchableOpacity, View } from 'react-native'; // External dependencies. import Avatar, { AvatarSize, AvatarVariant } from '../../Avatars/Avatar'; @@ -15,8 +15,7 @@ import { strings } from '../../../../../locales/i18n'; import PickerBase from '../PickerBase'; import { PickerAccountProps } from './PickerAccount.types'; import styleSheet from './PickerAccount.styles'; -import generateTestId from '../../../../../wdio/utils/generateTestId'; -import { WALLET_ACCOUNT_NAME_LABEL_TEXT } from '../../../../../wdio/screen-objects/testIDs/Screens/WalletView.testIds'; +import { WalletViewSelectorsIDs } from '../../../../../e2e/selectors/wallet/WalletView.selectors'; const PickerAccount: React.ForwardRefRenderFunction< TouchableOpacity, @@ -52,7 +51,7 @@ const PickerAccount: React.ForwardRefRenderFunction< {accountName} diff --git a/app/component-library/components/Pickers/PickerNetwork/PickerNetwork.tsx b/app/component-library/components/Pickers/PickerNetwork/PickerNetwork.tsx index 586c53d09fb..9c4dd1f150a 100644 --- a/app/component-library/components/Pickers/PickerNetwork/PickerNetwork.tsx +++ b/app/component-library/components/Pickers/PickerNetwork/PickerNetwork.tsx @@ -2,7 +2,7 @@ // Third party dependencies. import React from 'react'; -import { Platform, TouchableOpacity } from 'react-native'; +import { TouchableOpacity } from 'react-native'; // External dependencies. import Avatar, { AvatarSize, AvatarVariant } from '../../Avatars/Avatar'; @@ -13,8 +13,7 @@ import { useStyles } from '../../../hooks'; // Internal dependencies. import { PickerNetworkProps } from './PickerNetwork.types'; import stylesheet from './PickerNetwork.styles'; -import generateTestId from '../../../../../wdio/utils/generateTestId'; -import { NAVBAR_NETWORK_TEXT } from '../../../../../wdio/screen-objects/testIDs/Screens/WalletView.testIds'; +import { WalletViewSelectorsIDs } from '../../../../../e2e/selectors/wallet/WalletView.selectors'; import { PICKERNETWORK_ARROW_TESTID } from './PickerNetwork.constants'; const PickerNetwork = ({ @@ -38,7 +37,7 @@ const PickerNetwork = ({ style={styles.label} numberOfLines={1} variant={TextVariant.BodyMD} - {...generateTestId(Platform, NAVBAR_NETWORK_TEXT)} + testID={WalletViewSelectorsIDs.NAVBAR_NETWORK_TEXT} > {label} diff --git a/app/component-library/components/Toast/Toast.tsx b/app/component-library/components/Toast/Toast.tsx index e4a37893279..b7916a32b93 100644 --- a/app/component-library/components/Toast/Toast.tsx +++ b/app/component-library/components/Toast/Toast.tsx @@ -10,7 +10,6 @@ import React, { import { Dimensions, LayoutChangeEvent, - Platform, StyleProp, View, ViewStyle, @@ -39,8 +38,7 @@ import { ToastVariants, } from './Toast.types'; import styles from './Toast.styles'; -import generateTestId from '../../../../wdio/utils/generateTestId'; -import { TOAST_ID } from '../../../../wdio/screen-objects/testIDs/Common.testIds'; +import { ToastSelectorsIDs } from '../../../../e2e/selectors/Modals/ToastModal.selectors'; import { ButtonProps } from '../Buttons/Button/Button.types'; const visibilityDuration = 2750; @@ -210,7 +208,7 @@ const Toast = forwardRef((_, ref: React.ForwardedRef) => { {renderAvatar()} {renderLabel(labelOptions)} {renderButtonLink(linkButtonOptions)} diff --git a/app/components/Approvals/InstallSnapApproval/components/InstallSnapPermissionsRequest/test/InstallSnapPermissionsRequest.test.tsx b/app/components/Approvals/InstallSnapApproval/components/InstallSnapPermissionsRequest/test/InstallSnapPermissionsRequest.test.tsx index 3210c026195..556d7fb5681 100644 --- a/app/components/Approvals/InstallSnapApproval/components/InstallSnapPermissionsRequest/test/InstallSnapPermissionsRequest.test.tsx +++ b/app/components/Approvals/InstallSnapApproval/components/InstallSnapPermissionsRequest/test/InstallSnapPermissionsRequest.test.tsx @@ -52,6 +52,7 @@ describe('InstallSnapPermissionsRequest', () => { approvalRequest={installSnapDataApprovalRequest} onConfirm={onConfirm} onCancel={onCancel} + snapId="mockId" />, ); const permissionCells = getAllByTestId(SNAP_PERMISSION_CELL); diff --git a/app/components/Approvals/PermissionApproval/PermissionApproval.test.tsx b/app/components/Approvals/PermissionApproval/PermissionApproval.test.tsx index 3333b2b3501..fd84b0f9915 100644 --- a/app/components/Approvals/PermissionApproval/PermissionApproval.test.tsx +++ b/app/components/Approvals/PermissionApproval/PermissionApproval.test.tsx @@ -6,7 +6,7 @@ import PermissionApproval from './PermissionApproval'; import { createAccountConnectNavDetails } from '../../Views/AccountConnect'; import { useSelector } from 'react-redux'; import { MetaMetricsEvents } from '../../../core/Analytics'; -import initialBackgroundState from '../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../util/test/initial-root-state'; import { render } from '@testing-library/react-native'; import { useMetrics } from '../../../components/hooks/useMetrics'; @@ -129,7 +129,7 @@ describe('PermissionApproval', () => { mockSelectorState({ engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, AccountTrackerController: { accounts: { 1: 'testAccount', diff --git a/app/components/Base/Alert.stories.tsx b/app/components/Base/Alert.stories.tsx index 6c856a69f9d..e792fae8e35 100644 --- a/app/components/Base/Alert.stories.tsx +++ b/app/components/Base/Alert.stories.tsx @@ -28,7 +28,9 @@ storiesOf('Components / Base / Alert', module) small={boolean('small', false)} renderIcon={ renderIconKnob - ? () => + ? //@ts-expect-error needs to be replaced by Icon of component library, EvilIcons are deprecated + // All this component is deprecated so it should be replaced and removed + () => : () => null } onPress={action('onPress')} diff --git a/app/components/Base/Alert.tsx b/app/components/Base/Alert.tsx index 56c46c7e226..5b2fd5f5abc 100644 --- a/app/components/Base/Alert.tsx +++ b/app/components/Base/Alert.tsx @@ -141,7 +141,11 @@ const Alert = ({ onPress={onDismiss} hitSlop={{ top: 20, left: 20, right: 20, bottom: 20 }} > - + { + //@ts-expect-error needs to be replaced by Icon of component library, IonicIcon are deprecated + // All this component is deprecated so it should be replaced and removed + + } )} diff --git a/app/components/Base/AnimatedFox/__snapshots__/index.test.tsx.snap b/app/components/Base/AnimatedFox/__snapshots__/index.test.tsx.snap new file mode 100644 index 00000000000..b00de69953c --- /dev/null +++ b/app/components/Base/AnimatedFox/__snapshots__/index.test.tsx.snap @@ -0,0 +1,1511 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AnimatedFox renders correctly and matches snapshot 1`] = ` + + + + + --- + + + +
+ + + + ", + } + } + style={ + { + "flex": 1, + } + } +/> +`; diff --git a/app/components/Base/AnimatedFox/index.test.tsx b/app/components/Base/AnimatedFox/index.test.tsx new file mode 100644 index 00000000000..31d3fde17b3 --- /dev/null +++ b/app/components/Base/AnimatedFox/index.test.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { render } from '@testing-library/react-native'; +import AnimatedFox from './'; +import { getTotalMemorySync } from 'react-native-device-info'; + +jest.mock('react-native-device-info', () => ({ + getTotalMemorySync: jest.fn(), +})); + +jest.mock('react-native-sensors', () => ({ + gyroscope: { + subscribe: jest.fn(({ next }) => { + next({ x: 1, y: 2 }); + + return { unsubscribe: jest.fn() }; + }), + }, + setUpdateIntervalForType: jest.fn(), + SensorTypes: { + gyroscope: 'gyroscope', + }, +})); + +describe('AnimatedFox', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + it('renders correctly and matches snapshot', () => { + // Mock device memory to ensure consistent environment for snapshot + (getTotalMemorySync as jest.Mock).mockReturnValueOnce( + 3 * 1024 * 1024 * 1024, + ); // Mock 3GB device + + const { toJSON } = render(); + expect(toJSON()).toMatchSnapshot(); + }); +}); diff --git a/app/components/Base/AnimatedFox/index.tsx b/app/components/Base/AnimatedFox/index.tsx new file mode 100644 index 00000000000..7fbc6749588 --- /dev/null +++ b/app/components/Base/AnimatedFox/index.tsx @@ -0,0 +1,1581 @@ +/* eslint-disable react/prop-types */ +import React, { useEffect, useRef } from 'react'; +import { WebView } from '@metamask/react-native-webview'; +import { + gyroscope, + SensorTypes, + setUpdateIntervalForType, +} from 'react-native-sensors'; +import { getTotalMemorySync } from 'react-native-device-info'; + +interface AnimatedFoxProps { + bgColor: string; +} +const round = (value: number, decimals: number): number => + Number(Math.round(Number(value + 'e' + decimals)) + 'e-' + decimals); + +const styles = { flex: 1 }; + +const AnimatedFox: React.FC = ({ bgColor }) => { + const webviewRef = useRef(null); + const position = useRef({ beta: 0, gamma: 0 }); + + /** + * If a device have lower than 2GB Ram we consider a low end device + * @returns boolean + */ + const isLowEndDevice = () => { + // Total device memory in bytes. + const totalMemory = getTotalMemorySync(); + const oneGigaByte = 1024 * 1024 * 1024; + return totalMemory <= 2 * oneGigaByte; + }; + + useEffect(() => { + const updateInterval = isLowEndDevice() ? 1000 / 30 : 1000 / 60; // 30Hz for low-end, 60Hz for others. + setUpdateIntervalForType(SensorTypes.gyroscope, updateInterval); + + const subscription = gyroscope.subscribe({ + next: ({ x, y }) => { + position.current = { + beta: position.current.beta - round(x * -10, 4), + gamma: position.current.gamma - round(y * -10, 4), + }; + + requestAnimationFrame(() => { + const JS = ` + (function () { + const event = new CustomEvent('nativedeviceorientation', { + detail: { + beta:${position.current.beta}, + gamma:${position.current.gamma} + } + }); + + window.dispatchEvent(event); + })(); + `; + webviewRef.current?.injectJavaScript(JS); + }); + }, + error: () => { + // gyroscope is not available + }, + }); + + return () => { + subscription.unsubscribe(); + }; + }, []); + + return ( + + + + + --- + + + +
+ + + + `, + }} + javaScriptEnabled + bounces={false} + scrollEnabled={false} + injectedJavaScript={`document.body.style.background="${bgColor}"`} + /> + ); +}; + +AnimatedFox.defaultProps = { + bgColor: 'white', +}; + +export default AnimatedFox; diff --git a/app/components/Base/RemoteImage/index.js b/app/components/Base/RemoteImage/index.js index 53aba49ae0d..f47269f60ce 100644 --- a/app/components/Base/RemoteImage/index.js +++ b/app/components/Base/RemoteImage/index.js @@ -10,6 +10,7 @@ import ComponentErrorBoundary from '../../UI/ComponentErrorBoundary'; import useIpfsGateway from '../../hooks/useIpfsGateway'; import { getFormattedIpfsUrl } from '@metamask/assets-controllers'; import Identicon from '../../UI/Identicon'; +import useSvgUriViewBox from '../../hooks/useSvgUriViewBox'; const createStyles = () => StyleSheet.create({ @@ -40,16 +41,19 @@ const RemoteImage = (props) => { const onError = ({ nativeEvent: { error } }) => setError(error); + const isSVG = + source && + source.uri && + source.uri.match('.svg') && + (isImageUrl || resolvedIpfsUrl); + + const viewbox = useSvgUriViewBox(uri, isSVG); + if (error && props.address) { return ; } - if ( - source && - source.uri && - source.uri.match('.svg') && - (isImageUrl || resolvedIpfsUrl) - ) { + if (isSVG) { const style = props.style || {}; if (source.__packager_asset && typeof style !== 'number') { if (!style.width) { @@ -66,7 +70,13 @@ const RemoteImage = (props) => { componentLabel="RemoteImage-SVG" > - + ); diff --git a/app/components/Nav/App/index.js b/app/components/Nav/App/index.js index f0316226cd3..04af644e3f6 100644 --- a/app/components/Nav/App/index.js +++ b/app/components/Nav/App/index.js @@ -6,7 +6,13 @@ import React, { useState, } from 'react'; import { CommonActions, NavigationContainer } from '@react-navigation/native'; -import { Animated, Linking } from 'react-native'; +import { + Animated, + Linking, + ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) + View, + ///: END:ONLY_INCLUDE_IF +} from 'react-native'; import { createStackNavigator } from '@react-navigation/stack'; import Login from '../../Views/Login'; import QRScanner from '../../Views/QRScanner'; @@ -44,7 +50,6 @@ import { } from '../../../actions/navigation'; import { findRouteNameFromNavigatorState } from '../../../util/general'; import { Authentication } from '../../../core/'; -import { isBlockaidFeatureEnabled } from '../../../util/blockaid'; import { useTheme } from '../../../util/theme'; import Device from '../../../util/device'; import SDKConnect from '../../../core/SDKConnect/SDKConnect'; @@ -67,8 +72,6 @@ import ImportPrivateKey from '../../Views/ImportPrivateKey'; import ImportPrivateKeySuccess from '../../Views/ImportPrivateKeySuccess'; import ConnectQRHardware from '../../Views/ConnectQRHardware'; import SelectHardwareWallet from '../../Views/ConnectHardware/SelectHardware'; -import LedgerAccountInfo from '../../Views/LedgerAccountInfo'; -import LedgerConnect from '../../Views/LedgerConnect'; import { AUTHENTICATION_APP_TRIGGERED_AUTH_NO_CREDENTIALS } from '../../../constants/error'; import { UpdateNeeded } from '../../../components/UI/UpdateNeeded'; import { EnableAutomaticSecurityChecksModal } from '../../../components/UI/EnableAutomaticSecurityChecksModal'; @@ -106,11 +109,15 @@ import { MetaMetrics } from '../../../core/Analytics'; import trackErrorAsAnalytics from '../../../util/metrics/TrackError/trackErrorAsAnalytics'; import generateDeviceAnalyticsMetaData from '../../../util/metrics/DeviceAnalyticsMetaData/generateDeviceAnalyticsMetaData'; import generateUserSettingsAnalyticsMetaData from '../../../util/metrics/UserSettingsAnalyticsMetaData/generateUserProfileAnalyticsMetaData'; +import LedgerSelectAccount from '../../Views/LedgerSelectAccount'; import OnboardingSuccess from '../../Views/OnboardingSuccess'; import DefaultSettings from '../../Views/OnboardingSuccess/DefaultSettings'; import BasicFunctionalityModal from '../../UI/BasicFunctionality/BasicFunctionalityModal/BasicFunctionalityModal'; import SmartTransactionsOptInModal from '../../Views/SmartTransactionsOptInModal/SmartTranactionsOptInModal'; import NFTAutoDetectionModal from '../../../../app/components/Views/NFTAutoDetectionModal/NFTAutoDetectionModal'; +///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) +import { SnapsExecutionWebView } from '../../../lib/snaps'; +///: END:ONLY_INCLUDE_IF const clearStackNavigatorOptions = { headerShown: false, @@ -465,6 +472,7 @@ const App = ({ userLoggedIn }) => { } } } + initSDKConnect() .then(() => { queueOfHandleDeeplinkFunctions.current.forEach((func) => func()); @@ -733,8 +741,16 @@ const App = ({ userLoggedIn }) => { ); const LedgerConnectFlow = () => ( - - + + ); @@ -745,7 +761,6 @@ const App = ({ userLoggedIn }) => { component={SelectHardwareWallet} options={SelectHardwareWallet.navigationOptions} /> - ); @@ -771,7 +786,16 @@ const App = ({ userLoggedIn }) => { // do not render unless a route is defined (route && ( <> - {isBlockaidFeatureEnabled() && } + { + ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) + } + + + + { + ///: END:ONLY_INCLUDE_IF + } + ( /* eslint-disable react/prop-types */ const NotificationsModeView = (props) => ( - - { ) : ( renderLoader() )} - { - ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) - } - - - - { - ///: END:ONLY_INCLUDE_IF - } diff --git a/app/components/UI/AccountApproval/__snapshots__/index.test.tsx.snap b/app/components/UI/AccountApproval/__snapshots__/index.test.tsx.snap index 9864f17e99f..c5704972d10 100644 --- a/app/components/UI/AccountApproval/__snapshots__/index.test.tsx.snap +++ b/app/components/UI/AccountApproval/__snapshots__/index.test.tsx.snap @@ -334,7 +334,7 @@ exports[`AccountApproval should render correctly 1`] = ` } } > - 0xC496...a756 + Account 2 { + const { currentPageInformation, chainId, selectedAddress, accountsLength } = + this.props; + let urlHostName = 'N/A'; + try { - const { - currentPageInformation, - chainId, - selectedAddress, - accountsLength, - } = this.props; - const url = new URL(currentPageInformation?.url); - return { - account_type: getAddressAccountType(selectedAddress), - dapp_host_name: url?.host, - chain_id: getDecimalChainId(chainId), - number_of_accounts: accountsLength, - number_of_accounts_connected: 1, - source: 'SDK / WalletConnect', - ...currentPageInformation?.analytics, - }; + if (currentPageInformation?.url) { + const url = new URL(currentPageInformation.url); + urlHostName = url.host; + } } catch (error) { - return {}; + console.error('URL conversion error:', error); } + + return { + account_type: selectedAddress + ? getAddressAccountType(selectedAddress) + : null, + dapp_host_name: urlHostName, + chain_id: chainId ? getDecimalChainId(chainId) : null, + number_of_accounts: accountsLength, + number_of_accounts_connected: 1, + source: 'SDK / WalletConnect', + ...currentPageInformation?.analytics, + }; }; componentDidMount = () => { diff --git a/app/components/UI/AccountApproval/index.test.tsx b/app/components/UI/AccountApproval/index.test.tsx index 75fc09d5abc..231845fa1f8 100644 --- a/app/components/UI/AccountApproval/index.test.tsx +++ b/app/components/UI/AccountApproval/index.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import AccountApproval from '.'; -import initialBackgroundState from '../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../util/test/initial-root-state'; import renderWithProvider from '../../../util/test/renderWithProvider'; import { MOCK_ACCOUNTS_CONTROLLER_STATE } from '../../../util/test/accountsControllerTestUtils'; @@ -18,7 +18,7 @@ jest.mock('../../../core/Engine', () => ({ state: { keyrings: [ { - accounts: ['0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272'], + accounts: ['0xC4966c0D659D99699BFD7EB54D8fafEE40e4a756'], }, ], }, @@ -29,8 +29,17 @@ jest.mock('../../../core/Engine', () => ({ const mockInitialState = { engine: { backgroundState: { - ...initialBackgroundState, - AccountsController: MOCK_ACCOUNTS_CONTROLLER_STATE, + ...backgroundState, + AccountsController: { + ...MOCK_ACCOUNTS_CONTROLLER_STATE, + accounts: { + '0xC4966c0D659D99699BFD7EB54D8fafEE40e4a756': { + balance: '0x0', + name: 'Account 1', + address: '0xC4966c0D659D99699BFD7EB54D8fafEE40e4a756', + }, + }, + }, }, }, }; diff --git a/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.test.tsx b/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.test.tsx index 5f9914e8d3e..eb71e64fec7 100644 --- a/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.test.tsx +++ b/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.test.tsx @@ -8,7 +8,7 @@ import { ENSCache } from '../../../util/ENSUtils'; import { Transaction } from './AccountFromToInfoCard.types'; import AccountFromToInfoCard from '.'; import Engine from '../../../core/Engine'; -import initialBackgroundState from '../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../util/test/initial-root-state'; import { createMockAccountsControllerState } from '../../../util/test/accountsControllerTestUtils'; const MOCK_ADDRESS_1 = '0xe64dD0AB5ad7e8C5F2bf6Ce75C34e187af8b920A'; @@ -23,7 +23,7 @@ const mockInitialState = { settings: {}, engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, AccountTrackerController: { accounts: { [MOCK_ADDRESS_1]: { @@ -39,19 +39,6 @@ const mockInitialState = { '0x326836cc6cd09B5aa59B81A7F72F25FcC0136b95': '0x5', }, }, - PreferencesController: { - selectedAddress: MOCK_ADDRESS_1, - identities: { - [MOCK_ADDRESS_1]: { - address: MOCK_ADDRESS_1, - name: 'Account 1', - }, - [MOCK_ADDRESS_2]: { - address: MOCK_ADDRESS_2, - name: 'Account 2', - }, - }, - }, AccountsController: MOCK_ACCOUNTS_CONTROLLER_STATE, }, }, diff --git a/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.tsx b/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.tsx index f567bfee7b7..599042c9476 100644 --- a/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.tsx +++ b/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.tsx @@ -9,7 +9,6 @@ import { selectChainId, selectTicker, } from '../../../selectors/networkController'; -import { selectIdentities } from '../../../selectors/preferencesController'; import { collectConfusables } from '../../../util/confusables'; import { decodeTransferData } from '../../../util/transactions'; import { doENSReverseLookup } from '../../../util/ENSUtils'; @@ -20,10 +19,13 @@ import useExistingAddress from '../../hooks/useExistingAddress'; import { AddressFrom, AddressTo } from '../AddressInputs'; import createStyles from './AccountFromToInfoCard.styles'; import { AccountFromToInfoCardProps } from './AccountFromToInfoCard.types'; +import { selectInternalAccounts } from '../../../selectors/accountsController'; +import { toLowerCaseEquals } from '../../../util/general'; +import { RootState } from '../../../reducers'; const AccountFromToInfoCard = (props: AccountFromToInfoCardProps) => { const { - identities, + internalAccounts, chainId, onPressFromAddressIcon, ticker, @@ -33,7 +35,6 @@ const AccountFromToInfoCard = (props: AccountFromToInfoCardProps) => { const { transaction: { from: rawFromAddress, data, to }, transactionTo, - transactionToName, transactionFromName, selectedAsset, ensRecipient, @@ -56,50 +57,88 @@ const AccountFromToInfoCard = (props: AccountFromToInfoCardProps) => { ); useEffect(() => { - if (!fromAddress) { - return; - } - if (transactionFromName) { - setFromAccountName(transactionFromName); - return; - } - (async () => { + const fetchFromAccountDetails = async () => { + if (!fromAddress) { + return; + } + + if (transactionFromName) { + if (fromAccountName !== transactionFromName) { + setFromAccountName(transactionFromName); + } + return; + } + const fromEns = await doENSReverseLookup(fromAddress, chainId); if (fromEns) { - setFromAccountName(fromEns); + if (fromAccountName !== fromEns) { + setFromAccountName(fromEns); + } } else { - const { name: fromName } = identities[fromAddress]; - setFromAccountName(fromName); + const accountWithMatchingFromAddress = internalAccounts.find( + (account) => toLowerCaseEquals(account.address, fromAddress), + ); + + const newName = accountWithMatchingFromAddress + ? accountWithMatchingFromAddress.metadata.name + : fromAddress; + + if (fromAccountName !== newName) { + setFromAccountName(newName); + } } - })(); - }, [fromAddress, identities, transactionFromName, chainId]); + }; + + fetchFromAccountDetails(); + }, [ + fromAddress, + transactionFromName, + chainId, + internalAccounts, + fromAccountName, + ]); useEffect(() => { - if (existingToAddress) { - setToAccountName(existingToAddress?.name); - return; - } - (async () => { + const fetchAccountDetails = async () => { + if (existingToAddress) { + if (toAccountName !== existingToAddress.name) { + setToAccountName(existingToAddress.name); + } + return; + } + const toEns = await doENSReverseLookup(toAddress, chainId); if (toEns) { - setToAccountName(toEns); - } else if (identities[toAddress]) { - const { name: toName } = identities[toAddress]; - setToAccountName(toName); + if (toAccountName !== toEns) { + setToAccountName(toEns); + } + } else { + const accountWithMatchingToAddress = internalAccounts.find((account) => + toLowerCaseEquals(account.address, toAddress), + ); + + const newName = accountWithMatchingToAddress + ? accountWithMatchingToAddress.metadata.name + : toAddress; + + if (toAccountName !== newName) { + setToAccountName(newName); + } } - })(); - }, [existingToAddress, identities, chainId, toAddress, transactionToName]); + }; + + fetchAccountDetails(); + }, [existingToAddress, chainId, toAddress, internalAccounts, toAccountName]); useEffect(() => { - const accountNames = - (identities && - Object.keys(identities).map((hash) => identities[hash].name)) || - []; + const accountNames = internalAccounts.map( + (account) => account.metadata.name, + ); const isOwnAccount = ensRecipient && accountNames.includes(ensRecipient); if (ensRecipient && !isOwnAccount) { setConfusableCollection(collectConfusables(ensRecipient)); } - }, [identities, ensRecipient]); + }, [internalAccounts, ensRecipient]); useEffect(() => { let toAddr; @@ -172,10 +211,8 @@ const AccountFromToInfoCard = (props: AccountFromToInfoCardProps) => { ); }; -// TODO: Replace "any" with type -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const mapStateToProps = (state: any) => ({ - identities: selectIdentities(state), +const mapStateToProps = (state: RootState) => ({ + internalAccounts: selectInternalAccounts(state), chainId: selectChainId(state), ticker: selectTicker(state), }); diff --git a/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.types.tsx b/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.types.tsx index ece567c7bdd..84690a6c732 100644 --- a/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.types.tsx +++ b/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.types.tsx @@ -1,9 +1,4 @@ -interface Identity { - address: string; - name: string; -} - -type Identities = Record; +import { InternalAccount } from '@metamask/keyring-api'; interface SelectedAsset { isETH: boolean; @@ -26,7 +21,7 @@ export interface Transaction { } export interface AccountFromToInfoCardProps { - identities: Identities; + internalAccounts: InternalAccount[]; chainId: string; onPressFromAddressIcon?: () => void; ticker?: string; diff --git a/app/components/UI/AccountInfoCard/__snapshots__/index.test.tsx.snap b/app/components/UI/AccountInfoCard/__snapshots__/index.test.tsx.snap index b8b3d0ae551..6a4a79370c7 100644 --- a/app/components/UI/AccountInfoCard/__snapshots__/index.test.tsx.snap +++ b/app/components/UI/AccountInfoCard/__snapshots__/index.test.tsx.snap @@ -232,7 +232,7 @@ exports[`AccountInfoCard should match snapshot 1`] = ` } } > - 0xC495...D272 + Account 1 StyleSheet.create({ @@ -111,9 +111,9 @@ class AccountInfoCard extends PureComponent { */ accounts: PropTypes.object, /** - * List of accounts from the PreferencesController + * List of accounts from the AccountsController */ - identities: PropTypes.object, + internalAccounts: PropTypes.array, /** * A number that specifies the ETH/USD conversion rate */ @@ -142,7 +142,7 @@ class AccountInfoCard extends PureComponent { render() { const { accounts, - identities, + internalAccounts, conversionRate, currentCurrency, operation, @@ -162,7 +162,7 @@ class AccountInfoCard extends PureComponent { ? hexToBN(accounts[fromAddress].balance) : 0; const balance = `${renderFromWei(weiBalance)} ${getTicker(ticker)}`; - const accountLabel = renderAccountName(fromAddress, identities); + const accountLabel = renderAccountName(fromAddress, internalAccounts); const address = renderShortAddress(fromAddress); const dollarBalance = showFiatBalance ? weiToFiat(weiBalance, conversionRate, currentCurrency, 2)?.toUpperCase() @@ -248,7 +248,7 @@ class AccountInfoCard extends PureComponent { const mapStateToProps = (state) => ({ accounts: selectAccounts(state), - identities: selectIdentities(state), + internalAccounts: selectInternalAccounts(state), conversionRate: selectConversionRate(state), currentCurrency: selectCurrentCurrency(state), ticker: selectTicker(state), diff --git a/app/components/UI/AccountInfoCard/index.test.tsx b/app/components/UI/AccountInfoCard/index.test.tsx index d8500ab9d38..2462c41d648 100644 --- a/app/components/UI/AccountInfoCard/index.test.tsx +++ b/app/components/UI/AccountInfoCard/index.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import AccountInfoCard from './'; import renderWithProvider from '../../../util/test/renderWithProvider'; -import initialBackgroundState from '../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../util/test/initial-root-state'; import { MOCK_ACCOUNTS_CONTROLLER_STATE, MOCK_ADDRESS_1, @@ -27,7 +27,7 @@ const mockInitialState = { }, engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, AccountTrackerController: { accounts: { [MOCK_ADDRESS_1]: { diff --git a/app/components/UI/AccountOverview/index.js b/app/components/UI/AccountOverview/index.js index c65706e35a0..b844d7ac534 100644 --- a/app/components/UI/AccountOverview/index.js +++ b/app/components/UI/AccountOverview/index.js @@ -2,7 +2,6 @@ import PropTypes from 'prop-types'; import React, { PureComponent } from 'react'; import { InteractionManager, - Platform, ScrollView, StyleSheet, TextInput, @@ -11,12 +10,7 @@ import { } from 'react-native'; import { connect } from 'react-redux'; import { strings } from '../../../../locales/i18n'; -import { - WALLET_ACCOUNT_ICON, - WALLET_ACCOUNT_NAME_LABEL_INPUT, - WALLET_ACCOUNT_NAME_LABEL_TEXT, -} from '../../../../wdio/screen-objects/testIDs/Screens/WalletView.testIds'; -import generateTestId from '../../../../wdio/utils/generateTestId'; +import { WalletViewSelectorsIDs } from '../../../../e2e/selectors/wallet/WalletView.selectors'; import { showAlert } from '../../../actions/alert'; import { toggleReceiveModal } from '../../../actions/modals'; import { newAssetTransaction } from '../../../actions/transaction'; @@ -41,14 +35,17 @@ import AppConstants from '../../../core/AppConstants'; import Engine from '../../../core/Engine'; import { selectChainId } from '../../../selectors/networkController'; import { selectCurrentCurrency } from '../../../selectors/currencyRateController'; -import { selectIdentities } from '../../../selectors/preferencesController'; -import { selectSelectedInternalAccountChecksummedAddress } from '../../../selectors/accountsController'; +import { + selectInternalAccounts, + selectSelectedInternalAccountChecksummedAddress, +} from '../../../selectors/accountsController'; import { createAccountSelectorNavDetails } from '../../Views/AccountSelector'; import Text, { TextVariant, } from '../../../component-library/components/Texts/Text'; import { withMetricsAwareness } from '../../../components/hooks/useMetrics'; import { isPortfolioUrl } from '../../../util/url'; +import { toLowerCaseEquals } from '../../../util/general'; const createStyles = (colors) => StyleSheet.create({ @@ -157,18 +154,14 @@ class AccountOverview extends PureComponent { */ selectedAddress: PropTypes.string, /** - /* Identities object required to get account name + /* InternalAccounts object required to get account name */ - identities: PropTypes.object, + internalAccounts: PropTypes.object, /** * Object that represents the selected account */ account: PropTypes.object, /** - /* Selected currency - */ - currentCurrency: PropTypes.string, - /** /* Triggers global alert */ showAlert: PropTypes.func, @@ -184,19 +177,11 @@ class AccountOverview extends PureComponent { * Prompts protect wallet modal */ protectWalletModalVisible: PropTypes.func, - /** - * Start transaction with asset - */ - newAssetTransaction: PropTypes.func, /** /* navigation object required to access the props /* passed by the parent component */ navigation: PropTypes.object, - /** - * Action that toggles the receive modal - */ - toggleReceiveModal: PropTypes.func, /** * The chain ID for the current selected network */ @@ -234,8 +219,8 @@ class AccountOverview extends PureComponent { input = React.createRef(); componentDidMount = () => { - const { identities, selectedAddress, onRef } = this.props; - const accountLabel = renderAccountName(selectedAddress, identities); + const { internalAccounts, selectedAddress, onRef } = this.props; + const accountLabel = renderAccountName(selectedAddress, internalAccounts); this.setState({ accountLabel }); onRef && onRef(this); InteractionManager.runAfterInteractions(() => { @@ -259,16 +244,18 @@ class AccountOverview extends PureComponent { } setAccountLabel = () => { - const { selectedAddress, identities } = this.props; + const { selectedAddress, internalAccounts } = this.props; const { accountLabel } = this.state; - const lastAccountLabel = identities[selectedAddress].name; + const accountWithMatchingToAddress = internalAccounts.find((account) => + toLowerCaseEquals(account.address, selectedAddress), + ); Engine.setAccountLabel( selectedAddress, this.isAccountLabelDefined(accountLabel) ? accountLabel - : lastAccountLabel, + : accountWithMatchingToAddress.metadata.name, ); this.setState({ accountLabelEditable: false }); }; @@ -278,8 +265,8 @@ class AccountOverview extends PureComponent { }; setAccountLabelEditable = () => { - const { identities, selectedAddress } = this.props; - const accountLabel = renderAccountName(selectedAddress, identities); + const { internalAccounts, selectedAddress } = this.props; + const accountLabel = renderAccountName(selectedAddress, internalAccounts); this.setState({ accountLabelEditable: true, accountLabel }); setTimeout(() => { this.input && this.input.current && this.input.current.focus(); @@ -287,8 +274,8 @@ class AccountOverview extends PureComponent { }; cancelAccountLabelEdition = () => { - const { identities, selectedAddress } = this.props; - const accountLabel = renderAccountName(selectedAddress, identities); + const { internalAccounts, selectedAddress } = this.props; + const accountLabel = renderAccountName(selectedAddress, internalAccounts); this.setState({ accountLabelEditable: false, accountLabel }); }; @@ -368,7 +355,7 @@ class AccountOverview extends PureComponent { style={styles.identiconBorder} disabled={onboardingWizard} onPress={this.openAccountSelector} - {...generateTestId(Platform, WALLET_ACCOUNT_ICON)} + testID={WalletViewSelectorsIDs.ACCOUNT_ICON} > {isDefaultAccountName(name) && ens ? ens : name} @@ -461,7 +445,7 @@ class AccountOverview extends PureComponent { const mapStateToProps = (state) => ({ selectedAddress: selectSelectedInternalAccountChecksummedAddress(state), - identities: selectIdentities(state), + internalAccounts: selectInternalAccounts(state), currentCurrency: selectCurrentCurrency(state), chainId: selectChainId(state), browserTabs: state.browser.tabs, diff --git a/app/components/UI/AccountOverview/index.test.tsx b/app/components/UI/AccountOverview/index.test.tsx index 90d3b464d96..303d3fe177d 100644 --- a/app/components/UI/AccountOverview/index.test.tsx +++ b/app/components/UI/AccountOverview/index.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import renderWithProvider from '../../../util/test/renderWithProvider'; import AccountOverview from './'; -import initialBackgroundState from '../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../util/test/initial-root-state'; import Engine from '../../../core/Engine'; import { @@ -33,7 +33,7 @@ const mockInitialState = { settings: {}, engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, PreferencesController: { selectedAddress: MOCK_ADDRESS_1, }, diff --git a/app/components/UI/AccountRightButton/index.test.tsx b/app/components/UI/AccountRightButton/index.test.tsx index 8d8b409c78f..ce5b1f12c23 100644 --- a/app/components/UI/AccountRightButton/index.test.tsx +++ b/app/components/UI/AccountRightButton/index.test.tsx @@ -1,12 +1,12 @@ import { renderScreen } from '../../../util/test/renderWithProvider'; import AccountRightButton from './'; -import initialBackgroundState from '../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../util/test/initial-root-state'; const mockInitialState = { settings: {}, engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, NetworkController: { providerConfig: { chainId: 0x1, diff --git a/app/components/UI/AccountSelectorList/AccountSelector.test.tsx b/app/components/UI/AccountSelectorList/AccountSelector.test.tsx index 8961cd92278..5b26df8e705 100644 --- a/app/components/UI/AccountSelectorList/AccountSelector.test.tsx +++ b/app/components/UI/AccountSelectorList/AccountSelector.test.tsx @@ -6,7 +6,7 @@ import AccountSelectorList from './AccountSelectorList'; import { useAccounts } from '../../../components/hooks/useAccounts'; import { View } from 'react-native'; import { ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID } from '../../../../wdio/screen-objects/testIDs/Components/AccountListComponent.testIds'; -import initialBackgroundState from '../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../util/test/initial-root-state'; import { regex } from '../../../../app/util/regex'; import { createMockAccountsControllerState } from '../../../util/test/accountsControllerTestUtils'; @@ -29,7 +29,7 @@ jest.mock('../../../util/address', () => { const initialState = { engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, NetworkController: { network: '1', providerConfig: { @@ -48,16 +48,6 @@ const initialState = { PreferencesController: { isMultiAccountBalancesEnabled: true, selectedAddress: BUSINESS_ACCOUNT, - identities: { - [BUSINESS_ACCOUNT]: { - address: BUSINESS_ACCOUNT, - name: 'Business Account', - }, - [PERSONAL_ACCOUNT]: { - address: PERSONAL_ACCOUNT, - name: 'Personal Account', - }, - }, }, CurrencyRateController: { currentCurrency: 'usd', @@ -120,12 +110,12 @@ describe('AccountSelectorList', () => { onRemoveImportedAccount.mockClear(); }); - it('should render correctly', async () => { + it('renders correctly', async () => { const { toJSON } = renderComponent(initialState); await waitFor(() => expect(toJSON()).toMatchSnapshot()); }); - it('should render all accounts with balances', async () => { + it('renders all accounts with balances', async () => { const { queryByTestId, getAllByTestId, toJSON } = renderComponent(initialState); @@ -186,7 +176,7 @@ describe('AccountSelectorList', () => { }); }); - it('should render all accounts with right acessory', async () => { + it('renders all accounts with right accessory', async () => { const { getAllByTestId, toJSON } = renderComponent( initialState, AccountSelectorListRightAccessoryUseAccounts, @@ -199,4 +189,13 @@ describe('AccountSelectorList', () => { expect(toJSON()).toMatchSnapshot(); }); }); + it('renders correct account names', async () => { + const { getAllByTestId } = renderComponent(initialState); + + await waitFor(() => { + const accountNameItems = getAllByTestId('cellbase-avatar-title'); + expect(within(accountNameItems[0]).getByText('Account 1')).toBeDefined(); + expect(within(accountNameItems[1]).getByText('Account 2')).toBeDefined(); + }); + }); }); diff --git a/app/components/UI/AccountSelectorList/__snapshots__/AccountSelector.test.tsx.snap b/app/components/UI/AccountSelectorList/__snapshots__/AccountSelector.test.tsx.snap index 8b7c6c08b72..1e4cbf6b45b 100644 --- a/app/components/UI/AccountSelectorList/__snapshots__/AccountSelector.test.tsx.snap +++ b/app/components/UI/AccountSelectorList/__snapshots__/AccountSelector.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`AccountSelectorList should render all accounts but only the balance for selected account 1`] = ` +exports[`AccountSelectorList renders all accounts with balances 1`] = ` - + > + + + + + + + +
- - - - - Account 2 - - - 0xd018...78E7 - - - - - - - - -`; - -exports[`AccountSelectorList should render all accounts with balances 1`] = ` - + + + + + + + + + + + Account 2 + + + 0xd018...78E7 + + + + + + $6400.00 +2 ETH + + + + + + + + + +`; + +exports[`AccountSelectorList renders all accounts with right accessory 1`] = ` + - - $3200.00 -1 ETH - + 0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272 - Account 1 + + + + - - - - - - - - - $6400.00 -2 ETH - + 0xd018538C87232FF95acbCe4870629b75640a78E7 - Account 2 @@ -937,7 +1139,7 @@ exports[`AccountSelectorList should render all accounts with balances 1`] = ` `; -exports[`AccountSelectorList should render all accounts with right acessory 1`] = ` +exports[`AccountSelectorList renders correctly 1`] = ` - 0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272 - Account 1 + + $3200.00 +1 ETH + + + + - 0xd018538C87232FF95acbCe4870629b75640a78E7 - Account 2 + + $6400.00 +2 ETH + @@ -1473,7 +1742,7 @@ exports[`AccountSelectorList should render all accounts with right acessory 1`] `; -exports[`AccountSelectorList should render correctly 1`] = ` +exports[`AccountSelectorList should render all accounts but only the balance for selected account 1`] = ` - - - - - - - - + } + /> - - - - - - - - + } + /> - - - - $6400.00 -2 ETH - - - diff --git a/app/components/UI/AddCustomCollectible/index.test.tsx b/app/components/UI/AddCustomCollectible/index.test.tsx index 3ca9f3d80e2..33afc2910a9 100644 --- a/app/components/UI/AddCustomCollectible/index.test.tsx +++ b/app/components/UI/AddCustomCollectible/index.test.tsx @@ -3,15 +3,11 @@ import { shallow } from 'enzyme'; import AddCustomCollectible from './'; import configureMockStore from 'redux-mock-store'; import { Provider } from 'react-redux'; -import initialBackgroundState from '../../../util/test/initial-background-state.json'; +import initialRootState from '../../../util/test/initial-root-state'; const mockStore = configureMockStore(); -const initialState = { - engine: { - backgroundState: initialBackgroundState, - }, -}; -const store = mockStore(initialState); + +const store = mockStore(initialRootState); jest.mock('react-redux', () => ({ ...jest.requireActual('react-redux'), diff --git a/app/components/UI/AddToAddressBookWrapper/AddToAddressBookWrapper.test.tsx b/app/components/UI/AddToAddressBookWrapper/AddToAddressBookWrapper.test.tsx index b2ce68ace60..00ad7df7a8c 100644 --- a/app/components/UI/AddToAddressBookWrapper/AddToAddressBookWrapper.test.tsx +++ b/app/components/UI/AddToAddressBookWrapper/AddToAddressBookWrapper.test.tsx @@ -5,36 +5,33 @@ import { Text } from 'react-native'; import AddToAddressBookWrapper from './AddToAddressBookWrapper'; import { AddAddressModalSelectorsIDs } from '../../../../e2e/selectors/Modals/AddAddressModal.selectors'; import renderWithProvider from '../../../util/test/renderWithProvider'; -import initialBackgroundState from '../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../util/test/initial-root-state'; +import { createMockAccountsControllerState } from '../../../util/test/accountsControllerTestUtils'; + +const MOCK_ADDRESS_1 = '0x0'; +const MOCK_ADDRESS_2 = '0x1'; + +const MOCK_ACCOUNTS_CONTROLLER_STATE = createMockAccountsControllerState([ + MOCK_ADDRESS_1, + MOCK_ADDRESS_2, +]); const initialState = { settings: {}, engine: { backgroundState: { - ...initialBackgroundState, - PreferencesController: { - selectedAddress: '0x0', - identities: { - '0x0': { - address: '0x0', - name: 'Account 1', - }, - '0x1': { - address: '0x1', - name: 'Account 2', - }, - }, - }, + ...backgroundState, AddressBookController: { addressBook: { - '0x1': { - '0x1': { - address: '0x1', + [MOCK_ADDRESS_2]: { + [MOCK_ADDRESS_2]: { + address: MOCK_ADDRESS_2, name: 'Account 2', }, }, }, }, + AccountsController: MOCK_ACCOUNTS_CONTROLLER_STATE, }, }, }; @@ -66,7 +63,7 @@ describe('AddToAddressBookWrapper', () => { }); it('should not render touchable wrapper if address is already saved', async () => { const { queryByText } = renderWithProvider( - + DUMMY , { state: initialState }, @@ -78,7 +75,7 @@ describe('AddToAddressBookWrapper', () => { }); it('should return null if address is already saved and defaultNull is true', async () => { const { queryByText } = renderWithProvider( - + DUMMY , { state: initialState }, diff --git a/app/components/UI/AddressCopy/AddressCopy.tsx b/app/components/UI/AddressCopy/AddressCopy.tsx index 80af16dcbeb..17f8dbd1550 100644 --- a/app/components/UI/AddressCopy/AddressCopy.tsx +++ b/app/components/UI/AddressCopy/AddressCopy.tsx @@ -18,17 +18,17 @@ import ClipboardManager from '../../../core/ClipboardManager'; import { showAlert } from '../../../actions/alert'; import { protectWalletModalVisible } from '../../../actions/user'; import { strings } from '../../../../locales/i18n'; -import { Platform, View } from 'react-native'; +import { View } from 'react-native'; import { MetaMetricsEvents } from '../../../core/Analytics'; import { useStyles } from '../../../component-library/hooks'; -import generateTestId from '../../../../wdio/utils/generateTestId'; +import { WalletViewSelectorsIDs } from '../../../../e2e/selectors/wallet/WalletView.selectors'; // Internal dependencies import styleSheet from './AddressCopy.styles'; import { AddressCopyProps } from './AddressCopy.types'; -import { selectIdentities } from '../../../selectors/preferencesController'; -import { selectSelectedInternalAccountChecksummedAddress } from '../../../selectors/accountsController'; +import { selectSelectedInternalAccount } from '../../../selectors/accountsController'; import { useMetrics } from '../../../components/hooks/useMetrics'; +import { toChecksumHexAddress } from '@metamask/controller-utils'; const AddressCopy = ({ formatAddressType = 'full' }: AddressCopyProps) => { const { styles } = useStyles(styleSheet, {}); @@ -49,22 +49,14 @@ const AddressCopy = ({ formatAddressType = 'full' }: AddressCopyProps) => { /** * A string that represents the selected address */ - const selectedAddress = useSelector( - selectSelectedInternalAccountChecksummedAddress, - ); - - /** - * An object containing each identity in the format address => account - */ - const identities = useSelector(selectIdentities); - - const account = { - ...identities[selectedAddress], - address: selectedAddress, - }; + const selectedInternalAccount = useSelector(selectSelectedInternalAccount); const copyAccountToClipboard = async () => { - await ClipboardManager.setString(selectedAddress); + if (selectedInternalAccount?.address) { + await ClipboardManager.setString( + toChecksumHexAddress(selectedInternalAccount.address), + ); + } handleShowAlert({ isVisible: true, autodismiss: 1500, @@ -83,14 +75,16 @@ const AddressCopy = ({ formatAddressType = 'full' }: AddressCopyProps) => { - {formatAddress(account.address, formatAddressType)} + {selectedInternalAccount + ? formatAddress(selectedInternalAccount.address, formatAddressType) + : null} { - {renderSlightlyLongAddress(toSelectedAddress, 4, 9)} + {toSelectedAddress + ? renderSlightlyLongAddress(toSelectedAddress, 4, 9) + : ''} diff --git a/app/components/UI/AddressInputs/index.test.jsx b/app/components/UI/AddressInputs/index.test.jsx index a32e4577c0d..73a80cda421 100644 --- a/app/components/UI/AddressInputs/index.test.jsx +++ b/app/components/UI/AddressInputs/index.test.jsx @@ -3,26 +3,16 @@ import { fireEvent } from '@testing-library/react-native'; import renderWithProvider from '../../../util/test/renderWithProvider'; import { AddressFrom, AddressTo } from './index'; -import initialBackgroundState from '../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../util/test/initial-root-state'; import { AddAddressModalSelectorsIDs } from '../../../../e2e/selectors/Modals/AddAddressModal.selectors'; const initialState = { settings: {}, engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, PreferencesController: { - selectedAddress: '0xe64dD0AB5ad7e8C5F2bf6Ce75C34e187af8b920A', - identities: { - '0xe64dD0AB5ad7e8C5F2bf6Ce75C34e187af8b920A': { - address: '0xe64dD0AB5ad7e8C5F2bf6Ce75C34e187af8b920A', - name: 'Account 1', - }, - '0x519d2CE57898513F676a5C3b66496c3C394c9CC7': { - address: '0x519d2CE57898513F676a5C3b66496c3C394c9CC7', - name: 'Account 2', - }, - }, + useTokenDetection: false, }, AddressBookController: { addressBook: { @@ -61,7 +51,7 @@ describe('AddressInputs', () => { fromAccountBalance="0x5" fromAccountName="DUMMY_ACCOUNT" />, - {}, + { state: initialState }, ); expect(container).toMatchSnapshot(); }); @@ -74,7 +64,7 @@ describe('AddressInputs', () => { fromAccountName="DUMMY_ACCOUNT" layout="vertical" />, - {}, + { state: initialState }, ); expect(container).toMatchSnapshot(); }); diff --git a/app/components/UI/AssetElement/__snapshots__/index.test.tsx.snap b/app/components/UI/AssetElement/__snapshots__/index.test.tsx.snap index 5704e5960e9..99fc71bcbfc 100644 --- a/app/components/UI/AssetElement/__snapshots__/index.test.tsx.snap +++ b/app/components/UI/AssetElement/__snapshots__/index.test.tsx.snap @@ -14,5 +14,15 @@ exports[`AssetElement should render correctly 1`] = ` } } testID="asset-DAI" -/> +> + + `; diff --git a/app/components/UI/AssetElement/index.tsx b/app/components/UI/AssetElement/index.tsx index e95c3a3a1f6..fdf638bb125 100644 --- a/app/components/UI/AssetElement/index.tsx +++ b/app/components/UI/AssetElement/index.tsx @@ -1,6 +1,6 @@ /* eslint-disable react/prop-types */ import React from 'react'; -import { TouchableOpacity, StyleSheet, Platform } from 'react-native'; +import { TouchableOpacity, StyleSheet, Platform, View } from 'react-native'; import Text, { TextVariant, } from '../../../component-library/components/Texts/Text'; @@ -12,15 +12,20 @@ import { TOKEN_BALANCE_LOADING, TOKEN_RATE_UNDEFINED, } from '../Tokens/constants'; +import { Colors } from '../../../util/theme/models'; +import { fontStyles } from '../../../styles/common'; +import { useTheme } from '../../../util/theme'; + interface AssetElementProps { children?: React.ReactNode; asset: TokenI; onPress?: (asset: TokenI) => void; onLongPress?: ((asset: TokenI) => void) | null; balance?: string; + mainBalance?: string | null; } -const createStyles = () => +const createStyles = (colors: Colors) => StyleSheet.create({ itemWrapper: { flex: 1, @@ -32,6 +37,7 @@ const createStyles = () => arrow: { flex: 1, alignSelf: 'flex-end', + alignItems: 'flex-end', }, arrowIcon: { marginTop: 16, @@ -39,6 +45,12 @@ const createStyles = () => skeleton: { width: 50, }, + balanceFiat: { + color: colors.text.alternative, + paddingHorizontal: 0, + ...fontStyles.normal, + textTransform: 'uppercase', + }, }); /** @@ -48,10 +60,12 @@ const AssetElement: React.FC = ({ children, balance, asset, + mainBalance = null, onPress, onLongPress, }) => { - const styles = createStyles(); + const { colors } = useTheme(); + const styles = createStyles(colors); const handleOnPress = () => { onPress?.(asset); @@ -70,21 +84,32 @@ const AssetElement: React.FC = ({ > {children} - {balance && ( - - {balance === TOKEN_BALANCE_LOADING ? ( - - ) : ( - balance - )} - - )} + + {balance && ( + + {balance === TOKEN_BALANCE_LOADING ? ( + + ) : ( + balance + )} + + )} + {mainBalance ? ( + + {mainBalance === TOKEN_BALANCE_LOADING ? ( + + ) : ( + mainBalance + )} + + ) : null} + ); }; diff --git a/app/components/UI/AssetIcon/index.test.tsx b/app/components/UI/AssetIcon/index.test.tsx index fd256ddb41e..626428cd36a 100644 --- a/app/components/UI/AssetIcon/index.test.tsx +++ b/app/components/UI/AssetIcon/index.test.tsx @@ -2,21 +2,14 @@ import React from 'react'; import renderWithProvider from '../../../util/test/renderWithProvider'; import AssetIcon from './'; const sampleLogo = 'https://s3.amazonaws.com/airswap-token-images/WBTC.png'; -import initialBackgroundState from '../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../util/test/initial-root-state'; const mockInitialState = { engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, PreferencesController: { featureFlags: {}, - identities: { - '0x76cf1CdD1fcC252442b50D6e97207228aA4aefC3': { - address: '0x76cf1CdD1fcC252442b50D6e97207228aA4aefC3', - name: 'Account 1', - importTime: 1684232000456, - }, - }, ipfsGateway: 'https://cloudflare-ipfs.com/ipfs/', lostIdentities: {}, selectedAddress: '0x76cf1CdD1fcC252442b50D6e97207228aA4aefC3', @@ -34,13 +27,6 @@ const mockInitialState = { _W: { featureFlags: {}, frequentRpcList: [], - identities: { - '0x76cf1CdD1fcC252442b50D6e97207228aA4aefC3': { - address: '0x76cf1CdD1fcC252442b50D6e97207228aA4aefC3', - name: 'Account 1', - importTime: 1684232000456, - }, - }, ipfsGateway: 'https://cloudflare-ipfs.com/ipfs/', lostIdentities: {}, selectedAddress: '0x76cf1CdD1fcC252442b50D6e97207228aA4aefC3', diff --git a/app/components/UI/AssetSearch/index.test.tsx b/app/components/UI/AssetSearch/index.test.tsx index e58041c7314..9838088af3b 100644 --- a/app/components/UI/AssetSearch/index.test.tsx +++ b/app/components/UI/AssetSearch/index.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import renderWithProvider from '../../../util/test/renderWithProvider'; import AssetSearch from './'; -import initialBackgroundState from '../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../util/test/initial-root-state'; import Engine from '../../../core/Engine'; const mockedEngine = Engine; @@ -48,7 +48,7 @@ jest.mock('../../../core/Engine.ts', () => ({ const initialState = { engine: { - backgroundState: initialBackgroundState, + backgroundState, }, }; diff --git a/app/components/UI/BlockingActionModal/__snapshots__/index.test.tsx.snap b/app/components/UI/BlockingActionModal/__snapshots__/index.test.tsx.snap index 877f2d6e738..757058bdf97 100644 --- a/app/components/UI/BlockingActionModal/__snapshots__/index.test.tsx.snap +++ b/app/components/UI/BlockingActionModal/__snapshots__/index.test.tsx.snap @@ -16,7 +16,7 @@ exports[`BlockingActionModal should render correctly 1`] = ` deviceHeight={null} deviceWidth={null} hasBackdrop={true} - hideModalContentWhileAnimating={false} + hideModalContentWhileAnimating={true} isVisible={true} onBackButtonPress={[Function]} onBackdropPress={[Function]} diff --git a/app/components/UI/BlockingActionModal/index.js b/app/components/UI/BlockingActionModal/index.js index a4b03ec8ed6..b126438edf0 100644 --- a/app/components/UI/BlockingActionModal/index.js +++ b/app/components/UI/BlockingActionModal/index.js @@ -33,6 +33,7 @@ export default function BlockingActionModal({ children, modalVisible, isLoadingAction, + onAnimationCompleted, }) { const { colors } = useTheme(); const styles = createStyles(colors); @@ -43,6 +44,8 @@ export default function BlockingActionModal({ backdropOpacity={1} isVisible={modalVisible} style={styles.modal} + onModalShow={onAnimationCompleted} + hideModalContentWhileAnimating > @@ -69,4 +72,6 @@ BlockingActionModal.propTypes = { * Content to display above the action buttons */ children: PropTypes.node, + + onAnimationCompleted: PropTypes.func, }; diff --git a/app/components/UI/CollectibleContractInformation/index.test.tsx b/app/components/UI/CollectibleContractInformation/index.test.tsx index 526e2d4159b..2629113abd8 100644 --- a/app/components/UI/CollectibleContractInformation/index.test.tsx +++ b/app/components/UI/CollectibleContractInformation/index.test.tsx @@ -3,12 +3,12 @@ import { shallow } from 'enzyme'; import CollectibleContractInformation from './'; import configureMockStore from 'redux-mock-store'; import { Provider } from 'react-redux'; -import initialBackgroundState from '../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../util/test/initial-root-state'; const mockStore = configureMockStore(); const initialState = { engine: { - backgroundState: initialBackgroundState, + backgroundState, }, }; const store = mockStore(initialState); diff --git a/app/components/UI/CollectibleContractOverview/index.test.tsx b/app/components/UI/CollectibleContractOverview/index.test.tsx index df1b0e37866..e1e358db0ad 100644 --- a/app/components/UI/CollectibleContractOverview/index.test.tsx +++ b/app/components/UI/CollectibleContractOverview/index.test.tsx @@ -3,12 +3,12 @@ import CollectibleContractOverview from './'; import configureMockStore from 'redux-mock-store'; import { shallow } from 'enzyme'; import { Provider } from 'react-redux'; -import initialBackgroundState from '../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../util/test/initial-root-state'; const mockStore = configureMockStore(); const initialState = { engine: { - backgroundState: initialBackgroundState, + backgroundState, }, }; const store = mockStore(initialState); diff --git a/app/components/UI/CollectibleContracts/index.js b/app/components/UI/CollectibleContracts/index.js index 866d8b96d7b..d03c8bce4d3 100644 --- a/app/components/UI/CollectibleContracts/index.js +++ b/app/components/UI/CollectibleContracts/index.js @@ -5,7 +5,6 @@ import { StyleSheet, View, Image, - Platform, FlatList, RefreshControl, ActivityIndicator, @@ -30,7 +29,6 @@ import { compareTokenIds } from '../../../util/tokens'; import CollectibleDetectionModal from '../CollectibleDetectionModal'; import { useTheme } from '../../../util/theme'; import { MAINNET } from '../../../constants/network'; -import generateTestId from '../../../../wdio/utils/generateTestId'; import { selectChainId, selectProviderType, @@ -41,10 +39,7 @@ import { selectUseNftDetection, } from '../../../selectors/preferencesController'; import { selectSelectedInternalAccountChecksummedAddress } from '../../../selectors/accountsController'; -import { - IMPORT_NFT_BUTTON_ID, - NFT_TAB_CONTAINER_ID, -} from '../../../../wdio/screen-objects/testIDs/Screens/WalletView.testIds'; +import { WalletViewSelectorsIDs } from '../../../../e2e/selectors/wallet/WalletView.selectors'; import { useMetrics } from '../../../components/hooks/useMetrics'; import { RefreshTestId, SpinnerTestId } from './constants'; @@ -239,7 +234,7 @@ const CollectibleContracts = ({ {strings('wallet.add_collectibles')} @@ -376,7 +371,7 @@ const CollectibleContracts = ({ return ( {renderList()} diff --git a/app/components/UI/CollectibleContracts/index.test.tsx b/app/components/UI/CollectibleContracts/index.test.tsx index 946a654ba62..db7045fdb84 100644 --- a/app/components/UI/CollectibleContracts/index.test.tsx +++ b/app/components/UI/CollectibleContracts/index.test.tsx @@ -3,7 +3,7 @@ import { shallow } from 'enzyme'; import CollectibleContracts from './'; import configureMockStore from 'redux-mock-store'; import { Provider } from 'react-redux'; -import initialBackgroundState from '../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../util/test/initial-root-state'; import renderWithProvider from '../../../util/test/renderWithProvider'; import { act } from '@testing-library/react-hooks'; @@ -51,7 +51,7 @@ const initialState = { favorites: {}, }, engine: { - backgroundState: initialBackgroundState, + backgroundState, }, }; const store = mockStore(initialState); @@ -79,7 +79,7 @@ describe('CollectibleContracts', () => { }, engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, NetworkController: { network: '1', providerConfig: { @@ -196,7 +196,7 @@ describe('CollectibleContracts', () => { }, engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, NetworkController: { network: '1', providerConfig: { @@ -309,7 +309,7 @@ describe('CollectibleContracts', () => { }, engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, NetworkController: { network: '1', providerConfig: { @@ -426,7 +426,7 @@ describe('CollectibleContracts', () => { }, engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, NetworkController: { network: '1', providerConfig: { @@ -505,7 +505,7 @@ describe('CollectibleContracts', () => { }, engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, NetworkController: { network: '1', providerConfig: { @@ -564,7 +564,7 @@ describe('CollectibleContracts', () => { }, engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, NetworkController: { network: '1', providerConfig: { diff --git a/app/components/UI/CollectibleMedia/CollectibleMedia.test.tsx b/app/components/UI/CollectibleMedia/CollectibleMedia.test.tsx index 978e0e12042..f45d0051d4c 100644 --- a/app/components/UI/CollectibleMedia/CollectibleMedia.test.tsx +++ b/app/components/UI/CollectibleMedia/CollectibleMedia.test.tsx @@ -3,11 +3,11 @@ import React from 'react'; import CollectibleMedia from './CollectibleMedia'; import renderWithProvider from '../../../util/test/renderWithProvider'; -import initialBackgroundState from '../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../util/test/initial-root-state'; const mockInitialState = { engine: { - backgroundState: initialBackgroundState, + backgroundState, }, }; diff --git a/app/components/UI/CollectibleModal/CollectibleModal.test.tsx b/app/components/UI/CollectibleModal/CollectibleModal.test.tsx index 3d69d9c6a27..c7c9a52687e 100644 --- a/app/components/UI/CollectibleModal/CollectibleModal.test.tsx +++ b/app/components/UI/CollectibleModal/CollectibleModal.test.tsx @@ -3,7 +3,7 @@ import React from 'react'; import CollectibleModal from './CollectibleModal'; import renderWithProvider from '../../../util/test/renderWithProvider'; -import initialBackgroundState from '../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../util/test/initial-root-state'; import { collectiblesSelector } from '../../../reducers/collectibles'; import { selectDisplayNftMedia, @@ -15,7 +15,7 @@ import { MOCK_ACCOUNTS_CONTROLLER_STATE } from '../../../util/test/accountsContr const mockInitialState = { engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, AccountsController: MOCK_ACCOUNTS_CONTROLLER_STATE, }, }, diff --git a/app/components/UI/CollectibleOverview/index.test.tsx b/app/components/UI/CollectibleOverview/index.test.tsx index ea570feb266..6bc40a57a64 100644 --- a/app/components/UI/CollectibleOverview/index.test.tsx +++ b/app/components/UI/CollectibleOverview/index.test.tsx @@ -3,7 +3,7 @@ import CollectibleOverview from './'; import configureMockStore from 'redux-mock-store'; import { shallow } from 'enzyme'; import { Provider } from 'react-redux'; -import initialBackgroundState from '../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../util/test/initial-root-state'; const mockStore = configureMockStore(); const initialState = { @@ -11,7 +11,7 @@ const initialState = { favorites: {}, }, engine: { - backgroundState: initialBackgroundState, + backgroundState, }, }; const store = mockStore(initialState); diff --git a/app/components/UI/ConfirmAddAsset/ConfirmAddAsset.test.tsx b/app/components/UI/ConfirmAddAsset/ConfirmAddAsset.test.tsx index 9288d2fbcbb..73647c006f0 100644 --- a/app/components/UI/ConfirmAddAsset/ConfirmAddAsset.test.tsx +++ b/app/components/UI/ConfirmAddAsset/ConfirmAddAsset.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import ConfirmAddAsset from './ConfirmAddAsset'; -import initialBackgroundState from '../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../util/test/initial-root-state'; import renderWithProvider from '../../../util/test/renderWithProvider'; import useBalance from '../Ramp/hooks/useBalance'; import { toTokenMinimalUnit } from '../../../util/number'; @@ -58,7 +58,7 @@ const mockInitialState = { settings: {}, engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, AccountTrackerController: { accounts: { '0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272': { diff --git a/app/components/UI/ConfirmAddAsset/__snapshots__/ConfirmAddAsset.test.tsx.snap b/app/components/UI/ConfirmAddAsset/__snapshots__/ConfirmAddAsset.test.tsx.snap index 60c2c813da7..6054c9100ac 100644 --- a/app/components/UI/ConfirmAddAsset/__snapshots__/ConfirmAddAsset.test.tsx.snap +++ b/app/components/UI/ConfirmAddAsset/__snapshots__/ConfirmAddAsset.test.tsx.snap @@ -168,7 +168,7 @@ exports[`ConfirmAddAsset render matches previous snapshot 1`] = ` "opacity": 0, } } - testID="badge-network" + testID="badgenetwork" > StyleSheet.create({ @@ -340,18 +340,14 @@ class DrawerView extends PureComponent { * Object representing the configuration of the current selected network */ providerConfig: PropTypes.object.isRequired, - /** - * Selected address as string - */ - selectedAddress: PropTypes.string, /** * List of accounts from the AccountTrackerController */ accounts: PropTypes.object, /** - * List of accounts from the PreferencesController + * Currently selected account */ - identities: PropTypes.object, + selectedInternalAccount: PropTypes.object, /** /* Selected currency */ @@ -474,16 +470,19 @@ class DrawerView extends PureComponent { previousBalance = null; processedNewBalance = false; animatingNetworksModal = false; + selectedChecksummedAddress = toChecksumHexAddress( + this.props.selectedInternalAccount.address, + ); isCurrentAccountImported() { let ret = false; - const { keyrings, selectedAddress } = this.props; + const { keyrings } = this.props; const allKeyrings = keyrings && keyrings.length ? keyrings : Engine.context.KeyringController.state.keyrings; for (const keyring of allKeyrings) { - if (keyring.accounts.includes(selectedAddress)) { + if (keyring.accounts.includes(this.selectedChecksummedAddress)) { ret = keyring.type !== 'HD Key Tree'; break; } @@ -495,7 +494,7 @@ class DrawerView extends PureComponent { renderTag() { const colors = this.context.colors || mockTheme.colors; const styles = createStyles(colors); - const label = getLabelTextByAddress(this.props.selectedAddress); + const label = getLabelTextByAddress(this.selectedChecksummedAddress); return label ? ( @@ -579,16 +578,16 @@ class DrawerView extends PureComponent { } updateAccountInfo = async () => { - const { identities, providerConfig, selectedAddress } = this.props; + const { providerConfig, selectedInternalAccount } = this.props; const { currentChainId, address, name } = this.state.account; - const accountName = identities[selectedAddress]?.name; + const accountName = selectedInternalAccount.metadata.name; if ( currentChainId !== providerConfig.chainId || - address !== selectedAddress || + address !== this.selectedChecksummedAddress || name !== accountName ) { const ens = await doENSReverseLookup( - selectedAddress, + this.selectedChecksummedAddress, providerConfig.chainId, ); this.setState((state) => ({ @@ -596,7 +595,7 @@ class DrawerView extends PureComponent { ens, name: accountName, currentChainId: providerConfig.chainId, - address: selectedAddress, + address: this.selectedChecksummedAddress, }, })); } @@ -697,18 +696,20 @@ class DrawerView extends PureComponent { }; viewInEtherscan = () => { - const { selectedAddress, providerConfig, networkConfigurations } = - this.props; + const { providerConfig, networkConfigurations } = this.props; if (providerConfig.type === RPC) { const blockExplorer = findBlockExplorerForRpc( providerConfig.rpcUrl, networkConfigurations, ); - const url = `${blockExplorer}/address/${selectedAddress}`; + const url = `${blockExplorer}/address/${this.selectedChecksummedAddress}`; const title = new URL(blockExplorer).hostname; this.goToBrowserUrl(url, title); } else { - const url = getEtherscanAddressUrl(providerConfig.type, selectedAddress); + const url = getEtherscanAddressUrl( + providerConfig.type, + this.selectedChecksummedAddress, + ); const etherscan_url = getEtherscanBaseUrl(providerConfig.type).replace( 'https://', '', @@ -892,8 +893,7 @@ class DrawerView extends PureComponent { }; copyAccountToClipboard = async () => { - const { selectedAddress } = this.props; - await ClipboardManager.setString(selectedAddress); + await ClipboardManager.setString(this.selectedChecksummedAddress); this.toggleReceiveModal(); InteractionManager.runAfterInteractions(() => { this.props.showAlert({ @@ -906,9 +906,8 @@ class DrawerView extends PureComponent { }; onShare = () => { - const { selectedAddress } = this.props; Share.open({ - message: selectedAddress, + message: this.selectedChecksummedAddress, }) .then(() => { this.props.protectWalletModalVisible(); @@ -996,8 +995,7 @@ class DrawerView extends PureComponent { const { providerConfig, accounts, - identities, - selectedAddress, + selectedInternalAccount, currentCurrency, seedphraseBackedUp, currentRoute, @@ -1011,16 +1009,16 @@ class DrawerView extends PureComponent { } = this.state; const account = { - address: selectedAddress, + address: this.selectedChecksummedAddress, name: nameFromState, ens: ensFromState, - ...identities[selectedAddress], - ...accounts[selectedAddress], + ...selectedInternalAccount, + ...accounts[this.selectedChecksummedAddress], }; const { name, ens } = account; account.balance = - (accounts[selectedAddress] && - renderFromWei(accounts[selectedAddress].balance)) || + (accounts[this.selectedChecksummedAddress] && + renderFromWei(accounts[this.selectedChecksummedAddress].balance)) || 0; const fiatBalance = Engine.getTotalFiatAccountBalance(); const totalFiatBalance = fiatBalance.ethFiat + fiatBalance.tokenFiat; @@ -1056,7 +1054,10 @@ class DrawerView extends PureComponent { testID={'navbar-account-identicon'} > - + ({ providerConfig: selectProviderConfig(state), accounts: selectAccounts(state), - selectedAddress: selectSelectedInternalAccountChecksummedAddress(state), - identities: selectIdentities(state), + selectedInternalAccount: selectSelectedInternalAccount(state), networkConfigurations: selectNetworkConfigurations(state), currentCurrency: selectCurrentCurrency(state), keyrings: state.engine.backgroundState.KeyringController.keyrings, diff --git a/app/components/UI/DrawerView/index.test.tsx b/app/components/UI/DrawerView/index.test.tsx index 9d06c981241..6eebe48bc6e 100644 --- a/app/components/UI/DrawerView/index.test.tsx +++ b/app/components/UI/DrawerView/index.test.tsx @@ -2,28 +2,16 @@ import React from 'react'; import renderWithProvider from '../../../util/test/renderWithProvider'; import DrawerView from './'; -import initialBackgroundState from '../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../util/test/initial-root-state'; import Engine from '../../../core/Engine'; -import { - MOCK_ACCOUNTS_CONTROLLER_STATE, - MOCK_ADDRESS_1, -} from '../../../util/test/accountsControllerTestUtils'; +import { MOCK_ACCOUNTS_CONTROLLER_STATE } from '../../../util/test/accountsControllerTestUtils'; const mockedEngine = Engine; const mockInitialState = { engine: { backgroundState: { - ...initialBackgroundState, - PreferencesController: { - selectedAddress: MOCK_ADDRESS_1, - identities: { - [MOCK_ADDRESS_1]: { - name: 'Account 1', - address: MOCK_ADDRESS_1, - }, - }, - }, + ...backgroundState, AccountsController: MOCK_ACCOUNTS_CONTROLLER_STATE, }, }, diff --git a/app/components/UI/EthereumAddress/index.js b/app/components/UI/EthereumAddress/index.js index b522b2bf0cd..82b2ce368a8 100644 --- a/app/components/UI/EthereumAddress/index.js +++ b/app/components/UI/EthereumAddress/index.js @@ -21,7 +21,7 @@ class EthereumAddress extends PureComponent { address: PropTypes.string, /** * Type of formatting for the address - * can be "short" or "full" + * can be "short", "mid" or "full" */ type: PropTypes.string, }; diff --git a/app/components/UI/Fox/index.js b/app/components/UI/Fox/index.js index 8e18da54c6f..8767c05f41f 100644 --- a/app/components/UI/Fox/index.js +++ b/app/components/UI/Fox/index.js @@ -1,7 +1,7 @@ import React, { forwardRef } from 'react'; import PropTypes from 'prop-types'; import { StyleSheet } from 'react-native'; -import { WebView } from 'react-native-webview'; +import { WebView } from '@metamask/react-native-webview'; import { useTheme } from '../../../util/theme'; import Animated, { useAnimatedStyle, diff --git a/app/components/UI/HardwareWallet/AccountSelector/index.tsx b/app/components/UI/HardwareWallet/AccountSelector/index.tsx index ed5cc1a6725..ab2378c90c0 100644 --- a/app/components/UI/HardwareWallet/AccountSelector/index.tsx +++ b/app/components/UI/HardwareWallet/AccountSelector/index.tsx @@ -19,7 +19,7 @@ interface ISelectQRAccountsProps { selectedAccounts: string[]; nextPage: () => void; prevPage: () => void; - toggleAccount: (index: number) => void; + onCheck?: (index: number) => void; onUnlock: (accountIndex: number[]) => void; onForget: () => void; title: string; @@ -30,7 +30,7 @@ const AccountSelector = (props: ISelectQRAccountsProps) => { accounts, prevPage, nextPage, - toggleAccount, + onCheck, selectedAccounts, onForget, onUnlock, @@ -69,9 +69,12 @@ const AccountSelector = (props: ISelectQRAccountsProps) => { prev.has(index) ? prev.delete(index) : prev.add(index); return new Set(prev); }); - toggleAccount(index); + + if (onCheck) { + onCheck(index); + } }, - [toggleAccount], + [onCheck], ); return ( diff --git a/app/components/UI/HardwareWallet/AccountSelector/styles.tsx b/app/components/UI/HardwareWallet/AccountSelector/styles.tsx index 306d52a971e..9d8463fa895 100644 --- a/app/components/UI/HardwareWallet/AccountSelector/styles.tsx +++ b/app/components/UI/HardwareWallet/AccountSelector/styles.tsx @@ -50,7 +50,7 @@ export const createStyle = (colors: any) => bottom: { alignItems: 'center', justifyContent: 'space-between', - paddingTop: 70, + paddingTop: 30, paddingBottom: Device.isIphoneX() ? 20 : 10, }, button: { diff --git a/app/components/UI/Identicon/__snapshots__/index.test.tsx.snap b/app/components/UI/Identicon/__snapshots__/index.test.tsx.snap index 18c2cca7567..9707813bf99 100644 --- a/app/components/UI/Identicon/__snapshots__/index.test.tsx.snap +++ b/app/components/UI/Identicon/__snapshots__/index.test.tsx.snap @@ -1,5 +1,25 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Identicon should render correctly when provided address found in tokenList and iconUrl is available 1`] = ` + +`; + exports[`Identicon should render correctly when useBlockieIcon is false 1`] = ` { const mockStore = configureMockStore(); + const mockUseTokenList = jest + .mocked(useTokenList) + .mockImplementation(() => ({})); + + it('should render correctly when provided address found in tokenList and iconUrl is available', () => { + const addressMock = '0x0439e60f02a8900a951603950d8d4527f400c3f1'; + mockUseTokenList.mockImplementation(() => [ + { + address: addressMock, + iconUrl: 'https://example.com/icon.png', + }, + ]); + + const initialState = { + settings: { useBlockieIcon: true }, + }; + const store = mockStore(initialState); + + const wrapper = render( + + + , + ); + expect(wrapper).toMatchSnapshot(); + }); it('should render correctly when useBlockieIcon is true', () => { const initialState = { settings: { useBlockieIcon: true }, diff --git a/app/components/UI/Identicon/index.tsx b/app/components/UI/Identicon/index.tsx index 945419c69b5..8daa81dbd6d 100644 --- a/app/components/UI/Identicon/index.tsx +++ b/app/components/UI/Identicon/index.tsx @@ -6,6 +6,8 @@ import FadeIn from 'react-native-fade-in-image'; import Jazzicon from 'react-native-jazzicon'; import { connect } from 'react-redux'; import { useTheme } from '../../../util/theme'; +import { useTokenListEntry } from '../../../components/hooks/DisplayName/useTokenListEntry'; +import { NameType } from '../../UI/Name/Name.types'; interface IdenticonProps { /** @@ -43,23 +45,35 @@ const Identicon: React.FC = ({ useBlockieIcon = true, }) => { const { colors } = useTheme(); + const tokenListIcon = useTokenListEntry( + address || '', + NameType.EthereumAddress, + )?.iconUrl; if (!address) return null; const uri = useBlockieIcon && toDataUrl(address); + const styleForBlockieAndTokenIcon = [ + { + height: diameter, + width: diameter, + borderRadius: diameter / 2, + }, + customStyle, + ]; + + if (tokenListIcon) { + return ( + + ); + } + const image = useBlockieIcon ? ( - + ) : ( diff --git a/app/components/UI/LedgerModals/LedgerConfirmationModal.test.tsx b/app/components/UI/LedgerModals/LedgerConfirmationModal.test.tsx index 5f5dbe702a0..e4445891966 100644 --- a/app/components/UI/LedgerModals/LedgerConfirmationModal.test.tsx +++ b/app/components/UI/LedgerModals/LedgerConfirmationModal.test.tsx @@ -15,15 +15,11 @@ import { BluetoothPermissionErrors, LedgerCommunicationErrors, } from '../../../core/Ledger/ledgerErrors'; -import { unlockLedgerDefaultAccount } from '../../../core/Ledger/Ledger'; import { strings } from '../../../../locales/i18n'; import { useMetrics } from '../../hooks/useMetrics'; import { MetaMetricsEvents } from '../../../core/Analytics'; import { fireEvent } from '@testing-library/react-native'; - -jest.mock('../../../core/Ledger/Ledger', () => ({ - unlockLedgerDefaultAccount: jest.fn(), -})); +import { HardwareDeviceTypes } from '../../../constants/keyringTypes'; jest.mock('../../hooks/Ledger/useBluetooth', () => ({ __esModule: true, @@ -340,7 +336,6 @@ describe('LedgerConfirmationModal', () => { it('calls onConfirmation when ledger commands are being sent and confirmed have been received.', async () => { const onConfirmation = jest.fn(); - unlockLedgerDefaultAccount.mockReturnValue(Promise.resolve(true)); useLedgerBluetooth.mockReturnValue({ isSendingLedgerCommands: true, isAppLaunchConfirmationNeeded: false, @@ -359,11 +354,10 @@ describe('LedgerConfirmationModal', () => { // eslint-disable-next-line @typescript-eslint/no-empty-function await act(async () => {}); - expect(unlockLedgerDefaultAccount).toHaveBeenCalled(); expect(onConfirmation).toHaveBeenCalled(); }); - it('logs LEDGER_HARDWARE_WALLET_ERROR thrown by unlockLedgerDefaultAccount', async () => { + it('logs LEDGER_HARDWARE_WALLET_ERROR event when the ledger error occurs', async () => { const onConfirmation = jest.fn(); const ledgerLogicToRun = jest.fn(); @@ -400,7 +394,7 @@ describe('LedgerConfirmationModal', () => { 1, MetaMetricsEvents.LEDGER_HARDWARE_WALLET_ERROR, { - device_type: 'Ledger', + device_type: HardwareDeviceTypes.LEDGER, error: 'LEDGER_ETH_APP_NOT_INSTALLED', }, ); diff --git a/app/components/UI/LedgerModals/LedgerConfirmationModal.tsx b/app/components/UI/LedgerModals/LedgerConfirmationModal.tsx index d6bc54187f6..5a765624e04 100644 --- a/app/components/UI/LedgerModals/LedgerConfirmationModal.tsx +++ b/app/components/UI/LedgerModals/LedgerConfirmationModal.tsx @@ -10,13 +10,13 @@ import ConfirmationStep from './Steps/ConfirmationStep'; import ErrorStep from './Steps/ErrorStep'; import OpenETHAppStep from './Steps/OpenETHAppStep'; import SearchingForDeviceStep from './Steps/SearchingForDeviceStep'; -import { unlockLedgerDefaultAccount } from '../../../core/Ledger/Ledger'; import { MetaMetricsEvents } from '../../../core/Analytics'; import { useMetrics } from '../../../components/hooks/useMetrics'; import { BluetoothPermissionErrors, LedgerCommunicationErrors, } from '../../../core/Ledger/ledgerErrors'; +import { HardwareDeviceTypes } from '../../../constants/keyringTypes'; const createStyles = (colors: Colors) => StyleSheet.create({ @@ -71,14 +71,13 @@ const LedgerConfirmationModal = ({ const connectLedger = () => { try { ledgerLogicToRun(async () => { - await unlockLedgerDefaultAccount(false); await onConfirmation(); }); } catch (_e) { // Handle a super edge case of the user starting a transaction with the device connected // After arriving to confirmation the ETH app is not installed anymore this causes a crash. trackEvent(MetaMetricsEvents.LEDGER_HARDWARE_WALLET_ERROR, { - device_type: 'Ledger', + device_type: HardwareDeviceTypes.LEDGER, error: 'LEDGER_ETH_APP_NOT_INSTALLED', }); } @@ -90,7 +89,7 @@ const LedgerConfirmationModal = ({ onRejection(); } finally { trackEvent(MetaMetricsEvents.LEDGER_HARDWARE_TRANSACTION_CANCELLED, { - device_type: 'Ledger', + device_type: HardwareDeviceTypes.LEDGER, }); } }; @@ -179,7 +178,7 @@ const LedgerConfirmationModal = ({ } if (ledgerError !== LedgerCommunicationErrors.UserRefusedConfirmation) { trackEvent(MetaMetricsEvents.LEDGER_HARDWARE_WALLET_ERROR, { - device_type: 'Ledger', + device_type: HardwareDeviceTypes.LEDGER, error: `${ledgerError}`, }); } @@ -208,7 +207,7 @@ const LedgerConfirmationModal = ({ } setPermissionErrorShown(true); trackEvent(MetaMetricsEvents.LEDGER_HARDWARE_WALLET_ERROR, { - device_type: 'Ledger', + device_type: HardwareDeviceTypes.LEDGER, error: 'LEDGER_BLUETOOTH_PERMISSION_ERR', }); } @@ -219,7 +218,7 @@ const LedgerConfirmationModal = ({ subtitle: strings('ledger.bluetooth_off_message'), }); trackEvent(MetaMetricsEvents.LEDGER_HARDWARE_WALLET_ERROR, { - device_type: 'Ledger', + device_type: HardwareDeviceTypes.LEDGER, error: 'LEDGER_BLUETOOTH_CONNECTION_ERR', }); } diff --git a/app/components/UI/Name/Name.test.tsx b/app/components/UI/Name/Name.test.tsx index b927c680ba8..a233e535f09 100644 --- a/app/components/UI/Name/Name.test.tsx +++ b/app/components/UI/Name/Name.test.tsx @@ -14,6 +14,11 @@ jest.mock('../../hooks/DisplayName/useDisplayName', () => ({ default: jest.fn(), })); +jest.mock('../Identicon', () => ({ + __esModule: true, + default: () => 'Identicon', +})); + const UNKNOWN_ADDRESS_CHECKSUMMED = '0x299007B3F9E23B8d432D5f545F8a4a2B3E9A5B4e'; const EXPECTED_UNKNOWN_ADDRESS_CHECKSUMMED = '0x29900...A5B4e'; diff --git a/app/components/UI/Name/__snapshots__/Name.test.tsx.snap b/app/components/UI/Name/__snapshots__/Name.test.tsx.snap index 75d9eb71ec6..2d61fb1f825 100644 --- a/app/components/UI/Name/__snapshots__/Name.test.tsx.snap +++ b/app/components/UI/Name/__snapshots__/Name.test.tsx.snap @@ -16,64 +16,7 @@ exports[`Name recognized address should return name 1`] = ` } } > - - - - - - + Identicon -`; \ No newline at end of file +`; diff --git a/app/components/UI/Navbar/index.js b/app/components/UI/Navbar/index.js index 53b37cd4c9a..d56aec0c3db 100644 --- a/app/components/UI/Navbar/index.js +++ b/app/components/UI/Navbar/index.js @@ -27,12 +27,10 @@ import Device from '../../../util/device'; import PickerNetwork from '../../../component-library/components/Pickers/PickerNetwork'; import BrowserUrlBar from '../BrowserUrlBar'; import generateTestId from '../../../../wdio/utils/generateTestId'; -import { NAVBAR_NETWORK_BUTTON } from '../../../../wdio/screen-objects/testIDs/Screens/WalletView.testIds'; import { NAV_ANDROID_BACK_BUTTON } from '../../../../wdio/screen-objects/testIDs/Screens/NetworksScreen.testids'; import { ASSET_BACK_BUTTON } from '../../../../wdio/screen-objects/testIDs/Screens/TokenOverviewScreen.testIds'; import { REQUEST_SEARCH_RESULTS_BACK_BUTTON } from '../../../../wdio/screen-objects/testIDs/Screens/RequestToken.testIds'; import { BACK_BUTTON_SIMPLE_WEBVIEW } from '../../../../wdio/screen-objects/testIDs/Components/SimpleWebView.testIds'; -import { EDIT_BUTTON } from '../../../../wdio/screen-objects/testIDs/Common.testIds'; import Routes from '../../../constants/navigation/Routes'; import ButtonIcon, { @@ -53,6 +51,7 @@ import { NetworksViewSelectorsIDs } from '../../../../e2e/selectors/Settings/Net import { SendLinkViewSelectorsIDs } from '../../../../e2e/selectors/SendLinkView.selectors'; import { SendViewSelectorsIDs } from '../../../../e2e/selectors/SendView.selectors'; import { getBlockaidTransactionMetricsParams } from '../../../util/blockaid'; +import { AddContactViewSelectorsIDs } from '../../../../e2e/selectors/Settings/Contacts/AddContactView.selectors'; const trackEvent = (event, params = {}) => { MetaMetrics.getInstance().trackEvent(event, params); @@ -290,7 +289,7 @@ export function getEditableOptions(title, navigation, route, themeColors) { {editMode @@ -984,7 +983,7 @@ export function getWalletNavbarOptions( label={networkName} imageSource={networkImageSource} onPress={onPressTitle} - {...generateTestId(Platform, NAVBAR_NETWORK_BUTTON)} + testID={WalletViewSelectorsIDs.NAVBAR_NETWORK_BUTTON} /> ), diff --git a/app/components/UI/NavbarBrowserTitle/index.test.tsx b/app/components/UI/NavbarBrowserTitle/index.test.tsx index 65a46192a79..5d7e771bedc 100644 --- a/app/components/UI/NavbarBrowserTitle/index.test.tsx +++ b/app/components/UI/NavbarBrowserTitle/index.test.tsx @@ -1,22 +1,14 @@ import React from 'react'; import renderWithProvider from '../../../util/test/renderWithProvider'; import NavbarBrowserTitle from './'; -import initialBackgroundState from '../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../util/test/initial-root-state'; import Engine from '../../../core/Engine'; const mockedEngine = Engine; const mockInitialState = { engine: { - backgroundState: { - ...initialBackgroundState, - PreferencesController: { - selectedAddress: '0x', - identities: { - '0x': { name: 'Account 1', address: '0x' }, - }, - }, - }, + backgroundState, }, }; diff --git a/app/components/UI/NetworkInfo/index.test.tsx b/app/components/UI/NetworkInfo/index.test.tsx index 5e9cb5dfadf..e52882bc2a9 100644 --- a/app/components/UI/NetworkInfo/index.test.tsx +++ b/app/components/UI/NetworkInfo/index.test.tsx @@ -3,7 +3,7 @@ import { shallow } from 'enzyme'; import NetworkInfo from './'; import configureMockStore from 'redux-mock-store'; import { Provider } from 'react-redux'; -import initialBackgroundState from '../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../util/test/initial-root-state'; const mockStore = configureMockStore(); const initialState = { @@ -11,7 +11,7 @@ const initialState = { approvedHosts: {}, }, engine: { - backgroundState: initialBackgroundState, + backgroundState, }, }; diff --git a/app/components/UI/NetworkMainAssetLogo/index.test.tsx b/app/components/UI/NetworkMainAssetLogo/index.test.tsx index 687a841d2be..84a625a7c16 100644 --- a/app/components/UI/NetworkMainAssetLogo/index.test.tsx +++ b/app/components/UI/NetworkMainAssetLogo/index.test.tsx @@ -5,7 +5,7 @@ import { ChainId } from '@metamask/controller-utils'; import { render } from '@testing-library/react-native'; import NetworkMainAssetLogo from '.'; -import initialBackgroundState from '../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../util/test/initial-root-state'; jest.mock('../Swaps/components/TokenIcon', () => { const originalModule = jest.requireActual('../Swaps/components/TokenIcon'); @@ -18,9 +18,7 @@ jest.mock('../Swaps/components/TokenIcon', () => { const mockInitialState = { engine: { - backgroundState: { - ...initialBackgroundState, - }, + backgroundState, }, network: { provider: { diff --git a/app/components/UI/Notification/List/__snapshots__/index.test.tsx.snap b/app/components/UI/Notification/List/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 0e82c36eca5..00000000000 --- a/app/components/UI/Notification/List/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,28 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`NotificationsList should render correctly 1`] = ` - - - - - -`; diff --git a/app/components/UI/Notification/List/index.test.tsx b/app/components/UI/Notification/List/index.test.tsx deleted file mode 100644 index 76dbc482957..00000000000 --- a/app/components/UI/Notification/List/index.test.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React from 'react'; -import NotificationsList from './'; -import renderWithProvider from '../../../../util/test/renderWithProvider'; -import MOCK_NOTIFICATIONS from '../__mocks__/mock_notifications'; -import { - FeatureAnnouncementRawNotification, - HalRawNotification, -} from '../../../../util/notifications'; -const navigationMock = { - navigate: jest.fn(), -}; -describe('NotificationsList', () => { - it('should render correctly', () => { - const { toJSON } = renderWithProvider( - , - ); - expect(toJSON()).toMatchSnapshot(); - }); -}); diff --git a/app/components/UI/Notification/List/index.tsx b/app/components/UI/Notification/List/index.tsx deleted file mode 100644 index dd5676ad02c..00000000000 --- a/app/components/UI/Notification/List/index.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import React, { useCallback, useMemo } from 'react'; -import { ActivityIndicator, FlatList, View } from 'react-native'; -import ScrollableTabView, { - DefaultTabBar, -} from 'react-native-scrollable-tab-view'; -import { strings } from '../../../../../locales/i18n'; -import { MetaMetricsEvents } from '../../../../core/Analytics'; - -import { useTheme } from '../../../../util/theme'; -import { createStyles } from './styles'; -import { useMetrics } from '../../../hooks/useMetrics'; -import Empty from '../Empty'; -import { NotificationRow } from '../Row'; -import { - FeatureAnnouncementRawNotification, - HalRawNotification, - Notification, - getRowDetails, -} from '../../../../util/notifications'; -import { NotificationsViewSelectorsIDs } from '../../../../../e2e/selectors/NotificationsView.selectors'; - -interface NotificationsList { - // TODO: Replace "any" with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - navigation: any; - allNotifications: Notification[]; - walletNotifications: HalRawNotification[]; - annoucementsNotifications: FeatureAnnouncementRawNotification[]; - loading: boolean; -} - -const Notifications = ({ - navigation, - allNotifications, - walletNotifications, - annoucementsNotifications, - loading, -}: NotificationsList) => { - const theme = useTheme(); - const { colors } = theme; - const styles = createStyles(theme); - const { trackEvent } = useMetrics(); - - const onPress = useCallback( - (item) => { - //TODO: details will be implemented on a separete PR - navigation.navigate('NotificationsDetails', { notification: item }); - }, - [navigation], - ); - - const renderTabBar = useCallback( - (props) => ( - - - - ), - [styles, colors], - ); - - const onChangeTab = useCallback( - (obj) => { - switch (obj.ref.props.tabLabel) { - case strings('notifications.all'): - trackEvent(MetaMetricsEvents.ALL_NOTIFICATIONS); - break; - case strings('notifications.wallet'): - trackEvent(MetaMetricsEvents.WALLET_NOTIFICATIONS); - break; - case strings('notifications.web3'): - trackEvent(MetaMetricsEvents.ANNOUCEMENTS_NOTIFICATIONS); - break; - default: - break; - } - }, - [trackEvent], - ); - - const combinedLists = useMemo( - () => [allNotifications, walletNotifications, annoucementsNotifications], - [allNotifications, walletNotifications, annoucementsNotifications], - ); - - const renderNotificationRow = useCallback( - (notification) => { - const hasActions = - !!notification.data?.link || !!notification.data?.action; - const { title, description, badgeIcon, createdAt, imageUrl, value } = - getRowDetails(notification)?.row || {}; - return ( - onPress(notification)} - styles={styles} - > - - - {hasActions && ( - - )} - - ); - }, - [onPress, styles], - ); - - const renderList = useCallback( - (list, idx) => ( - index.toString()} - key={combinedLists.indexOf(list)} - data={list} - ListEmptyComponent={ - - } - contentContainerStyle={styles.list} - renderItem={({ item }) => renderNotificationRow(item)} - initialNumToRender={10} - maxToRenderPerBatch={2} - onEndReachedThreshold={0.5} - /> - ), - [combinedLists, renderNotificationRow, styles.list], - ); - - return ( - - {loading ? ( - - - - ) : ( - - {combinedLists.map((list, idx) => renderList(list, idx))} - - )} - - ); -}; - -export default Notifications; diff --git a/app/components/UI/Notification/List/styles.ts b/app/components/UI/Notification/List/styles.ts deleted file mode 100644 index 672c5acedad..00000000000 --- a/app/components/UI/Notification/List/styles.ts +++ /dev/null @@ -1,141 +0,0 @@ -/* eslint-disable import/prefer-default-export */ -import { StyleSheet, TextStyle } from 'react-native'; -import type { Theme } from '@metamask/design-tokens'; - -export const createStyles = ({ colors, typography }: Theme) => - StyleSheet.create({ - container: { - flex: 1, - backgroundColor: colors.background.default, - }, - wrapper: { - flex: 1, - paddingHorizontal: 16, - paddingVertical: 10, - justifyContent: 'center', - borderRadius: 10, - }, - loaderContainer: { - position: 'absolute', - zIndex: 999, - width: '100%', - height: '100%', - }, - base: { - paddingHorizontal: 16, - }, - tabUnderlineStyle: { - height: 2, - backgroundColor: colors.primary.default, - }, - tabStyle: { - paddingBottom: 0, - paddingVertical: 8, - }, - tabBar: { - borderColor: colors.background.default, - }, - textStyle: { - ...(typography.sBodyMD as TextStyle), - fontWeight: '500', - }, - loader: { - backgroundColor: colors.background.default, - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - TabWrapper: { - backgroundColor: colors.background.default, - flex: 1, - }, - list: { flexGrow: 1 }, - fox: { - width: 20, - height: 20, - }, - badgeWrapper: { - alignItems: 'center', - justifyContent: 'center', - alignSelf: 'flex-start', - position: 'absolute', - top: '10%', - }, - assetLogo: { - width: 32, - height: 32, - borderRadius: 16, - overflow: 'hidden', - borderWidth: 0.5, - borderColor: colors.background.alternative, - }, - assetPlaceholder: { - backgroundColor: colors.background.alternative, - width: 32, - height: 32, - borderRadius: 16, - borderWidth: 0.5, - borderColor: colors.background.alternative, - }, - nftLogo: { - width: 32, - height: 32, - borderRadius: 8, - overflow: 'hidden', - borderWidth: 0.5, - borderColor: colors.background.alternative, - }, - nftPlaceholder: { - backgroundColor: colors.background.alternative, - width: 32, - height: 32, - borderRadius: 8, - borderWidth: 0.5, - borderColor: colors.background.alternative, - }, - rowContainer: { flex: 1, marginLeft: 42, alignItem: 'center' }, - rowInsider: { - flex: 1, - flexDirection: 'row', - justifyContent: 'space-between', - }, - ethLogo: { - width: 32, - height: 32, - borderRadius: 16, - }, - foxWrapper: { - width: 32, - height: 32, - borderRadius: 16, - backgroundColor: colors.background.alternative, - alignItems: 'center', - justifyContent: 'center', - alignSelf: 'flex-start', - position: 'absolute', - top: '25%', - }, - textBox: { - flexShrink: 1, - maxWidth: '85%', - }, - button: { - marginTop: 16, - width: '100%', - alignSelf: 'center', - }, - trashIconContainer: { - position: 'absolute', - paddingHorizontal: 24, - flex: 1, - flexDirection: 'row', - backgroundColor: colors.background.hover, - justifyContent: 'flex-end', - alignItems: 'center', - overflow: 'hidden', - height: '100%', - right: 0, - left: 0, - zIndex: -1, - }, - }); diff --git a/app/components/UI/Notification/Row/Actions.test.tsx b/app/components/UI/Notification/Row/Actions.test.tsx deleted file mode 100644 index 596523d7cdd..00000000000 --- a/app/components/UI/Notification/Row/Actions.test.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import React from 'react'; -import { Linking } from 'react-native'; -import { fireEvent } from '@testing-library/react-native'; - -import renderWithProvider from '../../../../util/test/renderWithProvider'; - -import NotificationActions from './Actions'; -import { ACTIONS, PREFIXES } from '../../../../constants/deeplinks'; -import { NOTIFICATION_TEST_ID_TYPES } from './constants'; - -Linking.openURL = jest.fn(() => Promise.resolve('opened https://metamask.io!')); - -describe('NotificationActions', () => { - const styles = { - button: {}, - }; - const action = { - actionText: 'Send now!', - actionUrl: PREFIXES[ACTIONS.SEND], - isExternal: false, - }; - const link = { - linkText: 'Learn more', - linkUrl: 'https://metamask.io', - isExternal: true, - }; - - it('matches snapshot', () => { - const { toJSON } = renderWithProvider( - , - ); - expect(toJSON()).toMatchSnapshot(); - }); - - it('matches snapshot with LINK only', () => { - const { toJSON } = renderWithProvider( - , - ); - expect(toJSON()).toMatchSnapshot(); - }); - - it('renders link title', () => { - const { getByText } = renderWithProvider( - , - ); - - expect(getByText('Learn more')).toBeTruthy(); - }); - - it('calls Linking.openURL when link CTA is clicked', async () => { - const { getByTestId } = renderWithProvider( - , - ); - - fireEvent.press( - getByTestId(NOTIFICATION_TEST_ID_TYPES.NOTIFICATION_ACTION_BUTTON), - ); - expect(Linking.openURL).toHaveBeenCalledTimes(1); - }); -}); diff --git a/app/components/UI/Notification/Row/Actions.tsx b/app/components/UI/Notification/Row/Actions.tsx deleted file mode 100644 index fa286dcdb5f..00000000000 --- a/app/components/UI/Notification/Row/Actions.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react'; -import { Linking } from 'react-native'; -import Button, { - ButtonVariants, -} from '../../../../component-library/components/Buttons/Button'; -import { IconName } from '../../../../component-library/components/Icons/Icon'; -import { NOTIFICATION_TEST_ID_TYPES } from './constants'; - -interface NotificationActionsProps { - link?: { - linkText: string; - linkUrl: string; - isExternal: boolean; - }; - action?: { - actionText: string; - actionUrl: string; - isExternal: boolean; - }; - // TODO: Replace "any" with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - styles: any; -} - -function NotificationActions({ - action, - link, - styles, -}: NotificationActionsProps) { - function handleCTAPress() { - if (link?.linkUrl) { - return Linking.openURL(link.linkUrl); - } - } - - return ( - ); diff --git a/app/components/Views/SmartTransactionsOptInModal/SmartTransactionsOptInModal.test.tsx b/app/components/Views/SmartTransactionsOptInModal/SmartTransactionsOptInModal.test.tsx index 50309cc0b0a..82299764f95 100644 --- a/app/components/Views/SmartTransactionsOptInModal/SmartTransactionsOptInModal.test.tsx +++ b/app/components/Views/SmartTransactionsOptInModal/SmartTransactionsOptInModal.test.tsx @@ -1,14 +1,13 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ import React from 'react'; -import { fireEvent, waitFor } from '@testing-library/react-native'; +import { fireEvent } from '@testing-library/react-native'; import SmartTransactionsOptInModal from './SmartTranactionsOptInModal'; import renderWithProvider from '../../../util/test/renderWithProvider'; -import initialBackgroundState from '../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../util/test/initial-root-state'; import { strings } from '../../../../locales/i18n'; import Engine from '../../../core/Engine'; import { shouldShowWhatsNewModal } from '../../../util/onboarding'; import { updateOptInModalAppVersionSeen } from '../../../core/redux/slices/smartTransactions'; -import Routes from '../../../constants/navigation/Routes'; const mockNavigate = jest.fn(); jest.mock('@react-navigation/native', () => { @@ -47,7 +46,7 @@ jest.mock('../../../core/redux/slices/smartTransactions', () => ({ const initialState = { engine: { - backgroundState: initialBackgroundState, + backgroundState, }, }; @@ -79,11 +78,10 @@ describe('SmartTransactionsOptInModal', () => { const primaryButton = getByText(strings('whats_new.stx.primary_button')); expect(primaryButton).toBeDefined(); - const secondaryButton = getByText( - strings('whats_new.stx.secondary_button'), - ); + const secondaryButton = getByText(strings('whats_new.stx.no_thanks')); expect(secondaryButton).toBeDefined(); }); + it('should opt user in when primary button is pressed', () => { const { getByText } = renderWithProvider(, { state: initialState, @@ -96,26 +94,20 @@ describe('SmartTransactionsOptInModal', () => { Engine.context.PreferencesController.setSmartTransactionsOptInStatus, ).toHaveBeenCalledWith(true); }); - it('opts user out when secondary button is pressed and navigate to Advanced Settings', async () => { + + it('opts user out when secondary button is pressed', async () => { const { getByText } = renderWithProvider(, { state: initialState, }); - const secondaryButton = getByText( - strings('whats_new.stx.secondary_button'), - ); + const secondaryButton = getByText(strings('whats_new.stx.no_thanks')); fireEvent.press(secondaryButton); expect( Engine.context.PreferencesController.setSmartTransactionsOptInStatus, ).toHaveBeenCalledWith(false); - expect(updateOptInModalAppVersionSeen).toHaveBeenCalledWith(VERSION); - await waitFor(() => { - expect(mockNavigate).toHaveBeenCalledWith(Routes.SETTINGS_VIEW, { - screen: Routes.SETTINGS.ADVANCED_SETTINGS, - }); - }); }); + it('should update last app version seen on primary button press', () => { const { getByText } = renderWithProvider(, { state: initialState, @@ -126,14 +118,13 @@ describe('SmartTransactionsOptInModal', () => { expect(updateOptInModalAppVersionSeen).toHaveBeenCalledWith(VERSION); }); + it('should update last app version seen on secondary button press', () => { const { getByText } = renderWithProvider(, { state: initialState, }); - const secondaryButton = getByText( - strings('whats_new.stx.secondary_button'), - ); + const secondaryButton = getByText(strings('whats_new.stx.no_thanks')); fireEvent.press(secondaryButton); expect(updateOptInModalAppVersionSeen).toHaveBeenCalledWith(VERSION); diff --git a/app/components/Views/TransactionsView/index.js b/app/components/Views/TransactionsView/index.js index df07b80907f..4f60b3fb2d5 100644 --- a/app/components/Views/TransactionsView/index.js +++ b/app/components/Views/TransactionsView/index.js @@ -28,13 +28,12 @@ import { selectCurrentCurrency, } from '../../../selectors/currencyRateController'; import { selectTokens } from '../../../selectors/tokensController'; -import { WalletViewSelectorsIDs } from '../../../../e2e/selectors/wallet/WalletView.selectors'; -import { selectIdentities } from '../../../selectors/preferencesController'; -import { selectSelectedInternalAccountChecksummedAddress } from '../../../selectors/accountsController'; +import { selectSelectedInternalAccount } from '../../../selectors/accountsController'; import { store } from '../../../store'; import { NETWORK_ID_LOADING } from '../../../core/redux/slices/inpageProvider'; import { selectPendingSmartTransactionsBySender } from '../../../selectors/smartTransactionsController'; import { selectNonReplacedTransactions } from '../../../selectors/transactionController'; +import { toChecksumHexAddress } from '@metamask/controller-utils'; const styles = StyleSheet.create({ wrapper: { @@ -45,8 +44,7 @@ const styles = StyleSheet.create({ const TransactionsView = ({ navigation, conversionRate, - selectedAddress, - identities, + selectedInternalAccount, networkType, currentCurrency, transactions, @@ -58,12 +56,16 @@ const TransactionsView = ({ const [confirmedTxs, setConfirmedTxs] = useState([]); const [loading, setLoading] = useState(); + const selectedAddress = toChecksumHexAddress( + selectedInternalAccount?.address, + ); + const filterTransactions = useCallback( (networkId) => { if (networkId === NETWORK_ID_LOADING) return; let accountAddedTimeInsertPointFound = false; - const addedAccountTime = identities[selectedAddress]?.importTime; + const addedAccountTime = selectedInternalAccount?.metadata.importTime; const submittedTxs = []; const confirmedTxs = []; @@ -141,7 +143,7 @@ const TransactionsView = ({ setConfirmedTxs(confirmedTxs); setLoading(false); }, - [transactions, identities, selectedAddress, tokens, chainId], + [transactions, selectedInternalAccount, selectedAddress, tokens, chainId], ); useEffect(() => { @@ -159,10 +161,7 @@ const TransactionsView = ({ }, [filterTransactions]); return ( - + { - const selectedAddress = - selectSelectedInternalAccountChecksummedAddress(state); const chainId = selectChainId(state); // Remove duplicate confirmed STX @@ -233,8 +226,7 @@ const mapStateToProps = (state) => { conversionRate: selectConversionRate(state), currentCurrency: selectCurrentCurrency(state), tokens: selectTokens(state), - selectedAddress, - identities: selectIdentities(state), + selectedInternalAccount: selectSelectedInternalAccount(state), transactions: [ ...nonReplacedTransactions, ...pendingSmartTransactions, diff --git a/app/components/Views/Wallet/index.test.tsx b/app/components/Views/Wallet/index.test.tsx index ad0765d26c4..9dfd1afc473 100644 --- a/app/components/Views/Wallet/index.test.tsx +++ b/app/components/Views/Wallet/index.test.tsx @@ -8,8 +8,10 @@ import { screen } from '@testing-library/react-native'; import Engine from '../../../core/Engine'; import ScrollableTabView from 'react-native-scrollable-tab-view'; import Routes from '../../../constants/navigation/Routes'; -import initialBackgroundState from '../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../util/test/initial-root-state'; import { createMockAccountsControllerState } from '../../../util/test/accountsControllerTestUtils'; +import { WalletViewSelectorsIDs } from '../../../../e2e/selectors/wallet/WalletView.selectors'; +import { CommonSelectorsIDs } from '../../../../e2e/selectors/Common.selectors'; const mockEngine = Engine; @@ -23,15 +25,6 @@ jest.mock('../../../core/Engine', () => ({ init: () => mockEngine.init({}), getTotalFiatAccountBalance: jest.fn(), context: { - PreferencesController: { - selectedAddress: MOCK_ADDRESS, - identities: { - [MOCK_ADDRESS]: { - name: 'Account 1', - address: MOCK_ADDRESS, - }, - }, - }, NftController: { allNfts: { [MOCK_ADDRESS]: { @@ -93,16 +86,7 @@ const mockInitialState = { }, engine: { backgroundState: { - ...initialBackgroundState, - PreferencesController: { - selectedAddress: MOCK_ADDRESS, - identities: { - [MOCK_ADDRESS]: { - name: 'Account 1', - address: MOCK_ADDRESS, - }, - }, - }, + ...backgroundState, AccountsController: { ...MOCK_ACCOUNTS_CONTROLLER_STATE, }, @@ -156,7 +140,9 @@ describe('Wallet', () => { }); it('should render scan qr icon', () => { render(Wallet); - const scanButton = screen.getByTestId('wallet-scan-button'); + const scanButton = screen.getByTestId( + WalletViewSelectorsIDs.WALLET_SCAN_BUTTON, + ); expect(scanButton).toBeDefined(); }); it('should render ScrollableTabView', () => { @@ -165,7 +151,7 @@ describe('Wallet', () => { }); it('should render fox icon', () => { render(Wallet); - const foxIcon = screen.getByTestId('fox-icon'); + const foxIcon = screen.getByTestId(CommonSelectorsIDs.FOX_ICON); expect(foxIcon).toBeDefined(); }); }); diff --git a/app/components/Views/Wallet/index.tsx b/app/components/Views/Wallet/index.tsx index a77d9d97d81..6314503c508 100644 --- a/app/components/Views/Wallet/index.tsx +++ b/app/components/Views/Wallet/index.tsx @@ -45,7 +45,6 @@ import { getIsNetworkOnboarded, isMainNet, } from '../../../util/networks'; -import generateTestId from '../../../../wdio/utils/generateTestId'; import { selectProviderConfig, selectTicker, @@ -355,12 +354,16 @@ const Wallet = ({ // Fired on the first load of the wallet and also on network switch const checkSmartTransactionsOptInModal = async () => { try { - const showShowStxOptInModal = + const accountHasZeroBalance = hexToBN( + accountBalanceByChainId?.balance || '0x0', + ).isZero(); + const shouldShowStxOptInModal = await shouldShowSmartTransactionsOptInModal( providerConfig.chainId, providerConfig.rpcUrl, + accountHasZeroBalance, ); - if (showShowStxOptInModal) { + if (shouldShowStxOptInModal) { navigation.navigate(Routes.MODAL.ROOT_MODAL_FLOW, { screen: Routes.MODAL.SMART_TRANSACTIONS_OPT_IN, }); @@ -386,6 +389,7 @@ const Wallet = ({ networkOnboardingState, prevChainId, checkNftAutoDetectionModal, + accountBalanceByChainId?.balance, ]); useEffect( @@ -589,7 +593,7 @@ const Wallet = ({ return ( - + {selectedAddress ? renderContent() : renderLoader()} {renderOnboardingWizard()} diff --git a/app/components/Views/WalletActions/WalletActions.test.tsx b/app/components/Views/WalletActions/WalletActions.test.tsx index a9a1c06f6a7..80751b04c04 100644 --- a/app/components/Views/WalletActions/WalletActions.test.tsx +++ b/app/components/Views/WalletActions/WalletActions.test.tsx @@ -6,7 +6,7 @@ import renderWithProvider from '../../../util/test/renderWithProvider'; import WalletActions from './WalletActions'; import { WalletActionsModalSelectorsIDs } from '../../../../e2e/selectors/Modals/WalletActionsModal.selectors'; import Engine from '../../../core/Engine'; -import initialBackgroundState from '../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../util/test/initial-root-state'; const mockEngine = Engine; @@ -24,7 +24,7 @@ const mockInitialState = { }, engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, NetworkController: { providerConfig: { type: 'mainnet', chainId: '0x1', ticker: 'ETH' }, }, @@ -110,7 +110,7 @@ describe('WalletActions', () => { }, engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, NetworkController: { providerConfig: { type: 'mainnet', diff --git a/app/components/Views/WalletActions/WalletActions.tsx b/app/components/Views/WalletActions/WalletActions.tsx index a7cbc6e4d9c..4f093c75ef3 100644 --- a/app/components/Views/WalletActions/WalletActions.tsx +++ b/app/components/Views/WalletActions/WalletActions.tsx @@ -19,7 +19,6 @@ import { toggleReceiveModal } from '../../../actions/modals'; import { isSwapsAllowed } from '../../../components/UI/Swaps/utils'; import isBridgeAllowed from '../../UI/Bridge/utils/isBridgeAllowed'; import useGoToBridge from '../../../components/UI/Bridge/utils/useGoToBridge'; -import Routes from '../../../constants/navigation/Routes'; import { MetaMetricsEvents } from '../../../core/Analytics'; import { getEther } from '../../../util/transactions'; import { newAssetTransaction } from '../../../actions/transaction'; @@ -35,6 +34,10 @@ import { WalletActionsModalSelectorsIDs } from '../../../../e2e/selectors/Modals // Internal dependencies import styleSheet from './WalletActions.styles'; import { useMetrics } from '../../../components/hooks/useMetrics'; +import { + createBuyNavigationDetails, + createSellNavigationDetails, +} from '../../UI/Ramp/routes/utils'; const WalletActions = () => { const { styles } = useStyles(styleSheet, {}); @@ -62,7 +65,7 @@ const WalletActions = () => { const onBuy = () => { sheetRef.current?.onCloseBottomSheet(() => { - navigate(Routes.RAMP.BUY); + navigate(...createBuyNavigationDetails()); trackEvent(MetaMetricsEvents.BUY_BUTTON_CLICKED, { text: 'Buy', location: 'TabBar', @@ -73,7 +76,7 @@ const WalletActions = () => { const onSell = () => { sheetRef.current?.onCloseBottomSheet(() => { - navigate(Routes.RAMP.SELL); + navigate(...createSellNavigationDetails()); trackEvent(MetaMetricsEvents.SELL_BUTTON_CLICKED, { text: 'Sell', location: 'TabBar', diff --git a/app/components/Views/confirmations/Approval/components/TransactionEditor/index.test.tsx b/app/components/Views/confirmations/Approval/components/TransactionEditor/index.test.tsx index 08afd71ef87..cd42b4579e7 100644 --- a/app/components/Views/confirmations/Approval/components/TransactionEditor/index.test.tsx +++ b/app/components/Views/confirmations/Approval/components/TransactionEditor/index.test.tsx @@ -3,12 +3,12 @@ import TransactionEditor from '.'; import configureMockStore from 'redux-mock-store'; import { shallow } from 'enzyme'; import { Provider } from 'react-redux'; -import initialBackgroundState from '../../../../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../../../../util/test/initial-root-state'; const mockStore = configureMockStore(); const initialState = { engine: { - backgroundState: initialBackgroundState, + backgroundState, }, transaction: { value: 0, diff --git a/app/components/Views/confirmations/Approval/index.test.tsx b/app/components/Views/confirmations/Approval/index.test.tsx index 5a56312af6f..d8cfc5060cd 100644 --- a/app/components/Views/confirmations/Approval/index.test.tsx +++ b/app/components/Views/confirmations/Approval/index.test.tsx @@ -3,7 +3,7 @@ import Approval from '.'; import configureMockStore from 'redux-mock-store'; import { shallow } from 'enzyme'; import { Provider } from 'react-redux'; -import initialBackgroundState from '../../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../../util/test/initial-root-state'; const mockStore = configureMockStore(); const initialState = { @@ -21,7 +21,7 @@ const initialState = { assetType: undefined, }, engine: { - backgroundState: initialBackgroundState, + backgroundState, }, }; const store = mockStore(initialState); diff --git a/app/components/Views/confirmations/ApproveView/Approve/index.test.tsx b/app/components/Views/confirmations/ApproveView/Approve/index.test.tsx index dee1384b50e..95ebc3e7d0b 100644 --- a/app/components/Views/confirmations/ApproveView/Approve/index.test.tsx +++ b/app/components/Views/confirmations/ApproveView/Approve/index.test.tsx @@ -3,7 +3,7 @@ import { shallow } from 'enzyme'; import Approve from '.'; import configureMockStore from 'redux-mock-store'; import { Provider } from 'react-redux'; -import initialBackgroundState from '../../../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../../../util/test/initial-root-state'; const mockStore = configureMockStore(); const initialState = { @@ -16,7 +16,7 @@ const initialState = { tabs: [{ id: 1592878266671, url: 'https://metamask.github.io/test-dapp/' }], }, engine: { - backgroundState: initialBackgroundState, + backgroundState, }, }; const store = mockStore(initialState); diff --git a/app/components/Views/confirmations/Send/index.js b/app/components/Views/confirmations/Send/index.js index 6b2bf18baa5..2b98788ba80 100644 --- a/app/components/Views/confirmations/Send/index.js +++ b/app/components/Views/confirmations/Send/index.js @@ -57,12 +57,15 @@ import { selectTokenList } from '../../../../selectors/tokenListController'; import { selectTokens } from '../../../../selectors/tokensController'; import { selectAccounts } from '../../../../selectors/accountTrackerController'; import { selectContractBalances } from '../../../../selectors/tokenBalancesController'; -import { selectIdentities } from '../../../../selectors/preferencesController'; -import { selectSelectedInternalAccountChecksummedAddress } from '../../../../selectors/accountsController'; +import { + selectInternalAccounts, + selectSelectedInternalAccountChecksummedAddress, +} from '../../../../selectors/accountsController'; import { providerErrors } from '@metamask/rpc-errors'; import { withMetricsAwareness } from '../../../../components/hooks/useMetrics'; import { selectShouldUseSmartTransaction } from '../../../../selectors/smartTransactionsController'; import { STX_NO_HASH_ERROR } from '../../../../util/smart-transactions/smart-publish-hook'; +import { toLowerCaseEquals } from '../../../../util/general'; const REVIEW = 'review'; const EDIT = 'edit'; @@ -124,9 +127,9 @@ class Send extends PureComponent { */ chainId: PropTypes.string, /** - * List of accounts from the PreferencesController + * List of accounts from the AccountsController */ - identities: PropTypes.object, + internalAccounts: PropTypes.array, /** * Selected address as string */ @@ -315,7 +318,8 @@ class Send extends PureComponent { function_name = null, // eslint-disable-line no-unused-vars parameters = null, }) => { - const { addressBook, chainId, identities, selectedAddress } = this.props; + const { addressBook, chainId, internalAccounts, selectedAddress } = + this.props; let newTxMeta = {}; let txRecipient; @@ -342,7 +346,7 @@ class Send extends PureComponent { addressBook, chainId, toAddress: newTxMeta.to, - identities, + internalAccounts, ensRecipient: newTxMeta.ensRecipient, }); @@ -382,7 +386,7 @@ class Send extends PureComponent { addressBook, chainId, toAddress: to, - identities, + internalAccounts, ensRecipient, }); break; @@ -416,7 +420,10 @@ class Send extends PureComponent { } newTxMeta.from = selectedAddress; - newTxMeta.transactionFromName = identities[selectedAddress].name; + const fromAccount = internalAccounts.find((account) => + toLowerCaseEquals(account.address, selectedAddress), + ); + newTxMeta.transactionFromName = fromAccount.metadata.name; this.props.setTransactionObject(newTxMeta); this.mounted && this.setState({ ready: true, transactionKey: Date.now() }); }; @@ -781,7 +788,7 @@ const mapStateToProps = (state) => ({ networkType: selectProviderType(state), tokens: selectTokens(state), chainId: selectChainId(state), - identities: selectIdentities(state), + internalAccounts: selectInternalAccounts(state), selectedAddress: selectSelectedInternalAccountChecksummedAddress(state), dappTransactionModalVisible: state.modals.dappTransactionModalVisible, tokenList: selectTokenList(state), diff --git a/app/components/Views/confirmations/Send/index.test.tsx b/app/components/Views/confirmations/Send/index.test.tsx index 6432727b813..e42b8fcc600 100644 --- a/app/components/Views/confirmations/Send/index.test.tsx +++ b/app/components/Views/confirmations/Send/index.test.tsx @@ -4,6 +4,7 @@ import { MOCK_ACCOUNTS_CONTROLLER_STATE, MOCK_ADDRESS_1, } from '../../../../util/test/accountsControllerTestUtils'; +import { MOCK_KEYRING_CONTROLLER } from '../../../../selectors/keyringController/testUtils'; const initialState = { transaction: { @@ -21,7 +22,6 @@ const initialState = { settings: {}, engine: { backgroundState: { - // ...initialBackgroundState, AccountTrackerController: { accounts: { [MOCK_ADDRESS_1]: { @@ -50,15 +50,11 @@ const initialState = { TokenBalancesController: { contractBalances: {}, }, + TokenListController: { + tokenList: [], + }, PreferencesController: { featureFlags: {}, - identities: { - [MOCK_ADDRESS_1]: { - address: MOCK_ADDRESS_1, - name: 'Account 1', - importTime: 1684232000456, - }, - }, ipfsGateway: 'https://cloudflare-ipfs.com/ipfs/', lostIdentities: {}, selectedAddress: MOCK_ADDRESS_1, @@ -76,13 +72,6 @@ const initialState = { _W: { featureFlags: {}, frequentRpcList: [], - identities: { - [MOCK_ADDRESS_1]: { - address: MOCK_ADDRESS_1, - name: 'Account 1', - importTime: 1684232000456, - }, - }, ipfsGateway: 'https://cloudflare-ipfs.com/ipfs/', lostIdentities: {}, selectedAddress: MOCK_ADDRESS_1, @@ -120,6 +109,7 @@ const initialState = { _X: null, }, AccountsController: MOCK_ACCOUNTS_CONTROLLER_STATE, + KeyringController: MOCK_KEYRING_CONTROLLER, NetworkController: { network: '1', providerConfig: { diff --git a/app/components/Views/confirmations/SendFlow/AddressElement/AddressElement.test.tsx b/app/components/Views/confirmations/SendFlow/AddressElement/AddressElement.test.tsx index be16c62fcc2..acc4fe102ba 100644 --- a/app/components/Views/confirmations/SendFlow/AddressElement/AddressElement.test.tsx +++ b/app/components/Views/confirmations/SendFlow/AddressElement/AddressElement.test.tsx @@ -4,7 +4,7 @@ import renderWithProvider from '../../../../../util/test/renderWithProvider'; import AddressElement from '.'; import Engine from '../../../../../core/Engine'; import { renderShortAddress } from '../../../../../util/address'; -import initialBackgroundState from '../../../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../../../util/test/initial-root-state'; const mockEngine = Engine; @@ -30,7 +30,7 @@ jest.mock('../../../../../core/Engine', () => ({ const initialState = { engine: { - backgroundState: initialBackgroundState, + backgroundState, }, }; diff --git a/app/components/Views/confirmations/SendFlow/AddressFrom/AddressFrom.test.tsx b/app/components/Views/confirmations/SendFlow/AddressFrom/AddressFrom.test.tsx index c9ca8d7042c..b1752f5f005 100644 --- a/app/components/Views/confirmations/SendFlow/AddressFrom/AddressFrom.test.tsx +++ b/app/components/Views/confirmations/SendFlow/AddressFrom/AddressFrom.test.tsx @@ -5,7 +5,7 @@ import configureMockStore from 'redux-mock-store'; import { render } from '@testing-library/react-native'; import SendFlowAddressFrom from '.'; -import initialBackgroundState from '../../../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../../../util/test/initial-root-state'; jest.mock('../../../../../util/ENSUtils', () => ({ ...jest.requireActual('../../../../../util/ENSUtils'), @@ -44,7 +44,7 @@ const mockInitialState = { }, engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, AccountTrackerController: { accounts: { '0xd018538C87232FF95acbCe4870629b75640a78E7': { diff --git a/app/components/Views/confirmations/SendFlow/AddressList/AddressList.test.tsx b/app/components/Views/confirmations/SendFlow/AddressList/AddressList.test.tsx index b011a3c69c0..9160a516c29 100644 --- a/app/components/Views/confirmations/SendFlow/AddressList/AddressList.test.tsx +++ b/app/components/Views/confirmations/SendFlow/AddressList/AddressList.test.tsx @@ -1,7 +1,14 @@ import React from 'react'; import renderWithProvider from '../../../../../util/test/renderWithProvider'; -import initialBackgroundState from '../../../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../../../util/test/initial-root-state'; import AddressList from '.'; +import { createMockAccountsControllerState } from '../../../../../util/test/accountsControllerTestUtils'; + +const MOCK_ADDRESS = '0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272'; + +const MOCK_ACCOUNTS_CONTROLLER_STATE = createMockAccountsControllerState([ + MOCK_ADDRESS, +]); jest.mock('../../../../../core/Engine', () => ({ context: { @@ -16,12 +23,12 @@ jest.mock('../../../../../core/Engine', () => ({ const initialState = { engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, AddressBookController: { addressBook: { '0x1': { - '0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272': { - address: '0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272', + [MOCK_ADDRESS]: { + address: MOCK_ADDRESS, chainId: '0x1', isEns: false, memo: '', @@ -30,14 +37,7 @@ const initialState = { }, }, }, - PreferencesController: { - identities: { - '0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272': { - address: '0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272', - name: 'Account 1', - }, - }, - }, + AccountsController: MOCK_ACCOUNTS_CONTROLLER_STATE, }, }, }; diff --git a/app/components/Views/confirmations/SendFlow/AddressList/AddressList.tsx b/app/components/Views/confirmations/SendFlow/AddressList/AddressList.tsx index bb993718ad5..8022c337dc8 100644 --- a/app/components/Views/confirmations/SendFlow/AddressList/AddressList.tsx +++ b/app/components/Views/confirmations/SendFlow/AddressList/AddressList.tsx @@ -16,13 +16,14 @@ import { useTheme } from '../../../../../util/theme'; import Text from '../../../../../component-library/components/Texts/Text/Text'; import { TextVariant } from '../../../../../component-library/components/Texts/Text'; import { selectChainId } from '../../../../../selectors/networkController'; -import { selectIdentities } from '../../../../../selectors/preferencesController'; import { regex } from '../../../../../util/regex'; import { SendViewSelectorsIDs } from '../../../../../../e2e/selectors/SendView.selectors'; +import { selectInternalAccounts } from '../../../../../selectors/accountsController'; // Internal dependencies import { AddressListProps, Contact } from './AddressList.types'; import styleSheet from './AddressList.styles'; +import { toChecksumHexAddress } from '@metamask/controller-utils'; // TODO: Replace "any" with type // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -49,7 +50,7 @@ const AddressList: React.FC = ({ // eslint-disable-next-line @typescript-eslint/no-explicit-any const [fuse, setFuse] = useState(undefined); const chainId = useSelector(selectChainId); - const identities = useSelector(selectIdentities); + const internalAccounts = useSelector(selectInternalAccounts); const addressBook = useSelector( // TODO: Replace "any" with type // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -183,11 +184,11 @@ const AddressList: React.FC = ({ > {strings('onboarding_wizard.step2.title')} - {Object.keys(identities).map((address) => ( + {internalAccounts.map((account) => ( ({ useNavigation: () => ({ @@ -17,7 +17,7 @@ jest.mock('@react-navigation/native', () => ({ const mockInitialState = { settings: {}, engine: { - backgroundState: initialBackgroundState, + backgroundState, }, }; diff --git a/app/components/Views/confirmations/SendFlow/Amount/index.js b/app/components/Views/confirmations/SendFlow/Amount/index.js index 60ef28bd8e1..4004f78d9d8 100644 --- a/app/components/Views/confirmations/SendFlow/Amount/index.js +++ b/app/components/Views/confirmations/SendFlow/Amount/index.js @@ -102,6 +102,7 @@ import { isNetworkRampNativeTokenSupported } from '../../../../../components/UI/ import { withMetricsAwareness } from '../../../../../components/hooks/useMetrics'; import { selectGasFeeEstimates } from '../../../../../selectors/confirmTransaction'; import { selectGasFeeControllerEstimateType } from '../../../../../selectors/gasFeeController'; +import { createBuyNavigationDetails } from '../../../../UI/Ramp/routes/utils'; const KEYBOARD_OFFSET = Device.isSmallDevice() ? 80 : 120; @@ -1277,7 +1278,7 @@ class Amount extends PureComponent { location: 'insufficient_funds_warning', text: 'buy_more', }); - navigation.navigate(Routes.RAMP.BUY); + navigation.navigate(...createBuyNavigationDetails()); } }; diff --git a/app/components/Views/confirmations/SendFlow/Amount/index.test.tsx b/app/components/Views/confirmations/SendFlow/Amount/index.test.tsx index 4ecbb9893e5..093b5f394ec 100644 --- a/app/components/Views/confirmations/SendFlow/Amount/index.test.tsx +++ b/app/components/Views/confirmations/SendFlow/Amount/index.test.tsx @@ -9,7 +9,7 @@ import TransactionTypes from '../../../../../core/TransactionTypes'; import { AmountViewSelectorsIDs } from '../../../../../../e2e/selectors/SendFlow/AmountView.selectors'; -import initialBackgroundState from '../../../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../../../util/test/initial-root-state'; import { createMockAccountsControllerState } from '../../../../../util/test/accountsControllerTestUtils'; const mockEngine = Engine; @@ -83,7 +83,7 @@ const MOCK_ACCOUNTS_CONTROLLER_STATE = createMockAccountsControllerState([ const initialState = { engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, NetworkController: { providerConfig: { ticker: 'ETH', diff --git a/app/components/Views/confirmations/SendFlow/Confirm/components/CustomGasModal/CustomGasModal.test.tsx b/app/components/Views/confirmations/SendFlow/Confirm/components/CustomGasModal/CustomGasModal.test.tsx index f99787275da..d28636f46d0 100644 --- a/app/components/Views/confirmations/SendFlow/Confirm/components/CustomGasModal/CustomGasModal.test.tsx +++ b/app/components/Views/confirmations/SendFlow/Confirm/components/CustomGasModal/CustomGasModal.test.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { fireEvent } from '@testing-library/react-native'; import Engine from '../../../../../../../core/Engine'; -import initialBackgroundState from '../../../../../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../../../../../util/test/initial-root-state'; import renderWithProvider from '../../../../../../../util/test/renderWithProvider'; import CustomGasModal from '.'; @@ -34,7 +34,7 @@ const mockInitialState = { }, engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, }, }, }; diff --git a/app/components/Views/confirmations/SendFlow/Confirm/index.js b/app/components/Views/confirmations/SendFlow/Confirm/index.js index 45705c0f935..2cb28cfc850 100644 --- a/app/components/Views/confirmations/SendFlow/Confirm/index.js +++ b/app/components/Views/confirmations/SendFlow/Confirm/index.js @@ -118,6 +118,7 @@ import { selectCurrentTransactionMetadata, } from '../../../../../selectors/confirmTransaction'; import { selectGasFeeControllerEstimateType } from '../../../../../selectors/gasFeeController'; +import { createBuyNavigationDetails } from '../../../../UI/Ramp/routes/utils'; import { updateTransaction } from '../../../../../util/transaction-controller'; import { selectShouldUseSmartTransaction } from '../../../../../selectors/smartTransactionsController'; import { STX_NO_HASH_ERROR } from '../../../../../util/smart-transactions/smart-publish-hook'; @@ -474,7 +475,9 @@ class Confirm extends PureComponent { id, jsonrpc: '2.0', method: 'eth_sendTransaction', - origin: TransactionTypes.MM, + origin: isPaymentRequest + ? AppConstants.DEEPLINKS.ORIGIN_DEEPLINK + : TransactionTypes.MM, params: [ { from, @@ -1101,7 +1104,7 @@ class Confirm extends PureComponent { buyEth = () => { const { navigation } = this.props; try { - navigation.navigate(Routes.RAMP.BUY); + navigation.navigate(...createBuyNavigationDetails()); } catch (error) { Logger.error(error, 'Navigation: Error when navigating to buy ETH.'); } diff --git a/app/components/Views/confirmations/SendFlow/Confirm/index.test.tsx b/app/components/Views/confirmations/SendFlow/Confirm/index.test.tsx index 32c857af5a5..e2aa33c9c3c 100644 --- a/app/components/Views/confirmations/SendFlow/Confirm/index.test.tsx +++ b/app/components/Views/confirmations/SendFlow/Confirm/index.test.tsx @@ -4,7 +4,7 @@ import { waitFor } from '@testing-library/react-native'; import Confirm from '.'; import { renderScreen } from '../../../../../util/test/renderWithProvider'; import Routes from '../../../../../constants/navigation/Routes'; -import initialBackgroundState from '../../../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../../../util/test/initial-root-state'; import { TESTID_ACCORDION_CONTENT } from '../../../../../component-library/components/Accordions/Accordion/Accordion.constants'; import { FALSE_POSITIVE_REPOST_LINE_TEST_ID } from '../../components/BlockaidBanner/BlockaidBanner.constants'; import { createMockAccountsControllerState } from '../../../../../util/test/accountsControllerTestUtils'; @@ -18,7 +18,7 @@ const MOCK_ACCOUNTS_CONTROLLER_STATE = createMockAccountsControllerState([ const mockInitialState = { engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, NetworkController: { network: '1', providerConfig: { @@ -41,9 +41,6 @@ const mockInitialState = { }, }, PreferencesController: { - identities: { - [MOCK_ADDRESS]: { name: 'Account1' }, - }, securityAlertsEnabled: true, }, KeyringController: { @@ -131,6 +128,11 @@ jest.mock('../../../../../core/Engine', () => ({ }), updateSecurityAlertResponse: jest.fn(), }, + PreferencesController: { + state: { + securityAlertsEnabled: true, + }, + }, }, })); jest.mock('../../../../../util/custom-gas', () => ({ diff --git a/app/components/Views/confirmations/SendFlow/SendTo/SendTo.test.tsx b/app/components/Views/confirmations/SendFlow/SendTo/SendTo.test.tsx index 53eaaa428b1..6c032062a80 100644 --- a/app/components/Views/confirmations/SendFlow/SendTo/SendTo.test.tsx +++ b/app/components/Views/confirmations/SendFlow/SendTo/SendTo.test.tsx @@ -3,12 +3,12 @@ import { shallow } from 'enzyme'; import SendTo from '.'; import configureMockStore from 'redux-mock-store'; import { Provider } from 'react-redux'; -import initialBackgroundState from '../../../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../../../util/test/initial-root-state'; const mockStore = configureMockStore(); const initialState = { engine: { - backgroundState: initialBackgroundState, + backgroundState, }, settings: { primaryCurrency: 'fiat', diff --git a/app/components/Views/confirmations/SendFlow/SendTo/index.js b/app/components/Views/confirmations/SendFlow/SendTo/index.js index 7e26407d988..bbc56008b98 100644 --- a/app/components/Views/confirmations/SendFlow/SendTo/index.js +++ b/app/components/Views/confirmations/SendFlow/SendTo/index.js @@ -48,16 +48,20 @@ import { selectProviderType, selectTicker, } from '../../../../../selectors/networkController'; -import { selectIdentities } from '../../../../../selectors/preferencesController'; -import { selectSelectedInternalAccountChecksummedAddress } from '../../../../../selectors/accountsController'; +import { + selectInternalAccounts, + selectSelectedInternalAccountChecksummedAddress, +} from '../../../../../selectors/accountsController'; import AddToAddressBookWrapper from '../../../../UI/AddToAddressBookWrapper'; -import { isNetworkRampNativeTokenSupported } from '../../../../../components/UI/Ramp/utils'; +import { isNetworkRampNativeTokenSupported } from '../../../../UI/Ramp/utils'; +import { createBuyNavigationDetails } from '../../../../UI/Ramp/routes/utils'; import { getRampNetworks } from '../../../../../reducers/fiatOrders'; import SendFlowAddressFrom from '../AddressFrom'; import SendFlowAddressTo from '../AddressTo'; import { includes } from 'lodash'; import { SendViewSelectorsIDs } from '../../../../../../e2e/selectors/SendView.selectors'; import { withMetricsAwareness } from '../../../../../components/hooks/useMetrics'; +import { toLowerCaseEquals } from '../../../../../util/general'; const dummy = () => true; @@ -87,9 +91,9 @@ class SendFlow extends PureComponent { */ selectedAddress: PropTypes.string, /** - * List of accounts from the PreferencesController + * List of accounts from the AccountsController */ - identities: PropTypes.object, + internalAccounts: PropTypes.array, /** * Current provider ticker */ @@ -217,11 +221,14 @@ class SendFlow extends PureComponent { isAddressSaved = () => { const { toAccount } = this.state; - const { addressBook, chainId, identities } = this.props; + const { addressBook, chainId, internalAccounts } = this.props; const networkAddressBook = addressBook[chainId] || {}; const checksummedAddress = toChecksumAddress(toAccount); return !!( - networkAddressBook[checksummedAddress] || identities[checksummedAddress] + networkAddressBook[checksummedAddress] || + internalAccounts.find((account) => + toLowerCaseEquals(account.address, checksummedAddress), + ) ); }; @@ -303,7 +310,7 @@ class SendFlow extends PureComponent { }; goToBuy = () => { - this.props.navigation.navigate(Routes.RAMP.BUY); + this.props.navigation.navigate(...createBuyNavigationDetails()); this.props.metrics.trackEvent(MetaMetricsEvents.BUY_BUTTON_CLICKED, { button_location: 'Send Flow warning', @@ -354,23 +361,26 @@ class SendFlow extends PureComponent { this.setState({ fromSelectedAddress: address }); }; - getAddressNameFromBookOrIdentities = (toAccount) => { - const { addressBook, identities, chainId } = this.props; + getAddressNameFromBookOrInternalAccounts = (toAccount) => { + const { addressBook, internalAccounts, chainId } = this.props; if (!toAccount) return; const networkAddressBook = addressBook[chainId] || {}; const checksummedAddress = toChecksumAddress(toAccount); + const matchingAccount = internalAccounts.find((account) => + toLowerCaseEquals(account.address, checksummedAddress), + ); return networkAddressBook[checksummedAddress] ? networkAddressBook[checksummedAddress].name - : identities[checksummedAddress] - ? identities[checksummedAddress].name + : matchingAccount + ? matchingAccount.metadata.name : null; }; validateAddressOrENSFromInput = async (toAccount) => { - const { addressBook, identities, chainId } = this.props; + const { addressBook, internalAccounts, chainId } = this.props; const { addressError, toEnsName, @@ -381,12 +391,12 @@ class SendFlow extends PureComponent { errorContinue, isOnlyWarning, confusableCollection, - } = await validateAddressOrENS({ + } = await validateAddressOrENS( toAccount, addressBook, - identities, + internalAccounts, chainId, - }); + ); this.setState({ addressError, @@ -415,7 +425,8 @@ class SendFlow extends PureComponent { }, ); } - const addressName = this.getAddressNameFromBookOrIdentities(toAccount); + const addressName = + this.getAddressNameFromBookOrInternalAccounts(toAccount); /** * If the address is from addressBook or identities @@ -471,7 +482,7 @@ class SendFlow extends PureComponent { const styles = createStyles(colors); const checksummedAddress = toAccount && toChecksumAddress(toAccount); - const existingAddressName = this.getAddressNameFromBookOrIdentities( + const existingAddressName = this.getAddressNameFromBookOrInternalAccounts( toEnsAddressResolved || toAccount, ); const existingContact = @@ -661,7 +672,7 @@ const mapStateToProps = (state) => ({ chainId: selectChainId(state), selectedAddress: selectSelectedInternalAccountChecksummedAddress(state), selectedAsset: state.transaction.selectedAsset, - identities: selectIdentities(state), + internalAccounts: selectInternalAccounts(state), ticker: selectTicker(state), providerType: selectProviderType(state), isPaymentRequest: state.transaction.paymentRequest, diff --git a/app/components/Views/confirmations/components/ApproveTransactionHeader/ApproveTransactionHeader.test.tsx b/app/components/Views/confirmations/components/ApproveTransactionHeader/ApproveTransactionHeader.test.tsx index dd20029ead9..2234a178f97 100644 --- a/app/components/Views/confirmations/components/ApproveTransactionHeader/ApproveTransactionHeader.test.tsx +++ b/app/components/Views/confirmations/components/ApproveTransactionHeader/ApproveTransactionHeader.test.tsx @@ -2,7 +2,7 @@ import React from 'react'; import renderWithProvider from '../../../../../util/test/renderWithProvider'; import ApproveTransactionHeader from '.'; -import initialBackgroundState from '../../../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../../../util/test/initial-root-state'; import { APPROVE_TRANSACTION_ORIGIN_PILL } from './ApproveTransactionHeader.constants'; import { createMockAccountsControllerState } from '../../../../../util/test/accountsControllerTestUtils'; @@ -31,7 +31,7 @@ const mockInitialState = { settings: {}, engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, AccountTrackerController: { accounts: { [MOCK_ADDRESS_1]: { @@ -42,19 +42,6 @@ const mockInitialState = { }, }, }, - PreferencesController: { - selectedAddress: MOCK_ADDRESS_1, - identities: { - [MOCK_ADDRESS_1]: { - address: MOCK_ADDRESS_1, - name: 'Account 1', - }, - [MOCK_ADDRESS_2]: { - address: MOCK_ADDRESS_2, - name: 'Account 2', - }, - }, - }, AccountsController: MOCK_ACCOUNTS_CONTROLLER_STATE, NetworkController: { providerConfig: { diff --git a/app/components/Views/confirmations/components/ApproveTransactionHeader/ApproveTransactionHeader.tsx b/app/components/Views/confirmations/components/ApproveTransactionHeader/ApproveTransactionHeader.tsx index 26e616e27bd..b0749ac8469 100644 --- a/app/components/Views/confirmations/components/ApproveTransactionHeader/ApproveTransactionHeader.tsx +++ b/app/components/Views/confirmations/components/ApproveTransactionHeader/ApproveTransactionHeader.tsx @@ -14,7 +14,6 @@ import { selectNetworkImageSource, selectNetworkName, } from '../../../../../selectors/networkInfos'; -import { selectIdentities } from '../../../../../selectors/preferencesController'; import { getLabelTextByAddress, renderAccountName, @@ -30,6 +29,7 @@ import { } from './ApproveTransactionHeader.constants'; import stylesheet from './ApproveTransactionHeader.styles'; import { ApproveTransactionHeaderI } from './ApproveTransactionHeader.types'; +import { selectInternalAccounts } from '../../../../../selectors/accountsController'; const ApproveTransactionHeader = ({ from, @@ -51,7 +51,7 @@ const ApproveTransactionHeader = ({ const accountsByChainId = useSelector(selectAccountsByChainId); - const identities = useSelector(selectIdentities); + const internalAccounts = useSelector(selectInternalAccounts); const activeAddress = toChecksumAddress(from); const networkName = useSelector(selectNetworkName); @@ -64,7 +64,7 @@ const ApproveTransactionHeader = ({ useEffect(() => { const accountNameVal = activeAddress - ? renderAccountName(activeAddress, identities) + ? renderAccountName(activeAddress, internalAccounts) : ''; const isOriginDeepLinkVal = @@ -84,7 +84,7 @@ const ApproveTransactionHeader = ({ setIsOriginMMSDKRemoteConn( origin.startsWith(AppConstants.MM_SDK.SDK_REMOTE_ORIGIN), ); - }, [accountsByChainId, identities, activeAddress, origin]); + }, [accountsByChainId, internalAccounts, activeAddress, origin]); const networkImage = useSelector(selectNetworkImageSource); diff --git a/app/components/Views/confirmations/components/ApproveTransactionHeader/__snapshots__/ApproveTransactionHeader.test.tsx.snap b/app/components/Views/confirmations/components/ApproveTransactionHeader/__snapshots__/ApproveTransactionHeader.test.tsx.snap index 1a99dd78182..ec15b5fa271 100644 --- a/app/components/Views/confirmations/components/ApproveTransactionHeader/__snapshots__/ApproveTransactionHeader.test.tsx.snap +++ b/app/components/Views/confirmations/components/ApproveTransactionHeader/__snapshots__/ApproveTransactionHeader.test.tsx.snap @@ -295,7 +295,7 @@ exports[`ApproveTransactionHeader should render correctly 1`] = ` "opacity": 0, } } - testID="badge-network" + testID="badgenetwork" > ({}); @@ -52,7 +53,7 @@ const AddNickname = (props: AddNicknameProps) => { providerChainId, providerRpcTarget, addressBook, - identities, + internalAccounts, networkConfigurations, } = props; @@ -73,18 +74,17 @@ const AddNickname = (props: AddNicknameProps) => { }; const validateAddressOrENSFromInput = useCallback(async () => { - const { addressError, errorContinue } = await validateAddressOrENS({ - toAccount: address, + const { addressError, errorContinue } = await validateAddressOrENS( + address, addressBook, - identities, - // TODO: This parameters is effectively ignored, it should be named `chainId` + internalAccounts, providerChainId, - }); + ); setAddressErr(addressError); setErrContinue(errorContinue); setAddressHasError(addressError); - }, [address, addressBook, identities, providerChainId]); + }, [address, addressBook, internalAccounts, providerChainId]); useEffect(() => { validateAddressOrENSFromInput(); @@ -155,11 +155,11 @@ const AddNickname = (props: AddNicknameProps) => { return errorMessage; }; - const hasBlockExplorer = shouldShowBlockExplorer({ + const hasBlockExplorer = shouldShowBlockExplorer( providerType, providerRpcTarget, networkConfigurations, - }); + ); return ( @@ -263,14 +263,12 @@ const AddNickname = (props: AddNicknameProps) => { ); }; -// TODO: Replace "any" with type -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const mapStateToProps = (state: any) => ({ +const mapStateToProps = (state: RootState) => ({ providerType: selectProviderType(state), providerRpcTarget: selectRpcUrl(state), providerChainId: selectChainId(state), addressBook: state.engine.backgroundState.AddressBookController.addressBook, - identities: selectIdentities(state), + internalAccounts: selectInternalAccounts(state), networkConfigurations: selectNetworkConfigurations(state), }); diff --git a/app/components/Views/confirmations/components/ApproveTransactionReview/AddNickname/types.ts b/app/components/Views/confirmations/components/ApproveTransactionReview/AddNickname/types.ts index 3bc8fa80f17..4e7da1e8f0b 100644 --- a/app/components/Views/confirmations/components/ApproveTransactionReview/AddNickname/types.ts +++ b/app/components/Views/confirmations/components/ApproveTransactionReview/AddNickname/types.ts @@ -1,4 +1,8 @@ +import { AddressBookState } from '@metamask/address-book-controller'; +import { NetworkType } from '@metamask/controller-utils'; +import { InternalAccount } from '@metamask/keyring-api'; import type { NetworkState } from '@metamask/network-controller'; +import { Hex } from '@metamask/utils'; export interface AddNicknameProps { closeModal: () => void; @@ -9,19 +13,10 @@ export interface AddNicknameProps { // TODO: Replace "any" with type // eslint-disable-next-line @typescript-eslint/no-explicit-any showModalAlert: (config: any) => void; - providerType: string; - providerChainId: string; + providerType: NetworkType; + providerChainId: Hex; providerNetwork: string; - providerRpcTarget?: string; - addressBook: { - [key: string]: { - address: string; - chainId: string; - memo: string; - name: string; - }; - }; - // TODO: Replace "any" with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - identities: any; + providerRpcTarget: string; + addressBook: AddressBookState['addressBook']; + internalAccounts: InternalAccount[]; } diff --git a/app/components/Views/confirmations/components/ApproveTransactionReview/ShowBlockExplorer/index.tsx b/app/components/Views/confirmations/components/ApproveTransactionReview/ShowBlockExplorer/index.tsx index 601b18837cd..1b9d97bb4f5 100644 --- a/app/components/Views/confirmations/components/ApproveTransactionReview/ShowBlockExplorer/index.tsx +++ b/app/components/Views/confirmations/components/ApproveTransactionReview/ShowBlockExplorer/index.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { SafeAreaView, StyleSheet, View } from 'react-native'; import AntDesignIcon from 'react-native-vector-icons/AntDesign'; -import { WebView } from 'react-native-webview'; +import { WebView } from '@metamask/react-native-webview'; import type { NetworkState } from '@metamask/network-controller'; import Text, { diff --git a/app/components/Views/confirmations/components/ApproveTransactionReview/VerifyContractDetails/VerifyContractDetails.test.tsx b/app/components/Views/confirmations/components/ApproveTransactionReview/VerifyContractDetails/VerifyContractDetails.test.tsx index 323db752d63..371d9cd9b48 100644 --- a/app/components/Views/confirmations/components/ApproveTransactionReview/VerifyContractDetails/VerifyContractDetails.test.tsx +++ b/app/components/Views/confirmations/components/ApproveTransactionReview/VerifyContractDetails/VerifyContractDetails.test.tsx @@ -1,11 +1,11 @@ import React from 'react'; import renderWithProvider from '../../../../../../util/test/renderWithProvider'; import VerifyContractDetails from './VerifyContractDetails'; -import initialBackgroundState from '../../../../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../../../../util/test/initial-root-state'; const initialState = { engine: { - backgroundState: initialBackgroundState, + backgroundState, }, settings: { primaryCurrency: 'ETH', diff --git a/app/components/Views/confirmations/components/ApproveTransactionReview/index.js b/app/components/Views/confirmations/components/ApproveTransactionReview/index.js index b5523c828f1..a4886954040 100644 --- a/app/components/Views/confirmations/components/ApproveTransactionReview/index.js +++ b/app/components/Views/confirmations/components/ApproveTransactionReview/index.js @@ -100,6 +100,7 @@ import TransactionBlockaidBanner from '../TransactionBlockaidBanner/TransactionB import { regex } from '../../../../../util/regex'; import { withMetricsAwareness } from '../../../../../components/hooks/useMetrics'; import { selectShouldUseSmartTransaction } from '../../../../../selectors/smartTransactionsController'; +import { createBuyNavigationDetails } from '../../../../UI/Ramp/routes/utils'; const { ORIGIN_DEEPLINK, ORIGIN_QR_CODE } = AppConstants.DEEPLINKS; const POLLING_INTERVAL_ESTIMATED_L1_FEE = 30000; @@ -815,11 +816,11 @@ class ApproveTransactionReview extends PureComponent { gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET || gasEstimateType === GAS_ESTIMATE_TYPES.NONE; - const hasBlockExplorer = shouldShowBlockExplorer({ + const hasBlockExplorer = shouldShowBlockExplorer( providerType, providerRpcTarget, networkConfigurations, - }); + ); const tokenLabel = `${ tokenName || tokenSymbol || strings(`spend_limit_edition.nft`) @@ -1182,7 +1183,7 @@ class ApproveTransactionReview extends PureComponent { /* this is kinda weird, we have to reject the transaction to collapse the modal */ this.onCancelPress(); try { - navigation.navigate(Routes.RAMP.BUY); + navigation.navigate(...createBuyNavigationDetails()); } catch (error) { Logger.error(error, 'Navigation: Error when navigating to buy ETH.'); } diff --git a/app/components/Views/confirmations/components/ApproveTransactionReview/index.test.tsx b/app/components/Views/confirmations/components/ApproveTransactionReview/index.test.tsx index 0298c258c7c..6801b3f4392 100644 --- a/app/components/Views/confirmations/components/ApproveTransactionReview/index.test.tsx +++ b/app/components/Views/confirmations/components/ApproveTransactionReview/index.test.tsx @@ -1,5 +1,5 @@ import ApproveTransactionModal from '.'; -import initialBackgroundState from '../../../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../../../util/test/initial-root-state'; import { renderScreen } from '../../../../../util/test/renderWithProvider'; import mockedEngine from '../../../../../core/__mocks__/MockedEngine'; import { SET_APPROVAL_FOR_ALL_SIGNATURE } from '../../../../../util/transactions'; @@ -42,7 +42,7 @@ const transaction = { const initialState = { engine: { - backgroundState: initialBackgroundState, + backgroundState, }, transaction, settings: { diff --git a/app/components/Views/confirmations/components/BlockaidBanner/BlockaidBanner.styles.ts b/app/components/Views/confirmations/components/BlockaidBanner/BlockaidBanner.styles.ts index fcb30529658..e62e15af4d2 100644 --- a/app/components/Views/confirmations/components/BlockaidBanner/BlockaidBanner.styles.ts +++ b/app/components/Views/confirmations/components/BlockaidBanner/BlockaidBanner.styles.ts @@ -36,16 +36,9 @@ const styleSheet = (_params: { marginBottom: 20, }, bannerSectionSmall: { - flexDirection: 'row', alignItems: 'center', - borderWidth: 1, - borderRadius: 4, - borderColor: _params.theme.colors.border.default, - marginTop: 20, - marginLeft: 10, - marginRight: 10, - marginBottom: 20, - padding: 10, + marginTop: 10, + marginBottom: 10, }, bannerSectionSmallSpaced: { flexDirection: 'row', diff --git a/app/components/Views/confirmations/components/BlockaidBanner/BlockaidBanner.test.tsx b/app/components/Views/confirmations/components/BlockaidBanner/BlockaidBanner.test.tsx index 34b90c98aa6..c782fa68445 100644 --- a/app/components/Views/confirmations/components/BlockaidBanner/BlockaidBanner.test.tsx +++ b/app/components/Views/confirmations/components/BlockaidBanner/BlockaidBanner.test.tsx @@ -12,7 +12,6 @@ import renderWithProvider from '../../../../../util/test/renderWithProvider'; jest.mock('../../../../../util/blockaid', () => ({ isBlockaidFeatureEnabled: jest.fn().mockReturnValue(true), - isBlockaidSupportedOnCurrentChain: jest.fn().mockReturnValue(true), })); jest.mock('react-native-gzip', () => ({ @@ -202,16 +201,19 @@ describe('BlockaidBanner', () => { }); it('should render loader if reason is requestInProgress', async () => { - const wrapper = renderWithProvider(, { - state: mockState, - }); + const wrapper = renderWithProvider( + , + { + state: mockState, + }, + ); expect(wrapper).toMatchSnapshot(); - expect( - await wrapper.queryByText( - 'We’re still evaluating the safety of this request. Wait or proceed with caution.', - ), - ).toBeDefined(); }); it('should not render if resultType is benign', async () => { diff --git a/app/components/Views/confirmations/components/BlockaidBanner/BlockaidBanner.tsx b/app/components/Views/confirmations/components/BlockaidBanner/BlockaidBanner.tsx index 564f3d268a8..3747ac45c9b 100644 --- a/app/components/Views/confirmations/components/BlockaidBanner/BlockaidBanner.tsx +++ b/app/components/Views/confirmations/components/BlockaidBanner/BlockaidBanner.tsx @@ -1,6 +1,5 @@ import React, { useEffect, useState } from 'react'; import { ActivityIndicator } from 'react-native'; -import { useSelector } from 'react-redux'; import { View } from 'react-native-animatable'; import { captureException } from '@sentry/react-native'; import { deflate } from 'react-native-gzip'; @@ -11,17 +10,9 @@ import Accordion from '../../../../../component-library/components/Accordions/Ac import { BannerAlertSeverity } from '../../../../../component-library/components/Banners/Banner'; import { DEFAULT_BANNERBASE_DESCRIPTION_TEXTVARIANT } from '../../../../../component-library/components/Banners/Banner/foundation/BannerBase/BannerBase.constants'; import BannerAlert from '../../../../../component-library/components/Banners/Banner/variants/BannerAlert/BannerAlert'; -import { - IconName, - IconSize, -} from '../../../../../component-library/components/Icons/Icon'; -import Icon from '../../../../../component-library/components/Icons/Icon/Icon'; import Text from '../../../../../component-library/components/Texts/Text/Text'; import { useStyles } from '../../../../../component-library/hooks/useStyles'; -import { - isBlockaidFeatureEnabled, - isBlockaidSupportedOnCurrentChain, -} from '../../../../../util/blockaid'; +import { isBlockaidFeatureEnabled } from '../../../../../util/blockaid'; import { FALSE_POSITIVE_REPOST_LINE_TEST_ID, REASON_DESCRIPTION_I18N_KEY_MAP, @@ -39,11 +30,7 @@ import { UTM_SOURCE, } from '../../../../../constants/urls'; import { BLOCKAID_SUPPORTED_NETWORK_NAMES } from '../../../../../util/networks'; -import { selectIsSecurityAlertsEnabled } from '../../../../../selectors/preferencesController'; import BlockaidVersionInfo from '../../../../../lib/ppom/blockaid-version'; -import ButtonIcon, { - ButtonIconSizes, -} from '../../../../../component-library/components/Buttons/ButtonIcon'; import { WALLET_CONNECT_ORIGIN } from '../../../../../util/walletconnect'; import AppConstants from '../../../../../core/AppConstants'; @@ -72,15 +59,7 @@ const BlockaidBanner = (bannerProps: BlockaidBannerProps) => { onContactUsClicked, } = bannerProps; const { styles, theme } = useStyles(styleSheet, { style }); - const [displayPositiveResponse, setDisplayPositiveResponse] = useState(false); const [reportUrl, setReportUrl] = useState(''); - const isSecurityAlertsEnabled = useSelector(selectIsSecurityAlertsEnabled); - - useEffect(() => { - if (securityAlertResponse?.reason === Reason.requestInProgress) { - setDisplayPositiveResponse(true); - } - }, [securityAlertResponse]); useEffect(() => { if (!securityAlertResponse) { @@ -115,12 +94,7 @@ const BlockaidBanner = (bannerProps: BlockaidBannerProps) => { })(); }, [securityAlertResponse]); - if ( - !securityAlertResponse || - !isBlockaidFeatureEnabled() || - !isBlockaidSupportedOnCurrentChain() || - !isSecurityAlertsEnabled - ) { + if (!securityAlertResponse || !isBlockaidFeatureEnabled()) { return null; } @@ -128,33 +102,15 @@ const BlockaidBanner = (bannerProps: BlockaidBannerProps) => { if (securityAlertResponse.reason === Reason.requestInProgress) { return ( - - - - {strings('blockaid_banner.loading_title')} - - + ); } if (result_type === ResultType.Benign) { - if (displayPositiveResponse) { - return ( - - - - - {strings('blockaid_banner.loading_complete_title')} - - - setDisplayPositiveResponse(false)} - iconName={IconName.Close} - /> - - ); - } return null; } diff --git a/app/components/Views/confirmations/components/BlockaidBanner/__snapshots__/BlockaidBanner.test.tsx.snap b/app/components/Views/confirmations/components/BlockaidBanner/__snapshots__/BlockaidBanner.test.tsx.snap index 6ce4bb3545b..d41fa642f2e 100644 --- a/app/components/Views/confirmations/components/BlockaidBanner/__snapshots__/BlockaidBanner.test.tsx.snap +++ b/app/components/Views/confirmations/components/BlockaidBanner/__snapshots__/BlockaidBanner.test.tsx.snap @@ -467,7 +467,19 @@ exports[`BlockaidBanner should render correctly with reason "raw_signature_farmi `; -exports[`BlockaidBanner should render loader if reason is requestInProgress 1`] = `null`; +exports[`BlockaidBanner should render loader if reason is requestInProgress 1`] = ` + +`; exports[`BlockaidBanner should render normal banner alert if resultType is failed 1`] = ` ({ engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, GasFeeController: { gasEstimateType: txnType, }, diff --git a/app/components/Views/confirmations/components/MessageSign/index.test.tsx b/app/components/Views/confirmations/components/MessageSign/index.test.tsx index 780206ccfba..168133cce27 100644 --- a/app/components/Views/confirmations/components/MessageSign/index.test.tsx +++ b/app/components/Views/confirmations/components/MessageSign/index.test.tsx @@ -6,7 +6,7 @@ import NotificationManager from '../../../../../core/NotificationManager'; import { InteractionManager } from 'react-native'; import AppConstants from '../../../../../core/AppConstants'; import { strings } from '../../../../../../locales/i18n'; -import initialBackgroundState from '../../../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../../../util/test/initial-root-state'; import renderWithProvider from '../../../../../util/test/renderWithProvider'; import { act, waitFor } from '@testing-library/react-native'; // eslint-disable-next-line import/no-namespace @@ -94,7 +94,7 @@ const messageParamsMock = { const initialState = { engine: { - backgroundState: initialBackgroundState, + backgroundState, }, signatureRequest: { securityAlertResponse: { diff --git a/app/components/Views/confirmations/components/PersonalSign/index.test.tsx b/app/components/Views/confirmations/components/PersonalSign/index.test.tsx index 7051c54d037..6502a57e4eb 100644 --- a/app/components/Views/confirmations/components/PersonalSign/index.test.tsx +++ b/app/components/Views/confirmations/components/PersonalSign/index.test.tsx @@ -10,7 +10,7 @@ import NotificationManager from '../../../../../core/NotificationManager'; import { InteractionManager } from 'react-native'; import AppConstants from '../../../../../core/AppConstants'; import { strings } from '../../../../../../locales/i18n'; -import initialBackgroundState from '../../../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../../../util/test/initial-root-state'; import { useMetrics } from '../../../../../components/hooks/useMetrics'; jest.mock('../../../../../components/hooks/useMetrics'); @@ -28,6 +28,11 @@ jest.mock('../../../../../core/Engine', () => ({ keyrings: [], }, }, + PreferencesController: { + state: { + securityAlertsEnabled: true, + }, + }, }, })); @@ -48,7 +53,7 @@ const mockStore = configureMockStore(); const initialState = { engine: { - backgroundState: initialBackgroundState, + backgroundState, }, }; diff --git a/app/components/Views/confirmations/components/SignatureRequest/Root/Root.test.tsx b/app/components/Views/confirmations/components/SignatureRequest/Root/Root.test.tsx index c326484baba..51308a69eeb 100644 --- a/app/components/Views/confirmations/components/SignatureRequest/Root/Root.test.tsx +++ b/app/components/Views/confirmations/components/SignatureRequest/Root/Root.test.tsx @@ -2,7 +2,7 @@ import React from 'react'; import configureMockStore from 'redux-mock-store'; import { Provider } from 'react-redux'; import { render } from '@testing-library/react-native'; -import initialBackgroundState from '../../../../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../../../../util/test/initial-root-state'; import { ThemeContext, mockTheme } from '../../../../../../util/theme'; @@ -67,7 +67,7 @@ const initialState = { signatureRequest: {}, engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, AccountTrackerController: { accounts: { '0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272': { @@ -75,15 +75,6 @@ const initialState = { }, }, }, - PreferencesController: { - selectedAddress: '0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272', - identities: { - '0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272': { - address: '0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272', - name: 'Account 1', - }, - }, - }, CurrencyRateController: { currentCurrency: 'usd', currencyRates: { diff --git a/app/components/Views/confirmations/components/SignatureRequest/index.test.tsx b/app/components/Views/confirmations/components/SignatureRequest/index.test.tsx index 349b1df2a97..d63ddc6280f 100644 --- a/app/components/Views/confirmations/components/SignatureRequest/index.test.tsx +++ b/app/components/Views/confirmations/components/SignatureRequest/index.test.tsx @@ -3,12 +3,12 @@ import { shallow } from 'enzyme'; import SignatureRequest from '.'; import configureMockStore from 'redux-mock-store'; import { Provider } from 'react-redux'; -import initialBackgroundState from '../../../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../../../util/test/initial-root-state'; const mockStore = configureMockStore(); const initialState = { engine: { - backgroundState: initialBackgroundState, + backgroundState, }, }; const store = mockStore(initialState); diff --git a/app/components/Views/confirmations/components/TransactionBlockaidBanner/TransactionBlockaidBanner.test.tsx b/app/components/Views/confirmations/components/TransactionBlockaidBanner/TransactionBlockaidBanner.test.tsx index b97644ac494..ab839a17d20 100644 --- a/app/components/Views/confirmations/components/TransactionBlockaidBanner/TransactionBlockaidBanner.test.tsx +++ b/app/components/Views/confirmations/components/TransactionBlockaidBanner/TransactionBlockaidBanner.test.tsx @@ -7,9 +7,14 @@ import { TESTID_ACCORDIONHEADER } from '../../../../../component-library/compone import { ResultType, Reason } from '../BlockaidBanner/BlockaidBanner.types'; import TransactionBlockaidBanner from './TransactionBlockaidBanner'; -jest.mock('../../../../../util/blockaid', () => ({ - isBlockaidFeatureEnabled: jest.fn().mockReturnValue(true), - isBlockaidSupportedOnCurrentChain: jest.fn().mockReturnValue(true), +jest.mock('../../../../../core/Engine', () => ({ + context: { + PreferencesController: { + state: { + securityAlertsEnabled: true, + }, + }, + }, })); jest.mock('react-native-gzip', () => ({ diff --git a/app/components/Views/confirmations/components/TransactionReview/TransactionReviewData/index.test.tsx b/app/components/Views/confirmations/components/TransactionReview/TransactionReviewData/index.test.tsx index f56892d81f3..735e776b82a 100644 --- a/app/components/Views/confirmations/components/TransactionReview/TransactionReviewData/index.test.tsx +++ b/app/components/Views/confirmations/components/TransactionReview/TransactionReviewData/index.test.tsx @@ -3,12 +3,12 @@ import TransactionReviewData from '.'; import configureMockStore from 'redux-mock-store'; import { shallow } from 'enzyme'; import { Provider } from 'react-redux'; -import initialBackgroundState from '../../../../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../../../../util/test/initial-root-state'; const mockStore = configureMockStore(); const initialState = { engine: { - backgroundState: initialBackgroundState, + backgroundState, }, transaction: { transaction: { diff --git a/app/components/Views/confirmations/components/TransactionReview/TransactionReviewDetailsCard/index.test.js b/app/components/Views/confirmations/components/TransactionReview/TransactionReviewDetailsCard/index.test.js index a18a5da4918..e220c85f339 100644 --- a/app/components/Views/confirmations/components/TransactionReview/TransactionReviewDetailsCard/index.test.js +++ b/app/components/Views/confirmations/components/TransactionReview/TransactionReviewDetailsCard/index.test.js @@ -3,12 +3,12 @@ import TransactionReviewDetailsCard from '.'; import { shallow } from 'enzyme'; import configureMockStore from 'redux-mock-store'; import { Provider } from 'react-redux'; -import initialBackgroundState from '../../../../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../../../../util/test/initial-root-state'; const mockStore = configureMockStore(); const initialState = { engine: { - backgroundState: initialBackgroundState, + backgroundState, }, }; const store = mockStore(initialState); diff --git a/app/components/Views/confirmations/components/TransactionReview/TransactionReviewEIP1559/index.test.tsx b/app/components/Views/confirmations/components/TransactionReview/TransactionReviewEIP1559/index.test.tsx index c30705d191e..4a2ea9aeb5b 100644 --- a/app/components/Views/confirmations/components/TransactionReview/TransactionReviewEIP1559/index.test.tsx +++ b/app/components/Views/confirmations/components/TransactionReview/TransactionReviewEIP1559/index.test.tsx @@ -1,11 +1,11 @@ import React from 'react'; import TransactionReviewEIP1559 from '.'; import renderWithProvider from '../../../../../../util/test/renderWithProvider'; -import initialBackgroundState from '../../../../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../../../../util/test/initial-root-state'; const initialState = { engine: { - backgroundState: initialBackgroundState, + backgroundState, }, }; diff --git a/app/components/Views/confirmations/components/TransactionReview/TransactionReviewEIP1559Update/index.test.tsx b/app/components/Views/confirmations/components/TransactionReview/TransactionReviewEIP1559Update/index.test.tsx index b065a8fbc70..780aba9cc12 100644 --- a/app/components/Views/confirmations/components/TransactionReview/TransactionReviewEIP1559Update/index.test.tsx +++ b/app/components/Views/confirmations/components/TransactionReview/TransactionReviewEIP1559Update/index.test.tsx @@ -4,13 +4,13 @@ import renderWithProvider, { renderHookWithProvider, } from '../../../../../../util/test/renderWithProvider'; import TransactionReviewEIP1559 from '.'; -import initialBackgroundState from '../../../../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../../../../util/test/initial-root-state'; const initialState = { settings: {}, engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, AccountTrackerController: { accounts: { '0x0': { diff --git a/app/components/Views/confirmations/components/TransactionReview/TransactionReviewInformation/index.js b/app/components/Views/confirmations/components/TransactionReview/TransactionReviewInformation/index.js index a7c565fb357..ec92704823e 100644 --- a/app/components/Views/confirmations/components/TransactionReview/TransactionReviewInformation/index.js +++ b/app/components/Views/confirmations/components/TransactionReview/TransactionReviewInformation/index.js @@ -60,7 +60,7 @@ import { selectContractExchangeRates } from '../../../../../../selectors/tokenRa import { createBrowserNavDetails } from '../../../../Browser'; import { isNetworkRampNativeTokenSupported } from '../../../../../../components/UI/Ramp/utils'; import { getRampNetworks } from '../../../../../../reducers/fiatOrders'; -import Routes from '../../../../../../constants/navigation/Routes'; +import { createBuyNavigationDetails } from '../../../../../UI/Ramp/routes/utils'; import { withMetricsAwareness } from '../../../../../../components/hooks/useMetrics'; import { selectShouldUseSmartTransaction } from '../../../../../../selectors/smartTransactionsController'; @@ -303,7 +303,7 @@ class TransactionReviewInformation extends PureComponent { /* this is kinda weird, we have to reject the transaction to collapse the modal */ this.onCancelPress(); try { - navigation.navigate(Routes.RAMP.BUY); + navigation.navigate(...createBuyNavigationDetails()); } catch (error) { Logger.error(error, 'Navigation: Error when navigating to buy ETH.'); } diff --git a/app/components/Views/confirmations/components/TransactionReview/TransactionReviewInformation/index.test.tsx b/app/components/Views/confirmations/components/TransactionReview/TransactionReviewInformation/index.test.tsx index 5232ae17c34..f64b601408f 100644 --- a/app/components/Views/confirmations/components/TransactionReview/TransactionReviewInformation/index.test.tsx +++ b/app/components/Views/confirmations/components/TransactionReview/TransactionReviewInformation/index.test.tsx @@ -3,12 +3,12 @@ import TransactionReviewInformation from '.'; import configureMockStore from 'redux-mock-store'; import { shallow } from 'enzyme'; import { Provider } from 'react-redux'; -import initialBackgroundState from '../../../../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../../../../util/test/initial-root-state'; const mockStore = configureMockStore(); const initialState = { engine: { - backgroundState: initialBackgroundState, + backgroundState, }, transaction: { value: '', diff --git a/app/components/Views/confirmations/components/TransactionReview/TransactionReviewSummary/index.test.tsx b/app/components/Views/confirmations/components/TransactionReview/TransactionReviewSummary/index.test.tsx index b177e75ecde..f557753735d 100644 --- a/app/components/Views/confirmations/components/TransactionReview/TransactionReviewSummary/index.test.tsx +++ b/app/components/Views/confirmations/components/TransactionReview/TransactionReviewSummary/index.test.tsx @@ -3,12 +3,12 @@ import TransactionReviewSummary from '.'; import configureMockStore from 'redux-mock-store'; import { shallow } from 'enzyme'; import { Provider } from 'react-redux'; -import initialBackgroundState from '../../../../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../../../../util/test/initial-root-state'; const mockStore = configureMockStore(); const initialState = { engine: { - backgroundState: initialBackgroundState, + backgroundState, }, settings: { showHexData: true, diff --git a/app/components/Views/confirmations/components/TransactionReview/__snapshots__/index.test.tsx.snap b/app/components/Views/confirmations/components/TransactionReview/__snapshots__/index.test.tsx.snap index 40f0061f256..ec7e0a2c76a 100644 --- a/app/components/Views/confirmations/components/TransactionReview/__snapshots__/index.test.tsx.snap +++ b/app/components/Views/confirmations/components/TransactionReview/__snapshots__/index.test.tsx.snap @@ -235,7 +235,7 @@ exports[`TransactionReview should match snapshot 1`] = ` "opacity": 0, } } - testID="badge-network" + testID="badgenetwork" > ({ ], }, }, + PreferencesController: { + state: { + securityAlertsEnabled: true, + }, + }, }, })); @@ -86,7 +91,7 @@ jest.mock('react-native-gzip', () => ({ const mockState = { engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, AccountTrackerController: { accounts: { [MOCK_ADDRESS_1]: { @@ -95,12 +100,6 @@ const mockState = { }, }, PreferencesController: { - selectedAddress: MOCK_ADDRESS_3, - identities: { - [MOCK_ADDRESS_1]: { name: 'Account 1' }, - [MOCK_ADDRESS_2]: { name: 'Account 2' }, - [MOCK_ADDRESS_3]: { name: 'Account 3' }, - }, securityAlertsEnabled: true, }, AccountsController: MOCK_ACCOUNTS_CONTROLLER_STATE, diff --git a/app/components/Views/confirmations/components/TypedSign/__snapshots__/index.test.tsx.snap b/app/components/Views/confirmations/components/TypedSign/__snapshots__/index.test.tsx.snap index cbe02c8942f..6ccbf64873c 100644 --- a/app/components/Views/confirmations/components/TypedSign/__snapshots__/index.test.tsx.snap +++ b/app/components/Views/confirmations/components/TypedSign/__snapshots__/index.test.tsx.snap @@ -304,7 +304,7 @@ exports[`TypedSign onConfirm signs message 1`] = ` "opacity": 0, } } - testID="badge-network" + testID="badgenetwork" > - 0xC495...D272 + Account 1 @@ -975,7 +975,7 @@ exports[`TypedSign onReject rejects message 1`] = ` "opacity": 0, } } - testID="badge-network" + testID="badgenetwork" > - 0xC495...D272 + Account 1 diff --git a/app/components/Views/confirmations/components/TypedSign/index.test.tsx b/app/components/Views/confirmations/components/TypedSign/index.test.tsx index f5bc2e32bcf..dcf6497258d 100644 --- a/app/components/Views/confirmations/components/TypedSign/index.test.tsx +++ b/app/components/Views/confirmations/components/TypedSign/index.test.tsx @@ -9,7 +9,7 @@ import { WALLET_CONNECT_ORIGIN } from '../../../../../util/walletconnect'; import { InteractionManager } from 'react-native'; import { strings } from '../../../../../../locales/i18n'; import AppConstants from '../../../../../core/AppConstants'; -import initialBackgroundState from '../../../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../../../util/test/initial-root-state'; import renderWithProvider from '../../../../../util/test/renderWithProvider'; import { fireEvent, waitFor } from '@testing-library/react-native'; import { MetaMetrics } from '../../../../../core/Analytics'; @@ -41,6 +41,11 @@ jest.mock('../../../../../core/Engine', () => ({ removeListener: jest.fn(), }, }, + PreferencesController: { + state: { + securityAlertsEnabled: true, + }, + }, }, controllerMessenger: { subscribe: jest.fn(), @@ -66,7 +71,7 @@ const mockStore = configureMockStore(); const initialState = { engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, AccountsController: MOCK_ACCOUNTS_CONTROLLER_STATE, }, }, diff --git a/app/components/Views/confirmations/components/UpdateEIP1559Tx/index.tsx b/app/components/Views/confirmations/components/UpdateEIP1559Tx/index.tsx index d105475bf96..794a37e81df 100644 --- a/app/components/Views/confirmations/components/UpdateEIP1559Tx/index.tsx +++ b/app/components/Views/confirmations/components/UpdateEIP1559Tx/index.tsx @@ -1,10 +1,14 @@ -/* eslint-disable no-mixed-spaces-and-tabs */ import React, { useState, useEffect, useCallback, useRef } from 'react'; import EditGasFee1559Update from '../EditGasFee1559Update'; import { connect } from 'react-redux'; import { CANCEL_RATE, SPEED_UP_RATE } from '@metamask/transaction-controller'; import { GAS_ESTIMATE_TYPES } from '@metamask/gas-fee-controller'; -import { hexToBN, fromWei, renderFromWei } from '../../../../../util/number'; +import { + hexToBN, + fromWei, + renderFromWei, + addHexPrefix, +} from '../../../../../util/number'; import BigNumber from 'bignumber.js'; import { getTicker } from '../../../../../util/transactions'; import AppConstants from '../../../../../core/AppConstants'; @@ -24,6 +28,7 @@ import { selectSelectedInternalAccountChecksummedAddress } from '../../../../../ import { getDecimalChainId } from '../../../../../util/networks'; import { selectGasFeeEstimates } from '../../../../../selectors/confirmTransaction'; import { selectGasFeeControllerEstimateType } from '../../../../../selectors/gasFeeController'; +import { isHexString } from '@metamask/utils'; const UpdateEIP1559Tx = ({ gas, @@ -101,13 +106,14 @@ const UpdateEIP1559Tx = ({ const validateAmount = useCallback( (updateTx) => { let error; + const totalMaxHexPrefixed = addHexPrefix(updateTx.totalMaxHex); - if (isNaN(updateTx.totalMaxHex)) { - return strings('invalid_amount'); + if (!isHexString(totalMaxHexPrefixed)) { + return strings('transaction.invalid_amount'); } // TODO: Replace "any" with type // eslint-disable-next-line @typescript-eslint/no-explicit-any - const updateTxCost: any = hexToBN(`0x${updateTx.totalMaxHex}`); + const updateTxCost: any = hexToBN(totalMaxHexPrefixed); // TODO: Replace "any" with type // eslint-disable-next-line @typescript-eslint/no-explicit-any const accountBalance: any = hexToBN(accounts[selectedAddress].balance); diff --git a/app/components/Views/confirmations/components/WatchAssetRequest/index.test.tsx b/app/components/Views/confirmations/components/WatchAssetRequest/index.test.tsx index 8b7796754e6..04c294bc56e 100644 --- a/app/components/Views/confirmations/components/WatchAssetRequest/index.test.tsx +++ b/app/components/Views/confirmations/components/WatchAssetRequest/index.test.tsx @@ -3,12 +3,12 @@ import { shallow } from 'enzyme'; import WatchAssetRequest from '.'; import configureMockStore from 'redux-mock-store'; import { Provider } from 'react-redux'; -import initialBackgroundState from '../../../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../../../util/test/initial-root-state'; const mockStore = configureMockStore(); const initialState = { engine: { - backgroundState: initialBackgroundState, + backgroundState, }, }; const store = mockStore(initialState); diff --git a/app/components/hooks/DisplayName/useTokenList.test.ts b/app/components/hooks/DisplayName/useTokenList.test.ts index 856ed1c042d..acd96d4e973 100644 --- a/app/components/hooks/DisplayName/useTokenList.test.ts +++ b/app/components/hooks/DisplayName/useTokenList.test.ts @@ -1,19 +1,19 @@ import React from 'react'; -import { type TokenListMap } from '@metamask/assets-controllers'; +import { type TokenListToken } from '@metamask/assets-controllers'; import { selectChainId } from '../../../selectors/networkController'; import { selectUseTokenDetection } from '../../../selectors/preferencesController'; -import { selectTokenList } from '../../../selectors/tokenListController'; +import { selectTokenListArray } from '../../../selectors/tokenListController'; import { isMainnetByChainId } from '../../../util/networks'; import useTokenList from './useTokenList'; const MAINNET_TOKEN_ADDRESS_MOCK = '0xdAC17F958D2ee523a2206206994597C13D831ec7'; const MAINNET_TOKEN_NAME_MOCK = 'Tether USD'; -const normalizedMainnetTokenListMock = { - [MAINNET_TOKEN_ADDRESS_MOCK.toLowerCase()]: { +const normalizedMainnetTokenListMock = [ + { name: MAINNET_TOKEN_NAME_MOCK, }, -}; +]; jest.mock('@metamask/contract-metadata', () => ({ __esModule: true, default: { @@ -38,7 +38,7 @@ jest.mock('../../../selectors/preferencesController', () => ({ })); jest.mock('../../../selectors/tokenListController', () => ({ - selectTokenList: jest.fn(), + selectTokenListArray: jest.fn(), })); jest.mock('../../../util/networks', () => ({ @@ -48,27 +48,29 @@ jest.mock('../../../util/networks', () => ({ const CHAIN_ID_MOCK = '0x1'; const TOKEN_NAME_MOCK = 'MetaMask Token'; const TOKEN_ADDRESS_MOCK = '0x0439e60F02a8900a951603950d8D4527f400C3f1'; -const TOKEN_LIST_MOCK = { - [TOKEN_ADDRESS_MOCK]: { +const TOKEN_LIST_ARRAY_MOCK = [ + { name: TOKEN_NAME_MOCK, + address: TOKEN_ADDRESS_MOCK, }, -} as unknown as TokenListMap; -const normalizedTokenListMock = { - [TOKEN_ADDRESS_MOCK.toLowerCase()]: { +] as unknown as TokenListToken[]; +const normalizedTokenListMock = [ + { + address: TOKEN_ADDRESS_MOCK, name: TOKEN_NAME_MOCK, }, -}; +]; describe('useTokenList', () => { const selectChainIdMock = jest.mocked(selectChainId); const selectUseTokenDetectionMock = jest.mocked(selectUseTokenDetection); - const selectTokenListMock = jest.mocked(selectTokenList); + const selectTokenListArrayMock = jest.mocked(selectTokenListArray); const isMainnetByChainIdMock = jest.mocked(isMainnetByChainId); beforeEach(() => { jest.resetAllMocks(); selectChainIdMock.mockReturnValue(CHAIN_ID_MOCK); selectUseTokenDetectionMock.mockReturnValue(true); - selectTokenListMock.mockReturnValue(TOKEN_LIST_MOCK); + selectTokenListArrayMock.mockReturnValue(TOKEN_LIST_ARRAY_MOCK); isMainnetByChainIdMock.mockReturnValue(true); const memoizedValues = new Map(); diff --git a/app/components/hooks/DisplayName/useTokenList.ts b/app/components/hooks/DisplayName/useTokenList.ts index 6726fffbbf3..3e99976658e 100644 --- a/app/components/hooks/DisplayName/useTokenList.ts +++ b/app/components/hooks/DisplayName/useTokenList.ts @@ -1,38 +1,27 @@ import { useMemo } from 'react'; -import { type TokenListMap } from '@metamask/assets-controllers'; import contractMap from '@metamask/contract-metadata'; - +import { TokenListToken } from '@metamask/assets-controllers'; import { useSelector } from 'react-redux'; import { selectChainId } from '../../../selectors/networkController'; import { selectUseTokenDetection } from '../../../selectors/preferencesController'; -import { selectTokenList } from '../../../selectors/tokenListController'; +import { selectTokenListArray } from '../../../selectors/tokenListController'; import { isMainnetByChainId } from '../../../util/networks'; -function normalizeTokenAddresses(tokenMap: TokenListMap) { - return Object.keys(tokenMap).reduce((acc, address) => { - const tokenMetadata = tokenMap[address]; - return { - ...acc, - [address.toLowerCase()]: { - ...tokenMetadata, - }, - }; - }, {}); -} - -const NORMALIZED_MAINNET_TOKEN_LIST = normalizeTokenAddresses(contractMap); +const NORMALIZED_MAINNET_TOKEN_ARRAY = Object.values( + contractMap, +) as TokenListToken[]; -export default function useTokenList(): TokenListMap { +export default function useTokenList(): TokenListToken[] { const chainId = useSelector(selectChainId); const isMainnet = isMainnetByChainId(chainId); const isTokenDetectionEnabled = useSelector(selectUseTokenDetection); - const tokenList = useSelector(selectTokenList); + const tokenListArray = useSelector(selectTokenListArray); const shouldUseStaticList = !isTokenDetectionEnabled && isMainnet; return useMemo(() => { if (shouldUseStaticList) { - return NORMALIZED_MAINNET_TOKEN_LIST; + return NORMALIZED_MAINNET_TOKEN_ARRAY; } - return normalizeTokenAddresses(tokenList); - }, [shouldUseStaticList, tokenList]); + return tokenListArray; + }, [shouldUseStaticList, tokenListArray]); } diff --git a/app/components/hooks/DisplayName/useTokenListEntry.test.ts b/app/components/hooks/DisplayName/useTokenListEntry.test.ts index 7aeb4144129..bd576c0bb28 100644 --- a/app/components/hooks/DisplayName/useTokenListEntry.test.ts +++ b/app/components/hooks/DisplayName/useTokenListEntry.test.ts @@ -1,4 +1,4 @@ -import { TokenListMap } from '@metamask/assets-controllers'; +import { TokenListToken } from '@metamask/assets-controllers'; import { NameType } from '../../UI/Name/Name.types'; import { useTokenListEntry } from './useTokenListEntry'; import useTokenList from './useTokenList'; @@ -18,12 +18,13 @@ describe('useTokenListEntry', () => { beforeEach(() => { jest.resetAllMocks(); - useTokenListMock.mockReturnValue({ - [TOKEN_ADDRESS_MOCK.toLowerCase()]: { + useTokenListMock.mockReturnValue([ + { + address: TOKEN_ADDRESS_MOCK.toLowerCase(), name: TOKEN_NAME_MOCK, symbol: TOKEN_SYMBOL_MOCK, }, - } as TokenListMap); + ] as unknown as TokenListToken[]); }); it('returns undefined if no token found', () => { diff --git a/app/components/hooks/DisplayName/useTokenListEntry.ts b/app/components/hooks/DisplayName/useTokenListEntry.ts index 6fecba6303f..607ceab0eda 100644 --- a/app/components/hooks/DisplayName/useTokenListEntry.ts +++ b/app/components/hooks/DisplayName/useTokenListEntry.ts @@ -1,3 +1,4 @@ +import { TokenListToken } from '@metamask/assets-controllers'; import { NameType } from '../../UI/Name/Name.types'; import useTokenList from './useTokenList'; @@ -7,16 +8,18 @@ export interface UseTokenListEntriesRequest { } export function useTokenListEntries(requests: UseTokenListEntriesRequest[]) { - const tokenList = useTokenList(); + const tokenListArray = useTokenList(); return requests.map(({ value, type }) => { - if (type !== NameType.EthereumAddress) { + if (type !== NameType.EthereumAddress || !value) { return null; } const normalizedValue = value.toLowerCase(); - return tokenList[normalizedValue]; + return tokenListArray.find( + (token: TokenListToken) => token.address === normalizedValue, + ); }); } diff --git a/app/components/hooks/Ledger/useLedgerBluetooth.ts b/app/components/hooks/Ledger/useLedgerBluetooth.ts index 02b5330e91e..59a40bd2dcc 100644 --- a/app/components/hooks/Ledger/useLedgerBluetooth.ts +++ b/app/components/hooks/Ledger/useLedgerBluetooth.ts @@ -33,7 +33,7 @@ const RESTART_LIMIT = 5; // Assumptions // 1. One big code block - logic all encapsulated in logicToRun // 2. logicToRun calls setUpBluetoothConnection -function useLedgerBluetooth(deviceId?: string): UseLedgerBluetoothHook { +function useLedgerBluetooth(deviceId: string): UseLedgerBluetoothHook { // This is to track if we are expecting code to run or connection operational const [isSendingLedgerCommands, setIsSendingLedgerCommands] = useState(false); @@ -130,7 +130,7 @@ function useLedgerBluetooth(deviceId?: string): UseLedgerBluetoothHook { // Must do this at start of every code block to run to ensure transport is set await setUpBluetoothConnection(); - if (!transportRef.current || !deviceId) { + if (!transportRef.current) { throw new Error('transportRef.current is undefined'); } // Initialise the keyring and check for pre-conditions (is the correct app running?) diff --git a/app/components/hooks/useAccounts/useAccounts.test.ts b/app/components/hooks/useAccounts/useAccounts.test.ts index 0f0d7277bb4..e1f640c7ae2 100644 --- a/app/components/hooks/useAccounts/useAccounts.test.ts +++ b/app/components/hooks/useAccounts/useAccounts.test.ts @@ -2,7 +2,7 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { KeyringTypes } from '@metamask/keyring-controller'; import { toChecksumAddress } from 'ethereumjs-util'; import useAccounts from './useAccounts'; -import initialBackgroundState from '../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../util/test/initial-root-state'; import { MOCK_ACCOUNTS_CONTROLLER_STATE } from '../../../util/test/accountsControllerTestUtils'; import { Account } from './useAccounts.types'; @@ -41,7 +41,7 @@ const MOCK_STORE_STATE = { settings: {}, engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, AccountsController: MOCK_ACCOUNTS_CONTROLLER_STATE, AccountTrackerController: { accounts: { diff --git a/app/components/hooks/useAddressBalance/useAddressBalance.test.tsx b/app/components/hooks/useAddressBalance/useAddressBalance.test.tsx index b4dc5073cd1..a0db32d78a5 100644 --- a/app/components/hooks/useAddressBalance/useAddressBalance.test.tsx +++ b/app/components/hooks/useAddressBalance/useAddressBalance.test.tsx @@ -6,9 +6,8 @@ import { renderHook } from '@testing-library/react-native'; import Engine from '../../../core/Engine'; import { Asset } from './useAddressBalance.types'; import useAddressBalance from './useAddressBalance'; -import initialBackgroundState from '../../../util/test/initial-background-state.json'; +import backgroundState from '../../../util/test/initial-root-state'; import { createMockAccountsControllerState } from '../../../util/test/accountsControllerTestUtils'; - const MOCK_ADDRESS_1 = '0x0'; const MOCK_ADDRESS_2 = '0x1'; @@ -22,7 +21,7 @@ const mockInitialState = { settings: {}, engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, AccountTrackerController: { accounts: { [MOCK_ADDRESS_1]: { diff --git a/app/components/hooks/useAddressBalance/useAddressBalance.ts b/app/components/hooks/useAddressBalance/useAddressBalance.ts index 19d97b394f3..71c51307954 100644 --- a/app/components/hooks/useAddressBalance/useAddressBalance.ts +++ b/app/components/hooks/useAddressBalance/useAddressBalance.ts @@ -65,6 +65,7 @@ const useAddressBalance = ( useEffect(() => { const setBalance = () => { + if (!address) return; const parsedTicker = getTicker(ticker); const checksumAddress = safeToChecksumAddress(address); if (!checksumAddress) { diff --git a/app/components/hooks/useBlockExplorer.test.ts b/app/components/hooks/useBlockExplorer.test.ts index 6cd18887766..57b7550bfb6 100644 --- a/app/components/hooks/useBlockExplorer.test.ts +++ b/app/components/hooks/useBlockExplorer.test.ts @@ -2,7 +2,7 @@ import { NetworkController } from '@metamask/network-controller'; import { useNavigation } from '@react-navigation/native'; import Routes from '../../constants/navigation/Routes'; import { LINEA_GOERLI, RPC } from '../../../app/constants/network'; -import initialBackgroundState from '../../util/test/initial-background-state.json'; +import { backgroundState } from '../../util/test/initial-root-state'; import { renderHookWithProvider } from '../../util/test/renderWithProvider'; import useBlockExplorer from './useBlockExplorer'; @@ -10,7 +10,7 @@ const mockInitialState = { settings: {}, engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, NetworkController: { providerConfig: { type: LINEA_GOERLI, @@ -62,7 +62,7 @@ describe('useBlockExplorer', () => { settings: {}, engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, NetworkController: { providerConfig: { type: RPC, diff --git a/app/components/hooks/useBluetoothPermissions.test.ts b/app/components/hooks/useBluetoothPermissions.test.ts index c08a9fd7556..33022e093f8 100644 --- a/app/components/hooks/useBluetoothPermissions.test.ts +++ b/app/components/hooks/useBluetoothPermissions.test.ts @@ -51,8 +51,8 @@ describe('useBluetoothPermissions', () => { }); it('grants permissions on iOS', async () => { - Device.isIos.mockReturnValue(true); - request.mockResolvedValue(RESULTS.GRANTED); + (Device.isIos as jest.Mock).mockReturnValue(true); + (request as jest.Mock).mockResolvedValue(RESULTS.GRANTED); const { result, waitForNextUpdate } = renderHook(() => useBluetoothPermissions(), @@ -64,8 +64,8 @@ describe('useBluetoothPermissions', () => { }); it('denies permissions on iOS', async () => { - Device.isIos.mockReturnValue(true); - request.mockResolvedValue(RESULTS.DENIED); + (Device.isIos as jest.Mock).mockReturnValue(true); + (request as jest.Mock).mockResolvedValue(RESULTS.DENIED); const { result, waitForNextUpdate } = renderHook(() => useBluetoothPermissions(), @@ -79,9 +79,9 @@ describe('useBluetoothPermissions', () => { }); it('grants permissions on Android 12+', async () => { - Device.isAndroid.mockReturnValue(true); - getSystemVersion.mockReturnValue('12'); - requestMultiple.mockResolvedValue({ + (Device.isAndroid as jest.Mock).mockReturnValue(true); + (getSystemVersion as jest.Mock).mockReturnValue('12'); + (requestMultiple as jest.Mock).mockResolvedValue({ [PERMISSIONS.ANDROID.BLUETOOTH_CONNECT]: RESULTS.GRANTED, [PERMISSIONS.ANDROID.BLUETOOTH_SCAN]: RESULTS.GRANTED, }); @@ -96,9 +96,9 @@ describe('useBluetoothPermissions', () => { }); it('denies permissions on Android 12+', async () => { - Device.isAndroid.mockReturnValue(true); - getSystemVersion.mockReturnValue('12'); - requestMultiple.mockResolvedValue({ + (Device.isAndroid as jest.Mock).mockReturnValue(true); + (getSystemVersion as jest.Mock).mockReturnValue('12'); + (requestMultiple as jest.Mock).mockResolvedValue({ [PERMISSIONS.ANDROID.BLUETOOTH_CONNECT]: RESULTS.DENIED, [PERMISSIONS.ANDROID.BLUETOOTH_SCAN]: RESULTS.DENIED, }); @@ -115,9 +115,9 @@ describe('useBluetoothPermissions', () => { }); it('grants permissions on Android <12', async () => { - Device.isAndroid.mockReturnValue(true); - getSystemVersion.mockReturnValue('11'); - request.mockResolvedValue(RESULTS.GRANTED); + (Device.isAndroid as jest.Mock).mockReturnValue(true); + (getSystemVersion as jest.Mock).mockReturnValue('11'); + (request as jest.Mock).mockResolvedValue(RESULTS.GRANTED); const { result, waitForNextUpdate } = renderHook(() => useBluetoothPermissions(), @@ -129,9 +129,9 @@ describe('useBluetoothPermissions', () => { }); it('denies permissions on Android <12', async () => { - Device.isAndroid.mockReturnValue(true); - getSystemVersion.mockReturnValue('11'); - request.mockResolvedValue(RESULTS.DENIED); + (Device.isAndroid as jest.Mock).mockReturnValue(true); + (getSystemVersion as jest.Mock).mockReturnValue('11'); + (request as jest.Mock).mockResolvedValue(RESULTS.DENIED); const { result, waitForNextUpdate } = renderHook(() => useBluetoothPermissions(), @@ -145,9 +145,9 @@ describe('useBluetoothPermissions', () => { }); it('checks permissions when app state changes to active', async () => { - Device.isAndroid.mockReturnValue(true); - getSystemVersion.mockReturnValue('12'); - requestMultiple.mockResolvedValue({ + (Device.isAndroid as jest.Mock).mockReturnValue(true); + (getSystemVersion as jest.Mock).mockReturnValue('12'); + (requestMultiple as jest.Mock).mockResolvedValue({ [PERMISSIONS.ANDROID.BLUETOOTH_CONNECT]: RESULTS.GRANTED, [PERMISSIONS.ANDROID.BLUETOOTH_SCAN]: RESULTS.GRANTED, }); @@ -158,7 +158,7 @@ describe('useBluetoothPermissions', () => { expect(requestMultiple).toHaveBeenCalledTimes(1); act(() => { - AppState.addEventListener.mock.calls[0][1]('active'); + (AppState.addEventListener as jest.Mock).mock.calls[0][1]('active'); }); //checkPermission run again when app state changes to active @@ -166,9 +166,9 @@ describe('useBluetoothPermissions', () => { }); it('does not check permissions when app state changes to background', async () => { - Device.isAndroid.mockReturnValue(true); - getSystemVersion.mockReturnValue('12'); - requestMultiple.mockResolvedValue({ + (Device.isAndroid as jest.Mock).mockReturnValue(true); + (getSystemVersion as jest.Mock).mockReturnValue('12'); + (requestMultiple as jest.Mock).mockResolvedValue({ [PERMISSIONS.ANDROID.BLUETOOTH_CONNECT]: RESULTS.GRANTED, [PERMISSIONS.ANDROID.BLUETOOTH_SCAN]: RESULTS.GRANTED, }); @@ -179,7 +179,7 @@ describe('useBluetoothPermissions', () => { expect(requestMultiple).toHaveBeenCalledTimes(1); act(() => { - AppState.addEventListener.mock.calls[0][1]('inactive'); + (AppState.addEventListener as jest.Mock).mock.calls[0][1]('inactive'); }); //checkPermission does not run when app state changes to inactive @@ -187,9 +187,9 @@ describe('useBluetoothPermissions', () => { }); it('grants permissions when getSystemVersion is null', async () => { - Device.isAndroid.mockReturnValue(true); - getSystemVersion.mockReturnValue(null); - request.mockResolvedValue(RESULTS.GRANTED); + (Device.isAndroid as jest.Mock).mockReturnValue(true); + (getSystemVersion as jest.Mock).mockReturnValue(null); + (request as jest.Mock).mockResolvedValue(RESULTS.GRANTED); const { result, waitForNextUpdate } = renderHook(() => useBluetoothPermissions(), @@ -201,9 +201,9 @@ describe('useBluetoothPermissions', () => { }); it('grants permissions when getSystemVersion return is not a number', async () => { - Device.isAndroid.mockReturnValue(true); - getSystemVersion.mockReturnValue('adbd'); - request.mockResolvedValue(RESULTS.GRANTED); + (Device.isAndroid as jest.Mock).mockReturnValue(true); + (getSystemVersion as jest.Mock).mockReturnValue('adbd'); + (request as jest.Mock).mockResolvedValue(RESULTS.GRANTED); const { result, waitForNextUpdate } = renderHook(() => useBluetoothPermissions(), diff --git a/app/components/hooks/useExistingAddress.test.ts b/app/components/hooks/useExistingAddress.test.ts index aad6c1c2736..452a0eaf76f 100644 --- a/app/components/hooks/useExistingAddress.test.ts +++ b/app/components/hooks/useExistingAddress.test.ts @@ -1,21 +1,21 @@ import { renderHookWithProvider } from '../../util/test/renderWithProvider'; import useExistingAddress from './useExistingAddress'; -import initialBackgroundState from '../../util/test/initial-background-state.json'; +import { backgroundState } from '../../util/test/initial-root-state'; +import { createMockAccountsControllerState } from '../../util/test/accountsControllerTestUtils'; + +const MOCK_ADDRESS_1 = '0x0'; +const MOCK_ADDRESS_2 = '0x1'; + +const MOCK_ACCOUNTS_CONTROLLER_STATE = createMockAccountsControllerState([ + MOCK_ADDRESS_1, + MOCK_ADDRESS_2, +]); const mockInitialState = { settings: {}, engine: { backgroundState: { - ...initialBackgroundState, - PreferencesController: { - selectedAddress: '0x0', - identities: { - '0x0': { - address: '0x0', - name: 'Account 1', - }, - }, - }, + ...backgroundState, NetworkController: { providerConfig: { chainId: '0x1', @@ -23,14 +23,15 @@ const mockInitialState = { }, AddressBookController: { addressBook: { - '0x1': { - '0x1': { - address: '0x1', + [MOCK_ADDRESS_2]: { + [MOCK_ADDRESS_2]: { + address: MOCK_ADDRESS_2, name: 'Account 2', }, }, }, }, + AccountsController: MOCK_ACCOUNTS_CONTROLLER_STATE, }, }, }; @@ -43,16 +44,22 @@ jest.mock('react-redux', () => ({ })); describe('useExistingAddress', () => { - it('should return existing address from identities', async () => { - const { result } = renderHookWithProvider(() => useExistingAddress('0x0'), { - state: mockInitialState, - }); + it('should return existing address from accounts controller', async () => { + const { result } = renderHookWithProvider( + () => useExistingAddress(MOCK_ADDRESS_1), + { + state: mockInitialState, + }, + ); expect(result?.current?.name).toEqual('Account 1'); }); it('should return existing address from address book', async () => { - const { result } = renderHookWithProvider(() => useExistingAddress('0x1'), { - state: mockInitialState, - }); + const { result } = renderHookWithProvider( + () => useExistingAddress(MOCK_ADDRESS_2), + { + state: mockInitialState, + }, + ); expect(result?.current?.name).toEqual('Account 2'); }); it('should return undefined address not in identities or address book', async () => { diff --git a/app/components/hooks/useExistingAddress.ts b/app/components/hooks/useExistingAddress.ts index 297863dac0e..77b75ef7771 100644 --- a/app/components/hooks/useExistingAddress.ts +++ b/app/components/hooks/useExistingAddress.ts @@ -2,24 +2,18 @@ import { toChecksumAddress } from 'ethereumjs-util'; import { useSelector } from 'react-redux'; import { selectChainId } from '../../selectors/networkController'; -import { selectIdentities } from '../../selectors/preferencesController'; - -export interface Address { - address: string; - chainId: string; - isEns: boolean; - isSmartContract: boolean; - memo: string; - name: string; -} - -const useExistingAddress = (address?: string): Address | undefined => { +import { selectInternalAccounts } from '../../selectors/accountsController'; +import { toLowerCaseEquals } from '../../util/general'; +import { AddressBookEntry } from '@metamask/address-book-controller'; +import { RootState } from '../../reducers'; + +type AccountInfo = Pick; + +const useExistingAddress = (address?: string): AccountInfo | undefined => { const chainId = useSelector(selectChainId); - // TODO: Replace "any" with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const { addressBook, identities } = useSelector((state: any) => ({ + const { addressBook, internalAccounts } = useSelector((state: RootState) => ({ addressBook: state.engine.backgroundState.AddressBookController.addressBook, - identities: selectIdentities(state), + internalAccounts: selectInternalAccounts(state), })); if (!address) return; @@ -27,11 +21,28 @@ const useExistingAddress = (address?: string): Address | undefined => { const networkAddressBook = addressBook[chainId] || {}; const checksummedAddress = toChecksumAddress(address); - return ( - networkAddressBook?.[checksummedAddress] ?? - identities?.[checksummedAddress] ?? - undefined + const matchingAddressBookEntry: AddressBookEntry | undefined = + networkAddressBook?.[checksummedAddress]; + + if (matchingAddressBookEntry) { + return { + name: matchingAddressBookEntry.name, + address: matchingAddressBookEntry.address, + }; + } + + const accountWithMatchingAddress = internalAccounts.find((account) => + toLowerCaseEquals(account.address, address), ); + + if (accountWithMatchingAddress) { + return { + address: accountWithMatchingAddress.address, + name: accountWithMatchingAddress.metadata.name, + }; + } + + return undefined; }; export default useExistingAddress; diff --git a/app/components/hooks/useIsOriginalNativeTokenSymbol/useIsOriginalNativeTokenSymbol.test.ts b/app/components/hooks/useIsOriginalNativeTokenSymbol/useIsOriginalNativeTokenSymbol.test.ts index 06359ab4da4..1addec22b1b 100644 --- a/app/components/hooks/useIsOriginalNativeTokenSymbol/useIsOriginalNativeTokenSymbol.test.ts +++ b/app/components/hooks/useIsOriginalNativeTokenSymbol/useIsOriginalNativeTokenSymbol.test.ts @@ -1,7 +1,7 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { useSelector } from 'react-redux'; import useIsOriginalNativeTokenSymbol from './useIsOriginalNativeTokenSymbol'; -import initialBackgroundState from '../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../../app/util/test/initial-root-state'; import axios from 'axios'; jest.mock('react-redux', () => ({ @@ -25,7 +25,7 @@ describe('useIsOriginalNativeTokenSymbol', () => { mockSelectorState({ engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, PreferencesController: { useSafeChainsListValidation: true, }, @@ -69,7 +69,7 @@ describe('useIsOriginalNativeTokenSymbol', () => { mockSelectorState({ engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, PreferencesController: { useSafeChainsListValidation: true, }, @@ -112,7 +112,7 @@ describe('useIsOriginalNativeTokenSymbol', () => { mockSelectorState({ engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, PreferencesController: { useSafeChainsListValidation: true, }, @@ -144,7 +144,7 @@ describe('useIsOriginalNativeTokenSymbol', () => { mockSelectorState({ engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, PreferencesController: { useSafeChainsListValidation: true, }, @@ -188,7 +188,7 @@ describe('useIsOriginalNativeTokenSymbol', () => { mockSelectorState({ engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, PreferencesController: { useSafeChainsListValidation: true, }, @@ -233,7 +233,7 @@ describe('useIsOriginalNativeTokenSymbol', () => { mockSelectorState({ engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, PreferencesController: { useSafeChainsListValidation: false, }, @@ -276,7 +276,7 @@ describe('useIsOriginalNativeTokenSymbol', () => { mockSelectorState({ engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, PreferencesController: { useSafeChainsListValidation: true, }, diff --git a/app/components/hooks/useSvgUriViewBox.test.ts b/app/components/hooks/useSvgUriViewBox.test.ts new file mode 100644 index 00000000000..30bf04a88f8 --- /dev/null +++ b/app/components/hooks/useSvgUriViewBox.test.ts @@ -0,0 +1,42 @@ +import { renderHook, waitFor } from '@testing-library/react-native'; +import useSvgUriViewBox from './useSvgUriViewBox'; + +describe('useSvgUriViewBox()', () => { + const MOCK_SVG_WITH_VIEWBOX = ``; + const MOCK_SVG_WITHOUT_VIEWBOX = ``; + + function arrangeMocks() { + const mockResponseTextFn = jest + .fn() + .mockResolvedValue(MOCK_SVG_WITHOUT_VIEWBOX); + jest + .spyOn(global, 'fetch') + .mockResolvedValue({ text: mockResponseTextFn } as unknown as Response); + + return { mockText: mockResponseTextFn }; + } + + it('should return view-box if svg if missing a view-box', async () => { + const { mockText } = arrangeMocks(); + mockText.mockResolvedValueOnce(MOCK_SVG_WITHOUT_VIEWBOX); + + const hook = renderHook(() => useSvgUriViewBox('URI', true)); + await waitFor(() => expect(hook.result.current).toBeDefined()); + }); + + it('should return view-box if svg already has view-box', async () => { + const { mockText } = arrangeMocks(); + mockText.mockResolvedValueOnce(MOCK_SVG_WITH_VIEWBOX); + + const hook = renderHook(() => useSvgUriViewBox('URI', true)); + await waitFor(() => expect(hook.result.current).toBeDefined()); + }); + + it('should not make async calls if image is not an svg', async () => { + const mocks = arrangeMocks(); + const hook = renderHook(() => useSvgUriViewBox('URI', false)); + + await waitFor(() => expect(hook.result.current).toBeUndefined()); + expect(mocks.mockText).not.toHaveBeenCalled(); + }); +}); diff --git a/app/components/hooks/useSvgUriViewBox.ts b/app/components/hooks/useSvgUriViewBox.ts new file mode 100644 index 00000000000..bbfb1ee2bc3 --- /dev/null +++ b/app/components/hooks/useSvgUriViewBox.ts @@ -0,0 +1,49 @@ +import { useEffect, useState } from 'react'; + +/** + * Support svg images urls that do not have a view box + * See: https://github.com/software-mansion/react-native-svg/issues/1202#issuecomment-1891110599 + * + * This will return the default SVG ViewBox from an SVG URI + * @param uri - uri to fetch + * @param isSVG - check to see if the uri is an svg + * @returns viewbox string + */ +export default function useSvgUriViewBox( + uri: string, + isSVG: boolean, +): string | undefined { + const [viewBox, setViewBox] = useState(undefined); + + useEffect(() => { + if (!isSVG) { + return; + } + + fetch(uri) + .then((response) => response.text()) + .then((svgContent) => { + const widthMatch = svgContent.match(/width="([^"]+)"/); + const heightMatch = svgContent.match(/height="([^"]+)"/); + const viewBoxMatch = svgContent.match(/viewBox="([^"]+)"/); + + if (viewBoxMatch?.[1]) { + setViewBox(viewBoxMatch[1]); + return; + } + + if (widthMatch?.[1] && heightMatch?.[1]) { + const width = widthMatch[1]; + const height = heightMatch[1]; + setViewBox(`0 0 ${width} ${height}`); + } + }) + .catch((error) => console.error('Error fetching SVG:', error)); + }, [isSVG, uri]); + + if (!viewBox) { + return undefined; + } + + return viewBox; +} diff --git a/app/components/hooks/useThunkDispatch.ts b/app/components/hooks/useThunkDispatch.ts index 87224dbd534..6c7013d4c91 100644 --- a/app/components/hooks/useThunkDispatch.ts +++ b/app/components/hooks/useThunkDispatch.ts @@ -1,16 +1,12 @@ +import { ThunkAction as ReduxThunkAction, ThunkDispatch } from 'redux-thunk'; import { useDispatch } from 'react-redux'; -import { AnyAction, Store } from 'redux'; +import { AnyAction } from 'redux'; +import { RootState } from '../../reducers'; -// TODO: Replace "any" with type -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type Dispatch = Store['dispatch']; -// TODO: Replace "any" with type -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type GetState = Store['getState']; - -export type ThunkAction = (dispatch: Dispatch, getState: GetState) => void; +export type ThunkAction = ReduxThunkAction; function useThunkDispatch() { - return useDispatch<(thunkAction: ThunkAction) => void>(); + return useDispatch>(); } + export default useThunkDispatch; diff --git a/app/components/hooks/useTokenBalance.tsx b/app/components/hooks/useTokenBalance.tsx index 1fec379f399..8962c9ce352 100644 --- a/app/components/hooks/useTokenBalance.tsx +++ b/app/components/hooks/useTokenBalance.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, Dispatch, SetStateAction } from 'react'; import Engine from '../../core/Engine'; -import { BN } from '@metamask/assets-controllers'; +import { BN } from 'ethereumjs-util'; /** * Hook to handle the balance of ERC20 tokens diff --git a/app/components/hooks/useTokenBalancesController/useTokenBalancesController.test.tsx b/app/components/hooks/useTokenBalancesController/useTokenBalancesController.test.tsx index 8f6802b2e0e..b575c66c4cc 100644 --- a/app/components/hooks/useTokenBalancesController/useTokenBalancesController.test.tsx +++ b/app/components/hooks/useTokenBalancesController/useTokenBalancesController.test.tsx @@ -6,13 +6,13 @@ import { act, render, waitFor } from '@testing-library/react-native'; import useTokenBalancesController from './useTokenBalancesController'; import { BN } from 'ethereumjs-util'; import { cloneDeep } from 'lodash'; -import initialBackgroundState from '../../../util/test/initial-background-state.json'; +import { backgroundState } from '../../../util/test/initial-root-state'; // initial state for the test store const mockInitialState = { engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, TokenBalancesController: { contractBalances: { '0x326836cc6cd09B5aa59B81A7F72F25FcC0136b95': new BN(0x2a), diff --git a/app/components/hooks/useTokenBalancesController/useTokenBalancesController.ts b/app/components/hooks/useTokenBalancesController/useTokenBalancesController.ts index d9152d40728..f2c5261c289 100644 --- a/app/components/hooks/useTokenBalancesController/useTokenBalancesController.ts +++ b/app/components/hooks/useTokenBalancesController/useTokenBalancesController.ts @@ -1,11 +1,10 @@ import { useSelector } from 'react-redux'; import { isEqual } from 'lodash'; import { ControllerHookType } from '../controllerHook.types'; -import { BN } from 'ethereumjs-util'; import { selectContractBalances } from '../../../selectors/tokenBalancesController'; interface TokenBalances { - [address: string]: BN; + [address: string]: string; } const useTokenBalancesController = (): ControllerHookType => { diff --git a/app/constants/keyringTypes.ts b/app/constants/keyringTypes.ts index 45f2975a1ba..19811d67c29 100644 --- a/app/constants/keyringTypes.ts +++ b/app/constants/keyringTypes.ts @@ -6,3 +6,8 @@ enum ExtendedKeyringTypes { } export default ExtendedKeyringTypes; + +export enum HardwareDeviceTypes { + LEDGER = 'Ledger', + QR = 'QR Hardware', +} diff --git a/app/constants/navigation/Routes.ts b/app/constants/navigation/Routes.ts index 81b2182ba31..21393b7ef06 100644 --- a/app/constants/navigation/Routes.ts +++ b/app/constants/navigation/Routes.ts @@ -29,7 +29,6 @@ const Routes = { SELECT_DEVICE: 'SelectHardwareWallet', CONNECT_QR_DEVICE: 'ConnectQRHardwareFlow', CONNECT_LEDGER: 'ConnectLedgerFlow', - LEDGER_ACCOUNT: 'LedgerAccountInfo', LEDGER_CONNECT: 'LedgerConnect', }, LEDGER_MESSAGE_SIGN_MODAL: 'LedgerMessageSignModal', diff --git a/app/constants/network.js b/app/constants/network.js index a017fab3e1f..86b6489f677 100644 --- a/app/constants/network.js +++ b/app/constants/network.js @@ -37,6 +37,9 @@ export const NETWORKS_CHAIN_ID = { OPTIMISM_GOERLI: toHex('420'), MUMBAI: toHex('80001'), OPBNB: toHex('204'), + SCROLL: toHex('534352'), + BERACHAIN: toHex('80085'), + METACHAIN_ONE: toHex('112358'), }; // To add a deprecation warning to a network, add it to the array diff --git a/app/core/Analytics/MetaMetrics.events.ts b/app/core/Analytics/MetaMetrics.events.ts index 769ae8fd8a8..b45c55f0bc6 100644 --- a/app/core/Analytics/MetaMetrics.events.ts +++ b/app/core/Analytics/MetaMetrics.events.ts @@ -353,7 +353,12 @@ enum EVENT_NAME { CONNECT_LEDGER_SUCCESS = 'Connected Account with hardware wallet', LEDGER_HARDWARE_TRANSACTION_CANCELLED = 'User canceled Ledger hardware transaction', LEDGER_HARDWARE_WALLET_ERROR = 'Ledger hardware wallet error', - LEDGER_HARDWARE_WALLET_FORGOTTEN = 'Ledger hardware wallet forgotten', + + // common hardware wallet + HARDWARE_WALLET_FORGOTTEN = 'Hardware wallet forgotten', + + // Remove an account + ACCOUNT_REMOVED = 'Account removed', //Notifications ALL_NOTIFICATIONS = 'All Notifications', @@ -842,9 +847,10 @@ const events = { LEDGER_HARDWARE_WALLET_ERROR: generateOpt( EVENT_NAME.LEDGER_HARDWARE_WALLET_ERROR, ), - LEDGER_HARDWARE_WALLET_FORGOTTEN: generateOpt( - EVENT_NAME.LEDGER_HARDWARE_WALLET_FORGOTTEN, - ), + HARDWARE_WALLET_FORGOTTEN: generateOpt(EVENT_NAME.HARDWARE_WALLET_FORGOTTEN), + + // Remove an account + ACCOUNT_REMOVED: generateOpt(EVENT_NAME.ACCOUNT_REMOVED), // Smart transactions SMART_TRANSACTION_OPT_IN: generateOpt(EVENT_NAME.SMART_TRANSACTION_OPT_IN), @@ -925,6 +931,7 @@ enum DESCRIPTION { WALLET_QR_SCANNER = 'QR scanner', WALLET_COPIED_ADDRESS = 'Copied Address', WALLET_ADD_COLLECTIBLES = 'Add Collectibles', + // Transactions TRANSACTIONS_CONFIRM_STARTED = 'Confirm Started', TRANSACTIONS_EDIT_TRANSACTION = 'Edit Transaction', @@ -1176,6 +1183,7 @@ const legacyMetaMetricsEvents = { ACTIONS.WALLET_VIEW, DESCRIPTION.WALLET_ADD_COLLECTIBLES, ), + // Transactions TRANSACTIONS_CONFIRM_STARTED: generateOpt( EVENT_NAME.TRANSACTIONS, diff --git a/app/core/Authentication/Authentication.test.ts b/app/core/Authentication/Authentication.test.ts index c3982399a32..05d951415c4 100644 --- a/app/core/Authentication/Authentication.test.ts +++ b/app/core/Authentication/Authentication.test.ts @@ -1,4 +1,4 @@ -import AsyncStorage from '@react-native-async-storage/async-storage'; +import AsyncStorage from '../../store/async-storage-wrapper'; import { BIOMETRY_CHOICE_DISABLED, TRUE, @@ -27,7 +27,7 @@ describe('Authentication', () => { }); afterEach(() => { - AsyncStorage.clear(); + AsyncStorage.clearAll(); jest.restoreAllMocks(); }); diff --git a/app/core/BackgroundBridge/Port.ts b/app/core/BackgroundBridge/Port.ts index dd841df9516..83249bc413b 100644 --- a/app/core/BackgroundBridge/Port.ts +++ b/app/core/BackgroundBridge/Port.ts @@ -23,9 +23,7 @@ class Port extends EventEmitter { const js = this._isMainFrame ? JS_POST_MESSAGE_TO_PROVIDER(msg, origin) : JS_IFRAME_POST_MESSAGE_TO_PROVIDER(msg, origin); - if (this._window.webViewRef?.current) { - this._window?.injectJavaScript(js); - } + this._window?.injectJavaScript(js); }; } diff --git a/app/core/BackupVault/backupVault.ts b/app/core/BackupVault/backupVault.ts index 07b316443a4..b93fe4c1b6e 100644 --- a/app/core/BackupVault/backupVault.ts +++ b/app/core/BackupVault/backupVault.ts @@ -16,7 +16,7 @@ import { const VAULT_BACKUP_KEY = 'VAULT_BACKUP'; const options: Options = { - accessible: ACCESSIBLE.WHEN_UNLOCKED, + accessible: ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY, }; interface KeyringBackupResponse { diff --git a/app/core/DeeplinkManager/TransactionManager/approveTransaction.test.ts b/app/core/DeeplinkManager/TransactionManager/approveTransaction.test.ts index 757fe2518a6..7bc79fc7750 100644 --- a/app/core/DeeplinkManager/TransactionManager/approveTransaction.test.ts +++ b/app/core/DeeplinkManager/TransactionManager/approveTransaction.test.ts @@ -6,6 +6,15 @@ import Engine from '../../Engine'; import NotificationManager from '../../NotificationManager'; import approveTransaction from './approveTransaction'; import { addTransaction } from '../../../util/transaction-controller'; +import { createMockInternalAccount } from '../../../util/test/accountsControllerTestUtils'; + +const MOCK_SENDER_ADDRESS = '0xMockSenderAddress'; +const MOCK_TARGET_ADDRESS = '0xTargetAddress'; + +const MOCK_INTERNAL_ACCOUNT = createMockInternalAccount( + MOCK_SENDER_ADDRESS, + 'Account 1', +); jest.mock('../../../util/networks'); jest.mock('../../../util/transactions'); @@ -14,11 +23,15 @@ jest.mock('../../Engine', () => ({ context: { NetworkController: { setProviderType: jest.fn() }, PreferencesController: { - state: { selectedAddress: '0xMockSenderAddress' }, + state: { selectedAddress: MOCK_SENDER_ADDRESS }, }, TransactionController: { addTransaction: jest.fn(), }, + AccountsController: { + internalAccounts: MOCK_INTERNAL_ACCOUNT, + getSelectedAccount: jest.fn(() => MOCK_INTERNAL_ACCOUNT), + }, }, })); jest.mock('../../NotificationManager'); @@ -32,7 +45,7 @@ jest.mock('../../../util/transaction-controller', () => ({ const mockEthUrl = { parameters: { uint256: '123', address: '0xMockAddress' }, - target_address: '0xTargetAddress', + target_address: MOCK_TARGET_ADDRESS, chain_id: '1', }; const mockDeeplinkManager = { @@ -96,7 +109,7 @@ describe('approveTransaction', () => { }); it('calls generateApprovalData with the correct parameters', async () => { - spyGetAddress.mockReturnValue('0xMockAddress'); + spyGetAddress.mockResolvedValue('0xMockAddress'); await approveTransaction({ // TODO: Replace "any" with type @@ -127,8 +140,8 @@ describe('approveTransaction', () => { expect(spyAddTransaction).toHaveBeenCalledWith( { - to: '0xTargetAddress', - from: '0xMockSenderAddress', + to: MOCK_TARGET_ADDRESS, + from: MOCK_SENDER_ADDRESS, value: '0x0', data: 'fakeApproveData', }, @@ -157,7 +170,7 @@ describe('approveTransaction', () => { }); it('should call showSimpleNotification with the correct parameters if the spender address is invalid', async () => { - spyGetAddress.mockReturnValue(''); + spyGetAddress.mockResolvedValue(''); await approveTransaction({ // TODO: Replace "any" with type @@ -178,7 +191,7 @@ describe('approveTransaction', () => { }); it('should call navigate with the correct parameters if the spender address is invalid', async () => { - spyGetAddress.mockReturnValue(''); + spyGetAddress.mockResolvedValue(''); await approveTransaction({ // TODO: Replace "any" with type @@ -196,7 +209,7 @@ describe('approveTransaction', () => { }); it('should not call showSimpleNotification if the spender address is valid', async () => { - spyGetAddress.mockReturnValue('0xMockAddress'); + spyGetAddress.mockResolvedValue('0xMockAddress'); await approveTransaction({ // TODO: Replace "any" with type @@ -212,7 +225,7 @@ describe('approveTransaction', () => { }); it('should not call navigate if the spender address is valid', async () => { - spyGetAddress.mockReturnValue('0xMockAddress'); + spyGetAddress.mockResolvedValue('0xMockAddress'); await approveTransaction({ // TODO: Replace "any" with type diff --git a/app/core/DeeplinkManager/TransactionManager/approveTransaction.ts b/app/core/DeeplinkManager/TransactionManager/approveTransaction.ts index e2e37266059..72b5307fd8d 100644 --- a/app/core/DeeplinkManager/TransactionManager/approveTransaction.ts +++ b/app/core/DeeplinkManager/TransactionManager/approveTransaction.ts @@ -8,6 +8,7 @@ import DeeplinkManager from '../DeeplinkManager'; import Engine from '../../Engine'; import NotificationManager from '../../NotificationManager'; import { WalletDevice } from '@metamask/transaction-controller'; +import { toChecksumHexAddress } from '@metamask/controller-utils'; async function approveTransaction({ deeplinkManager, @@ -19,7 +20,7 @@ async function approveTransaction({ origin: string; }) { const { parameters, target_address, chain_id } = ethUrl; - const { PreferencesController, NetworkController } = Engine.context; + const { AccountsController, NetworkController } = Engine.context; if (chain_id) { const newNetworkType = getNetworkTypeById(chain_id); @@ -50,9 +51,11 @@ async function approveTransaction({ deeplinkManager.navigation.navigate('WalletView'); } + const selectedAccount = AccountsController.getSelectedAccount(); + const txParams = { to: target_address.toString(), - from: PreferencesController.state.selectedAddress.toString(), + from: toChecksumHexAddress(selectedAccount.address), value: '0x0', data: generateApprovalData({ spender: spenderAddress, value }), }; diff --git a/app/core/Engine.test.js b/app/core/Engine.test.js index c801685c83d..8ee16837372 100644 --- a/app/core/Engine.test.js +++ b/app/core/Engine.test.js @@ -1,5 +1,5 @@ import Engine from './Engine'; -import initialState from '../util/test/initial-background-state.json'; +import { backgroundState } from '../util/test/initial-root-state'; jest.unmock('./Engine'); @@ -25,6 +25,9 @@ describe('Engine', () => { expect(engine.context).toHaveProperty('LoggingController'); expect(engine.context).toHaveProperty('TransactionController'); expect(engine.context).toHaveProperty('SmartTransactionsController'); + expect(engine.context).toHaveProperty('AuthenticationController'); + expect(engine.context).toHaveProperty('UserStorageController'); + expect(engine.context).toHaveProperty('NotificationServicesController'); }); it('calling Engine.init twice returns the same instance', () => { @@ -43,22 +46,10 @@ describe('Engine', () => { // Use this to keep the unit test initial background state fixture up-to-date it('matches initial state fixture', () => { const engine = Engine.init({}); - let backgroundState = engine.datamodel.state; + const initialBackgroundState = engine.datamodel.state; - backgroundState = { + expect(initialBackgroundState).toStrictEqual({ ...backgroundState, - KeyringController: { - ...backgroundState.KeyringController, - vault: { - cipher: 'mock-cipher', - iv: 'mock-iv', - lib: 'original', - }, - }, - }; - - expect(backgroundState).toStrictEqual({ - ...initialState, // JSON cannot store the value undefined, so we append it here SmartTransactionsController: { @@ -93,7 +84,7 @@ describe('Engine', () => { }); it('setSelectedAccount throws an error if no account exists for the given address', () => { - const engine = Engine.init(initialState); + const engine = Engine.init(backgroundState); const invalidAddress = '0xInvalidAddress'; expect(() => engine.setSelectedAccount(invalidAddress)).toThrow( diff --git a/app/core/Engine.ts b/app/core/Engine.ts index 0330aa4a14d..eaece76f899 100644 --- a/app/core/Engine.ts +++ b/app/core/Engine.ts @@ -129,7 +129,11 @@ import { LoggingControllerState, LoggingControllerActions, } from '@metamask/logging-controller'; -import LedgerKeyring from '@consensys/ledgerhq-metamask-keyring'; +import { + LedgerKeyring, + LedgerMobileBridge, + LedgerTransportMiddleware, +} from '@metamask/eth-ledger-bridge-keyring'; import { Encryptor, LEGACY_DERIVATION_OPTIONS } from './Encryptor'; import { isMainnetByChainId, @@ -163,8 +167,13 @@ import { DetectSnapLocationOptions, } from './Snaps'; import { getRpcMethodMiddleware } from './RPCMethods/RPCMethodMiddleware'; + +import { + AuthenticationController, + UserStorageController, +} from '@metamask/profile-sync-controller'; +import { NotificationServicesController } from '@metamask/notification-services-controller'; ///: END:ONLY_INCLUDE_IF -import { isBlockaidFeatureEnabled } from '../util/blockaid'; import { getCaveatSpecifications, getPermissionSpecifications, @@ -203,6 +212,7 @@ import { selectSwapsChainFeatureFlags } from '../reducers/swaps'; import { SmartTransactionStatuses } from '@metamask/smart-transactions-controller/dist/types'; import { submitSmartTransactionHook } from '../util/smart-transactions/smart-publish-hook'; import { SmartTransactionsControllerState } from '@metamask/smart-transactions-controller/dist/SmartTransactionsController'; +import { zeroAddress } from 'ethereumjs-util'; const NON_EMPTY = 'NON_EMPTY'; @@ -226,6 +236,10 @@ interface TestOrigin { } type PhishingControllerActions = MaybeUpdateState | TestOrigin; +type AuthenticationControllerActions = AuthenticationController.AllowedActions; +type UserStorageControllerActions = UserStorageController.AllowedActions; +type NotificationsServicesControllerActions = + NotificationServicesController.AllowedActions; type SnapsGlobalActions = | SnapControllerActions @@ -251,6 +265,9 @@ type GlobalActions = | LoggingControllerActions ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) | SnapsGlobalActions + | AuthenticationControllerActions + | UserStorageControllerActions + | NotificationsServicesControllerActions ///: END:ONLY_INCLUDE_IF | KeyringControllerActions | AccountsControllerActions @@ -303,6 +320,9 @@ export interface EngineState { SnapController: PersistedSnapControllerState; SnapsRegistry: SnapsRegistryState; SubjectMetadataController: SubjectMetadataControllerState; + AuthenticationController: AuthenticationController.AuthenticationControllerState; + UserStorageController: UserStorageController.UserStorageControllerState; + NotificationServicesController: NotificationServicesController.NotificationServicesControllerState; ///: END:ONLY_INCLUDE_IF PermissionController: PermissionControllerState; ApprovalController: ApprovalControllerState; @@ -345,6 +365,9 @@ interface Controllers { ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) SnapController: SnapController; SubjectMetadataController: SubjectMetadataController; + AuthenticationController: AuthenticationController.Controller; + UserStorageController: UserStorageController.Controller; + NotificationServicesController: NotificationServicesController.Controller; ///: END:ONLY_INCLUDE_IF SwapsController: SwapsController; } @@ -620,6 +643,7 @@ class Engine { networkController.state.selectedNetworkClientId, ); const gasFeeController = new GasFeeController({ + // @ts-expect-error TODO: Resolve mismatch between base-controller versions. messenger: this.controllerMessenger.getRestricted({ name: 'GasFeeController', allowedActions: [ @@ -666,7 +690,8 @@ class Engine { }; qrKeyringBuilder.type = QRHardwareKeyring.type; - const ledgerKeyringBuilder = () => new LedgerKeyring(); + const bridge = new LedgerMobileBridge(new LedgerTransportMiddleware()); + const ledgerKeyringBuilder = () => new LedgerKeyring({ bridge }); ledgerKeyringBuilder.type = LedgerKeyring.type; const keyringController = new KeyringController({ @@ -908,7 +933,6 @@ class Engine { const requireAllowlist = process.env.METAMASK_BUILD_TYPE === 'main'; const disableSnapInstallation = process.env.METAMASK_BUILD_TYPE === 'main'; - const allowLocalSnaps = process.env.METAMASK_BUILD_TYPE === 'flask'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore TODO: Resolve/patch mismatch between base-controller versions. @@ -1011,6 +1035,75 @@ class Engine { store.getState().settings.basicFunctionalityEnabled === false, }), }); + + const authenticationController = new AuthenticationController.Controller({ + state: initialState.AuthenticationController, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore TODO: Resolve/patch mismatch between messenger types + messenger: this.controllerMessenger.getRestricted({ + name: 'AuthenticationController', + allowedActions: [ + 'SnapController:handleRequest', + 'UserStorageController:disableProfileSyncing', + ], + allowedEvents: [], + }), + // TODO: Fix this by (await MetaMetrics.getInstance().getMetaMetricsId()) before go live + metametrics: { + agent: 'mobile', + getMetaMetricsId: async () => Promise.resolve(''), + }, + }); + + const userStorageController = new UserStorageController.Controller({ + getMetaMetricsState: () => MetaMetrics.getInstance().isEnabled(), + state: initialState.UserStorageController, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore TODO: Resolve/patch mismatch between messenger types + messenger: this.controllerMessenger.getRestricted({ + name: 'UserStorageController', + allowedActions: [ + 'SnapController:handleRequest', + 'AuthenticationController:getBearerToken', + 'AuthenticationController:getSessionProfile', + 'AuthenticationController:isSignedIn', + 'AuthenticationController:performSignOut', + 'AuthenticationController:performSignIn', + 'NotificationServicesController:disableNotificationServices', + 'NotificationServicesController:selectIsNotificationServicesEnabled', + ], + allowedEvents: [], + }), + }); + + const notificationServicesController = + new NotificationServicesController.Controller({ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore TODO: Resolve/patch mismatch between messenger types + messenger: this.controllerMessenger.getRestricted({ + name: 'NotificationServicesController', + allowedActions: [ + 'KeyringController:getAccounts', + 'AuthenticationController:getBearerToken', + 'AuthenticationController:isSignedIn', + 'UserStorageController:enableProfileSyncing', + 'UserStorageController:getStorageKey', + 'UserStorageController:performGetStorage', + 'UserStorageController:performSetStorage', + ], + allowedEvents: ['KeyringController:stateChange'], + }), + state: initialState.NotificationServicesController, + env: { + isPushIntegrated: false, + featureAnnouncements: { + platform: 'mobile', + accessToken: process.env + .FEATURES_ANNOUNCEMENTS_ACCESS_TOKEN as string, + spaceId: process.env.FEATURES_ANNOUNCEMENTS_SPACE_ID as string, + }, + }, + }); ///: END:ONLY_INCLUDE_IF this.transactionController = new TransactionController({ @@ -1296,12 +1389,12 @@ class Engine { ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) snapController, subjectMetadataController, + authenticationController, + userStorageController, + notificationServicesController, ///: END:ONLY_INCLUDE_IF accountsController, - ]; - - if (isBlockaidFeatureEnabled()) { - const ppomController = new PPOMController({ + new PPOMController({ chainId: networkController.state.providerConfig.chainId, blockaidPublicKey: process.env.BLOCKAID_PUBLIC_KEY as string, cdnBaseUrl: process.env.BLOCKAID_FILE_CDN as string, @@ -1333,9 +1426,8 @@ class Engine { // TODO: Replace "any" with type // eslint-disable-next-line @typescript-eslint/no-explicit-any nativeCrypto: Crypto as any, - }); - controllers.push(ppomController); - } + }), + ]; // set initial state // TODO: Pass initial state into each controller constructor instead @@ -1498,6 +1590,8 @@ class Engine { getTotalFiatAccountBalance = (): { ethFiat: number; tokenFiat: number; + tokenFiat1dAgo: number; + ethFiat1dAgo: number; } => { const { CurrencyRateController, @@ -1516,7 +1610,7 @@ class Engine { } = store.getState(); if (isTestNet(chainId) && !showFiatOnTestnets) { - return { ethFiat: 0, tokenFiat: 0 }; + return { ethFiat: 0, tokenFiat: 0, ethFiat1dAgo: 0, tokenFiat1dAgo: 0 }; } const conversionRate = @@ -1525,9 +1619,12 @@ class Engine { const { accountsByChainId } = AccountTrackerController.state; const { tokens } = TokensController.state; + const { marketData: tokenExchangeRates } = TokenRatesController.state; let ethFiat = 0; + let ethFiat1dAgo = 0; let tokenFiat = 0; + let tokenFiat1dAgo = 0; const decimalsToShow = (currentCurrency === 'usd' && 2) || undefined; if (accountsByChainId?.[toHexadecimal(chainId)]?.[selectedAddress]) { ethFiat = weiToFiatNumber( @@ -1536,6 +1633,15 @@ class Engine { decimalsToShow, ); } + + ethFiat1dAgo = + ethFiat + + (ethFiat * + tokenExchangeRates?.[toHexadecimal(chainId)]?.[ + zeroAddress() as `0x${string}` + ]?.pricePercentChange1d) / + 100 || ethFiat; + if (tokens.length > 0) { const { contractBalances: tokenBalances } = TokenBalancesController.state; const { marketData } = TokenRatesController.state; @@ -1544,8 +1650,9 @@ class Engine { (item: { address: string; balance?: string; decimals: number }) => { const exchangeRate = tokenExchangeRates && item.address in tokenExchangeRates - ? tokenExchangeRates[item.address as Hex] + ? tokenExchangeRates[item.address as Hex]?.price : undefined; + const tokenBalance = item.balance || (item.address in tokenBalances @@ -1562,14 +1669,25 @@ class Engine { exchangeRate, decimalsToShow, ); + + const tokenBalance1dAgo = + tokenBalanceFiat + + (tokenBalanceFiat * + tokenExchangeRates?.[item.address as `0x${string}`] + ?.pricePercentChange1d) / + 100 || tokenBalanceFiat; + tokenFiat += tokenBalanceFiat; + tokenFiat1dAgo += tokenBalance1dAgo; }, ); } return { ethFiat: ethFiat ?? 0, + ethFiat1dAgo: ethFiat1dAgo ?? 0, tokenFiat: tokenFiat ?? 0, + tokenFiat1dAgo: tokenFiat1dAgo ?? 0, }; }; @@ -1792,6 +1910,9 @@ export default { ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) SnapController, SubjectMetadataController, + AuthenticationController, + UserStorageController, + NotificationServicesController, ///: END:ONLY_INCLUDE_IF PermissionController, ApprovalController, @@ -1833,6 +1954,9 @@ export default { ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) SnapController, SubjectMetadataController, + AuthenticationController, + UserStorageController, + NotificationServicesController, ///: END:ONLY_INCLUDE_IF PermissionController, ApprovalController, diff --git a/app/core/EngineService/EngineService.ts b/app/core/EngineService/EngineService.ts index ecec1d552cf..84d26c1324d 100644 --- a/app/core/EngineService/EngineService.ts +++ b/app/core/EngineService/EngineService.ts @@ -1,7 +1,6 @@ import UntypedEngine from '../Engine'; import AppConstants from '../AppConstants'; import { getVaultFromBackup } from '../BackupVault'; -import { isBlockaidFeatureEnabled } from '../../util/blockaid'; import { store as importedStore } from '../../store'; import Logger from '../../util/Logger'; import { @@ -115,14 +114,11 @@ class EngineService { name: 'AccountsController', key: `${engine.context.AccountsController.name}:stateChange`, }, - ]; - - if (isBlockaidFeatureEnabled()) { - controllers.push({ + { name: 'PPOMController', key: `${engine.context.PPOMController.name}:stateChange`, - }); - } + }, + ]; engine?.datamodel?.subscribe?.(() => { if (!engine.context.KeyringController.metadata.vault) { diff --git a/app/core/Ledger/Ledger.test.ts b/app/core/Ledger/Ledger.test.ts index 798ebe8d4c0..e078666d087 100644 --- a/app/core/Ledger/Ledger.test.ts +++ b/app/core/Ledger/Ledger.test.ts @@ -1,20 +1,17 @@ -import LedgerKeyring from '@consensys/ledgerhq-metamask-keyring'; import { - connectLedgerHardware, - openEthereumAppOnLedger, closeRunningAppOnLedger, + connectLedgerHardware, forgetLedger, - ledgerSignTypedMessage, - unlockLedgerDefaultAccount, getDeviceId, - withLedgerKeyring, + getLedgerAccountsByOperation, + ledgerSignTypedMessage, + openEthereumAppOnLedger, + unlockLedgerWalletAccount, } from './Ledger'; import Engine from '../../core/Engine'; -import { - KeyringTypes, - SignTypedDataVersion, -} from '@metamask/keyring-controller'; +import { SignTypedDataVersion } from '@metamask/keyring-controller'; import type BleTransport from '@ledgerhq/react-native-hw-transport-ble'; +import OperationTypes from './types'; jest.mock('../../core/Engine', () => ({ context: { @@ -26,27 +23,86 @@ jest.mock('../../core/Engine', () => ({ })); const MockEngine = jest.mocked(Engine); +interface mockKeyringType { + addAccounts: jest.Mock; + bridge: { + getAppNameAndVersion: jest.Mock; + updateTransportMethod: jest.Mock; + openEthApp: jest.Mock; + closeApps: jest.Mock; + }; + deserialize: jest.Mock; + forgetDevice: jest.Mock; + getDeviceId: jest.Mock; + getFirstPage: jest.Mock; + getNextPage: jest.Mock; + getPreviousPage: jest.Mock; + setDeviceId: jest.Mock; + setHdPath: jest.Mock; + setAccountToUnlock: jest.Mock; +} + describe('Ledger core', () => { - let ledgerKeyring: LedgerKeyring; + let ledgerKeyring: mockKeyringType; beforeEach(() => { jest.resetAllMocks(); - // @ts-expect-error This is a partial mock, not completely identical - // TODO: Replace this with a type-safe mock + const mockKeyringController = MockEngine.context.KeyringController; + ledgerKeyring = { addAccounts: jest.fn(), - setTransport: jest.fn(), - getAppAndVersion: jest.fn().mockResolvedValue({ appName: 'appName' }), - getDefaultAccount: jest.fn().mockResolvedValue('defaultAccount'), - openEthApp: jest.fn(), - quitApp: jest.fn(), - forgetDevice: jest.fn(), + bridge: { + getAppNameAndVersion: jest + .fn() + .mockResolvedValue({ appName: 'appName' }), + updateTransportMethod: jest.fn(), + openEthApp: jest.fn(), + closeApps: jest.fn(), + }, deserialize: jest.fn(), - deviceId: 'deviceId', - getName: jest.fn().mockResolvedValue('name'), + forgetDevice: jest.fn(), + getDeviceId: jest.fn().mockReturnValue('deviceId'), + getFirstPage: jest.fn().mockResolvedValue([ + { + balance: '0', + address: '0x49b6FFd1BD9d1c64EEf400a64a1e4bBC33E2CAB2', + index: 0, + }, + { + balance: '1', + address: '0x49b6FFd1BD9d1c64EEf400a64a1e4bBC33E2CAB3', + index: 1, + }, + ]), + getNextPage: jest.fn().mockResolvedValue([ + { + balance: '4', + address: '0x49b6FFd1BD9d1c64EEf400a64a1e4bBC33E2CAB4', + index: 4, + }, + { + balance: '5', + address: '0x49b6FFd1BD9d1c64EEf400a64a1e4bBC33E2CAB5', + index: 5, + }, + ]), + getPreviousPage: jest.fn().mockResolvedValue([ + { + balance: '2', + address: '0x49b6FFd1BD9d1c64EEf400a64a1e4bBC33E2CAB6', + index: 2, + }, + { + balance: '3', + address: '0x49b6FFd1BD9d1c64EEf400a64a1e4bBC33E2CAB7', + index: 3, + }, + ]), + setDeviceId: jest.fn(), + setHdPath: jest.fn(), + setAccountToUnlock: jest.fn(), }; - const mockKeyringController = MockEngine.context.KeyringController; mockKeyringController.withKeyring.mockImplementation( // @ts-expect-error The Ledger keyring is not compatible with our keyring type yet @@ -57,99 +113,73 @@ describe('Ledger core', () => { describe('connectLedgerHardware', () => { const mockTransport = 'foo' as unknown as BleTransport; - it('should call keyring.setTransport', async () => { + it('calls keyring.setTransport', async () => { await connectLedgerHardware(mockTransport, 'bar'); - expect(ledgerKeyring.setTransport).toHaveBeenCalled(); + expect(ledgerKeyring.bridge.updateTransportMethod).toHaveBeenCalled(); }); - it('should call keyring.getAppAndVersion', async () => { + it('calls keyring.getAppAndVersion', async () => { await connectLedgerHardware(mockTransport, 'bar'); - expect(ledgerKeyring.getAppAndVersion).toHaveBeenCalled(); + expect(ledgerKeyring.bridge.getAppNameAndVersion).toHaveBeenCalled(); }); - it('should return app name', async () => { + it('returns app name correctly', async () => { const value = await connectLedgerHardware(mockTransport, 'bar'); expect(value).toBe('appName'); }); - }); - - describe('withLedgerKeyring', () => { - it('runs the operation with a Ledger keyring', async () => { - const mockOperation = jest.fn(); - const mockLedgerKeyring = {}; - MockEngine.context.KeyringController.withKeyring.mockImplementation( - async ( - selector: Record, - operation: Parameters< - typeof MockEngine.context.KeyringController.withKeyring - >[1], - options?: Record, - ) => { - expect(selector).toStrictEqual({ type: KeyringTypes.ledger }); - expect(options).toStrictEqual({ createIfMissing: true }); - // @ts-expect-error This mock keyring is not type compatible - await operation(mockLedgerKeyring); - }, - ); - - await withLedgerKeyring(mockOperation); - - expect(mockOperation).toHaveBeenCalledWith(mockLedgerKeyring); - }); - }); - - describe('unlockLedgerDefaultAccount', () => { - it('should not call KeyringController.addNewAccountForKeyring if isAccountImportReq is false', async () => { - const account = await unlockLedgerDefaultAccount(false); - - expect(ledgerKeyring.getDefaultAccount).toHaveBeenCalled(); - expect(account).toEqual({ - address: 'defaultAccount', - balance: '0x0', - }); - }); - it('should call KeyringController.addNewAccountForKeyring if isAccountImportReq is true', async () => { - const account = await unlockLedgerDefaultAccount(true); - - expect(ledgerKeyring.getDefaultAccount).toHaveBeenCalled(); - expect(account).toEqual({ - address: 'defaultAccount', - balance: '0x0', - }); + it('calls keyring.setHdPath and keyring.setDeviceId if deviceId is different', async () => { + await connectLedgerHardware(mockTransport, 'bar'); + expect(ledgerKeyring.setHdPath).toHaveBeenCalled(); + expect(ledgerKeyring.setDeviceId).toHaveBeenCalled(); }); }); describe('openEthereumAppOnLedger', () => { - it('should call keyring.openEthApp', async () => { + it('calls keyring.openEthApp', async () => { await openEthereumAppOnLedger(); - expect(ledgerKeyring.openEthApp).toHaveBeenCalled(); + expect(ledgerKeyring.bridge.openEthApp).toHaveBeenCalled(); }); }); describe('closeRunningAppOnLedger', () => { - it('should call keyring.quitApp', async () => { + it('calls keyring.quitApp', async () => { await closeRunningAppOnLedger(); - expect(ledgerKeyring.quitApp).toHaveBeenCalled(); + expect(ledgerKeyring.bridge.closeApps).toHaveBeenCalled(); }); }); describe('forgetLedger', () => { - it('should call keyring.forgetDevice', async () => { + it('calls keyring.forgetDevice', async () => { await forgetLedger(); expect(ledgerKeyring.forgetDevice).toHaveBeenCalled(); }); }); describe('getDeviceId', () => { - it('should return deviceId', async () => { + it('returns deviceId', async () => { const value = await getDeviceId(); expect(value).toBe('deviceId'); }); }); + describe('getLedgerAccountsByOperation', () => { + it('calls ledgerKeyring.getNextPage on ledgerKeyring', async () => { + await getLedgerAccountsByOperation(OperationTypes.GET_NEXT_PAGE); + expect(ledgerKeyring.getNextPage).toHaveBeenCalled(); + }); + it('calls getPreviousPage on ledgerKeyring', async () => { + await getLedgerAccountsByOperation(OperationTypes.GET_PREVIOUS_PAGE); + expect(ledgerKeyring.getPreviousPage).toHaveBeenCalled(); + }); + it('calls getFirstPage on ledgerKeyring', async () => { + await getLedgerAccountsByOperation(OperationTypes.GET_FIRST_PAGE); + expect(ledgerKeyring.getFirstPage).toHaveBeenCalled(); + }); + }); + describe('ledgerSignTypedMessage', () => { - it('should call signTypedMessage from keyring controller and return correct signature', async () => { + it('calls signTypedMessage from keyring controller and return correct signature', async () => { const expectedArg = { from: '0x49b6FFd1BD9d1c64EEf400a64a1e4bBC33E2CAB2', data: 'data', @@ -164,4 +194,12 @@ describe('Ledger core', () => { expect(value).toBe('signature'); }); }); + + describe(`unlockLedgerWalletAccount`, () => { + it(`calls keyring.setAccountToUnlock and addAccounts`, async () => { + await unlockLedgerWalletAccount(1); + expect(ledgerKeyring.setAccountToUnlock).toHaveBeenCalled(); + expect(ledgerKeyring.addAccounts).toHaveBeenCalledWith(1); + }); + }); }); diff --git a/app/core/Ledger/Ledger.ts b/app/core/Ledger/Ledger.ts index 3e584ff7d06..d5ca3aacc5f 100644 --- a/app/core/Ledger/Ledger.ts +++ b/app/core/Ledger/Ledger.ts @@ -1,8 +1,13 @@ -import LedgerKeyring from '@consensys/ledgerhq-metamask-keyring'; import type BleTransport from '@ledgerhq/react-native-hw-transport-ble'; import { SignTypedDataVersion } from '@metamask/keyring-controller'; import ExtendedKeyringTypes from '../../constants/keyringTypes'; import Engine from '../Engine'; +import { + LedgerKeyring, + LedgerMobileBridge, +} from '@metamask/eth-ledger-bridge-keyring'; +import LEDGER_HD_PATH from './constants'; +import OperationTypes from './types'; /** * Perform an operation with the Ledger keyring. @@ -43,46 +48,25 @@ export const connectLedgerHardware = async ( ): Promise => { const appAndVersion = await withLedgerKeyring( async (keyring: LedgerKeyring) => { - // TODO: Replace "any" with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - keyring.setTransport(transport as unknown as any, deviceId); - return await keyring.getAppAndVersion(); + keyring.setHdPath(LEDGER_HD_PATH); + keyring.setDeviceId(deviceId); + + const bridge = keyring.bridge as LedgerMobileBridge; + await bridge.updateTransportMethod(transport); + return await bridge.getAppNameAndVersion(); }, ); return appAndVersion.appName; }; -/** - * Retrieve the first account from the Ledger device. - * @param isAccountImportReq - Whether we need to import a ledger account by calling addNewAccountForKeyring - * @returns The default (first) account on the device - */ -export const unlockLedgerDefaultAccount = async ( - isAccountImportReq: boolean, -): Promise<{ - address: string; - balance: string; -}> => { - const address = await withLedgerKeyring(async (keyring: LedgerKeyring) => { - if (isAccountImportReq) { - await keyring.addAccounts(1); - } - return await keyring.getDefaultAccount(); - }); - - return { - address, - balance: `0x0`, - }; -}; - /** * Automatically opens the Ethereum app on the Ledger device. */ export const openEthereumAppOnLedger = async (): Promise => { await withLedgerKeyring(async (keyring: LedgerKeyring) => { - await keyring.openEthApp(); + const bridge = keyring.bridge as LedgerMobileBridge; + await bridge.openEthApp(); }); }; @@ -91,7 +75,8 @@ export const openEthereumAppOnLedger = async (): Promise => { */ export const closeRunningAppOnLedger = async (): Promise => { await withLedgerKeyring(async (keyring: LedgerKeyring) => { - await keyring.quitApp(); + const bridge = keyring.bridge as LedgerMobileBridge; + await bridge.closeApps(); }); }; @@ -100,7 +85,7 @@ export const closeRunningAppOnLedger = async (): Promise => { */ export const forgetLedger = async (): Promise => { await withLedgerKeyring(async (keyring: LedgerKeyring) => { - await keyring.forgetDevice(); + keyring.forgetDevice(); }); }; @@ -110,7 +95,39 @@ export const forgetLedger = async (): Promise => { * @returns The DeviceId */ export const getDeviceId = async (): Promise => - await withLedgerKeyring(async (keyring: LedgerKeyring) => keyring.deviceId); + await withLedgerKeyring(async (keyring: LedgerKeyring) => + keyring.getDeviceId(), + ); + +/** + * Unlock Ledger Accounts by page + * @param operation - the operation number,
0: Get First Page
1: Get Next Page
-1: Get Previous Page + * @return The Ledger Accounts + */ +export const getLedgerAccountsByOperation = async ( + operation: number, +): Promise<{ balance: string; address: string; index: number }[]> => { + try { + const accounts = await withLedgerKeyring(async (keyring: LedgerKeyring) => { + switch (operation) { + case OperationTypes.GET_PREVIOUS_PAGE: + return await keyring.getPreviousPage(); + case OperationTypes.GET_NEXT_PAGE: + return await keyring.getNextPage(); + default: + return await keyring.getFirstPage(); + } + }); + + return accounts.map((account) => ({ + ...account, + balance: '0x0', + })); + } catch (e) { + /* istanbul ignore next */ + throw new Error(`Unspecified error when connect Ledger Hardware, ${e}`); + } +}; /** * signTypedMessage from Ledger Keyring @@ -137,3 +154,15 @@ export const ledgerSignTypedMessage = async ( version, ); }; + +/** + * Unlock Ledger Wallet Account with index, and add it that account to metamask + * + * @param index - The index of the account to unlock + */ +export const unlockLedgerWalletAccount = async (index: number) => { + await withLedgerKeyring(async (keyring: LedgerKeyring) => { + keyring.setAccountToUnlock(index); + await keyring.addAccounts(1); + }); +}; diff --git a/app/core/Ledger/constants.ts b/app/core/Ledger/constants.ts new file mode 100644 index 00000000000..f9bb42547fe --- /dev/null +++ b/app/core/Ledger/constants.ts @@ -0,0 +1,3 @@ +const LEDGER_HD_PATH = `m/44'/60'/0'/0`; + +export default LEDGER_HD_PATH; diff --git a/app/core/Ledger/types.ts b/app/core/Ledger/types.ts new file mode 100644 index 00000000000..c3f62ae8d93 --- /dev/null +++ b/app/core/Ledger/types.ts @@ -0,0 +1,7 @@ +enum OperationTypes { + GET_FIRST_PAGE = 0, + GET_NEXT_PAGE = 1, + GET_PREVIOUS_PAGE = -1, +} + +export default OperationTypes; diff --git a/app/core/RPCMethods/RPCMethodMiddleware.test.ts b/app/core/RPCMethods/RPCMethodMiddleware.test.ts index e99e934cea6..418ccf309c9 100644 --- a/app/core/RPCMethods/RPCMethodMiddleware.test.ts +++ b/app/core/RPCMethods/RPCMethodMiddleware.test.ts @@ -20,7 +20,7 @@ import { PermissionController, } from '@metamask/permission-controller'; import PPOMUtil from '../../lib/ppom/ppom-util'; -import initialBackgroundState from '../../util/test/initial-background-state.json'; +import { backgroundState } from '../../util/test/initial-root-state'; import { Store } from 'redux'; import { RootState } from 'app/reducers'; import { addTransaction } from '../../util/transaction-controller'; @@ -241,7 +241,7 @@ function setupGlobalState({ : {}, engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, NetworkController: { providerConfig: providerConfig || {}, }, diff --git a/app/core/RPCMethods/eth_sendTransaction.test.ts b/app/core/RPCMethods/eth_sendTransaction.test.ts index c1f064ffdba..c18374d5a73 100644 --- a/app/core/RPCMethods/eth_sendTransaction.test.ts +++ b/app/core/RPCMethods/eth_sendTransaction.test.ts @@ -24,6 +24,12 @@ jest.mock('../../core/Engine', () => ({ providerConfig: { chainId: '0x1' }, }, }, + AccountsController: { + state: { + internalAccounts: { accounts: [] }, + }, + listAccounts: () => [], + }, }, })); diff --git a/app/core/RPCMethods/wallet_addEthereumChain.js b/app/core/RPCMethods/wallet_addEthereumChain.js index 4de1edd074f..bd9568a2326 100644 --- a/app/core/RPCMethods/wallet_addEthereumChain.js +++ b/app/core/RPCMethods/wallet_addEthereumChain.js @@ -112,7 +112,9 @@ const wallet_addEthereumChain = async ({ ); } - if (Object.values(ChainId).find((value) => value === _chainId)) { + //TODO: Remove aurora from default chains in @metamask/controller-utils + const actualChains = { ...ChainId, aurora: undefined }; + if (Object.values(actualChains).find((value) => value === _chainId)) { throw rpcErrors.invalidParams(`May not specify default MetaMask chain.`); } diff --git a/app/core/SDKConnect/AndroidSDK/AndroidService.ts b/app/core/SDKConnect/AndroidSDK/AndroidService.ts index c53a1ab7428..8ebaa583977 100644 --- a/app/core/SDKConnect/AndroidSDK/AndroidService.ts +++ b/app/core/SDKConnect/AndroidSDK/AndroidService.ts @@ -28,27 +28,23 @@ import { PermissionController } from '@metamask/permission-controller'; import { PreferencesController } from '@metamask/preferences-controller'; import { PROTOCOLS } from '../../../constants/deeplinks'; import BatchRPCManager from '../BatchRPCManager'; -import { - DEFAULT_SESSION_TIMEOUT_MS, - METHODS_TO_DELAY, - RPC_METHODS, -} from '../SDKConnectConstants'; -import getDefaultBridgeParams from './getDefaultBridgeParams'; -import handleBatchRpcResponse from '../handlers/handleBatchRpcResponse'; +import { DEFAULT_SESSION_TIMEOUT_MS } from '../SDKConnectConstants'; import handleCustomRpcCalls from '../handlers/handleCustomRpcCalls'; import DevLogger from '../utils/DevLogger'; import AndroidSDKEventHandler from './AndroidNativeSDKEventHandler'; +import sendMessage from './AndroidService/sendMessage'; import { DappClient, DappConnections } from './dapp-sdk-types'; +import getDefaultBridgeParams from './getDefaultBridgeParams'; export default class AndroidService extends EventEmitter2 { - private communicationClient = NativeModules.CommunicationClient; - private connections: DappConnections = {}; - private rpcQueueManager = new RPCQueueManager(); - private bridgeByClientId: { [clientId: string]: BackgroundBridge } = {}; - private eventHandler: AndroidSDKEventHandler; - private batchRPCManager: BatchRPCManager = new BatchRPCManager('android'); + public communicationClient = NativeModules.CommunicationClient; + public connections: DappConnections = {}; + public rpcQueueManager = new RPCQueueManager(); + public bridgeByClientId: { [clientId: string]: BackgroundBridge } = {}; + public eventHandler: AndroidSDKEventHandler; + public batchRPCManager: BatchRPCManager = new BatchRPCManager('android'); // To keep track in order to get the associated bridge to handle batch rpc calls - private currentClientId?: string; + public currentClientId?: string; constructor() { super(); @@ -475,70 +471,6 @@ export default class AndroidService extends EventEmitter2 { // TODO: Replace "any" with type // eslint-disable-next-line @typescript-eslint/no-explicit-any async sendMessage(message: any, forceRedirect?: boolean) { - const id = message?.data?.id; - this.communicationClient.sendMessage(JSON.stringify(message)); - let rpcMethod = this.rpcQueueManager.getId(id); - - DevLogger.log(`AndroidService::sendMessage method=${rpcMethod}`, message); - // handle multichain rpc call responses separately - const chainRPCs = this.batchRPCManager.getById(id); - if (chainRPCs) { - const isLastRpcOrError = await handleBatchRpcResponse({ - chainRpcs: chainRPCs, - msg: message, - backgroundBridge: this.bridgeByClientId[this.currentClientId ?? ''], - batchRPCManager: this.batchRPCManager, - sendMessage: ({ msg }) => this.sendMessage(msg), - }); - DevLogger.log( - `AndroidService::sendMessage isLastRpc=${isLastRpcOrError}`, - chainRPCs, - ); - - if (!isLastRpcOrError) { - DevLogger.log( - `AndroidService::sendMessage NOT last rpc --- skip goBack()`, - chainRPCs, - ); - this.rpcQueueManager.remove(id); - // Only continue processing the message and goback if all rpcs in the batch have been handled - return; - } - - // Always set the method to metamask_batch otherwise it may not have been set correctly because of the batch rpc flow. - rpcMethod = RPC_METHODS.METAMASK_BATCH; - DevLogger.log( - `AndroidService::sendMessage chainRPCs=${chainRPCs} COMPLETED!`, - ); - } - - this.rpcQueueManager.remove(id); - - if (!rpcMethod && forceRedirect !== true) { - DevLogger.log( - `AndroidService::sendMessage no rpc method --- rpcMethod=${rpcMethod} forceRedirect=${forceRedirect} --- skip goBack()`, - ); - return; - } - - try { - if (METHODS_TO_DELAY[rpcMethod]) { - // Add delay to see the feedback modal - await wait(1000); - } - - if (!this.rpcQueueManager.isEmpty()) { - DevLogger.log( - `AndroidService::sendMessage NOT empty --- skip goBack()`, - this.rpcQueueManager.get(), - ); - return; - } - - DevLogger.log(`AndroidService::sendMessage empty --- goBack()`); - Minimizer.goBack(); - } catch (error) { - Logger.log(error, `AndroidService:: error waiting for empty rpc queue`); - } + return sendMessage(this, message, forceRedirect); } } diff --git a/app/core/SDKConnect/AndroidSDK/AndroidService/sendMessage.test.ts b/app/core/SDKConnect/AndroidSDK/AndroidService/sendMessage.test.ts new file mode 100644 index 00000000000..647b57884eb --- /dev/null +++ b/app/core/SDKConnect/AndroidSDK/AndroidService/sendMessage.test.ts @@ -0,0 +1,131 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import Logger from '../../../../util/Logger'; +import Engine from '../../../Engine'; +import { Minimizer } from '../../../NativeModules'; +import { RPC_METHODS } from '../../SDKConnectConstants'; +import handleBatchRpcResponse from '../../handlers/handleBatchRpcResponse'; +import { wait } from '../../utils/wait.util'; +import AndroidService from '../AndroidService'; +import sendMessage from './sendMessage'; + +jest.mock('../../../Engine'); +jest.mock('../../../NativeModules', () => ({ + Minimizer: { + goBack: jest.fn(), + }, +})); +jest.mock('../../../../util/Logger'); +jest.mock('../../utils/wait.util', () => ({ + wait: jest.fn().mockResolvedValue(undefined), +})); +jest.mock('@metamask/preferences-controller'); +jest.mock('../AndroidService'); +jest.mock('../../handlers/handleBatchRpcResponse', () => jest.fn()); +jest.mock('../../utils/DevLogger'); + +describe('sendMessage', () => { + let instance: jest.Mocked; + let message: any; + + const mockGetId = jest.fn(); + const mockRemove = jest.fn(); + const mockIsEmpty = jest.fn().mockReturnValue(true); + const mockGet = jest.fn(); + const mockSendMessage = jest.fn(); + const mockGetById = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + + instance = { + rpcQueueManager: { + getId: mockGetId, + remove: mockRemove, + isEmpty: mockIsEmpty, + get: mockGet, + }, + communicationClient: { + sendMessage: mockSendMessage, + }, + batchRPCManager: { + getById: mockGetById, + }, + bridgeByClientId: {}, + currentClientId: 'test-client-id', + } as unknown as jest.Mocked; + + message = { + data: { + id: 'test-id', + result: ['0x1', '0x2'], + }, + }; + + (Engine.context as any) = { + PreferencesController: { + state: { + selectedAddress: '0x1', + }, + }, + }; + }); + + it('should send message with reordered accounts if selectedAddress is in result', async () => { + mockGetId.mockReturnValue(RPC_METHODS.ETH_REQUESTACCOUNTS); + + await sendMessage(instance, message); + + expect(mockSendMessage).toHaveBeenCalledWith( + JSON.stringify({ + ...message, + data: { + ...message.data, + result: ['0x1', '0x2'], + }, + }), + ); + }); + + it('should send message without reordering if selectedAddress is not in result', async () => { + (Engine.context as any).PreferencesController.state.selectedAddress = '0x3'; + + mockGetId.mockReturnValue(RPC_METHODS.ETH_REQUESTACCOUNTS); + + await sendMessage(instance, message); + + expect(mockSendMessage).toHaveBeenCalledWith(JSON.stringify(message)); + }); + + it('should handle multichain rpc call responses separately', async () => { + mockGetId.mockReturnValue('someMethod'); + mockGetById.mockReturnValue(['rpc1', 'rpc2']); + (handleBatchRpcResponse as jest.Mock).mockResolvedValue(true); + + await sendMessage(instance, message); + + expect(handleBatchRpcResponse).toHaveBeenCalled(); + expect(mockRemove).toHaveBeenCalledWith('test-id'); + expect(mockSendMessage).toHaveBeenCalledWith(JSON.stringify(message)); + }); + + it('should not call goBack if rpcQueueManager is not empty', async () => { + mockGetId.mockReturnValue('someMethod'); + mockIsEmpty.mockReturnValue(false); + + await sendMessage(instance, message); + + expect(Minimizer.goBack).not.toHaveBeenCalled(); + }); + + it('should handle error when waiting for empty rpc queue', async () => { + mockGetId.mockReturnValue('someMethod'); + (wait as jest.Mock).mockRejectedValue(new Error('test error')); + + await sendMessage(instance, message); + + expect(Logger.log).toHaveBeenCalledWith( + expect.any(Error), + `AndroidService:: error waiting for empty rpc queue`, + ); + }); +}); diff --git a/app/core/SDKConnect/AndroidSDK/AndroidService/sendMessage.ts b/app/core/SDKConnect/AndroidSDK/AndroidService/sendMessage.ts new file mode 100644 index 00000000000..b795c789d0b --- /dev/null +++ b/app/core/SDKConnect/AndroidSDK/AndroidService/sendMessage.ts @@ -0,0 +1,128 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import Engine from '../../../Engine'; +import { Minimizer } from '../../../NativeModules'; +import Logger from '../../../../util/Logger'; +import { wait } from '../../utils/wait.util'; +import { PreferencesController } from '@metamask/preferences-controller'; +import AndroidService from '../AndroidService'; +import { METHODS_TO_DELAY, RPC_METHODS } from '../../SDKConnectConstants'; +import handleBatchRpcResponse from '../../handlers/handleBatchRpcResponse'; +import DevLogger from '../../utils/DevLogger'; + +async function sendMessage( + instance: AndroidService, + message: any, + forceRedirect?: boolean, +) { + const id = message?.data?.id; + let rpcMethod = instance.rpcQueueManager.getId(id); + + const isConnectionResponse = rpcMethod === RPC_METHODS.ETH_REQUESTACCOUNTS; + + if (isConnectionResponse) { + const preferencesController = ( + Engine.context as { + PreferencesController: PreferencesController; + } + ).PreferencesController; + + const selectedAddress = + preferencesController.state.selectedAddress.toLowerCase(); + + const lowercaseAccounts = (message.data.result as string[]).map( + (a: string) => a.toLowerCase(), + ); + + const isPartOfConnectedAddresses = + lowercaseAccounts.includes(selectedAddress); + + if (isPartOfConnectedAddresses) { + // Remove the selectedAddress from the lowercaseAccounts if it exists + const remainingAccounts = lowercaseAccounts.filter( + (account) => account !== selectedAddress, + ); + + // Create the reorderedAccounts array with selectedAddress as the first element + const reorderedAccounts: string[] = [ + selectedAddress, + ...remainingAccounts, + ]; + + message = { + ...message, + data: { + ...message.data, + result: reorderedAccounts, + }, + }; + } + } + + instance.communicationClient.sendMessage(JSON.stringify(message)); + + DevLogger.log(`AndroidService::sendMessage method=${rpcMethod}`, message); + + // handle multichain rpc call responses separately + const chainRPCs = instance.batchRPCManager.getById(id); + if (chainRPCs) { + const isLastRpcOrError = await handleBatchRpcResponse({ + chainRpcs: chainRPCs, + msg: message, + backgroundBridge: + instance.bridgeByClientId[instance.currentClientId ?? ''], + batchRPCManager: instance.batchRPCManager, + sendMessage: ({ msg }) => instance.sendMessage(msg), + }); + DevLogger.log( + `AndroidService::sendMessage isLastRpc=${isLastRpcOrError}`, + chainRPCs, + ); + + if (!isLastRpcOrError) { + DevLogger.log( + `AndroidService::sendMessage NOT last rpc --- skip goBack()`, + chainRPCs, + ); + instance.rpcQueueManager.remove(id); + // Only continue processing the message and goback if all rpcs in the batch have been handled + return; + } + + // Always set the method to metamask_batch otherwise it may not have been set correctly because of the batch rpc flow. + rpcMethod = RPC_METHODS.METAMASK_BATCH; + DevLogger.log( + `AndroidService::sendMessage chainRPCs=${chainRPCs} COMPLETED!`, + ); + } + + instance.rpcQueueManager.remove(id); + + if (!rpcMethod && forceRedirect !== true) { + DevLogger.log( + `AndroidService::sendMessage no rpc method --- rpcMethod=${rpcMethod} forceRedirect=${forceRedirect} --- skip goBack()`, + ); + return; + } + + try { + if (METHODS_TO_DELAY[rpcMethod]) { + // Add delay to see the feedback modal + await wait(1000); + } + + if (!instance.rpcQueueManager.isEmpty()) { + DevLogger.log( + `AndroidService::sendMessage NOT empty --- skip goBack()`, + instance.rpcQueueManager.get(), + ); + return; + } + + DevLogger.log(`AndroidService::sendMessage empty --- goBack()`); + Minimizer.goBack(); + } catch (error) { + Logger.log(error, `AndroidService:: error waiting for empty rpc queue`); + } +} + +export default sendMessage; diff --git a/app/core/SDKConnect/SDKDeeplinkProtocol/DeeplinkProtocolService.test.ts b/app/core/SDKConnect/SDKDeeplinkProtocol/DeeplinkProtocolService.test.ts index cc3f55c79f5..a91c09105ef 100644 --- a/app/core/SDKConnect/SDKDeeplinkProtocol/DeeplinkProtocolService.test.ts +++ b/app/core/SDKConnect/SDKDeeplinkProtocol/DeeplinkProtocolService.test.ts @@ -8,6 +8,8 @@ import handleBatchRpcResponse from '../handlers/handleBatchRpcResponse'; import handleCustomRpcCalls from '../handlers/handleCustomRpcCalls'; import DevLogger from '../utils/DevLogger'; import DeeplinkProtocolService from './DeeplinkProtocolService'; +import AppConstants from '../../AppConstants'; +import { DappClient } from '../AndroidSDK/dapp-sdk-types'; jest.mock('../SDKConnect'); jest.mock('../../../core/Engine'); @@ -134,7 +136,9 @@ describe('DeeplinkProtocolService', () => { it('should handle batch RPC responses', async () => { const mockChainRPCs = [{ id: '1' }]; - const mockMessage = { data: { id: '1', error: null } }; + const mockMessage = { + data: { id: '1', accounts: [], chainId: '0x1', error: null }, + }; service.batchRPCManager.getById = jest .fn() .mockReturnValue(mockChainRPCs); @@ -165,7 +169,14 @@ describe('DeeplinkProtocolService', () => { }); it('should handle error in message data', async () => { - const mockMessage = { data: { id: '1', error: new Error('Test error') } }; + const mockMessage = { + data: { + id: '1', + accounts: [], + chainId: '0x1', + error: new Error('Test error'), + }, + }; const openDeeplinkSpy = jest.spyOn(service, 'openDeeplink'); service.currentClientId = 'client1'; @@ -204,7 +215,14 @@ describe('DeeplinkProtocolService', () => { it('should handle non-final batch RPC response and error in message data', async () => { const mockChainRPCs = [{ id: '1' }]; - const mockMessage = { data: { id: '1', error: new Error('Test error') } }; + const mockMessage = { + data: { + id: '1', + accounts: [], + chainId: '0x1', + error: new Error('Test error'), + }, + }; const devLoggerSpy = jest.spyOn(DevLogger, 'log'); const openDeeplinkSpy = jest.spyOn(service, 'openDeeplink'); service.batchRPCManager.getById = jest @@ -322,6 +340,122 @@ describe('DeeplinkProtocolService', () => { }); }); + describe('handleConnectionEventAsync', () => { + let clientInfo: DappClient; + + let params: { + dappPublicKey: string; + url: string; + scheme: string; + channelId: string; + originatorInfo?: string; + request?: string; + }; + + beforeEach(() => { + clientInfo = { + clientId: 'client1', + originatorInfo: { + url: 'test.com', + title: 'Test', + platform: 'test', + dappId: 'dappId', + }, + connected: false, + validUntil: Date.now(), + scheme: 'scheme1', + }; + params = { + dappPublicKey: 'key', + url: 'url', + scheme: 'scheme1', + channelId: 'client1', + originatorInfo: Buffer.from( + JSON.stringify({ + originatorInfo: { + url: 'test.com', + title: 'Test', + platform: 'test', + dappId: 'dappId', + }, + }), + ).toString('base64'), + request: JSON.stringify({ id: '1', method: 'test', params: [] }), + }; + + // Mocking methods + service.checkPermission = jest.fn().mockResolvedValue(null); + service.setupBridge = jest.fn(); + service.sendMessage = jest.fn().mockResolvedValue(null); + service.processDappRpcRequest = jest.fn().mockResolvedValue(null); + service.openDeeplink = jest.fn().mockResolvedValue(null); + + (Engine.context as unknown) = { + PermissionController: { + requestPermissions: jest.fn().mockResolvedValue(null), + }, + KeyringController: { + unlock: jest.fn().mockResolvedValue(null), + isUnlocked: jest.fn().mockReturnValue(true), + }, + PreferencesController: { + state: { + selectedAddress: '0xAddress', + }, + }, + }; + }); + + it('should setup a new client bridge if the connection does not exist', async () => { + await service.handleConnectionEventAsync({ clientInfo, params }); + expect(service.checkPermission).toHaveBeenCalledWith({ + originatorInfo: clientInfo.originatorInfo, + channelId: clientInfo.clientId, + }); + expect(service.setupBridge).toHaveBeenCalledWith(clientInfo); + expect(SDKConnect.getInstance().addDappConnection).toHaveBeenCalledWith({ + id: clientInfo.clientId, + lastAuthorized: expect.any(Number), + origin: AppConstants.MM_SDK.IOS_SDK, + originatorInfo: clientInfo.originatorInfo, + otherPublicKey: service.dappPublicKeyByClientId[clientInfo.clientId], + validUntil: expect.any(Number), + scheme: clientInfo.scheme, + }); + }); + + it('should update existing client connection and process request if exists', async () => { + service.connections[clientInfo.clientId] = clientInfo; + await service.handleConnectionEventAsync({ clientInfo, params }); + expect(service.processDappRpcRequest).toHaveBeenCalledWith(params); + }); + + it('should send error message if connection event fails', async () => { + (service.checkPermission as jest.Mock).mockRejectedValue( + new Error('Permission error'), + ); + await service.handleConnectionEventAsync({ clientInfo, params }); + expect(service.sendMessage).toHaveBeenCalledWith({ + data: { + error: new Error('Permission error'), + jsonrpc: '2.0', + }, + name: 'metamask-provider', + }); + expect(service.openDeeplink).toHaveBeenCalledWith({ + message: { + data: { + error: new Error('Permission error'), + jsonrpc: '2.0', + }, + name: 'metamask-provider', + }, + clientId: '', + scheme: clientInfo.scheme, + }); + }); + }); + describe('processDappRpcRequest', () => { it('should process a dapp RPC request', async () => { const params = { diff --git a/app/core/SDKConnect/SDKDeeplinkProtocol/DeeplinkProtocolService.ts b/app/core/SDKConnect/SDKDeeplinkProtocol/DeeplinkProtocolService.ts index e68e333c1b7..5ca07e0bf1d 100644 --- a/app/core/SDKConnect/SDKDeeplinkProtocol/DeeplinkProtocolService.ts +++ b/app/core/SDKConnect/SDKDeeplinkProtocol/DeeplinkProtocolService.ts @@ -100,18 +100,7 @@ export default class DeeplinkProtocolService { isRemoteConn: true, // TODO: Replace "any" with type // eslint-disable-next-line @typescript-eslint/no-explicit-any - sendMessage: (msg: any) => { - const response = { - ...msg, - data: { - ...msg.data, - chainId: this.getChainId(), - accounts: this.getSelectedAccounts(), - }, - }; - - return this.sendMessage(response); - }, + sendMessage: (msg: any) => this.sendMessage(msg), ...defaultBridgeParams, }); @@ -121,6 +110,15 @@ export default class DeeplinkProtocolService { // TODO: Replace "any" with type // eslint-disable-next-line @typescript-eslint/no-explicit-any async sendMessage(message: any, forceRedirect?: boolean) { + const messageWithMetadata = { + ...message, + data: { + ...message.data, + chainId: this.getChainId(), + accounts: this.getSelectedAccounts(), + }, + }; + const id = message?.data?.id; DevLogger.log(`DeeplinkProtocolService::sendMessage id=${id}`); @@ -129,14 +127,14 @@ export default class DeeplinkProtocolService { DevLogger.log( `DeeplinkProtocolService::sendMessage method=${rpcMethod}`, - message, + messageWithMetadata, ); // handle multichain rpc call responses separately const chainRPCs = this.batchRPCManager.getById(id); if (chainRPCs) { const isLastRpcOrError = await handleBatchRpcResponse({ chainRpcs: chainRPCs, - msg: message, + msg: messageWithMetadata, backgroundBridge: this.bridgeByClientId[this.currentClientId ?? ''], batchRPCManager: this.batchRPCManager, sendMessage: ({ msg }) => this.sendMessage(msg), @@ -155,7 +153,7 @@ export default class DeeplinkProtocolService { if (hasError) { this.openDeeplink({ - message, + message: messageWithMetadata, clientId: this.currentClientId ?? '', }); return; @@ -201,12 +199,12 @@ export default class DeeplinkProtocolService { DevLogger.log( `DeeplinkProtocolService::sendMessage sending deeplink message=${JSON.stringify( - message, + messageWithMetadata, )}`, ); this.openDeeplink({ - message, + message: messageWithMetadata, clientId: this.currentClientId ?? '', }); } catch (error) { @@ -274,6 +272,116 @@ export default class DeeplinkProtocolService { ); } + public async handleConnectionEventAsync({ + clientInfo, + params, + }: { + clientInfo: DappClient; + params: { + dappPublicKey: string; + url: string; + scheme: string; + channelId: string; + originatorInfo?: string; + request?: string; + }; + }) { + const keyringController = ( + Engine.context as { KeyringController: KeyringController } + ).KeyringController; + + await waitForKeychainUnlocked({ + keyringController, + context: 'DeeplinkProtocolService::setupOnClientsConnectedListener', + }); + + try { + if (!this.connections?.[clientInfo.clientId]) { + DevLogger.log( + `DeeplinkProtocolService::clients_connected - new client ${clientInfo.clientId}}`, + this.connections, + ); + + await this.checkPermission({ + channelId: clientInfo.clientId, + originatorInfo: clientInfo.originatorInfo, + }); + + this.setupBridge(clientInfo); + + this.connections[clientInfo.clientId] = { + clientId: clientInfo.clientId, + connected: true, + validUntil: clientInfo.validUntil, + scheme: clientInfo.scheme, + originatorInfo: clientInfo.originatorInfo, + }; + + await SDKConnect.getInstance().addDappConnection({ + id: clientInfo.clientId, + origin: AppConstants.MM_SDK.IOS_SDK, + lastAuthorized: Date.now(), + otherPublicKey: this.dappPublicKeyByClientId[clientInfo.clientId], + originatorInfo: clientInfo.originatorInfo, + scheme: clientInfo.scheme, + validUntil: Date.now() + DEFAULT_SESSION_TIMEOUT_MS, + }); + } + + if (params.request) { + await this.processDappRpcRequest(params); + + return; + } + + this.sendMessage( + { + data: {}, + }, + true, + ).catch((err) => { + Logger.log( + err, + `DeeplinkProtocolService::clients_connected error sending READY message to client`, + ); + }); + } catch (error) { + Logger.log( + error, + `DeeplinkProtocolService::clients_connected sending jsonrpc error to client - connection rejected`, + ); + + this.sendMessage({ + data: { + error, + jsonrpc: '2.0', + }, + name: 'metamask-provider', + }).catch((err) => { + Logger.log( + err, + `DeeplinkProtocolService::clients_connected error failed sending jsonrpc error to client`, + ); + }); + + const message = { + data: { + error, + jsonrpc: '2.0', + }, + name: 'metamask-provider', + }; + + this.openDeeplink({ + message, + clientId: this.currentClientId ?? '', + scheme: clientInfo.scheme, + }); + + return; + } + } + public async handleConnection(params: { dappPublicKey: string; url: string; @@ -328,10 +436,7 @@ export default class DeeplinkProtocolService { } else { this.sendMessage( { - data: { - chainId: this.getChainId(), - accounts: this.getSelectedAccounts(), - }, + data: {}, }, true, ).catch((err) => { @@ -355,108 +460,10 @@ export default class DeeplinkProtocolService { scheme: clientInfo.scheme, }); - const handleEventAsync = async () => { - const keyringController = ( - Engine.context as { KeyringController: KeyringController } - ).KeyringController; - - await waitForKeychainUnlocked({ - keyringController, - context: 'DeeplinkProtocolService::setupOnClientsConnectedListener', - }); - - try { - if (!this.connections?.[clientInfo.clientId]) { - DevLogger.log( - `DeeplinkProtocolService::clients_connected - new client ${clientInfo.clientId}}`, - this.connections, - ); - // Ask for account permissions - await this.checkPermission({ - originatorInfo: clientInfo.originatorInfo, - channelId: clientInfo.clientId, - }); - - this.setupBridge(clientInfo); - // Save session to SDKConnect - // Save to local connections - this.connections[clientInfo.clientId] = { - connected: true, - clientId: clientInfo.clientId, - originatorInfo: clientInfo.originatorInfo, - validUntil: clientInfo.validUntil, - scheme: clientInfo.scheme, - }; - - await SDKConnect.getInstance().addDappConnection({ - id: clientInfo.clientId, - lastAuthorized: Date.now(), - origin: AppConstants.MM_SDK.IOS_SDK, - originatorInfo: clientInfo.originatorInfo, - otherPublicKey: this.dappPublicKeyByClientId[clientInfo.clientId], - validUntil: Date.now() + DEFAULT_SESSION_TIMEOUT_MS, - scheme: clientInfo.scheme, - }); - } - - if (params.request) { - await this.processDappRpcRequest(params); - - return; - } - - this.sendMessage( - { - data: { - chainId: this.getChainId(), - accounts: this.getSelectedAccounts(), - }, - }, - true, - ).catch((err) => { - Logger.log( - err, - `DeeplinkProtocolService::clients_connected error sending READY message to client`, - ); - }); - } catch (error) { - Logger.log( - error, - `DeeplinkProtocolService::clients_connected sending jsonrpc error to client - connection rejected`, - ); - - this.sendMessage({ - data: { - error, - jsonrpc: '2.0', - }, - name: 'metamask-provider', - }).catch((err) => { - Logger.log( - err, - `DeeplinkProtocolService::clients_connected error failed sending jsonrpc error to client`, - ); - }); - - const message = { - data: { - error, - jsonrpc: '2.0', - }, - name: 'metamask-provider', - }; - - this.openDeeplink({ - message, - clientId: this.currentClientId ?? '', - scheme: clientInfo.scheme, - }); - - return; - } - }; - - handleEventAsync().catch((err) => { + this.handleConnectionEventAsync({ + clientInfo, + params, + }).catch((err) => { Logger.log( err, `DeeplinkProtocolService::clients_connected error handling event`, @@ -666,17 +673,45 @@ export default class DeeplinkProtocolService { const isAccountChanged = dappAccountAddress !== walletSelectedAddress; const isChainChanged = dappAccountChainId !== walletSelectedChainId; - if (isAccountChanged || isChainChanged) { + const rpcMethod = data.method; + + const RPC_METHODS_TO_SKIP = [ + RPC_METHODS.WALLET_ADDETHEREUMCHAIN, + RPC_METHODS.WALLET_SWITCHETHEREUMCHAIN, + ]; + + const checkForRpcMethodToSkip = () => { + const isBatchRequest = rpcMethod === RPC_METHODS.METAMASK_BATCH; + + if (isBatchRequest) { + const batchRpcMethods: string[] = data.params.map( + (rpc: { method: string }) => rpc.method, + ); + + const shouldSkip = batchRpcMethods.some((r) => + RPC_METHODS_TO_SKIP.includes(r), + ); + + return shouldSkip; + } + + return RPC_METHODS_TO_SKIP.includes(rpcMethod); + }; + + const isRpcMethodToSkip = checkForRpcMethodToSkip(); + + if (isAccountChanged || (!isRpcMethodToSkip && isChainChanged)) { + const dynamicErrorMessage = `The selected ${ + isAccountChanged ? 'account' : 'chain' + } has changed. Please try again.`; + this.sendMessage( { data: { id: data.id, - accounts: this.getSelectedAccounts(), - chainId: this.getChainId(), error: { code: -32602, - message: - 'The selected account or chain has changed. Please try again.', + message: dynamicErrorMessage, }, jsonrpc: '2.0', }, diff --git a/app/core/SDKConnect/handlers/handleCustomRpcCalls.ts b/app/core/SDKConnect/handlers/handleCustomRpcCalls.ts index b7241cd22c9..f9f4380d4e5 100644 --- a/app/core/SDKConnect/handlers/handleCustomRpcCalls.ts +++ b/app/core/SDKConnect/handlers/handleCustomRpcCalls.ts @@ -9,6 +9,7 @@ import { NavigationContainerRef } from '@react-navigation/native'; import Routes from '../../../constants/navigation/Routes'; import handleSendMessage from './handleSendMessage'; import { Connection } from '../Connection'; +import { createBuyNavigationDetails } from '../../../components/UI/Ramp/routes/utils'; export const handleCustomRpcCalls = async ({ rpc, @@ -116,7 +117,7 @@ export const handleCustomRpcCalls = async ({ ); navigation?.navigate(Routes.SWAPS); } else { - navigation?.navigate(Routes.RAMP.BUY); + navigation?.navigate(...createBuyNavigationDetails()); } if (connection) { diff --git a/app/core/WalletConnect/WalletConnectV2.ts b/app/core/WalletConnect/WalletConnectV2.ts index 4bf37d69578..cecb6e131dc 100644 --- a/app/core/WalletConnect/WalletConnectV2.ts +++ b/app/core/WalletConnect/WalletConnectV2.ts @@ -24,7 +24,10 @@ import { updateWC2Metadata } from '../../../app/actions/sdk'; import Routes from '../../../app/constants/navigation/Routes'; import ppomUtil from '../../../app/lib/ppom/ppom-util'; import { WALLET_CONNECT_ORIGIN } from '../../../app/util/walletconnect'; -import { selectChainId } from '../../selectors/networkController'; +import { + selectChainId, + selectNetworkConfigurations, +} from '../../selectors/networkController'; import { store } from '../../store'; import AsyncStorage from '../../store/async-storage-wrapper'; import Device from '../../util/device'; @@ -41,6 +44,7 @@ import parseWalletConnectUri, { hideWCLoadingState, showWCLoadingState, } from './wc-utils'; +import { getDefaultNetworkByChainId } from '../../util/networks'; const { PROJECT_ID } = AppConstants.WALLET_CONNECT; export const isWC2Enabled = @@ -65,6 +69,9 @@ class WalletConnect2Session { private navigation?: NavigationContainerRef; private web3Wallet: Client; private deeplink: boolean; + // timeoutRef is used on android to prevent automatic redirect on switchChain and wait for wallet_addEthereumChain. + // If addEthereumChain is not received after 3 seconds, it will redirect. + private timeoutRef: NodeJS.Timeout | null = null; private session: SessionTypes.Struct; private requestsToRedirect: { [request: string]: boolean } = {}; private topicByRequestId: { [requestId: string]: string } = {}; @@ -184,11 +191,11 @@ class WalletConnect2Session { this.deeplink = deeplink; }; - redirect = () => { + redirect = (context?: string) => { DevLogger.log( - `WC2::redirect isDeeplink=${this.deeplink} navigation=${ - this.navigation !== undefined - }`, + `WC2::redirect context=${context} isDeeplink=${ + this.deeplink + } navigation=${this.navigation !== undefined}`, ); if (!this.deeplink) return; @@ -208,7 +215,7 @@ class WalletConnect2Session { needsRedirect = (id: string) => { if (this.requestsToRedirect[id]) { delete this.requestsToRedirect[id]; - this.redirect(); + this.redirect(`needsRedirect_${id}`); } }; @@ -368,6 +375,10 @@ class WalletConnect2Session { ); this.topicByRequestId[requestEvent.id] = requestEvent.topic; this.requestByRequestId[requestEvent.id] = requestEvent; + if (this.timeoutRef) { + // Always clear the timeout ref on new message, it is only used for wallet_switchEthereumChain auto reject on android + clearTimeout(this.timeoutRef); + } hideWCLoadingState({ navigation: this.navigation }); const verified = requestEvent.verifyContext?.verified; @@ -390,13 +401,62 @@ class WalletConnect2Session { const selectedChainId = parseInt(selectChainId(store.getState())); if (selectedChainId !== chainId) { + DevLogger.log( + `rejectRequest due to invalid chainId ${chainId} (selectedChainId=${selectedChainId})`, + ); await this.web3Wallet.rejectRequest({ - id: chainId, + id: requestEvent.id, topic: this.session.topic, error: { code: 1, message: ERROR_MESSAGES.INVALID_CHAIN }, }); } + // Android specific logic to prevent automatic redirect on switchChain and let the dapp call wallet_addEthereumChain on error. + if ( + method.toLowerCase() === RPC_WALLET_SWITCHETHEREUMCHAIN.toLowerCase() && + Device.isAndroid() + ) { + // extract first chainId param from request array + const params = requestEvent.params.request.params as [ + { chainId?: string }, + ]; + const _chainId = params[0]?.chainId; + DevLogger.log( + `formatting chainId=>${chainId} ==> 0x${chainId.toString(16)}`, + ); + const networkConfigurations = selectNetworkConfigurations( + store.getState(), + ); + const existingNetworkDefault = getDefaultNetworkByChainId(_chainId); + const existingEntry = Object.entries(networkConfigurations).find( + ([, networkConfiguration]) => networkConfiguration.chainId === _chainId, + ); + DevLogger.log( + `rpcMiddleWare -- check for auto rejection (_chainId=${_chainId}) networkConfigurations=${JSON.stringify( + networkConfigurations, + )} existingEntry=${existingEntry} existingNetworkDefault=${existingNetworkDefault}`, + ); + if (!existingEntry && !existingNetworkDefault) { + DevLogger.log( + `SKIP rpcMiddleWare -- auto rejection is detected android (_chainId=${_chainId})`, + ); + await this.web3Wallet.rejectRequest({ + id: requestEvent.id, + topic: requestEvent.topic, + error: { code: 32603, message: ERROR_MESSAGES.INVALID_CHAIN }, + }); + + showWCLoadingState({ navigation: this.navigation }); + this.timeoutRef = setTimeout(() => { + DevLogger.log(`wc2::timeoutRef redirecting...`); + hideWCLoadingState({ navigation: this.navigation }); + // Redirect or do nothing if timer gets cleared upon receiving wallet_addEthereumChain after automatic reject + this.redirect('handleRequestTimeout'); + }, 3000); + return; + } + } + // Manage redirects if (METHODS_TO_REDIRECT[method]) { this.requestsToRedirect[requestEvent.id] = true; @@ -475,7 +535,9 @@ export class WC2Manager { this.deeplinkSessions = deeplinkSessions; this.navigation = navigation; - const sessions = web3Wallet.getActiveSessions() || {}; + const sessions = web3Wallet.getActiveSessions + ? web3Wallet.getActiveSessions() + : {}; DevLogger.log(`WC2Manager::constructor()`, navigation); @@ -484,7 +546,7 @@ export class WC2Manager { web3Wallet.on( 'session_delete', async (event: SingleEthereumTypes.SessionDelete) => { - const session = sessions[event.topic]; + const session = sessions?.[event.topic]; if (session && deeplinkSessions[session?.pairingTopic]) { delete deeplinkSessions[session.pairingTopic]; await AsyncStorage.setItem( @@ -514,80 +576,82 @@ export class WC2Manager { } ).PermissionController; - Object.keys(sessions).forEach(async (sessionKey) => { - try { - const session = sessions[sessionKey]; - - this.sessions[sessionKey] = new WalletConnect2Session({ - web3Wallet, - channelId: sessionKey, - navigation: this.navigation, - deeplink: - typeof deeplinkSessions[session.pairingTopic] !== 'undefined', - session, - }); - - // Find approvedAccounts for current sessions - DevLogger.log( - `WC2::init getPermittedAccounts for ${sessionKey} origin=${session.peer.metadata.url}`, - JSON.stringify(permissionController.state, null, 2), - ); - const accountPermission = permissionController.getPermission( - session.peer.metadata.url, - 'eth_accounts', - ); - - DevLogger.log( - `WC2::init accountPermission`, - JSON.stringify(accountPermission, null, 2), - ); - let approvedAccounts = - (await getPermittedAccounts(accountPermission?.id ?? '')) ?? []; - const fromOrigin = await getPermittedAccounts( - session.peer.metadata.url, - ); + if (sessions) { + Object.keys(sessions).forEach(async (sessionKey) => { + try { + const session = sessions[sessionKey]; + + this.sessions[sessionKey] = new WalletConnect2Session({ + web3Wallet, + channelId: sessionKey, + navigation: this.navigation, + deeplink: + typeof deeplinkSessions[session.pairingTopic] !== 'undefined', + session, + }); + + // Find approvedAccounts for current sessions + DevLogger.log( + `WC2::init getPermittedAccounts for ${sessionKey} origin=${session.peer.metadata.url}`, + JSON.stringify(permissionController.state, null, 2), + ); + const accountPermission = permissionController.getPermission( + session.peer.metadata.url, + 'eth_accounts', + ); - DevLogger.log( - `WC2::init approvedAccounts id ${accountPermission?.id}`, - approvedAccounts, - ); - DevLogger.log( - `WC2::init fromOrigin ${session.peer.metadata.url}`, - fromOrigin, - ); + DevLogger.log( + `WC2::init accountPermission`, + JSON.stringify(accountPermission, null, 2), + ); + let approvedAccounts = + (await getPermittedAccounts(accountPermission?.id ?? '')) ?? []; + const fromOrigin = await getPermittedAccounts( + session.peer.metadata.url, + ); - // fallback to origin from metadata url - if (approvedAccounts.length === 0) { DevLogger.log( - `WC2::init fallback to metadata url ${session.peer.metadata.url}`, + `WC2::init approvedAccounts id ${accountPermission?.id}`, + approvedAccounts, + ); + DevLogger.log( + `WC2::init fromOrigin ${session.peer.metadata.url}`, + fromOrigin, ); - approvedAccounts = - (await getPermittedAccounts(session.peer.metadata.url)) ?? []; - } - if (approvedAccounts?.length === 0) { + // fallback to origin from metadata url + if (approvedAccounts.length === 0) { + DevLogger.log( + `WC2::init fallback to metadata url ${session.peer.metadata.url}`, + ); + approvedAccounts = + (await getPermittedAccounts(session.peer.metadata.url)) ?? []; + } + + if (approvedAccounts?.length === 0) { + DevLogger.log( + `WC2::init fallback to parsing accountPermission`, + accountPermission, + ); + // FIXME: Why getPermitted accounts doesn't work??? + approvedAccounts = extractApprovedAccounts(accountPermission); + DevLogger.log(`WC2::init approvedAccounts`, approvedAccounts); + } + + const nChainId = parseInt(chainId, 16); DevLogger.log( - `WC2::init fallback to parsing accountPermission`, - accountPermission, + `WC2::init updateSession session=${sessionKey} chainId=${chainId} nChainId=${nChainId} selectedAddress=${selectedAddress}`, + approvedAccounts, ); - // FIXME: Why getPermitted accounts doesn't work??? - approvedAccounts = extractApprovedAccounts(accountPermission); - DevLogger.log(`WC2::init approvedAccounts`, approvedAccounts); + await this.sessions[sessionKey].updateSession({ + chainId: nChainId, + accounts: approvedAccounts, + }); + } catch (err) { + console.warn(`WC2::init can't update session ${sessionKey}`); } - - const nChainId = parseInt(chainId, 16); - DevLogger.log( - `WC2::init updateSession session=${sessionKey} chainId=${chainId} nChainId=${nChainId} selectedAddress=${selectedAddress}`, - approvedAccounts, - ); - await this.sessions[sessionKey].updateSession({ - chainId: nChainId, - accounts: approvedAccounts, - }); - } catch (err) { - console.warn(`WC2::init can't update session ${sessionKey}`); - } - }); + }); + } } public static async init({ @@ -860,7 +924,7 @@ export class WC2Manager { this.sessions[activeSession.topic] = session; if (deeplink) { - session.redirect(); + session.redirect('onSessionProposal'); } } catch (err) { console.error(`invalid wallet status`, err); diff --git a/app/declarations.d.ts b/app/declarations.d.ts index fd9d9195a46..b0ec0b9cffa 100644 --- a/app/declarations.d.ts +++ b/app/declarations.d.ts @@ -272,3 +272,8 @@ declare module 'react-native-vector-icons/Zocial' { */ export default class Zocial extends ZocialIconType {} } + +declare module '@metamask/contract-metadata' { + const content: Record; + export default content; +} diff --git a/app/lib/ppom/PPOMView.test.tsx b/app/lib/ppom/PPOMView.test.tsx index c9093579f93..c6a78c3a717 100644 --- a/app/lib/ppom/PPOMView.test.tsx +++ b/app/lib/ppom/PPOMView.test.tsx @@ -4,8 +4,9 @@ import { render } from '@testing-library/react-native'; import { PPOMView } from './PPOMView'; describe('PPOMView', () => { - it('should render correctly deeply', () => { - const wrapper = render(); - expect(wrapper).toMatchSnapshot(); + it('should render correctly', () => { + expect(() => { + render(); + }).not.toThrow(); }); }); diff --git a/app/lib/ppom/PPOMView.tsx b/app/lib/ppom/PPOMView.tsx index 718d2a24438..f061caad6e0 100644 --- a/app/lib/ppom/PPOMView.tsx +++ b/app/lib/ppom/PPOMView.tsx @@ -1,6 +1,6 @@ import React, { Component, RefObject } from 'react'; import { StyleSheet, View } from 'react-native'; -import { WebView } from 'react-native-webview'; +import { WebView } from '@metamask/react-native-webview'; import createInvoke from 'react-native-webview-invoke/native'; import { fromByteArray } from 'react-native-quick-base64'; diff --git a/app/lib/ppom/__snapshots__/PPOMView.test.tsx.snap b/app/lib/ppom/__snapshots__/PPOMView.test.tsx.snap deleted file mode 100644 index 66d7efb75a2..00000000000 --- a/app/lib/ppom/__snapshots__/PPOMView.test.tsx.snap +++ /dev/null @@ -1,59 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`PPOMView should render correctly deeply 1`] = ` - - - - - -`; diff --git a/app/lib/ppom/blockaid-version.js b/app/lib/ppom/blockaid-version.js index e1ca49186b2..5b27f82a979 100644 --- a/app/lib/ppom/blockaid-version.js +++ b/app/lib/ppom/blockaid-version.js @@ -1 +1 @@ -var e={d:(o,r)=>{for(var t in r)e.o(r,t)&&!e.o(o,t)&&Object.defineProperty(o,t,{enumerable:!0,get:r[t]})},o:(e,o)=>Object.prototype.hasOwnProperty.call(e,o)},o={};e.d(o,{Z:()=>r});const r={BlockaidVersion:"1.4.8"};var t=o.Z;export{t as default}; \ No newline at end of file +var e={d:(o,r)=>{for(var t in r)e.o(r,t)&&!e.o(o,t)&&Object.defineProperty(o,t,{enumerable:!0,get:r[t]})},o:(e,o)=>Object.prototype.hasOwnProperty.call(e,o)},o={};e.d(o,{Z:()=>r});const r={BlockaidVersion:"1.4.9"};var t=o.Z;export{t as default}; \ No newline at end of file diff --git a/app/lib/ppom/ppom-util.test.ts b/app/lib/ppom/ppom-util.test.ts index b5f9faa220b..5179288d0a9 100644 --- a/app/lib/ppom/ppom-util.test.ts +++ b/app/lib/ppom/ppom-util.test.ts @@ -1,6 +1,7 @@ import { normalizeTransactionParams } from '@metamask/transaction-controller'; import * as SignatureRequestActions from '../../actions/signatureRequest'; // eslint-disable-line import/no-namespace import * as TransactionActions from '../../actions/transaction'; // eslint-disable-line import/no-namespace +import * as NetworkControllerSelectors from '../../selectors/networkController'; // eslint-disable-line import/no-namespace import Engine from '../../core/Engine'; import PPOMUtil from './ppom-util'; import { @@ -33,6 +34,19 @@ jest.mock('../../core/Engine', () => ({ providerConfig: { chainId: CHAIN_ID_MOCK }, }, }, + AccountsController: { + state: { + internalAccounts: { accounts: [] }, + }, + listAccounts: jest.fn().mockReturnValue([]), + }, + }, + backgroundState: { + NetworkController: { + providerConfig: { + chainId: 0x1, + }, + }, }, })); @@ -110,13 +124,36 @@ describe('PPOM Utils', () => { expect(spyTransactionAction).toBeCalledTimes(0); }); + it('should not validate if request is send to users own account ', async () => { + const spyTransactionAction = jest.spyOn( + TransactionActions, + 'setTransactionSecurityAlertResponse', + ); + MockEngine.context.AccountsController.listAccounts = jest + .fn() + .mockReturnValue([ + { + address: '0x0c54FcCd2e384b4BB6f2E405Bf5Cbc15a017AaFb', + }, + ]); + await PPOMUtil.validateRequest(mockRequest, CHAIN_ID_MOCK); + expect(MockEngine.context.PPOMController?.usePPOM).toHaveBeenCalledTimes( + 0, + ); + expect(spyTransactionAction).toHaveBeenCalledTimes(0); + MockEngine.context.AccountsController.listAccounts = jest + .fn() + .mockReturnValue([]); + }); + it('should not validate user if on a non supporting blockaid network', async () => { const spyTransactionAction = jest.spyOn( TransactionActions, 'setTransactionSecurityAlertResponse', ); - MockEngine.context.NetworkController.state.providerConfig.chainId = - '0xfa'; + jest + .spyOn(NetworkControllerSelectors, 'selectChainId') + .mockReturnValue('0xfa'); await PPOMUtil.validateRequest(mockRequest, CHAIN_ID_MOCK); expect(MockEngine.context.PPOMController?.usePPOM).toBeCalledTimes(0); expect(spyTransactionAction).toBeCalledTimes(0); diff --git a/app/lib/ppom/ppom-util.ts b/app/lib/ppom/ppom-util.ts index fa02d01ce0b..5b35790797b 100644 --- a/app/lib/ppom/ppom-util.ts +++ b/app/lib/ppom/ppom-util.ts @@ -1,6 +1,5 @@ import setSignatureRequestSecurityAlertResponse from '../../actions/signatureRequest'; import { setTransactionSecurityAlertResponse } from '../../actions/transaction'; -import { BLOCKAID_SUPPORTED_CHAIN_IDS } from '../../util/networks'; import { Reason, ResultType, @@ -58,28 +57,34 @@ const SECURITY_ALERT_RESPONSE_IN_PROGRESS = { async function validateRequest(req: PPOMRequest, transactionId?: string) { const { - PPOMController: ppomController, - PreferencesController, + AccountsController, NetworkController, + PPOMController: ppomController, } = Engine.context; const chainId = NetworkController.state.providerConfig.chainId; const isConfirmationMethod = CONFIRMATION_METHODS.includes(req.method); - const isSupportedChain = BLOCKAID_SUPPORTED_CHAIN_IDS.includes(chainId); - - const isSecurityAlertsEnabled = - PreferencesController.state.securityAlertsEnabled; - - if ( - !ppomController || - !isBlockaidFeatureEnabled() || - !isSecurityAlertsEnabled || - !isConfirmationMethod || - !isSupportedChain - ) { + + if (!ppomController || !isBlockaidFeatureEnabled() || !isConfirmationMethod) { return; } + if (req.method === 'eth_sendTransaction') { + const internalAccounts = AccountsController.listAccounts(); + const toAddress: string | undefined = ( + req?.params?.[0] as Record + ).to; + + if ( + internalAccounts.some( + ({ address }: { address: string }) => + address?.toLowerCase() === toAddress?.toLowerCase(), + ) + ) { + return; + } + } + const isTransaction = isTransactionRequest(req); let securityAlertResponse: SecurityAlertResponse | undefined; diff --git a/app/lib/snaps/SnapsExecutionWebView.tsx b/app/lib/snaps/SnapsExecutionWebView.tsx index 4959a6cce2a..146784f20a3 100644 --- a/app/lib/snaps/SnapsExecutionWebView.tsx +++ b/app/lib/snaps/SnapsExecutionWebView.tsx @@ -4,11 +4,13 @@ ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) import React, { Component, RefObject } from 'react'; import { View, ScrollView, NativeSyntheticEvent } from 'react-native'; -import WebView, { WebViewMessageEvent } from 'react-native-webview'; +import WebView, { WebViewMessageEvent } from '@metamask/react-native-webview'; import { createStyles } from './styles'; import { WebViewInterface } from '@metamask/snaps-controllers/dist/types/services/webview/WebViewMessageStream'; -import { WebViewError } from 'react-native-webview/lib/WebViewTypes'; +import { WebViewError } from '@metamask/react-native-webview/lib/WebViewTypes'; import { PostMessageEvent } from '@metamask/post-message-stream'; +// @ts-expect-error Can't type a distritibuted html file +import WebViewHTML from '@metamask/snaps-execution-environments/dist/browserify/webview/index.html'; const styles = createStyles(); @@ -21,8 +23,6 @@ interface SnapsExecutionWebViewProps { let resolveGetWebView: (arg0: SnapsExecutionWebViewProps) => void; let rejectGetWebView: (error: NativeSyntheticEvent) => void; -const SNAPS_EE_URL = 'https://execution.metamask.io/webview/6.5.0/index.html'; - export const getSnapsWebViewPromise = new Promise( (resolve, reject) => { resolveGetWebView = resolve; @@ -89,13 +89,11 @@ export class SnapsExecutionWebView extends Component { ref={ this.setWebViewRef as unknown as React.RefObject | null } - source={{ - uri: SNAPS_EE_URL, - }} + source={WebViewHTML} onMessage={this.onWebViewMessage} onError={this.onWebViewError} onLoadEnd={this.onWebViewLoad} - originWhitelist={['https://execution.metamask.io*']} + originWhitelist={['*']} javaScriptEnabled />
diff --git a/app/reducers/fiatOrders/index.test.ts b/app/reducers/fiatOrders/index.test.ts index 045102fd752..cd380b69736 100644 --- a/app/reducers/fiatOrders/index.test.ts +++ b/app/reducers/fiatOrders/index.test.ts @@ -148,84 +148,84 @@ const dummyCustomOrderIdData3: CustomIdData = { const networks: AggregatorNetwork[] = [ { active: true, - chainId: 1, + chainId: '1', chainName: 'Ethereum Mainnet', shortName: 'Ethereum', nativeTokenSupported: true, }, { active: true, - chainId: 10, + chainId: '10', chainName: 'Optimism Mainnet', shortName: 'Optimism', nativeTokenSupported: true, }, { active: true, - chainId: 25, + chainId: '25', chainName: 'Cronos Mainnet', shortName: 'Cronos', nativeTokenSupported: true, }, { active: true, - chainId: 56, + chainId: '56', chainName: 'BNB Chain Mainnet', shortName: 'BNB Chain', nativeTokenSupported: true, }, { active: true, - chainId: 137, + chainId: '137', chainName: 'Polygon Mainnet', shortName: 'Polygon', nativeTokenSupported: true, }, { active: true, - chainId: 250, + chainId: '250', chainName: 'Fantom Mainnet', shortName: 'Fantom', nativeTokenSupported: true, }, { active: true, - chainId: 1284, + chainId: '1284', chainName: 'Moonbeam Mainnet', shortName: 'Moonbeam', nativeTokenSupported: true, }, { active: true, - chainId: 42161, + chainId: '42161', chainName: 'Arbitrum Mainnet', shortName: 'Arbitrum', nativeTokenSupported: true, }, { active: true, - chainId: 42220, + chainId: '42220', chainName: 'Celo Mainnet', shortName: 'Celo', nativeTokenSupported: false, }, { active: true, - chainId: 43114, + chainId: '43114', chainName: 'Avalanche C-Chain Mainnet', shortName: 'Avalanche', nativeTokenSupported: true, }, { active: true, - chainId: 1313161554, + chainId: '1313161554', chainName: 'Aurora Mainnet', shortName: 'Aurora', nativeTokenSupported: false, }, { active: true, - chainId: 1666600000, + chainId: '1666600000', chainName: 'Harmony Mainnet (Shard 0)', shortName: 'Harmony (Shard 0)', nativeTokenSupported: true, diff --git a/app/reducers/index.ts b/app/reducers/index.ts index 463cc840ab3..45ff0dc3d38 100644 --- a/app/reducers/index.ts +++ b/app/reducers/index.ts @@ -53,7 +53,7 @@ export interface RootState { // TODO: Replace "any" with type // eslint-disable-next-line @typescript-eslint/no-explicit-any collectibles: any; - engine: { backgroundState: EngineState | Record }; + engine: { backgroundState: EngineState }; // TODO: Replace "any" with type // eslint-disable-next-line @typescript-eslint/no-explicit-any privacy: any; diff --git a/app/reducers/notification/index.js b/app/reducers/notification/index.js index bd60bfcb3b0..41137ca5757 100644 --- a/app/reducers/notification/index.js +++ b/app/reducers/notification/index.js @@ -3,9 +3,6 @@ const { TRANSACTION, SIMPLE } = NotificationTypes; export const initialState = { notifications: [], - notification: { - notificationsSettings: {}, - }, }; export const ACTIONS = { @@ -193,12 +190,6 @@ const notificationReducer = (state = initialState, action) => { notifications: visibleNotifications, }; } - case ACTIONS.UPDATE_NOTIFICATION_STATUS: { - return { - ...state, - notificationsSettings: action.notificationsSettings, - }; - } default: return state; } diff --git a/app/selectors/notifications/index.tsx b/app/selectors/notifications/index.tsx new file mode 100644 index 00000000000..021915387f6 --- /dev/null +++ b/app/selectors/notifications/index.tsx @@ -0,0 +1,115 @@ +/* eslint-disable import/prefer-default-export */ +import { createSelector } from 'reselect'; +import Engine from '../../core/Engine'; + +const { + AuthenticationController, + UserStorageController, + NotificationServicesController, +} = Engine.context; + +import { TRIGGER_TYPES } from '../../util/notifications'; + +import { createDeepEqualSelector } from '../util'; +import { RootState } from '../../reducers'; + +type NotificationServicesState = typeof NotificationServicesController.state; +type AuthenticationState = typeof AuthenticationController.state; +type UserStorageState = typeof UserStorageController.state; + +const selectAuthenticationControllerState = (state: RootState) => + state?.engine?.backgroundState?.AuthenticationController; + +const selectUserStorageControllerState = (state: RootState) => + state?.engine?.backgroundState?.UserStorageController; + +const selectNotificationServicesControllerState = (state: RootState) => + state?.engine?.backgroundState?.NotificationServicesController; + +export const selectIsProfileSyncingEnabled = createSelector( + selectUserStorageControllerState, + (userStorageControllerState: UserStorageState) => + userStorageControllerState.isProfileSyncingEnabled, +); +export const selectIsProfileSyncingUpdateLoading = createSelector( + selectUserStorageControllerState, + (userStorageControllerState: UserStorageState) => + userStorageControllerState.isProfileSyncingUpdateLoading, +); + +export const selectIsSignedIn = createSelector( + selectAuthenticationControllerState, + (authenticationControllerState: AuthenticationState) => + authenticationControllerState.isSignedIn, +); + +export const selectSessionData = createSelector( + selectAuthenticationControllerState, + (authenticationControllerState: AuthenticationState) => + authenticationControllerState.sessionData, +); + +export const selectIsMetamaskNotificationsEnabled = createSelector( + selectNotificationServicesControllerState, + (notificationServicesControllerState: NotificationServicesState) => + notificationServicesControllerState.isNotificationServicesEnabled, +); +export const selectIsMetamaskNotificationsFeatureSeen = createSelector( + selectNotificationServicesControllerState, + (notificationServicesControllerState: NotificationServicesState) => + notificationServicesControllerState.isMetamaskNotificationsFeatureSeen, +); +export const selectIsUpdatingMetamaskNotifications = createSelector( + selectNotificationServicesControllerState, + (notificationServicesControllerState: NotificationServicesState) => + notificationServicesControllerState.isUpdatingMetamaskNotifications, +); +export const selectIsFetchingMetamaskNotifications = createSelector( + selectNotificationServicesControllerState, + (notificationServicesControllerState: NotificationServicesState) => + notificationServicesControllerState.isFetchingMetamaskNotifications, +); +export const selectIsFeatureAnnouncementsEnabled = createSelector( + selectNotificationServicesControllerState, + (notificationServicesControllerState: NotificationServicesState) => + notificationServicesControllerState.isFeatureAnnouncementsEnabled, +); +export const selectIsUpdatingMetamaskNotificationsAccount = createSelector( + selectNotificationServicesControllerState, + (notificationServicesControllerState: NotificationServicesState) => + notificationServicesControllerState.isUpdatingMetamaskNotificationsAccount, +); +export const selectIsCheckingAccountsPresence = createSelector( + selectNotificationServicesControllerState, + (notificationServicesControllerState: NotificationServicesState) => + notificationServicesControllerState.isCheckingAccountsPresence, +); +export const getmetamaskNotificationsReadList = createSelector( + selectNotificationServicesControllerState, + (notificationServicesControllerState: NotificationServicesState) => + notificationServicesControllerState.metamaskNotificationsReadList, +); +export const getNotificationsList = createDeepEqualSelector( + selectNotificationServicesControllerState, + (notificationServicesControllerState: NotificationServicesState) => + notificationServicesControllerState.metamaskNotificationsList, +); + +export const getMetamaskNotificationsUnreadCount = createSelector( + selectNotificationServicesControllerState, + (notificationServicesControllerState: NotificationServicesState) => + ( + notificationServicesControllerState.metamaskNotificationsList ?? [] + ).filter((notification) => !notification.isRead).length, +); +export const getOnChainMetamaskNotificationsUnreadCount = createSelector( + selectNotificationServicesControllerState, + (notificationServicesControllerState: NotificationServicesState) => + ( + notificationServicesControllerState.metamaskNotificationsList ?? [] + ).filter( + (notification) => + !notification.isRead && + notification.type !== TRIGGER_TYPES.FEATURES_ANNOUNCEMENT, + ).length, +); diff --git a/app/selectors/preferencesController.ts b/app/selectors/preferencesController.ts index 83dae910b82..7d1b6b9a6dd 100644 --- a/app/selectors/preferencesController.ts +++ b/app/selectors/preferencesController.ts @@ -5,6 +5,9 @@ import { RootState } from '../reducers'; const selectPreferencesControllerState = (state: RootState) => state.engine.backgroundState.PreferencesController; +/** + * @deprecated use selectInternalAccounts rom selectors/accountsController.ts instead + */ export const selectIdentities = createSelector( selectPreferencesControllerState, (preferencesControllerState: PreferencesState) => @@ -18,7 +21,7 @@ export const selectIpfsGateway = createSelector( ); /** - * @deprecated use selectSelectedInternal or selectSelectedInternalAccountChecksummedAddress account from selectors/accountsController.ts + * @deprecated use selectSelectedInternal or selectSelectedInternalAccountChecksummedAddress from selectors/accountsController.ts */ export const selectSelectedAddress = createSelector( selectPreferencesControllerState, diff --git a/app/selectors/smartTransactionsController.test.ts b/app/selectors/smartTransactionsController.test.ts index d4606c9a90f..770852527ff 100644 --- a/app/selectors/smartTransactionsController.test.ts +++ b/app/selectors/smartTransactionsController.test.ts @@ -2,7 +2,7 @@ import { selectShouldUseSmartTransaction, selectSmartTransactionsEnabled, } from './smartTransactionsController'; -import initialBackgroundState from '../util/test/initial-background-state.json'; +import { backgroundState } from '../util/test/initial-root-state'; import { isHardwareAccount } from '../util/address'; import { cloneDeep } from 'lodash'; @@ -16,7 +16,7 @@ const getDefaultState = () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const defaultState: any = { engine: { - backgroundState: cloneDeep(initialBackgroundState), + backgroundState: cloneDeep(backgroundState), }, swaps: { featureFlags: { diff --git a/app/selectors/tokenListController.ts b/app/selectors/tokenListController.ts index 4cdee2c6586..294c3a6ed3e 100644 --- a/app/selectors/tokenListController.ts +++ b/app/selectors/tokenListController.ts @@ -2,6 +2,7 @@ import { createSelector } from 'reselect'; import { TokenListState } from '@metamask/assets-controllers'; import { RootState } from '../reducers'; import { tokenListToArray } from '../util/tokens'; +import { createDeepEqualSelector } from '../selectors/util'; const selectTokenLIstConstrollerState = (state: RootState) => state.engine.backgroundState.TokenListController; @@ -20,7 +21,7 @@ export const selectTokenList = createSelector( * Return token list array from TokenListController. * Can pass directly into useSelector. */ -export const selectTokenListArray = createSelector( +export const selectTokenListArray = createDeepEqualSelector( selectTokenList, tokenListToArray, ); diff --git a/app/store/async-storage-wrapper.js b/app/store/async-storage-wrapper.js index 7d5aabb639a..32366f2869b 100644 --- a/app/store/async-storage-wrapper.js +++ b/app/store/async-storage-wrapper.js @@ -1,9 +1,11 @@ import ReadOnlyNetworkStore from '../util/test/network-store'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { isE2E } from '../util/test/utils'; +import { MMKV } from 'react-native-mmkv'; /** * Wrapper class for AsyncStorage. + * (Will want to eventuall re-name since no longer async once migratted to mmkv) */ class AsyncStorageWrapper { constructor() { @@ -11,16 +13,21 @@ class AsyncStorageWrapper { * The underlying storage implementation. * Use `ReadOnlyNetworkStore` in test mode otherwise use `AsyncStorage`. */ - this.storage = isE2E ? ReadOnlyNetworkStore : AsyncStorage; + this.storage = isE2E ? ReadOnlyNetworkStore : new MMKV(); } async getItem(key) { try { - return await this.storage.getItem(key); + // asyncStorage returns null for no value + // mmkv returns undefined for no value + // therefore must return null if no value is found + // to keep app behavior consistent + const value = (await this.storage.getString(key)) ?? null; + return value; } catch (error) { if (isE2E) { // Fall back to AsyncStorage in test mode if ReadOnlyNetworkStore fails - return AsyncStorage.getItem(key); + return await AsyncStorage.getItem(key); } throw error; } @@ -28,7 +35,11 @@ class AsyncStorageWrapper { async setItem(key, value) { try { - return await this.storage.setItem(key, value); + if (typeof value !== 'string') + throw new Error( + `MMKV value must be a string, received value ${value} for key ${key}`, + ); + return await this.storage.set(key, value); } catch (error) { if (isE2E) { // Fall back to AsyncStorage in test mode if ReadOnlyNetworkStore fails @@ -40,7 +51,7 @@ class AsyncStorageWrapper { async removeItem(key) { try { - return await this.storage.removeItem(key); + return await this.storage.delete(key); } catch (error) { if (isE2E) { // Fall back to AsyncStorage in test mode if ReadOnlyNetworkStore fails @@ -49,6 +60,10 @@ class AsyncStorageWrapper { throw error; } } + + async clearAll() { + await this.storage.clearAll(); + } } export default new AsyncStorageWrapper(); diff --git a/app/store/migrations/020.test.js b/app/store/migrations/020.test.js index 794cff1b031..aad2070f5e1 100644 --- a/app/store/migrations/020.test.js +++ b/app/store/migrations/020.test.js @@ -183,8 +183,7 @@ describe('Migration #20', () => { chainId: '137', nickname: 'Polygon Mainnet', rpcPrefs: { blockExplorerUrl: 'https://polygonscan.com' }, - rpcUrl: - 'https://polygon-mainnet.infura.io/v3/cda392a134014865ad3c273dc7ddfff3', + rpcUrl: 'https://polygon-mainnet.infura.io/v3/12345', ticker: 'MATIC', }, { @@ -193,8 +192,7 @@ describe('Migration #20', () => { rpcPrefs: { blockExplorerUrl: 'https://optimistic.etherscan.io', }, - rpcUrl: - 'https://optimism-mainnet.infura.io/v3/cda392a134014865ad3c273dc7ddfff3', + rpcUrl: 'https://optimism-mainnet.infura.io/v3/12345', ticker: 'ETH', }, ], @@ -222,8 +220,7 @@ describe('Migration #20', () => { chainId: '137', nickname: 'Polygon Mainnet', rpcPrefs: { blockExplorerUrl: 'https://polygonscan.com' }, - rpcUrl: - 'https://polygon-mainnet.infura.io/v3/cda392a134014865ad3c273dc7ddfff3', + rpcUrl: 'https://polygon-mainnet.infura.io/v3/12345', ticker: 'MATIC', }, networkId3: { @@ -232,8 +229,7 @@ describe('Migration #20', () => { rpcPrefs: { blockExplorerUrl: 'https://optimistic.etherscan.io', }, - rpcUrl: - 'https://optimism-mainnet.infura.io/v3/cda392a134014865ad3c273dc7ddfff3', + rpcUrl: 'https://optimism-mainnet.infura.io/v3/12345', ticker: 'ETH', }, }, diff --git a/app/store/migrations/021.test.js b/app/store/migrations/021.test.js index d4906e2a38d..385ef6a008c 100644 --- a/app/store/migrations/021.test.js +++ b/app/store/migrations/021.test.js @@ -1,12 +1,12 @@ import migrate from './021'; import { IPFS_DEFAULT_GATEWAY_URL } from '../../../app/constants/network'; -import initialBackgroundState from '../../util/test/initial-background-state.json'; +import { backgroundState } from '../../util/test/initial-root-state'; describe('Migration #21', () => { it('should not change state if ipfs gateway in use is not outdated', () => { const currentState = { engine: { - backgroundState: initialBackgroundState, + backgroundState, }, }; @@ -19,9 +19,9 @@ describe('Migration #21', () => { const stateWithIpfsGateway = (ipfsGateway) => ({ engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, PreferencesController: { - ...initialBackgroundState.PreferencesController, + ...backgroundState.PreferencesController, ipfsGateway, }, }, diff --git a/app/store/migrations/023.test.js b/app/store/migrations/023.test.js index dab1d56dc6e..084baced05c 100644 --- a/app/store/migrations/023.test.js +++ b/app/store/migrations/023.test.js @@ -1,7 +1,8 @@ import migrate from './023'; import { merge } from 'lodash'; -import initialRootState from '../../util/test/initial-root-state'; -import initialBackgroundState from '../../util/test/initial-background-state.json'; +import initialRootState, { + backgroundState, +} from '../../util/test/initial-root-state'; import { captureException } from '@sentry/react-native'; jest.mock('@sentry/react-native', () => ({ @@ -212,7 +213,7 @@ describe('Migration #23', () => { expect(newState.user).toStrictEqual({}); expect(newState.engine.backgroundState).toStrictEqual( - merge({}, initialBackgroundState, { + merge({}, backgroundState, { AddressBookController: { addressBook: { // This is unchanged because the only configured network with a network ID 1 also has @@ -303,7 +304,7 @@ describe('Migration #23', () => { }, }); expect(newState.engine.backgroundState).toStrictEqual( - merge({}, initialBackgroundState, { + merge({}, backgroundState, { AddressBookController: { addressBook: { // This is unchanged because the only configured network with a network ID 1 also has @@ -383,7 +384,7 @@ describe('Migration #23', () => { expect(newState.user).toStrictEqual({}); expect(newState.engine.backgroundState).toStrictEqual( - merge({}, initialBackgroundState, { + merge({}, backgroundState, { AddressBookController: { addressBook: { // This is unchanged because the only configured network with a network ID 1 also has diff --git a/app/store/migrations/029.test.ts b/app/store/migrations/029.test.ts index 0a64a4eb36b..4a0890ccf71 100644 --- a/app/store/migrations/029.test.ts +++ b/app/store/migrations/029.test.ts @@ -1,7 +1,8 @@ import migration from './029'; import { merge } from 'lodash'; -import initialRootState from '../../util/test/initial-root-state'; -import initialBackgroundState from '../../util/test/initial-background-state.json'; +import initialRootState, { + backgroundState, +} from '../../util/test/initial-root-state'; import { captureException } from '@sentry/react-native'; const oldState = { @@ -280,7 +281,7 @@ describe('Migration #29', () => { engine: { backgroundState: { NetworkController: { - ...initialBackgroundState.NetworkController, + ...backgroundState.NetworkController, networkDetails: { isEIP1559Compatible: true, }, @@ -297,9 +298,9 @@ describe('Migration #29', () => { state: merge({}, initialRootState, { engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, NetworkController: { - ...initialBackgroundState.NetworkController, + ...backgroundState.NetworkController, networkDetails: { isEIP1559Compatible: true, }, @@ -316,9 +317,9 @@ describe('Migration #29', () => { state: merge({}, initialRootState, { engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, NetworkController: { - ...initialBackgroundState.NetworkController, + ...backgroundState.NetworkController, networkDetails: { isEIP1559Compatible: true, }, @@ -335,9 +336,9 @@ describe('Migration #29', () => { state: merge({}, initialRootState, { engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, NetworkController: { - ...initialBackgroundState.NetworkController, + ...backgroundState.NetworkController, networkDetails: { isEIP1559Compatible: true, }, @@ -354,9 +355,9 @@ describe('Migration #29', () => { state: merge({}, initialRootState, { engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, NetworkController: { - ...initialBackgroundState.NetworkController, + ...backgroundState.NetworkController, networkDetails: { isEIP1559Compatible: true, }, @@ -373,9 +374,9 @@ describe('Migration #29', () => { state: merge({}, initialRootState, { engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, NetworkController: { - ...initialBackgroundState.NetworkController, + ...backgroundState.NetworkController, networkDetails: { isEIP1559Compatible: true, }, @@ -392,9 +393,9 @@ describe('Migration #29', () => { state: merge({}, initialRootState, { engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, NetworkController: { - ...initialBackgroundState.NetworkController, + ...backgroundState.NetworkController, networkDetails: { isEIP1559Compatible: true, }, @@ -411,9 +412,9 @@ describe('Migration #29', () => { state: merge({}, initialRootState, { engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, NetworkController: { - ...initialBackgroundState.NetworkController, + ...backgroundState.NetworkController, networkDetails: { isEIP1559Compatible: true, }, diff --git a/app/store/migrations/049.test.ts b/app/store/migrations/049.test.ts new file mode 100644 index 00000000000..19af5ca140f --- /dev/null +++ b/app/store/migrations/049.test.ts @@ -0,0 +1,31 @@ +import migrate, { storage as mmkvStorage } from './049'; +import AsyncStorage from '@react-native-async-storage/async-storage'; + +const asyncStorageItems: { [key: string]: string } = { + valueA: 'a', + valueB: 'true', + valueC: 'myValue', +}; + +describe('Migration #49', () => { + it('migrates asyncStorage values to mmkv ', async () => { + // set asyncStorageItems to AsyncStorage + for (const key in asyncStorageItems) { + await AsyncStorage.setItem(key, asyncStorageItems[key]); + } + + await migrate({}); + + // make sure all AsyncStorage items are removed + const keys = await AsyncStorage.getAllKeys(); + // loop through all AsyncStorage keys and make sure empty + for (const key of keys) { + expect(await AsyncStorage.getItem(key)).toBeNull(); + } + + // now check that all MMKV values match original AsyncStorage values + for (const key in asyncStorageItems) { + expect(mmkvStorage.getString(key)).toEqual(asyncStorageItems[key]); + } + }); +}); diff --git a/app/store/migrations/049.ts b/app/store/migrations/049.ts new file mode 100644 index 00000000000..cc20ebd750b --- /dev/null +++ b/app/store/migrations/049.ts @@ -0,0 +1,25 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { captureException } from '@sentry/react-native'; +import { MMKV } from 'react-native-mmkv'; + +export const storage = new MMKV(); + +export default async function migrate(state: unknown) { + const keys = await AsyncStorage.getAllKeys(); + for (const key of keys) { + try { + const value = await AsyncStorage.getItem(key); + + if (value != null) { + storage.set(key, value); + } + await AsyncStorage.removeItem(key); + } catch (error) { + captureException( + `Failed to migrate key "${key}" from AsyncStorage to MMKV! Error: ${error}`, + ); + } + } + + return state; +} diff --git a/app/store/migrations/index.ts b/app/store/migrations/index.ts index 9e1131237fd..bfdbce04cb8 100644 --- a/app/store/migrations/index.ts +++ b/app/store/migrations/index.ts @@ -49,6 +49,7 @@ import migration45 from './045'; import migration46 from './046'; import migration47 from './047'; import migration48 from './048'; +import migration49 from './049'; type MigrationFunction = (state: unknown) => unknown; type AsyncMigrationFunction = (state: unknown) => Promise; @@ -110,6 +111,7 @@ export const migrationList: MigrationsList = { 46: migration46, 47: migration47, 48: migration48, + 49: migration49, }; // Enable both synchronous and asynchronous migrations diff --git a/app/util/address/index.test.ts b/app/util/address/index.test.ts index a0b6499b17f..e19035c34d2 100644 --- a/app/util/address/index.test.ts +++ b/app/util/address/index.test.ts @@ -1,3 +1,4 @@ +import { NetworkState } from '@metamask/network-controller'; import { isENS, renderSlightlyLongAddress, @@ -35,9 +36,6 @@ describe('isENS', () => { describe('renderSlightlyLongAddress', () => { const mockAddress = '0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272'; - it('should return the address when the address do not exist', () => { - expect(renderSlightlyLongAddress(null)).toBeNull(); - }); it('should return 5 characters before ellipsis and 4 final characters of the address after the ellipsis', () => { expect(renderSlightlyLongAddress(mockAddress).split('.')[0].length).toBe( 24, @@ -171,12 +169,13 @@ describe('getAddress', () => { }); describe('shouldShowBlockExplorer', () => { - const networkConfigurations = { + const networkConfigurations: NetworkState['networkConfigurations'] = { networkId1: { - chainId: '1', + id: 'networkId1', + chainId: '0x1', nickname: 'Main Ethereum Network', + ticker: 'USD', rpcUrl: 'https://mainnet.infura.io/v3/123', - rpcPrefs: {}, }, }; @@ -184,11 +183,11 @@ describe('shouldShowBlockExplorer', () => { const providerType = 'mainnet'; const providerRpcTarget = networkConfigurations.networkId1.rpcUrl; - const result = shouldShowBlockExplorer({ + const result = shouldShowBlockExplorer( providerType, providerRpcTarget, networkConfigurations, - }); + ); expect(result).toBe(true); }); @@ -199,11 +198,11 @@ describe('shouldShowBlockExplorer', () => { const blockExplorerUrl = 'https://rpc.testnet.fantom.network'; networkConfigurations.networkId1.rpcPrefs = { blockExplorerUrl }; - const result = shouldShowBlockExplorer({ + const result = shouldShowBlockExplorer( providerType, providerRpcTarget, networkConfigurations, - }); + ); expect(result).toBe(blockExplorerUrl); }); @@ -211,13 +210,13 @@ describe('shouldShowBlockExplorer', () => { it('returns undefined if block explorer URL is not defined', () => { const providerType = 'rpc'; const providerRpcTarget = networkConfigurations.networkId1.rpcUrl; - networkConfigurations.networkId1.rpcPrefs = {}; + networkConfigurations.networkId1.rpcPrefs = undefined; - const result = shouldShowBlockExplorer({ + const result = shouldShowBlockExplorer( providerType, providerRpcTarget, networkConfigurations, - }); + ); expect(result).toBe(undefined); }); diff --git a/app/util/address/index.js b/app/util/address/index.ts similarity index 74% rename from app/util/address/index.js rename to app/util/address/index.ts index 08aa9c22fb3..3f4d39e509f 100644 --- a/app/util/address/index.js +++ b/app/util/address/index.ts @@ -1,17 +1,16 @@ import { toChecksumAddress, isValidAddress, - isHexString, addHexPrefix, isValidChecksumAddress, + //@ts-expect-error - This error is expected, but ethereumjs-util exports this function isHexPrefixed, } from 'ethereumjs-util'; -import URL from 'url-parse'; import punycode from 'punycode/punycode'; import ExtendedKeyringTypes from '../../constants/keyringTypes'; import Engine from '../../core/Engine'; import { strings } from '../../../locales/i18n'; -import { tlc } from '../general'; +import { tlc, toLowerCaseEquals } from '../general'; import { doENSLookup, doENSReverseLookup, @@ -33,6 +32,12 @@ import TransactionTypes from '../../core/TransactionTypes'; import { selectChainId } from '../../selectors/networkController'; import { store } from '../../store'; import { regex } from '../../../app/util/regex'; +import { InternalAccount } from '@metamask/keyring-api'; +import { AddressBookState } from '@metamask/address-book-controller'; +import { NetworkType, toChecksumHexAddress } from '@metamask/controller-utils'; +import { NetworkState } from '@metamask/network-controller'; +import { AccountImportStrategy } from '@metamask/keyring-controller'; +import { Hex, isHexString } from '@metamask/utils'; const { ASSET: { ERC721, ERC1155 }, @@ -43,7 +48,7 @@ const { * @param {String} address - String corresponding to an address * @returns {String} - String corresponding to full checksummed address */ -export function renderFullAddress(address) { +export function renderFullAddress(address: string) { return address ? toChecksumAddress(address) : strings('transactions.tx_details_not_available'); @@ -55,7 +60,8 @@ export function renderFullAddress(address) { * @param {String} type - Format type * @returns {String} Formatted address */ -export const formatAddress = (rawAddress, type) => { +type FormatAddressType = 'short' | 'mid'; +export const formatAddress = (rawAddress: string, type: FormatAddressType) => { let formattedAddress = rawAddress; if (!isValidAddress(rawAddress)) { @@ -81,7 +87,7 @@ export const formatAddress = (rawAddress, type) => { * Defaults to 4. * @returns {String} - String corresponding to short address format */ -export function renderShortAddress(address, chars = 4) { +export function renderShortAddress(address: string, chars = 4) { if (!address) return address; const checksummedAddress = toChecksumAddress(address); return `${checksummedAddress.substr( @@ -91,11 +97,10 @@ export function renderShortAddress(address, chars = 4) { } export function renderSlightlyLongAddress( - address, + address: string, chars = 4, initialChars = 20, ) { - if (!address) return address; const checksummedAddress = toChecksumAddress(address); return `${checksummedAddress.slice( 0, @@ -104,33 +109,40 @@ export function renderSlightlyLongAddress( } /** - * Returns address name if it's in known identities + * Returns address name if it's in known InternalAccounts * * @param {String} address - String corresponding to an address - * @param {Object} identities - Identities object + * @param {Array} internalAccounts - Array of InternalAccounts objects * @returns {String} - String corresponding to account name. If there is no name, returns the original short format address */ -export function renderAccountName(address, identities) { +export function renderAccountName( + address: string, + internalAccounts: InternalAccount[], +) { const chainId = selectChainId(store.getState()); - address = safeToChecksumAddress(address); - if (identities && address && address in identities) { - const identityName = identities[address].name; + address = toChecksumHexAddress(address); + const account = internalAccounts.find((acc) => + toLowerCaseEquals(acc.address, address), + ); + if (account) { + const identityName = account.metadata.name; const ensName = getCachedENSName(address, chainId) || ''; return isDefaultAccountName(identityName) && ensName ? ensName : identityName; } + return renderShortAddress(address); } /** - * Imports a an account from a private key + * Imports an account from a private key * * @param {String} private_key - String corresponding to a private key * @returns {Promise} - Returns a promise */ -export async function importAccountFromPrivateKey(private_key) { +export async function importAccountFromPrivateKey(private_key: string) { const { KeyringController } = Engine.context; // Import private key let pkey = private_key; @@ -139,8 +151,11 @@ export async function importAccountFromPrivateKey(private_key) { pkey = pkey.substr(2); } const importedAccountAddress = - await KeyringController.importAccountWithStrategy('privateKey', [pkey]); - const checksummedAddress = safeToChecksumAddress(importedAccountAddress); + await KeyringController.importAccountWithStrategy( + AccountImportStrategy.privateKey, + [pkey], + ); + const checksummedAddress = toChecksumHexAddress(importedAccountAddress); return Engine.setSelectedAddress(checksummedAddress); } @@ -150,7 +165,7 @@ export async function importAccountFromPrivateKey(private_key) { * @param {String} address - String corresponding to an address * @returns {Boolean} - Returns a boolean */ -export function isQRHardwareAccount(address) { +export function isQRHardwareAccount(address: string) { if (!isValidHexAddress(address)) return false; const { KeyringController } = Engine.context; @@ -158,7 +173,7 @@ export function isQRHardwareAccount(address) { const qrKeyrings = keyrings.filter( (keyring) => keyring.type === ExtendedKeyringTypes.qr, ); - let qrAccounts = []; + let qrAccounts: string[] = []; for (const qrKeyring of qrKeyrings) { qrAccounts = qrAccounts.concat( qrKeyring.accounts.map((account) => account.toLowerCase()), @@ -173,7 +188,7 @@ export function isQRHardwareAccount(address) { * @param {String} address - String corresponding to an address * @returns {Keyring | undefined} - Returns the keyring of the provided address if keyring found, otherwise returns undefined */ -export function getKeyringByAddress(address) { +export function getKeyringByAddress(address: string) { if (!isValidHexAddress(address)) { return undefined; } @@ -194,11 +209,11 @@ export function getKeyringByAddress(address) { * @returns {Boolean} - Returns a boolean */ export function isHardwareAccount( - address, + address: string, accountTypes = [ExtendedKeyringTypes.qr, ExtendedKeyringTypes.ledger], ) { const keyring = getKeyringByAddress(address); - return keyring && accountTypes.includes(keyring.type); + return keyring && accountTypes.includes(keyring.type as ExtendedKeyringTypes); } /** @@ -207,7 +222,7 @@ export function isHardwareAccount( * @param {String} address - String corresponding to an address * @returns {Boolean} - Returns a boolean */ -export function isExternalHardwareAccount(address) { +export function isExternalHardwareAccount(address: string) { return isHardwareAccount(address, [ExtendedKeyringTypes.ledger]); } @@ -217,7 +232,7 @@ export function isExternalHardwareAccount(address) { * @param {String} address - String corresponding to an address * @returns {String} - Returns address's i18n label text */ -export function getLabelTextByAddress(address) { +export function getLabelTextByAddress(address: string) { if (!address) return null; const keyring = getKeyringByAddress(address); if (keyring) { @@ -239,7 +254,7 @@ export function getLabelTextByAddress(address) { * @param {String} address - String corresponding to an address * @returns {String} - Returns address's account type */ -export function getAddressAccountType(address) { +export function getAddressAccountType(address: string) { if (!isValidHexAddress(address)) { throw new Error(`Invalid address: ${address}`); } @@ -272,7 +287,7 @@ export function getAddressAccountType(address) { * @param {String} name - String corresponding to an ENS name * @returns {boolean} - Returns a boolean indicating if it is valid */ -export function isENS(name = undefined) { +export function isENS(name: string | undefined = undefined) { if (!name) return false; // Checks that the domain consists of at least one valid domain pieces separated by periods, followed by a tld @@ -281,7 +296,7 @@ export function isENS(name = undefined) { const match = punycode.toASCII(name).toLowerCase().match(regex.ensName); const OFFSET = 1; - const index = name && name.lastIndexOf('.'); + const index = name?.lastIndexOf('.'); const tld = index && index >= OFFSET && @@ -296,13 +311,13 @@ export function isENS(name = undefined) { * @param {string} address The 42 character Ethereum address composed of: * 2 ('0x': 2 char hex prefix) + 20 (last 20 bytes of public key) * 2 (as each byte is 2 chars in ascii) */ -export function resemblesAddress(address) { +export function resemblesAddress(address: string) { return address && address.length === 2 + 20 * 2; } -export function safeToChecksumAddress(address) { +export function safeToChecksumAddress(address: string) { if (!address) return undefined; - return toChecksumAddress(address); + return toChecksumAddress(address) as Hex; } /** @@ -317,14 +332,14 @@ export function safeToChecksumAddress(address) { * @param {string} possibleAddress - Input parameter to check against * @param {Object} [options] - options bag * @param {boolean} [options.allowNonPrefixed] - If true will first ensure '0x' - * is prepended to the string + * is prepended to the string * @param {boolean} [options.mixedCaseUseChecksum] - If true will treat mixed - * case addresses as checksum addresses and validate that proper checksum - * format is used + * case addresses as checksum addresses and validate that proper checksum + * format is used * @returns {boolean} whether or not the input is a valid hex address */ export function isValidHexAddress( - possibleAddress, + possibleAddress: string, { allowNonPrefixed = false, mixedCaseUseChecksum = false } = {}, ) { const addressToCheck = allowNonPrefixed @@ -351,21 +366,27 @@ export function isValidHexAddress( * @param {Object} params - Contains multiple variables that are needed to * check if the address is already saved in our contact list or in our accounts * Variables: - * address (String) - Represents the address of the account - * addressBook (Object) - Represents all the contacts that we have saved on the address book - * identities (Object) - Represents our accounts on the current network of the wallet - * chainId (string) - The chain ID for the current selected network + * address (String) - Represents the address of the account + * addressBook (Object) - Represents all the contacts that we have saved on the address book + * internalAccounts (Array) InternalAccount - Represents our accounts on the current network of the wallet + * chainId (string) - The chain ID for the current selected network * @returns String | undefined - When it is saved returns a string "contactAlreadySaved" if it's not reutrn undefined */ -function checkIfAddressAlreadySaved(params) { - const { address, addressBook, chainId, identities } = params; +function checkIfAddressAlreadySaved( + address: string, + addressBook: AddressBookState['addressBook'], + chainId: Hex, + internalAccounts: InternalAccount[], +) { if (address) { const networkAddressBook = addressBook[chainId] || {}; const checksummedResolvedAddress = toChecksumAddress(address); if ( networkAddressBook[checksummedResolvedAddress] || - identities[checksummedResolvedAddress] + internalAccounts.find((account) => + toLowerCaseEquals(account.address, checksummedResolvedAddress), + ) ) { return CONTACT_ALREADY_SAVED; } @@ -379,25 +400,29 @@ function checkIfAddressAlreadySaved(params) { * This function is needed in two place of the app, SendTo of SendFlow in order to send tokes and * is present in ContactForm of Contatcs, in order to add a new contact * Variables: - * toAccount (String) - Represents the account address or ens - * chainId (String) - Represents the current chain ID - * addressBook (Object) - Represents all the contacts that we have saved on the address book - * identities (Object) - Represents our accounts on the current network of the wallet - * providerType (String) - Represents the network name + * toAccount (String) - Represents the account address or ens + * chainId (Hex String) - Represents the current chain ID + * addressBook (Object) - Represents all the contacts that we have saved on the address book + * internalAccounts (Array) InternalAccount - Represents our accounts on the current network of the wallet + * providerType (String) - Represents the network name * @returns the variables that are needed for updating the state of the two flows metioned above * Variables: - * addressError (String) - Contains the message or the error - * toEnsName (String) - Represents the ens name of the destination account - * addressReady (Bollean) - Represents if the address is validated or not - * toEnsAddress (String) - Represents the address of the ens inserted - * addToAddressToAddressBook (Boolean) - Represents if the address it can be add to the address book - * toAddressName (String) - Represents the address of the destination account - * errorContinue (Boolean) - Represents if with one error we can proceed or not to the next step if we wish - * confusableCollection (Object) - Represents one array with the confusable characters of the ens + * addressError (String) - Contains the message or the error + * toEnsName (String) - Represents the ens name of the destination account + * addressReady (Bollean) - Represents if the address is validated or not + * toEnsAddress (String) - Represents the address of the ens inserted + * addToAddressToAddressBook (Boolean) - Represents if the address it can be add to the address book + * toAddressName (String) - Represents the address of the destination account + * errorContinue (Boolean) - Represents if with one error we can proceed or not to the next step if we wish + * confusableCollection (Object) - Represents one array with the confusable characters of the ens * */ -export async function validateAddressOrENS(params) { - const { toAccount, addressBook, identities, chainId } = params; +export async function validateAddressOrENS( + toAccount: string, + addressBook: AddressBookState['addressBook'], + internalAccounts: InternalAccount[], + chainId: Hex, +) { const { AssetsContractController } = Engine.context; let addressError, @@ -410,15 +435,20 @@ export async function validateAddressOrENS(params) { let [addressReady, addToAddressToAddressBook] = [false, false]; if (isValidHexAddress(toAccount, { mixedCaseUseChecksum: true })) { - const contactAlreadySaved = checkIfAddressAlreadySaved({ - address: toAccount, + const contactAlreadySaved = checkIfAddressAlreadySaved( + toAccount, addressBook, chainId, - identities, - }); + internalAccounts, + ); if (contactAlreadySaved) { - addressError = checkIfAddressAlreadySaved(toAccount); + addressError = checkIfAddressAlreadySaved( + toAccount, + addressBook, + chainId, + internalAccounts, + ); } const checksummedAddress = toChecksumAddress(toAccount); addressReady = true; @@ -467,12 +497,12 @@ export async function validateAddressOrENS(params) { toEnsName = toAccount; confusableCollection = collectConfusables(toEnsName); const resolvedAddress = await doENSLookup(toAccount, chainId); - const contactAlreadySaved = checkIfAddressAlreadySaved({ - address: resolvedAddress, + const contactAlreadySaved = checkIfAddressAlreadySaved( + resolvedAddress, addressBook, chainId, - identities, - }); + internalAccounts, + ); if (resolvedAddress) { if (!contactAlreadySaved) { @@ -508,10 +538,10 @@ export async function validateAddressOrENS(params) { * @param {string} input - a random string. * @returns {boolean} indicates if the string is a valid input. */ -export function isValidAddressInputViaQRCode(input) { +export function isValidAddressInputViaQRCode(input: string) { if (input.includes(PROTOCOLS.ETHEREUM)) { const { pathname } = new URL(input); - // eslint-disable-next-line no-unused-vars + // eslint-disable-next-line @typescript-eslint/no-unused-vars const [address, _] = pathname.split('@'); return isValidHexAddress(address); } @@ -523,7 +553,7 @@ export function isValidAddressInputViaQRCode(input) { * @param {string} str * @returns {string} */ -export const stripHexPrefix = (str) => { +export const stripHexPrefix = (str: string) => { if (typeof str !== 'string') { return str; } @@ -537,7 +567,10 @@ export const stripHexPrefix = (str) => { * @param {String} chainId - The chain ID for the given address * @returns {String} - Address or null */ -export async function getAddress(toAccount, chainId) { +export async function getAddress( + toAccount: string, + chainId: string, +): Promise { if (isENS(toAccount)) { return await doENSLookup(toAccount, chainId); } @@ -547,7 +580,11 @@ export async function getAddress(toAccount, chainId) { return null; } -export const getTokenDetails = async (tokenAddress, userAddress, tokenId) => { +export const getTokenDetails = async ( + tokenAddress: string, + userAddress: string, + tokenId: string, +) => { const { AssetsContractController } = Engine.context; const tokenData = await AssetsContractController.getTokenStandardAndDetails( tokenAddress, @@ -569,11 +606,11 @@ export const getTokenDetails = async (tokenAddress, userAddress, tokenId) => { }; }; -export const shouldShowBlockExplorer = ({ - providerType, - providerRpcTarget, - networkConfigurations, -}) => { +export const shouldShowBlockExplorer = ( + providerType: NetworkType, + providerRpcTarget: string, + networkConfigurations: NetworkState['networkConfigurations'], +) => { if (providerType === RPC) { return findBlockExplorerForRpc(providerRpcTarget, networkConfigurations); } diff --git a/app/util/blockaid/index.test.ts b/app/util/blockaid/index.test.ts index 0b456aba717..7047ea4fe45 100644 --- a/app/util/blockaid/index.test.ts +++ b/app/util/blockaid/index.test.ts @@ -7,13 +7,26 @@ import { // eslint-disable-next-line import/no-namespace import * as NetworkControllerMock from '../../selectors/networkController'; import { NETWORKS_CHAIN_ID } from '../../constants/network'; +import Engine from '../../core/Engine'; import { getBlockaidMetricsParams, isBlockaidSupportedOnCurrentChain, getBlockaidTransactionMetricsParams, + isBlockaidFeatureEnabled, } from '.'; +jest.mock('../../core/Engine', () => ({ + resetState: jest.fn(), + context: { + PreferencesController: { + state: { + securityAlertsEnabled: true, + }, + }, + }, +})); + describe('Blockaid util', () => { describe('getBlockaidTransactionMetricsParams', () => { beforeEach(() => { @@ -173,4 +186,33 @@ describe('Blockaid util', () => { expect(result).toEqual(false); }); }); + + describe('isBlockaidFeatureEnabled', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('return true if blockaid is supported on current network and its enabled by the user', () => { + jest + .spyOn(NetworkControllerMock, 'selectChainId') + .mockReturnValue(NETWORKS_CHAIN_ID.MAINNET); + const result = isBlockaidFeatureEnabled(); + expect(result).toEqual(true); + }); + + it('return false if blockaid is not supported on current network', () => { + jest.spyOn(NetworkControllerMock, 'selectChainId').mockReturnValue('0x9'); + const result = isBlockaidFeatureEnabled(); + expect(result).toEqual(false); + }); + + it('return false if blockaid is not enabled by the user', () => { + jest + .spyOn(NetworkControllerMock, 'selectChainId') + .mockReturnValue(NETWORKS_CHAIN_ID.MAINNET); + Engine.context.PreferencesController.state.securityAlertsEnabled = false; + const result = isBlockaidFeatureEnabled(); + expect(result).toEqual(false); + }); + }); }); diff --git a/app/util/blockaid/index.ts b/app/util/blockaid/index.ts index d9f8e62a99d..cb2205c6a59 100644 --- a/app/util/blockaid/index.ts +++ b/app/util/blockaid/index.ts @@ -1,3 +1,4 @@ +import Engine from '../../core/Engine'; import { ResultType, SecurityAlertResponse, @@ -34,15 +35,18 @@ export const isSupportedChainId = (chainId: string) => { return isSupported; }; -// eslint-disable-next-line import/prefer-default-export export const isBlockaidSupportedOnCurrentChain = () => { const chainId = selectChainId(store.getState()); return isSupportedChainId(chainId); }; -// eslint-disable-next-line import/prefer-default-export +export const isBlockaidPreferenceEnabled = () => { + const { PreferencesController } = Engine.context; + return PreferencesController.state.securityAlertsEnabled; +}; + export const isBlockaidFeatureEnabled = () => - process.env.MM_BLOCKAID_UI_ENABLED; + isBlockaidSupportedOnCurrentChain() && isBlockaidPreferenceEnabled(); export const getBlockaidMetricsParams = ( securityAlertResponse?: SecurityAlertResponse, @@ -51,11 +55,7 @@ export const getBlockaidMetricsParams = ( // eslint-disable-next-line @typescript-eslint/no-explicit-any const additionalParams: Record = {}; - if ( - securityAlertResponse && - isBlockaidFeatureEnabled() && - isBlockaidSupportedOnCurrentChain() - ) { + if (securityAlertResponse && isBlockaidFeatureEnabled()) { const { result_type, reason, providerRequestsCount, source } = securityAlertResponse; diff --git a/app/util/logs/index.test.ts b/app/util/logs/index.test.ts index 67ace4168ef..f6c79401545 100644 --- a/app/util/logs/index.test.ts +++ b/app/util/logs/index.test.ts @@ -1,5 +1,41 @@ -import { generateStateLogs } from '.'; -import initialBackgroundState from '../../util/test/initial-background-state.json'; +import { generateStateLogs, downloadStateLogs } from '.'; +import RNFS from 'react-native-fs'; +import Share from 'react-native-share'; +import { + getApplicationName, + getBuildNumber, + getVersion, +} from 'react-native-device-info'; +import Device from '../../util/device'; +import Logger from '../../util/Logger'; +import initialRootState, { + backgroundState, +} from '../../util/test/initial-root-state'; +import { merge } from 'lodash'; + +jest.mock('react-native-fs', () => ({ + DocumentDirectoryPath: '/mock/path', + writeFile: jest.fn(), +})); + +jest.mock('react-native-share', () => ({ + open: jest.fn(), +})); + +jest.mock('react-native-device-info', () => ({ + getApplicationName: jest.fn(), + getBuildNumber: jest.fn(), + getVersion: jest.fn(), +})); + +jest.mock('../../util/device', () => ({ + isIos: jest.fn(), + isAndroid: jest.fn(), +})); + +jest.mock('../../util/Logger', () => ({ + error: jest.fn(), +})); jest.mock('../../core/Engine', () => ({ context: { @@ -16,7 +52,7 @@ describe('logs :: generateStateLogs', () => { const mockStateInput = { engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, KeyringController: { vault: 'vault mock', }, @@ -40,7 +76,7 @@ describe('logs :: generateStateLogs', () => { buildNumber: '123', engine: { backgroundState: { - ...initialBackgroundState, + ...backgroundState, KeyringController: { vault: 'vault mock', }, @@ -60,3 +96,164 @@ describe('logs :: generateStateLogs', () => { expect(logs.includes('buildNumber')).toBe(true); }); }); + +describe('logs :: downloadStateLogs', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should generate and share logs successfully on iOS', async () => { + (getApplicationName as jest.Mock).mockResolvedValue('TestApp'); + (getVersion as jest.Mock).mockResolvedValue('1.0.0'); + (getBuildNumber as jest.Mock).mockResolvedValue('100'); + (Device.isIos as jest.Mock).mockReturnValue(true); + + const mockStateInput = merge({}, initialRootState, { + engine: { + backgroundState: { + ...backgroundState, + KeyringController: { + vault: 'vault mock', + }, + }, + }, + }); + + await downloadStateLogs(mockStateInput); + + expect(RNFS.writeFile).toHaveBeenCalledWith( + '/mock/path/state-logs-v1.0.0-(100).json', + expect.any(String), + 'utf8', + ); + expect(Share.open).toHaveBeenCalledWith({ + subject: 'TestApp State logs - v1.0.0 (100)', + title: 'TestApp State logs - v1.0.0 (100)', + url: '/mock/path/state-logs-v1.0.0-(100).json', + }); + }); + + it('should generate and share logs successfully on Android', async () => { + (getApplicationName as jest.Mock).mockResolvedValue('TestApp'); + (getVersion as jest.Mock).mockResolvedValue('1.0.0'); + (getBuildNumber as jest.Mock).mockResolvedValue('100'); + (Device.isIos as jest.Mock).mockReturnValue(false); + + const mockStateInput = merge({}, initialRootState, { + engine: { + backgroundState: { + ...backgroundState, + KeyringController: { + vault: 'vault mock', + }, + }, + }, + }); + + await downloadStateLogs(mockStateInput); + + expect(RNFS.writeFile).not.toHaveBeenCalled(); + expect(Share.open).toHaveBeenCalledWith({ + subject: 'TestApp State logs - v1.0.0 (100)', + title: 'TestApp State logs - v1.0.0 (100)', + url: expect.stringContaining('data:text/plain;base64,'), + }); + }); + + it('should handle errors during log generation', async () => { + (getApplicationName as jest.Mock).mockResolvedValue('TestApp'); + (getVersion as jest.Mock).mockResolvedValue('1.0.0'); + (getBuildNumber as jest.Mock).mockResolvedValue('100'); + (Device.isIos as jest.Mock).mockReturnValue(true); + + const mockStateInput = null; + + //@ts-expect-error - the test case is to test the input being not the expected + await downloadStateLogs(mockStateInput); + + expect(Logger.error).toHaveBeenCalledWith( + expect.any(Error), + 'State log error', + ); + }); + + it('should handle errors during file writing on iOS', async () => { + (getApplicationName as jest.Mock).mockResolvedValue('TestApp'); + (getVersion as jest.Mock).mockResolvedValue('1.0.0'); + (getBuildNumber as jest.Mock).mockResolvedValue('100'); + (Device.isIos as jest.Mock).mockReturnValue(true); + (RNFS.writeFile as jest.Mock).mockRejectedValue( + new Error('File write error'), + ); + + const mockStateInput = merge({}, initialRootState, { + engine: { + backgroundState: { + ...backgroundState, + KeyringController: { + vault: 'vault mock', + }, + }, + }, + }); + + await downloadStateLogs(mockStateInput); + + expect(Logger.error).toHaveBeenCalledWith( + expect.any(Error), + 'State log error', + ); + }); + + it('should handle errors during sharing', async () => { + (getApplicationName as jest.Mock).mockResolvedValue('TestApp'); + (getVersion as jest.Mock).mockResolvedValue('1.0.0'); + (getBuildNumber as jest.Mock).mockResolvedValue('100'); + (Device.isIos as jest.Mock).mockReturnValue(false); + (Share.open as jest.Mock).mockRejectedValue(new Error('Share error')); + + const mockStateInput = merge({}, initialRootState, { + engine: { + backgroundState: { + ...backgroundState, + KeyringController: { + vault: 'vault mock', + }, + }, + }, + }); + + await downloadStateLogs(mockStateInput); + + expect(Logger.error).toHaveBeenCalledWith( + expect.any(Error), + 'State log error', + ); + }); + + it('should handle loggedIn as false', async () => { + (getApplicationName as jest.Mock).mockResolvedValue('TestApp'); + (getVersion as jest.Mock).mockResolvedValue('1.0.0'); + (getBuildNumber as jest.Mock).mockResolvedValue('100'); + (Device.isIos as jest.Mock).mockReturnValue(false); + + const mockStateInput = merge({}, initialRootState, { + engine: { + backgroundState: { + ...backgroundState, + KeyringController: { + vault: 'vault mock', + }, + }, + }, + }); + + await downloadStateLogs(mockStateInput, false); + + expect(Share.open).toHaveBeenCalledWith({ + subject: 'TestApp State logs - v1.0.0 (100)', + title: 'TestApp State logs - v1.0.0 (100)', + url: expect.stringContaining('data:text/plain;base64,'), + }); + }); +}); diff --git a/app/util/logs/index.ts b/app/util/logs/index.ts index 459a0a3a546..64bfa2088b5 100644 --- a/app/util/logs/index.ts +++ b/app/util/logs/index.ts @@ -1,8 +1,20 @@ import Engine from '../../core/Engine'; +import { + getApplicationName, + getBuildNumber, + getVersion, +} from 'react-native-device-info'; +import Share from 'react-native-share'; // eslint-disable-line import/default +import RNFS from 'react-native-fs'; +// eslint-disable-next-line import/no-nodejs-modules +import { Buffer } from 'buffer'; +import Logger from '../../util/Logger'; +import { RootState } from '../../reducers'; +import Device from '../../util/device'; // TODO: Replace "any" with type // eslint-disable-next-line @typescript-eslint/no-explicit-any, import/prefer-default-export -export const generateStateLogs = (state: any): string => { +export const generateStateLogs = (state: any, loggedIn = true): string => { const fullState = JSON.parse(JSON.stringify(state)); delete fullState.engine.backgroundState.NftController; @@ -15,6 +27,9 @@ export const generateStateLogs = (state: any): string => { // Remove encrypted vault from logs delete fullState.engine.backgroundState.KeyringController.vault; + if (!loggedIn) { + return JSON.stringify(fullState); + } // TODO: Replace "any" with type // eslint-disable-next-line @typescript-eslint/no-explicit-any const { KeyringController } = Engine.context as any; @@ -34,3 +49,45 @@ export const generateStateLogs = (state: any): string => { return JSON.stringify(newState); }; + +export const downloadStateLogs = async ( + fullState: RootState, + loggedIn = true, +) => { + const appName = await getApplicationName(); + const appVersion = await getVersion(); + const buildNumber = await getBuildNumber(); + const path = + RNFS.DocumentDirectoryPath + + `/state-logs-v${appVersion}-(${buildNumber}).json`; + // A not so great way to copy objects by value + + try { + const stateLogsWithReleaseDetails = generateStateLogs( + { + ...fullState, + appVersion, + buildNumber, + }, + loggedIn, + ); + + let url = `data:text/plain;base64,${new Buffer( + stateLogsWithReleaseDetails, + ).toString('base64')}`; + // // Android accepts attachements as BASE64 + if (Device.isIos()) { + await RNFS.writeFile(path, stateLogsWithReleaseDetails, 'utf8'); + url = path; + } + + await Share.open({ + subject: `${appName} State logs - v${appVersion} (${buildNumber})`, + title: `${appName} State logs - v${appVersion} (${buildNumber})`, + url, + }); + } catch (err) { + const e = err as Error; + Logger.error(e, 'State log error'); + } +}; diff --git a/app/util/metrics/trackDappViewedEvent/index.test.ts b/app/util/metrics/trackDappViewedEvent/index.test.ts index f40d0796841..6eada90e61b 100644 --- a/app/util/metrics/trackDappViewedEvent/index.test.ts +++ b/app/util/metrics/trackDappViewedEvent/index.test.ts @@ -1,5 +1,16 @@ import trackDappViewedEvent from './index'; import { MetaMetrics, MetaMetricsEvents } from '../../../core/Analytics'; +import { createMockAccountsControllerState } from '../../test/accountsControllerTestUtils'; +import { MOCK_KEYRING_CONTROLLER } from '../../../selectors/keyringController/testUtils'; + +const MOCK_ADDRESS_1 = '0xe64dD0AB5ad7e8C5F2bf6Ce75C34e187af8b920A'; +const MOCK_ADDRESS_2 = '0x519d2CE57898513F676a5C3b66496c3C394c9CC7'; + +const MOCK_DEFAULT_ACCOUNTS_CONTROLLER_STATE = + createMockAccountsControllerState([MOCK_ADDRESS_1, MOCK_ADDRESS_2]); + +const MOCK_ACCOUNTS_CONTROLLER_STATE_WITH_ONE_ACCOUNT = + createMockAccountsControllerState([MOCK_ADDRESS_1]); jest.mock('../../../core/Analytics/MetaMetrics'); // Need to mock this module since it uses store.getState, which interferes with the mocks from this test file. @@ -24,9 +35,8 @@ jest.mock('../../../store', () => { }, engine: { backgroundState: { - PreferencesController: { - identities: { '0x1': true, '0x2': true }, - }, + AccountsController: MOCK_DEFAULT_ACCOUNTS_CONTROLLER_STATE, + KeyringController: MOCK_KEYRING_CONTROLLER, }, }, })); @@ -51,9 +61,8 @@ describe('trackDappViewedEvent', () => { }, engine: { backgroundState: { - PreferencesController: { - identities: { '0x1': true, '0x2': true }, - }, + AccountsController: MOCK_DEFAULT_ACCOUNTS_CONTROLLER_STATE, + KeyringController: MOCK_KEYRING_CONTROLLER, }, }, })); @@ -84,9 +93,8 @@ describe('trackDappViewedEvent', () => { }, engine: { backgroundState: { - PreferencesController: { - identities: { '0x1': true, '0x2': true }, - }, + AccountsController: MOCK_DEFAULT_ACCOUNTS_CONTROLLER_STATE, + KeyringController: MOCK_KEYRING_CONTROLLER, }, }, })); @@ -117,9 +125,8 @@ describe('trackDappViewedEvent', () => { }, engine: { backgroundState: { - PreferencesController: { - identities: { '0x1': true, '0x2': true }, - }, + AccountsController: MOCK_DEFAULT_ACCOUNTS_CONTROLLER_STATE, + KeyringController: MOCK_KEYRING_CONTROLLER, }, }, })); @@ -150,9 +157,8 @@ describe('trackDappViewedEvent', () => { }, engine: { backgroundState: { - PreferencesController: { - identities: { '0x1': true }, - }, + AccountsController: MOCK_ACCOUNTS_CONTROLLER_STATE_WITH_ONE_ACCOUNT, + KeyringController: MOCK_KEYRING_CONTROLLER, }, }, })); @@ -183,9 +189,8 @@ describe('trackDappViewedEvent', () => { }, engine: { backgroundState: { - PreferencesController: { - identities: { '0x1': true }, - }, + AccountsController: MOCK_ACCOUNTS_CONTROLLER_STATE_WITH_ONE_ACCOUNT, + KeyringController: MOCK_KEYRING_CONTROLLER, }, }, })); diff --git a/app/util/metrics/trackDappViewedEvent/index.ts b/app/util/metrics/trackDappViewedEvent/index.ts index 20e1dccd67d..ebdf2bb4634 100644 --- a/app/util/metrics/trackDappViewedEvent/index.ts +++ b/app/util/metrics/trackDappViewedEvent/index.ts @@ -1,8 +1,8 @@ import { store } from '../../../store'; -import { selectIdentities } from '../../../selectors/preferencesController'; import { addToViewedDapp } from '../../../actions/browser'; import { MetaMetrics, MetaMetricsEvents } from '../../../core/Analytics'; import { prefixUrlWithProtocol } from '../../browser'; +import { selectInternalAccounts } from '../../../selectors/accountsController'; /** * Tracks Dapp viewed event @@ -23,8 +23,8 @@ const trackDappViewedEvent = ({ const visitedDappsByHostname = store.getState().browser.visitedDappsByHostname; const isFirstVisit = !visitedDappsByHostname[hostname]; - const accountByAddress = selectIdentities(store.getState()); - const numberOfWalletAccounts = Object.keys(accountByAddress).length; + const internalAccounts = selectInternalAccounts(store.getState()); + const numberOfWalletAccounts = Object.keys(internalAccounts).length; // Add Dapp hostname to viewed dapps store.dispatch(addToViewedDapp(hostname)); diff --git a/app/util/networks/index.js b/app/util/networks/index.js index e22ade37dbd..ab425815f4f 100644 --- a/app/util/networks/index.js +++ b/app/util/networks/index.js @@ -122,6 +122,10 @@ export const BLOCKAID_SUPPORTED_CHAIN_IDS = [ NETWORKS_CHAIN_ID.LINEA_MAINNET, NETWORKS_CHAIN_ID.SEPOLIA, NETWORKS_CHAIN_ID.OPBNB, + NETWORKS_CHAIN_ID.ZKSYNC_ERA, + NETWORKS_CHAIN_ID.SCROLL, + NETWORKS_CHAIN_ID.BERACHAIN, + NETWORKS_CHAIN_ID.METACHAIN_ONE, ]; export const BLOCKAID_SUPPORTED_NETWORK_NAMES = { @@ -134,6 +138,10 @@ export const BLOCKAID_SUPPORTED_NETWORK_NAMES = { [NETWORKS_CHAIN_ID.LINEA_MAINNET]: 'Linea', [NETWORKS_CHAIN_ID.SEPOLIA]: 'Sepolia', [NETWORKS_CHAIN_ID.OPBNB]: 'opBNB', + [NETWORKS_CHAIN_ID.ZKSYNC_ERA]: 'zkSync Era Mainnet', + [NETWORKS_CHAIN_ID.SCROLL]: 'Scroll', + [NETWORKS_CHAIN_ID.BERACHAIN]: 'Berachain Artio', + [NETWORKS_CHAIN_ID.METACHAIN_ONE]: 'Metachain One Mainnet', }; export default NetworkList; diff --git a/app/util/notifications/constants/config.ts b/app/util/notifications/constants/config.ts index 4748d402334..dbe6ec7bdb3 100644 --- a/app/util/notifications/constants/config.ts +++ b/app/util/notifications/constants/config.ts @@ -70,3 +70,6 @@ function getFirebaseConfigEnv(): FirebaseAppOptions | null { } export const FIREBASE_CONFIG = getFirebaseConfigEnv(); + +export const isNotificationsFeatureEnabled = () => + process.env.MM_NOTIFICATIONS_UI_ENABLED === 'true'; diff --git a/app/util/notifications/index.ts b/app/util/notifications/index.ts index 1773615c4a9..ec78eb1f67a 100644 --- a/app/util/notifications/index.ts +++ b/app/util/notifications/index.ts @@ -4,3 +4,4 @@ export * from './setupAndroidChannels'; export * from './settings'; export * from './hooks'; export * from './methods'; +export * from './pushPermissions'; diff --git a/app/util/notifications/methods/common.ts b/app/util/notifications/methods/common.ts new file mode 100644 index 00000000000..56308b0e78f --- /dev/null +++ b/app/util/notifications/methods/common.ts @@ -0,0 +1,339 @@ +import { Web3Provider } from '@ethersproject/providers'; +import { toHex } from '@metamask/controller-utils'; +import { NotificationServicesController } from '@metamask/notification-services-controller'; +import Engine from '../../../core/Engine'; +import { IconName } from '../../../component-library/components/Icons/Icon'; +import { hexWEIToDecETH, hexWEIToDecGWEI } from '../../conversions'; +import { TRIGGER_TYPES } from '../constants'; +import { Notification } from '../types'; +import BigNumber from 'bignumber.js'; + +/** + * Checks if 2 date objects are on the same day + * + * @param currentDate + * @param dateToCheck + * @returns boolean if dates are same day. + */ +const isSameDay = (currentDate: Date, dateToCheck: Date) => + currentDate.getFullYear() === dateToCheck.getFullYear() && + currentDate.getMonth() === dateToCheck.getMonth() && + currentDate.getDate() === dateToCheck.getDate(); + +/** + * Checks if a date is "yesterday" from the current date + * + * @param currentDate + * @param dateToCheck + * @returns boolean if dates were "yesterday" + */ +const isYesterday = (currentDate: Date, dateToCheck: Date) => { + const yesterday = new Date(currentDate); + yesterday.setDate(currentDate.getDate() - 1); + return isSameDay(yesterday, dateToCheck); +}; + +/** + * Checks if 2 date objects are in the same year. + * + * @param currentDate + * @param dateToCheck + * @returns boolean if dates were in same year + */ +const isSameYear = (currentDate: Date, dateToCheck: Date) => + currentDate.getFullYear() === dateToCheck.getFullYear(); + +/** + * Formats a given date into different formats based on how much time has elapsed since that date. + * + * @param date - The date to be formatted. + * @returns The formatted date. + */ +export function formatMenuItemDate(date: Date) { + const currentDate = new Date(); + + // E.g. 12:21 + if (isSameDay(currentDate, date)) { + return new Intl.DateTimeFormat('en', { + hour: 'numeric', + minute: 'numeric', + hour12: false, + }).format(date); + } + + // E.g. Yesterday + if (isYesterday(currentDate, date)) { + return new Intl.RelativeTimeFormat('en', { numeric: 'auto' }).format( + -1, + 'day', + ); + } + + // E.g. 21 Oct + if (isSameYear(currentDate, date)) { + return new Intl.DateTimeFormat('en', { + month: 'short', + day: 'numeric', + }).format(date); + } + + // E.g. 21 Oct 2022 + return new Intl.DateTimeFormat('en', { + year: 'numeric', + month: 'short', + day: 'numeric', + }).format(date); +} +/** + * Generates a unique key based on the provided text, index, and a random string. + * + * @param text - The text to be included in the key. + * @param index - The index to be included in the key. + * @returns The generated unique key. + */ +export const getRandomKey = (text: string, index: number) => { + const key = `${text + .replace(/\s+/gu, '_') + .replace(/[^\w-]/gu, '')}-${index}-${Math.random() + .toString(36) + .substring(2, 15)}`; + + return key; +}; + +interface FormatOptions { + decimalPlaces?: number; + shouldEllipse?: boolean; +} +const defaultFormatOptions = { + decimalPlaces: 4, +}; + +/** + * Calculates the number of leading zeros in the fractional part of a number. + * + * This function converts a number or a string representation of a number into + * its decimal form and then counts the number of leading zeros present in the + * fractional part of the number. This is useful for determining the precision + * of very small numbers. + * + * @param num - The number to analyze, which can be in the form + * of a number or a string. + * @returns The count of leading zeros in the fractional part of the number. + */ +export const getLeadingZeroCount = (num: number | string) => { + const numToString = new BigNumber(num, 10).toString(10); + const fractionalPart = numToString.split('.')[1] ?? ''; + return fractionalPart.match(/^0*/u)?.[0]?.length || 0; +}; + +/** + * This formats a number using Intl + * It abbreviates large numbers (using K, M, B, T) + * And abbreviates small numbers in 2 ways: + * - Will format to the given number of decimal places + * - Will format up to 4 decimal places + * - Will ellipse the number if longer than given decimal places + * + * @param numericAmount + * @param opts + * @returns + */ + +export const formatAmount = (numericAmount: number, opts?: FormatOptions) => { + // create options with defaults + const options = { ...defaultFormatOptions, ...opts }; + + const leadingZeros = getLeadingZeroCount(numericAmount); + const isDecimal = + numericAmount.toString().includes('.') || + leadingZeros > 0 || + numericAmount.toString().includes('e-'); + const isLargeNumber = numericAmount > 999; + + const handleShouldEllipse = (decimalPlaces: number) => + Boolean(options?.shouldEllipse) && leadingZeros >= decimalPlaces; + + if (isLargeNumber) { + return Intl.NumberFormat('en-US', { + notation: 'compact', + compactDisplay: 'short', + maximumFractionDigits: 2, + }).format(numericAmount); + } + + if (isDecimal) { + const ellipse = handleShouldEllipse(options.decimalPlaces); + const formattedValue = Intl.NumberFormat('en-US', { + minimumFractionDigits: ellipse ? options.decimalPlaces : undefined, + maximumFractionDigits: options.decimalPlaces, + }).format(numericAmount); + + return ellipse ? `${formattedValue}...` : formattedValue; + } + + // Default to showing the raw amount + return numericAmount.toString(); +}; + +export function hasNetworkFeeFields( + notification: NotificationServicesController.Types.OnChainRawNotification, +): notification is NotificationServicesController.Types.OnChainRawNotificationsWithNetworkFields { + return 'network_fee' in notification.data; +} + +type HexChainId = `0x${string}`; + +export function getProviderByChainId(chainId: HexChainId) { + const networkClientId = + Engine.context.NetworkController.findNetworkClientIdByChainId(chainId); + const provider = + Engine.context.NetworkController.getNetworkClientById( + networkClientId, + )?.provider; + + // @ts-expect-error TODO: remove this annotation once the `Eip1193Provider` class is released + return provider && new Web3Provider(provider); +} + +export const getNetworkFees = async ( + notification: NotificationServicesController.Types.OnChainRawNotification, +) => { + if (!hasNetworkFeeFields(notification)) { + throw new Error('Invalid notification type'); + } + + const chainId = toHex(notification.chain_id); + const provider = getProviderByChainId(chainId); + + if (!provider) { + throw new Error(`No provider found for chainId ${chainId}`); + } + + try { + const receipt = await provider.getTransactionReceipt(notification.tx_hash); + const transaction = await provider.getTransaction(notification.tx_hash); + const block = await provider.getBlock(notification.block_number); + + const calculateUsdAmount = (value: string, decimalPlaces?: number) => + formatAmount( + parseFloat(value) * + parseFloat(notification.data.network_fee.native_token_price_in_usd), + { + decimalPlaces: decimalPlaces || 4, + }, + ); + + const transactionFeeInEth = hexWEIToDecETH( + receipt.gasUsed.mul(receipt.effectiveGasPrice)._hex, + )?.toString(); + const transactionFeeInUsd = calculateUsdAmount(transactionFeeInEth); + + const gasLimit = transaction.gasLimit.toNumber(); + const gasUsed = receipt.gasUsed.toNumber(); + + const baseFee = block.baseFeePerGas + ? hexWEIToDecGWEI(block.baseFeePerGas._hex) + : null; + const priorityFee = block.baseFeePerGas + ? hexWEIToDecGWEI(receipt.effectiveGasPrice.sub(block.baseFeePerGas)._hex) + : null; + + const maxFeePerGas = transaction.maxFeePerGas + ? hexWEIToDecGWEI(transaction.maxFeePerGas._hex) + : null; + + return { + transactionFeeInEth, + transactionFeeInUsd, + gasLimit, + gasUsed, + baseFee, + priorityFee, + maxFeePerGas, + }; + } catch (error) { + throw new Error( + `Error fetching network fees for chainId ${chainId}: ${error}`, + ); + } +}; + +export const getNotificationBadge = (trigger_type: string) => { + switch (trigger_type) { + case TRIGGER_TYPES.ERC20_SENT: + case TRIGGER_TYPES.ERC721_SENT: + case TRIGGER_TYPES.ERC1155_SENT: + case TRIGGER_TYPES.ETH_SENT: + return IconName.Arrow2Upright; + case TRIGGER_TYPES.ERC20_RECEIVED: + case TRIGGER_TYPES.ERC721_RECEIVED: + case TRIGGER_TYPES.ERC1155_RECEIVED: + case TRIGGER_TYPES.ETH_RECEIVED: + return IconName.Received; + case TRIGGER_TYPES.METAMASK_SWAP_COMPLETED: + return IconName.SwapHorizontal; + case TRIGGER_TYPES.ROCKETPOOL_STAKE_COMPLETED: + case TRIGGER_TYPES.ROCKETPOOL_UNSTAKE_COMPLETED: + case TRIGGER_TYPES.LIDO_STAKE_COMPLETED: + case TRIGGER_TYPES.LIDO_STAKE_READY_TO_BE_WITHDRAWN: + case TRIGGER_TYPES.LIDO_WITHDRAWAL_REQUESTED: + case TRIGGER_TYPES.LIDO_WITHDRAWAL_COMPLETED: + return IconName.Plant; + default: + return IconName.Sparkle; + } +}; + +// The character limit on ENS names, nicknames and addresses before we truncate +export const TRUNCATED_NAME_CHAR_LIMIT = 11; + +// The number of characters to slice from the beginning of an address for truncated format: +// `${TRUNCATED_ADDRESS_START_CHARS}...${TRUNCATED_ADDRESS_END_CHARS}` +export const TRUNCATED_ADDRESS_START_CHARS = 7; + +// The number of characters to slice from the end of an address for truncated format: +// `${TRUNCATED_ADDRESS_START_CHARS}...${TRUNCATED_ADDRESS_END_CHARS}` +export const TRUNCATED_ADDRESS_END_CHARS = 5; + +/** + * Shortens the given string, preserving the beginning and end. + * Returns the string it is no longer than truncatedCharLimit. + * + * @param {string} stringToShorten - The string to shorten. + * @param {object} options - The options to use when shortening the string. + * @param {number} options.truncatedCharLimit - The maximum length of the string. + * @param {number} options.truncatedStartChars - The number of characters to preserve at the beginning. + * @param {number} options.truncatedEndChars - The number of characters to preserve at the end. + * @returns {string} The shortened string. + */ +export function shortenString( + stringToShorten = '', + { truncatedCharLimit, truncatedStartChars, truncatedEndChars } = { + truncatedCharLimit: TRUNCATED_NAME_CHAR_LIMIT, + truncatedStartChars: TRUNCATED_ADDRESS_START_CHARS, + truncatedEndChars: TRUNCATED_ADDRESS_END_CHARS, + }, +) { + if (stringToShorten.length < truncatedCharLimit) { + return stringToShorten; + } + + return `${stringToShorten.slice( + 0, + truncatedStartChars, + )}...${stringToShorten.slice(-truncatedEndChars)}`; +} + +export const sortNotifications = ( + notifications: Notification[], +): Notification[] => { + if (!notifications) { + return []; + } + + // NOTE - sorting may be expensive due to re-creating Date obj. + return notifications.sort( + (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), + ); +}; diff --git a/app/util/notifications/methods/fcmHelper.test.ts b/app/util/notifications/methods/fcmHelper.test.ts new file mode 100644 index 00000000000..1dc2eca655b --- /dev/null +++ b/app/util/notifications/methods/fcmHelper.test.ts @@ -0,0 +1,83 @@ +import { + checkPlayServices, + registerAppWithFCM, + unRegisterAppWithFCM, + checkApplicationNotificationPermission, + getFcmToken, +} from './fcmHelper'; + +jest.mock('@react-native-firebase/app', () => ({ + utils: () => ({ + playServicesAvailability: { + status: 1, + isAvailable: false, + hasResolution: true, + isUserResolvableError: true, + }, + makePlayServicesAvailable: jest.fn(() => Promise.resolve()), + resolutionForPlayServices: jest.fn(() => Promise.resolve()), + promptForPlayServices: jest.fn(() => Promise.resolve()), + }), +})); + +jest.mock('@react-native-firebase/messaging', () => ({ + __esModule: true, + default: () => ({ + hasPermission: jest.fn(() => Promise.resolve(true)), + subscribeToTopic: jest.fn(), + unsubscribeFromTopic: jest.fn(), + isDeviceRegisteredForRemoteMessages: false, + registerDeviceForRemoteMessages: jest.fn(() => + Promise.resolve('registered'), + ), + unregisterDeviceForRemoteMessages: jest.fn(() => + Promise.resolve('unregistered'), + ), + deleteToken: jest.fn(() => Promise.resolve()), + requestPermission: jest.fn(() => Promise.resolve(1)), + getToken: jest.fn(() => Promise.resolve('fcm-token')), + }), + FirebaseMessagingTypes: { + AuthorizationStatus: { + AUTHORIZED: 1, + PROVISIONAL: 2, + }, + }, +})); + +jest.mock('react-native-permissions', () => ({ + PERMISSIONS: { + ANDROID: { + POST_NOTIFICATIONS: 'android.permission.POST_NOTIFICATIONS', + }, + }, + request: jest.fn(() => Promise.resolve('granted')), +})); + +describe('Firebase and Permission Functions', () => { + it('should check checkPlayServices function call for coverage', async () => { + await checkPlayServices(); + const token = await getFcmToken(); + + expect(token).toBe('fcm-token'); + }); + it('should check registerAppWithFCM function call for coverage', async () => { + await registerAppWithFCM(); + + const token = await getFcmToken(); + + expect(token).toBe('fcm-token'); + }); + it('should check unRegisterAppWithFCM function call for coverage', async () => { + await unRegisterAppWithFCM(); + const token = await getFcmToken(); + + expect(token).toBe('fcm-token'); + }); + it('should check checkApplicationNotificationPermission function call for coverage', async () => { + await checkApplicationNotificationPermission(); + const token = await getFcmToken(); + + expect(token).toBe('fcm-token'); + }); +}); diff --git a/app/util/notifications/methods/fcmHelper.ts b/app/util/notifications/methods/fcmHelper.ts new file mode 100644 index 00000000000..8b811605fc3 --- /dev/null +++ b/app/util/notifications/methods/fcmHelper.ts @@ -0,0 +1,99 @@ +import { utils } from '@react-native-firebase/app'; +import messaging, { + FirebaseMessagingTypes, +} from '@react-native-firebase/messaging'; +import Logger from '../../../util/Logger'; +import { PERMISSIONS, request } from 'react-native-permissions'; + +export async function checkPlayServices() { + const { status, isAvailable, hasResolution, isUserResolvableError } = + utils().playServicesAvailability; + if (isAvailable) return Promise.resolve(); + + if (isUserResolvableError || hasResolution) { + switch (status) { + case 1: + return utils().makePlayServicesAvailable(); + case 2: + return utils().resolutionForPlayServices(); + default: + if (isUserResolvableError) return utils().promptForPlayServices(); + if (hasResolution) return utils().resolutionForPlayServices(); + } + } + return Promise.reject( + new Error('Unable to find a valid play services version.'), + ); +} + +export async function registerAppWithFCM() { + Logger.log( + 'registerAppWithFCM status', + messaging().isDeviceRegisteredForRemoteMessages, + ); + if (!messaging().isDeviceRegisteredForRemoteMessages) { + await messaging() + .registerDeviceForRemoteMessages() + .then((status: unknown) => { + Logger.log('registerDeviceForRemoteMessages status', status); + }) + .catch((error: Error) => { + Logger.error(error); + }); + } +} + +export async function unRegisterAppWithFCM() { + Logger.log( + 'unRegisterAppWithFCM status', + messaging().isDeviceRegisteredForRemoteMessages, + ); + + if (messaging().isDeviceRegisteredForRemoteMessages) { + await messaging() + .unregisterDeviceForRemoteMessages() + .then((status: unknown) => { + Logger.log('unregisterDeviceForRemoteMessages status', status); + }) + .catch((error: Error) => { + Logger.error(error); + }); + } + await messaging().deleteToken(); + Logger.log( + 'unRegisterAppWithFCM status', + messaging().isDeviceRegisteredForRemoteMessages, + ); +} + +export const checkApplicationNotificationPermission = async () => { + const authStatus = await messaging().requestPermission(); + + const enabled = + authStatus === FirebaseMessagingTypes.AuthorizationStatus.AUTHORIZED || + authStatus === FirebaseMessagingTypes.AuthorizationStatus.PROVISIONAL; + + if (enabled) { + Logger.log('Authorization status:', authStatus); + } + request(PERMISSIONS.ANDROID.POST_NOTIFICATIONS) + .then((result) => { + Logger.log('POST_NOTIFICATIONS status:', result); + }) + .catch((error: Error) => { + Logger.error(error); + }); +}; + +export const getFcmToken = async () => { + let token = null; + await checkApplicationNotificationPermission(); + await registerAppWithFCM(); + try { + token = await messaging().getToken(); + Logger.log('getFcmToken-->', token); + } catch (error: unknown) { + Logger.error(error as Error); + } + return token; +}; diff --git a/app/util/notifications/methods/index.test.tsx b/app/util/notifications/methods/index.test.tsx deleted file mode 100644 index 1aec3e56c09..00000000000 --- a/app/util/notifications/methods/index.test.tsx +++ /dev/null @@ -1,398 +0,0 @@ -import { - formatDate, - formatNotificationTitle, - getNotificationBadge, - sortNotifications, - returnAvatarProps, - getRowDetails, - getNetwork, - TxStatus, -} from '.'; -import { TRIGGER_TYPES, Notification } from '../../../util/notifications'; -import { - IconColor, - IconName, -} from '../../../component-library/components/Icons/Icon'; -import { mockTheme } from '../../../util/theme'; -import { ETHEREUM_LOGO } from '../../../constants/urls'; - -const NOTIFICATIONS = [ - { - id: '1', - createdAt: new Date('2023-01-01'), - isRead: false, - type: TRIGGER_TYPES.ETH_SENT, - data: { - kind: 'eth_sent', - network_fee: { - gas_price: '0.003', - native_token_price_in_usd: '3.700', - }, - from: '0xABC123', - to: '0xDEF456', - amount: { usd: '0.000', eth: '1.5' }, - }, - } as Notification, - { - id: '2', - createdAt: new Date('2023-01-01'), - isRead: false, - type: TRIGGER_TYPES.ETH_SENT, - data: { - kind: 'eth_sent', - network_fee: { - gas_price: '0.003', - native_token_price_in_usd: '3.700', - }, - from: '0xABC123', - to: '0xDEF456', - amount: { usd: '0.000', eth: '1.5' }, - }, - } as Notification, - { - id: '3', - createdAt: new Date('2023-01-01'), - isRead: false, - type: TRIGGER_TYPES.ETH_SENT, - data: { - kind: 'eth_sent', - network_fee: { - gas_price: '0.003', - native_token_price_in_usd: '3.700', - }, - from: '0xABC123', - to: '0xDEF456', - amount: { usd: '0.000', eth: '1.5' }, - }, - } as Notification, -]; - -describe('formatDate', () => { - const realDateNow = Date.now.bind(global.Date); - - beforeEach(() => { - global.Date.now = jest.fn(() => new Date('2024-05-10T12:00:00Z').getTime()); - }); - - afterEach(() => { - global.Date.now = realDateNow; - }); - - it('returns formatted date as "May 8" if the date is earlier this year but not yesterday', () => { - const earlierThisYear = new Date('2024-05-08T12:00:00Z'); - expect(formatDate(earlierThisYear)).toBe('May 8'); - }); - - it('returns formatted date with year if the date is from a previous year', () => { - const lastYear = new Date('2023-12-25T12:00:00Z'); - expect(formatDate(lastYear)).toBe('Dec 25'); - }); - - it('removes the first word and returns the rest in lowercase', () => { - const title = 'ERROR_Unexpected_Error_Occurred'; - expect(formatNotificationTitle(title)).toBe('unexpected_error_occurred'); - }); - - it('returns an empty string if only one word is present', () => { - const title = 'ERROR'; - expect(formatNotificationTitle(title)).toBe(''); - }); - - it('returns an empty string if the original string is empty', () => { - const title = ''; - expect(formatNotificationTitle(title)).toBe(''); - }); - - it('handles strings with multiple leading underscores', () => { - const title = '__Sending_ETH_'; - expect(formatNotificationTitle(title)).toBe('_sending_eth_'); - }); - - it('processes case sensitivity by converting to lowercase', () => { - const title = 'ETH_Received_Completed'; - expect(formatNotificationTitle(title)).toBe('received_completed'); - }); -}); - -describe('getNotificationBadge', () => { - test.each([ - [TRIGGER_TYPES.ERC20_SENT, IconName.Arrow2Upright], - [TRIGGER_TYPES.ERC721_SENT, IconName.Arrow2Upright], - [TRIGGER_TYPES.ERC1155_SENT, IconName.Arrow2Upright], - [TRIGGER_TYPES.ETH_SENT, IconName.Arrow2Upright], - [TRIGGER_TYPES.ERC20_RECEIVED, IconName.Received], - [TRIGGER_TYPES.ERC721_RECEIVED, IconName.Received], - [TRIGGER_TYPES.ERC1155_RECEIVED, IconName.Received], - [TRIGGER_TYPES.ETH_RECEIVED, IconName.Received], - [TRIGGER_TYPES.METAMASK_SWAP_COMPLETED, IconName.SwapHorizontal], - [TRIGGER_TYPES.ROCKETPOOL_STAKE_COMPLETED, IconName.Plant], - [TRIGGER_TYPES.ROCKETPOOL_UNSTAKE_COMPLETED, IconName.Plant], - [TRIGGER_TYPES.LIDO_STAKE_COMPLETED, IconName.Plant], - [TRIGGER_TYPES.LIDO_STAKE_READY_TO_BE_WITHDRAWN, IconName.Plant], - [TRIGGER_TYPES.LIDO_WITHDRAWAL_REQUESTED, IconName.Plant], - [TRIGGER_TYPES.LIDO_WITHDRAWAL_COMPLETED, IconName.Plant], - ])('returns correct icon for %s', (triggerType, expectedIcon) => { - expect(getNotificationBadge(triggerType)).toBe(expectedIcon); - }); - - it('returns default icon for unknown trigger types', () => { - expect(getNotificationBadge('UNKNOWN_TRIGGER')).toBe(IconName.Sparkle); - }); -}); - -describe('sortNotifications', () => { - it('sorts notifications by createdAt in descending order', () => { - const notifications: Notification[] = [ - { id: '1', createdAt: new Date('2023-01-01') }, - { id: '3', createdAt: new Date('2023-01-03') }, - { id: '2', createdAt: new Date('2023-01-02') }, - ]; - const sortedNotifications = sortNotifications(notifications); - expect(sortedNotifications).toEqual([ - { id: '3', createdAt: new Date('2023-01-03') }, - { id: '2', createdAt: new Date('2023-01-02') }, - { id: '1', createdAt: new Date('2023-01-01') }, - ]); - }); - - it('handles empty array without error', () => { - expect(sortNotifications([])).toEqual([]); - }); - - it('handles array with single element', () => { - const singleNotification: Notification[] = [ - { id: '1', createdAt: new Date('2023-01-01') }, - ]; - expect(sortNotifications(singleNotification)).toEqual(singleNotification); - }); - - it('is stable for notifications with the same createdAt', () => { - const notifications: Notification[] = NOTIFICATIONS; - const sortedNotifications = sortNotifications(notifications); - expect(sortedNotifications).toEqual(notifications); - }); -}); - -describe('getRowDetails', () => { - it('handles LIDO_STAKE_COMPLETED notification', () => { - const notification: Notification = { - type: TRIGGER_TYPES.LIDO_STAKE_COMPLETED, - createdAt: new Date('2023-12-31'), - data: { - stake_out: { - symbol: 'ETH', - name: 'Ethereum', - image: ETHEREUM_LOGO, - amount: '1000000', - address: '0x0000000000000000000000000000000000000000', - decimals: '8', - usd: '0.00001', - }, - network_fee: { - gas_price: '1000000000', - native_token_price_in_usd: '0.00001', - }, - }, - } as Notification; - - // TODO: Replace "any" with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const expectedRow: any = { - badgeIcon: IconName.Plant, - title: 'Stake completed', - description: { - asset: { - symbol: 'ETH', - name: 'Ethereum', - }, - }, - createdAt: 'Dec 30', - imageUrl: ETHEREUM_LOGO, - value: '< 0.00001 ETH', - }; - - expect(getRowDetails(notification).row).toEqual(expectedRow); - }); - - it('handles METAMASK_SWAP_COMPLETED notification', () => { - const notification: Notification = { - type: TRIGGER_TYPES.METAMASK_SWAP_COMPLETED, - createdAt: new Date('2023-01-01'), - data: { - token_in: { - symbol: 'BTC', - image: ETHEREUM_LOGO, - }, - token_out: { - symbol: 'ETH', - name: 'Ethereum', - image: ETHEREUM_LOGO, - amount: '500000', - }, - network_fee: { - gas_price: '1000000000', - native_token_price_in_usd: '0.00001', - }, - }, - } as Notification; - - // TODO: Replace "any" with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const expected: any = { - badgeIcon: IconName.SwapHorizontal, - title: 'Swapped BTC for ETH', - description: { - asset: { - symbol: 'ETH', - name: 'Ethereum', - }, - }, - createdAt: 'Dec 31', - imageUrl: ETHEREUM_LOGO, - value: '< 0.00001 ETH', - }; - - expect(getRowDetails(notification).row).toEqual(expected); - }); - - it('handles ETH_SENT notification', () => { - const notification: Notification = { - type: TRIGGER_TYPES.ETH_SENT, - createdAt: new Date('2023-01-01'), - data: { - to: '0xABC123', - amount: { - eth: '1.5', - }, - network_fee: { - gas_price: '1000000000', - native_token_price_in_usd: '0.00001', - }, - }, - } as Notification; - - // TODO: Replace "any" with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const expected: any = { - badgeIcon: IconName.Arrow2Upright, - title: 'Sent to 0xABC123', - description: { - asset: { - symbol: 'ETH', - name: 'Ethereum', - }, - }, - createdAt: 'Dec 31', - value: '1.5 ETH', - }; - - expect(getRowDetails(notification).row).toEqual(expected); - }); - - it('handles ERC721_RECEIVED notification', () => { - const notification: Notification = { - type: TRIGGER_TYPES.ERC721_RECEIVED, - createdAt: new Date('2023-01-01'), - data: { - from: '0xDEF456', - nft: { - token_id: '1234', - collection: { - symbol: 'ART', - name: 'ArtCollection', - }, - image: ETHEREUM_LOGO, - }, - network_fee: { - gas_price: '1000000000', - native_token_price_in_usd: '0.00001', - }, - }, - } as Notification; - - // TODO: Replace "any" with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const expected: any = { - badgeIcon: IconName.Received, - title: 'Received NFT from 0xDEF456', - description: { - asset: { - symbol: 'ART', - name: 'ArtCollection', - }, - }, - createdAt: 'Dec 31', - imageUrl: ETHEREUM_LOGO, - value: '#1234', - }; - - expect(getRowDetails(notification).row).toEqual(expected); - }); -}); - -describe('getNetwork', () => { - it('should return the correct network for a valid chain_id', () => { - const chainId = 1; - const result = getNetwork(chainId); - expect(result).toBe('ETHEREUM'); - }); - - it('should return undefined for an invalid chain_id', () => { - const chainId = 2; - const result = getNetwork(chainId); - expect(result).toBeUndefined(); - }); -}); - -describe('returnAvatarProps', () => { - it('should return correct props for CONFIRMED and APPROVED statuses', () => { - const expectedProps = { - name: IconName.Check, - backgroundColor: mockTheme.colors.success.muted, - iconColor: IconColor.Success, - }; - expect(returnAvatarProps(TxStatus.CONFIRMED, mockTheme)).toEqual( - expectedProps, - ); - expect(returnAvatarProps(TxStatus.APPROVED, mockTheme)).toEqual( - expectedProps, - ); - }); - - it('should return correct props for UNAPPROVED, CANCELLED, FAILED, and REJECTED statuses', () => { - const expectedProps = { - name: IconName.Close, - backgroundColor: mockTheme.colors.error.muted, - iconColor: IconColor.Error, - }; - expect(returnAvatarProps(TxStatus.UNAPPROVED, mockTheme)).toEqual( - expectedProps, - ); - expect(returnAvatarProps(TxStatus.CANCELLED, mockTheme)).toEqual( - expectedProps, - ); - expect(returnAvatarProps(TxStatus.FAILED, mockTheme)).toEqual( - expectedProps, - ); - expect(returnAvatarProps(TxStatus.REJECTED, mockTheme)).toEqual( - expectedProps, - ); - }); - - it('should return correct props for PENDING, SUBMITTED, and SIGNED statuses', () => { - const expectedProps = { - name: IconName.Clock, - backgroundColor: mockTheme.colors.warning.muted, - iconColor: IconColor.Warning, - }; - expect(returnAvatarProps(TxStatus.PENDING, mockTheme)).toEqual( - expectedProps, - ); - expect(returnAvatarProps(TxStatus.SUBMITTED, mockTheme)).toEqual( - expectedProps, - ); - expect(returnAvatarProps(TxStatus.SIGNED, mockTheme)).toEqual( - expectedProps, - ); - }); -}); diff --git a/app/util/notifications/methods/index.ts b/app/util/notifications/methods/index.ts index a344abd98ee..17c65417c84 100644 --- a/app/util/notifications/methods/index.ts +++ b/app/util/notifications/methods/index.ts @@ -1,731 +1,2 @@ -import { Alert } from 'react-native'; -import { utils as ethersUtils } from 'ethers'; -import notifee, { AuthorizationStatus } from '@notifee/react-native'; -import { - IconColor, - IconName, -} from '../../../component-library/components/Icons/Icon'; -import { strings } from '../../../../locales/i18n'; -import Logger from '../../../util/Logger'; -import { Theme } from '../../../util/theme/models'; -import { - ChainId, - HalRawNotification, - HalRawNotificationsWithNetworkFields, - Notification, - TRIGGER_TYPES, - mmStorage, -} from '../../../util/notifications'; -import { formatAmount } from '../../../components/UI/Ramp/utils'; -import images from '../../../images/image-icons'; -import { formatAddress } from '../../../util/address'; -import { STORAGE_IDS } from '../settings/storage/constants'; -import Device from '../../../util/device'; -import { store } from '../../../store'; -import { renderFromWei } from '../../../util/number'; -import { updateNotificationStatus } from '../../../actions/notification'; -import Engine from '../../../core/Engine'; -import { query } from '@metamask/controller-utils'; - -export interface ViewOnEtherscanProps { - // TODO: Replace "any" with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - navigation: any; - transactionObject: { - networkID: string; - }; - transactionDetails: { - transactionHash: string; - }; - providerConfig: { - type: string; - }; - close?: () => void; -} - -export interface NotificationRowProps { - row: { - title: string; - createdAt: string; - - badgeIcon?: IconName; - imageUrl?: string; - description?: { - asset?: { - symbol?: string; - name?: string; - }; - text?: string; - }; - value: string; - }; - // TODO: Replace "any" with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - details: Record; -} - -export const sortNotifications = ( - notifications: Notification[], -): Notification[] => { - if (!notifications) { - return []; - } - return notifications.sort( - (a, b) => b.createdAt.getTime() - a.createdAt.getTime(), - ); -}; - -export const getNotificationBadge = (trigger_type: string) => { - switch (trigger_type) { - case TRIGGER_TYPES.ERC20_SENT: - case TRIGGER_TYPES.ERC721_SENT: - case TRIGGER_TYPES.ERC1155_SENT: - case TRIGGER_TYPES.ETH_SENT: - return IconName.Arrow2Upright; - case TRIGGER_TYPES.ERC20_RECEIVED: - case TRIGGER_TYPES.ERC721_RECEIVED: - case TRIGGER_TYPES.ERC1155_RECEIVED: - case TRIGGER_TYPES.ETH_RECEIVED: - return IconName.Received; - case TRIGGER_TYPES.METAMASK_SWAP_COMPLETED: - return IconName.SwapHorizontal; - case TRIGGER_TYPES.ROCKETPOOL_STAKE_COMPLETED: - case TRIGGER_TYPES.ROCKETPOOL_UNSTAKE_COMPLETED: - case TRIGGER_TYPES.LIDO_STAKE_COMPLETED: - case TRIGGER_TYPES.LIDO_STAKE_READY_TO_BE_WITHDRAWN: - case TRIGGER_TYPES.LIDO_WITHDRAWAL_REQUESTED: - case TRIGGER_TYPES.LIDO_WITHDRAWAL_COMPLETED: - return IconName.Plant; - default: - return IconName.Sparkle; - } -}; - -export function formatDate(createdAt: Date) { - const now = new Date(); - const date = createdAt; - - const isToday = - date.getDate() === now.getDate() && - date.getMonth() === now.getMonth() && - date.getFullYear() === now.getFullYear(); - - if (isToday) { - const diffMinutes = Math.floor( - (now.getTime() - date.getTime()) / (1000 * 60), - ); - if (diffMinutes < 60) { - return `${diffMinutes} min ago`; - } - const hours = date.getHours(); - const minutes = date.getMinutes(); - return `${hours}:${minutes < 10 ? '0' : ''}${minutes}`; - } - - const yesterday = new Date(now); - yesterday.setDate(now.getDate() - 1); - const isYesterday = - date.getDate() === yesterday.getDate() && - date.getMonth() === yesterday.getMonth() && - date.getFullYear() === yesterday.getFullYear(); - - if (isYesterday) { - return 'Yesterday'; - } - const options: Intl.DateTimeFormatOptions = { - month: 'short', - day: 'numeric', - }; - - const month = strings(`date.months.${date.getMonth()}`); - const day = date.getDate(); - if (date.getFullYear() === now.getFullYear()) { - return date.toLocaleDateString('default', options); - } - - return `${month} ${day}`; -} - -export function getNetwork(chain_id: HalRawNotification['chain_id']) { - return ChainId[chain_id]; -} - -export const isNotificationsFeatureEnabled = () => - process.env.MM_NOTIFICATIONS_UI_ENABLED === 'true'; - -export function formatNotificationTitle(rawTitle: string): string { - const words = rawTitle.split('_'); - words.shift(); - return words.join('_').toLowerCase(); -} - -export enum TxStatus { - UNAPPROVED = 'unapproved', - SUBMITTED = 'submitted', - SIGNED = 'signed', - PENDING = 'pending', - CONFIRMED = 'confirmed', - CANCELLED = 'cancelled', - APPROVED = 'approved', - FAILED = 'failed', - REJECTED = 'rejected', -} - -export function getRowDetails( - notification: Notification, -): NotificationRowProps { - switch (notification.type) { - case TRIGGER_TYPES.LIDO_STAKE_COMPLETED: - case TRIGGER_TYPES.LIDO_WITHDRAWAL_COMPLETED: - case TRIGGER_TYPES.LIDO_WITHDRAWAL_REQUESTED: - case TRIGGER_TYPES.ROCKETPOOL_STAKE_COMPLETED: - case TRIGGER_TYPES.ROCKETPOOL_UNSTAKE_COMPLETED: - return { - row: { - badgeIcon: getNotificationBadge(notification.type), - title: strings( - `notifications.${formatNotificationTitle(notification.type)}`, - ), - description: { - asset: { - symbol: notification.data.stake_out.symbol, - name: notification.data.stake_out.name, - }, - }, - createdAt: formatDate(notification.createdAt), - imageUrl: - notification.data.stake_out?.image || - notification.data.stake_in?.image, //using the stake_in image as a fallback, - value: `${renderFromWei(notification.data.stake_out.amount)} ${ - notification.data.stake_out.symbol - }`, - }, - details: { - type: notification.type, - stake_in: notification.data.stake_in, - stake_out: notification.data.stake_out, - tx_hash: notification.tx_hash, - status: notification.tx_hash - ? TxStatus.CONFIRMED - : TxStatus.UNAPPROVED, - network: { - name: getNetwork(notification.chain_id), - image: - notification.data.stake_out?.image || - notification.data.stake_in?.image, //using the stake_in image as a fallback, - }, - networkFee: { - gas_price: notification.data?.network_fee.gas_price, - native_token_price_in_usd: - notification.data?.network_fee.native_token_price_in_usd, - details: {}, - }, - }, - }; - case TRIGGER_TYPES.LIDO_STAKE_READY_TO_BE_WITHDRAWN: - return { - row: { - badgeIcon: getNotificationBadge(notification.type), - title: strings( - `notifications.${formatNotificationTitle(notification.type)}`, - ), - description: { - asset: { - symbol: notification.data.staked_eth.symbol, - name: notification.data.staked_eth.name, - }, - }, - createdAt: formatDate(notification.createdAt), - imageUrl: notification.data.staked_eth.image, - value: `${notification.data.staked_eth.amount} ${notification.data.staked_eth.symbol}`, - }, - details: { - type: notification.type, - request_id: notification.data.request_id, - staked_eth: notification.data.staked_eth, - tx_hash: notification.tx_hash, - status: notification.tx_hash - ? TxStatus.CONFIRMED - : TxStatus.UNAPPROVED, - network: { - name: getNetwork(notification.chain_id), - image: notification.data.staked_eth.image, - }, - }, - }; - case TRIGGER_TYPES.METAMASK_SWAP_COMPLETED: - return { - row: { - badgeIcon: getNotificationBadge(notification.type), - title: strings('notifications.swap_completed', { - from: notification.data.token_in.symbol, - to: notification.data.token_out.symbol, - }), - description: { - asset: { - symbol: notification.data.token_out.symbol, - name: notification.data.token_out.name, - }, - }, - createdAt: formatDate(notification.createdAt), - imageUrl: - notification.data.token_out.image || - notification.data.token_in.image, //using the token_in image as a fallback, - value: `${renderFromWei(notification.data.token_out.amount)} ${ - notification.data.token_out.symbol - }`, - }, - details: { - type: notification.type, - rate: notification.data.rate, - token_in: notification.data.token_in, - token_out: notification.data.token_out, - tx_hash: notification.tx_hash, - status: notification.tx_hash - ? TxStatus.CONFIRMED - : TxStatus.UNAPPROVED, - network: { - name: getNetwork(notification.chain_id), - image: notification.data.token_in.image, - }, - networkFee: { - gas_price: notification.data?.network_fee.gas_price, - native_token_price_in_usd: - notification.data?.network_fee.native_token_price_in_usd, - details: {}, - }, - }, - }; - case TRIGGER_TYPES.ETH_SENT: - case TRIGGER_TYPES.ETH_RECEIVED: - return { - row: { - badgeIcon: getNotificationBadge(notification.type), - createdAt: formatDate(notification.createdAt), - title: strings( - `notifications.${formatNotificationTitle(notification.type)}`, - { - address: formatAddress( - notification.type.includes('sent') - ? notification.data.to - : notification.data.from, - 'short', - ), - }, - ), - description: { - asset: { - symbol: 'ETH', - name: 'Ethereum', - }, - }, - value: `${notification.data.amount.eth} ETH`, - }, - details: { - type: notification.type, - amount: notification.data.amount, - from: notification.data.from, - to: notification.data.to, - tx_hash: notification.tx_hash, - status: notification.tx_hash - ? TxStatus.CONFIRMED - : TxStatus.UNAPPROVED, - network: { - name: getNetwork(notification.chain_id), - image: images.ETHEREUM, - }, - networkFee: { - gas_price: notification.data?.network_fee.gas_price, - native_token_price_in_usd: - notification.data?.network_fee.native_token_price_in_usd, - details: {}, - }, - token: { - name: 'Ethereum', - symbol: 'ETH', - image: images.ETHEREUM, - amount: notification.data.amount.eth, - address: '0xdb24b8170fc863c77f50a2b25297f642c5fe5010', - }, - }, - }; - case TRIGGER_TYPES.ERC20_SENT: - case TRIGGER_TYPES.ERC20_RECEIVED: - return { - row: { - badgeIcon: getNotificationBadge(notification.type), - title: strings( - `notifications.${formatNotificationTitle(notification.type)}`, - { - address: formatAddress( - notification.data.kind.includes('sent') - ? notification.data.to - : notification.data.from, - 'short', - ), - }, - ), - description: { - asset: { - symbol: notification.data.token.symbol, - name: notification.data.token.name, - }, - }, - createdAt: formatDate(notification.createdAt), - imageUrl: notification.data.token.image, - value: `${renderFromWei(notification.data.token.amount)} ${ - notification.data.token.symbol - }`, - }, - details: { - type: notification.type, - token: notification.data.token, - from: notification.data.from, - to: notification.data.to, - tx_hash: notification.tx_hash, - status: notification.tx_hash - ? TxStatus.CONFIRMED - : TxStatus.UNAPPROVED, - network: { - name: getNetwork(notification.chain_id), - image: notification.data.token.image, - }, - networkFee: { - gas_price: notification.data?.network_fee.gas_price, - native_token_price_in_usd: - notification.data?.network_fee.native_token_price_in_usd, - details: {}, - }, - }, - }; - case TRIGGER_TYPES.ERC721_SENT: - case TRIGGER_TYPES.ERC721_RECEIVED: - case TRIGGER_TYPES.ERC1155_SENT: - case TRIGGER_TYPES.ERC1155_RECEIVED: - return { - row: { - badgeIcon: getNotificationBadge(notification.type), - title: strings(`notifications.${notification.type}`, { - address: formatAddress( - notification.type.includes('sent') - ? notification.data.to - : notification.data.from, - 'short', - ), - }), - description: { - asset: { - symbol: notification.data?.nft?.collection.symbol, - name: notification.data?.nft?.collection.name, - }, - }, - createdAt: formatDate(notification.createdAt), - imageUrl: notification.data?.nft?.image, - value: `#${notification.data?.nft?.token_id}`, - }, - details: { - type: notification.type, - nft: { - name: notification.data?.nft?.name, - image: notification.data?.nft?.image, - }, - from: notification.data.from, - to: notification.data.to, - tx_hash: notification.tx_hash, - status: notification.tx_hash - ? TxStatus.CONFIRMED - : TxStatus.UNAPPROVED, - collection: notification.data?.nft?.collection, - network: { - name: getNetwork(notification.chain_id), - image: notification.data.nft?.image, - }, - networkFee: { - gas_price: notification.data?.network_fee.gas_price, - native_token_price_in_usd: - notification.data?.network_fee.native_token_price_in_usd, - details: {}, - }, - }, - }; - - default: - return { - row: { - title: notification.data.title, - description: { text: notification.data.shortDescription }, - createdAt: formatDate(notification.createdAt), - value: '', - }, - details: {}, - }; - } -} - -export enum AvatarType { - ADDRESS = 'address', - ASSET = 'asset', - NETWORK = 'network', - TXSTATUS = 'txstatus', -} - -export const returnAvatarProps = (status: TxStatus, theme: Theme) => { - switch (status) { - case TxStatus.CONFIRMED: - case TxStatus.APPROVED: - return { - name: IconName.Check, - backgroundColor: theme.colors.success.muted, - iconColor: IconColor.Success, - }; - case TxStatus.UNAPPROVED: - case TxStatus.CANCELLED: - case TxStatus.FAILED: - case TxStatus.REJECTED: - return { - name: IconName.Close, - backgroundColor: theme.colors.error.muted, - iconColor: IconColor.Error, - }; - case TxStatus.PENDING: - case TxStatus.SUBMITTED: - case TxStatus.SIGNED: - return { - name: IconName.Clock, - backgroundColor: theme.colors.warning.muted, - iconColor: IconColor.Warning, - }; - default: - return { - name: IconName.Info, - backgroundColor: theme.colors.background.alternative, - iconColor: IconColor.Info, - }; - } -}; - -export const notificationSettings = { - assetsReceived: false, - assetsSent: false, - deFi: false, - productAnnouncements: false, - snaps: false, -}; - -export const requestPushNotificationsPermission = async () => { - let permissionStatus; - interface NotificationEnabledState { - isEnabled: true; - notificationsOpts: { - [K in keyof typeof notificationSettings]: true; - }; - accounts: object; - } - - interface NotificationDisabledState { - isEnabled: false; - notificationsOpts: { - [K in keyof typeof notificationSettings]: false; - }; - accounts: object; - } - - const notificationDisabledState: NotificationDisabledState = { - isEnabled: false, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - notificationsOpts: Object.fromEntries( - Object.keys(notificationSettings).map((key) => [key, false] as const), - ), - }; - - const notificationEnabledState: NotificationEnabledState = { - isEnabled: true, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - notificationsOpts: Object.fromEntries( - Object.keys(notificationSettings).map((key) => [key, true] as const), - ), - accounts: [], - }; - - const promptCount = mmStorage.getLocal( - STORAGE_IDS.PUSH_NOTIFICATIONS_PROMPT_COUNT, - ); - - try { - permissionStatus = await notifee.requestPermission(); - - if (permissionStatus.authorizationStatus < AuthorizationStatus.AUTHORIZED) { - const times = promptCount + 1 || 1; - - Alert.alert( - strings('notifications.prompt_title'), - strings('notifications.prompt_desc'), - [ - { - text: strings('notifications.prompt_cancel'), - onPress: () => { - store.dispatch( - updateNotificationStatus(notificationDisabledState), - ); - mmStorage.saveLocal( - STORAGE_IDS.PUSH_NOTIFICATIONS_PROMPT_COUNT, - times, - ); - mmStorage.saveLocal( - STORAGE_IDS.PUSH_NOTIFICATIONS_PROMPT_TIME, - Date.now().toString(), - ); - - mmStorage.saveLocal( - STORAGE_IDS.NOTIFICATIONS_SETTINGS, - JSON.stringify(notificationDisabledState), - ); - }, - style: 'default', - }, - { - text: strings('notifications.prompt_ok'), - onPress: async () => { - if (Device.isIos()) { - permissionStatus = await notifee.requestPermission({ - provisional: true, - }); - } else { - permissionStatus = await notifee.requestPermission(); - } - store.dispatch( - updateNotificationStatus(notificationEnabledState), - ); - - mmStorage.saveLocal( - STORAGE_IDS.NOTIFICATIONS_SETTINGS, - JSON.stringify(notificationEnabledState), - ); - // await saveFCMToken(); - }, - }, - ], - { cancelable: false }, - ); - } - - return permissionStatus; - // TODO: Replace "any" with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (e: any) { - Logger.error(e, strings('notifications.error_checking_permission')); - } -}; - -function hasNetworkFeeFields( - notification: HalRawNotification, -): notification is HalRawNotificationsWithNetworkFields { - return 'network_fee' in notification.data; -} - -async function fetchTxDetails(tx_hash: string) { - // TODO: Replace "any" with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const { TransactionController } = Engine.context as any; - - try { - const receipt = await query( - TransactionController.ethQuery, - 'getTransactionReceipt', - [tx_hash], - ); - - if (!receipt) { - throw new Error('Transaction receipt not found'); - } - - const block = await query( - TransactionController.ethQuery, - 'getBlockByHash', - [receipt.blockHash, false], - ); - - if (!block) { - throw new Error('Transaction block not found'); - } - - const transaction = await query( - TransactionController.ethQuery, - 'eth_getTransactionByHash', - [receipt.blockHash], - ); - - if (!transaction) { - throw new Error('Transaction not found'); - } - - return { - receipt, - transaction, - block, - }; - } catch (error) { - console.error('Error fetching transaction details:', error); - throw error; - } -} - -export const getNetworkFees = async (notification: HalRawNotification) => { - if (!hasNetworkFeeFields(notification)) { - throw new Error('Invalid notification type'); - } - - try { - const { receipt, transaction, block } = await fetchTxDetails( - notification.tx_hash, - ); - const calculateUsdAmount = (value: string, decimalPlaces = 4) => - formatAmount( - (parseFloat(value) * - parseFloat( - notification.data?.network_fee.native_token_price_in_usd, - )) / - decimalPlaces, - ); - - const transactionFeeInEth = ethersUtils.formatUnits( - receipt.gasUsed.mul(receipt.effectiveGasPrice)._hex, - ); - const transactionFeeInUsd = calculateUsdAmount(transactionFeeInEth); - - const gasLimit = transaction.gasLimit.toNumber(); - const gasUsed = receipt.gasUsed.toNumber(); - - const baseFee = block.baseFeePerGas - ? ethersUtils.formatUnits(block.baseFeePerGas._hex, 'gwei') - : null; - const priorityFee = block.baseFeePerGas - ? ethersUtils.formatUnits( - receipt.effectiveGasPrice.sub(block.baseFeePerGas)._hex, - 'gwei', - ) - : null; - - const maxFeePerGas = transaction.maxFeePerGas - ? ethersUtils.formatUnits(transaction.maxFeePerGas._hex, 'gwei') - : null; - - return { - transactionFeeInEth, - transactionFeeInUsd, - gasLimit, - gasUsed, - baseFee, - priorityFee, - maxFeePerGas, - }; - } catch (error) { - console.error( - `Failed to get transaction network fees for ${notification.tx_hash}`, - error, - ); - throw error; - } -}; +export * from './fcmHelper'; +export * from './common'; diff --git a/app/util/notifications/pushPermissions.ts b/app/util/notifications/pushPermissions.ts new file mode 100644 index 00000000000..208996d4950 --- /dev/null +++ b/app/util/notifications/pushPermissions.ts @@ -0,0 +1,70 @@ +import notifee, { AuthorizationStatus } from '@notifee/react-native'; +import { Alert } from 'react-native'; + +import { strings } from '../../../locales/i18n'; +import Device from '../device'; +import { mmStorage } from './settings'; +import { STORAGE_IDS } from './settings/storage/constants'; +import Logger from '../Logger'; + +export const requestPushNotificationsPermission = async () => { + let permissionStatus; + + const promptCount = mmStorage.getLocal( + STORAGE_IDS.PUSH_NOTIFICATIONS_PROMPT_COUNT, + ); + + try { + permissionStatus = await notifee.requestPermission(); + + if (permissionStatus.authorizationStatus < AuthorizationStatus.AUTHORIZED) { + const times = promptCount + 1 || 1; + + Alert.alert( + strings('notifications.prompt_title'), + strings('notifications.prompt_desc'), + [ + { + text: strings('notifications.prompt_cancel'), + onPress: () => { + mmStorage.saveLocal( + STORAGE_IDS.PUSH_NOTIFICATIONS_PROMPT_COUNT, + times, + ); + mmStorage.saveLocal( + STORAGE_IDS.PUSH_NOTIFICATIONS_PROMPT_TIME, + Date.now().toString(), + ); + }, + style: 'default', + }, + { + text: strings('notifications.prompt_ok'), + onPress: async () => { + if (Device.isIos()) { + permissionStatus = await notifee.requestPermission({ + provisional: true, + }); + } else { + permissionStatus = await notifee.requestPermission(); + } + }, + }, + ], + { cancelable: false }, + ); + } + + return permissionStatus; + } catch (e) { + if (e instanceof Error) { + Logger.error(e, strings('notifications.error_checking_permission')); + } else { + Logger.error( + new Error(strings('notifications.error_checking_permission')), + ); + } + } +}; + +export default requestPushNotificationsPermission; diff --git a/app/util/onboarding/index.test.ts b/app/util/onboarding/index.test.ts index 6ad370d73e3..baa62eb10e0 100644 --- a/app/util/onboarding/index.test.ts +++ b/app/util/onboarding/index.test.ts @@ -25,50 +25,66 @@ describe('shouldShowSmartTransactionOptInModal', () => { (store.getState as jest.Mock).mockClear(); }); + it('returns true if a user has not seen the modal, is on Ethereum mainnet with default RPC URL and has non-zero balance', async () => { + (AsyncStorage.getItem as jest.Mock).mockResolvedValueOnce('7.24.0'); // currentAppVersion + (store.getState as jest.Mock).mockReturnValueOnce(getMockState(null)); // versionSeen + + const result = await shouldShowSmartTransactionsOptInModal( + NETWORKS_CHAIN_ID.MAINNET, + undefined, + false, + ); + expect(result).toBe(true); + }); + test.each([ [NETWORKS_CHAIN_ID.MAINNET, 'http://mainnet-url.example.com'], [NETWORKS_CHAIN_ID.ARBITRUM, 'http://arbitrum-url.example.com'], ])( - `should return false if chainId not ${NETWORKS_CHAIN_ID.MAINNET} or providerConfigRpcUrl is defined`, + `returns false if chainId is not ${NETWORKS_CHAIN_ID.MAINNET} or providerConfigRpcUrl is defined`, async (chainId, rpcUrl) => { const result = await shouldShowSmartTransactionsOptInModal( chainId, rpcUrl, + false, ); expect(result).toBe(false); }, ); - it('should return false if user has seen the modal', async () => { + it('returns false if user has seen the modal', async () => { (AsyncStorage.getItem as jest.Mock).mockResolvedValueOnce('7.24.0'); // currentAppVersion (store.getState as jest.Mock).mockReturnValueOnce(getMockState('7.24.0')); // versionSeen const result = await shouldShowSmartTransactionsOptInModal( NETWORKS_CHAIN_ID.MAINNET, undefined, + false, ); expect(result).toBe(false); }); - it('should return false if app version is not correct', async () => { + it('returns false if app version is not correct', async () => { (AsyncStorage.getItem as jest.Mock).mockResolvedValueOnce('7.0.0'); // currentAppVersion (store.getState as jest.Mock).mockReturnValueOnce(getMockState(null)); // versionSeen const result = await shouldShowSmartTransactionsOptInModal( NETWORKS_CHAIN_ID.MAINNET, undefined, + false, ); expect(result).toBe(false); }); - it('should return true if has not seen and is on mainnet with default RPC url', async () => { + it('returns false if a user has 0 balance on Ethereum Mainnet with default RPC URL', async () => { (AsyncStorage.getItem as jest.Mock).mockResolvedValueOnce('7.24.0'); // currentAppVersion (store.getState as jest.Mock).mockReturnValueOnce(getMockState(null)); // versionSeen const result = await shouldShowSmartTransactionsOptInModal( NETWORKS_CHAIN_ID.MAINNET, undefined, + true, ); - expect(result).toBe(true); + expect(result).toBe(false); }); }); diff --git a/app/util/onboarding/index.ts b/app/util/onboarding/index.ts index 3346aa6bff8..a5c381a61a8 100644 --- a/app/util/onboarding/index.ts +++ b/app/util/onboarding/index.ts @@ -26,14 +26,13 @@ const STX_OPT_IN_MIN_APP_VERSION = '7.24.0'; export const shouldShowSmartTransactionsOptInModal = async ( chainId: string, providerConfigRpcUrl: string | undefined, + accountHasZeroBalance: boolean, ) => { - // Check chain and RPC, undefined is the default RPC if ( - !( - chainId === NETWORKS_CHAIN_ID.MAINNET && - providerConfigRpcUrl === undefined - ) || - process.env.IS_TEST === 'true' + process.env.IS_TEST === 'true' || + chainId !== NETWORKS_CHAIN_ID.MAINNET || + providerConfigRpcUrl !== undefined || // undefined is the default RPC URL (Infura). + accountHasZeroBalance ) { return false; } diff --git a/app/util/sentry/__snapshots__/utils.test.ts.snap b/app/util/sentry/__snapshots__/utils.test.ts.snap new file mode 100644 index 00000000000..1b4d18ae6eb --- /dev/null +++ b/app/util/sentry/__snapshots__/utils.test.ts.snap @@ -0,0 +1,232 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`captureSentryFeedback maskObject masks initial root state fixture 1`] = ` +{ + "accounts": { + "reloadAccounts": false, + }, + "alert": { + "autodismiss": null, + "content": null, + "data": null, + "isVisible": false, + }, + "bookmarks": {}, + "browser": { + "activeTab": null, + "favicons": [], + "history": [], + "tabs": [], + "visitedDappsByHostname": {}, + "whitelist": [], + }, + "collectibles": { + "favorites": {}, + "isNftFetchingProgress": false, + }, + "engine": { + "backgroundState": { + "AccountTrackerController": { + "accounts": "object", + "accountsByChainId": "object", + }, + "AccountsController": { + "internalAccounts": { + "accounts": "object", + "selectedAccount": "string", + }, + }, + "AddressBookController": { + "addressBook": "object", + }, + "ApprovalController": { + "approvalFlows": "object", + "pendingApprovalCount": "number", + "pendingApprovals": "object", + }, + "AssetsContractController": {}, + "CurrencyRateController": { + "currencyRates": { + "ETH": { + "conversionDate": 1720196397083, + "conversionRate": 298514, + "usdConversionRate": 298514, + }, + }, + "currentCurrency": "usd", + }, + "GasFeeController": { + "estimatedGasFeeTimeBounds": {}, + "gasEstimateType": "none", + "gasFeeEstimates": {}, + "gasFeeEstimatesByChainId": {}, + "nonRPCGasFeeApisDisabled": "boolean", + }, + "KeyringController": { + "isUnlocked": true, + "keyrings": "object", + "vault": "string", + }, + "LoggingController": { + "logs": "object", + }, + "NetworkController": { + "networkConfigurations": "object", + "networksMetadata": { + "mainnet": { + "EIPS": { + "1559": true, + }, + "status": "available", + }, + }, + "providerConfig": { + "chainId": "0x1", + "ticker": "ETH", + "type": "mainnet", + }, + "selectedNetworkClientId": "string", + }, + "NftController": { + "allNftContracts": "object", + "allNfts": "object", + "ignoredNfts": "object", + }, + "NftDetectionController": "object", + "PermissionController": {}, + "PreferencesController": { + "disabledRpcMethodPreferences": { + "eth_sign": false, + }, + "displayNftMedia": true, + "featureFlags": {}, + "identities": "object", + "ipfsGateway": "string", + "isIpfsGatewayEnabled": true, + "isMultiAccountBalancesEnabled": "boolean", + "lostIdentities": "object", + "securityAlertsEnabled": "boolean", + "selectedAddress": "string", + "showIncomingTransactions": "object", + "showTestNetworks": "boolean", + "smartTransactionsOptInStatus": "boolean", + "useNftDetection": true, + "useSafeChainsListValidation": "boolean", + "useTokenDetection": true, + "useTransactionSimulations": true, + }, + "SmartTransactionsController": { + "smartTransactionsState": { + "fees": {}, + "feesByChainId": "object", + "liveness": true, + "livenessByChainId": "object", + "smartTransactions": "object", + }, + }, + }, + }, + "experimentalSettings": { + "securityAlertsEnabled": true, + }, + "fiatOrders": "object", + "infuraAvailability": { + "isBlocked": false, + }, + "inpageProvider": { + "networkId": "1", + }, + "legalNotices": { + "newPrivacyPolicyToastClickedOrClosed": true, + "newPrivacyPolicyToastShownDate": null, + }, + "modals": { + "collectibleContractModalVisible": false, + "dappTransactionModalVisible": false, + "networkModalVisible": false, + "receiveAsset": undefined, + "receiveModalVisible": false, + "shouldNetworkSwitchPopToWallet": true, + "signMessageModalVisible": true, + }, + "navigation": { + "currentBottomNavRoute": "Wallet", + "currentRoute": "Login", + }, + "networkOnboarded": { + "networkOnboardedState": {}, + "networkState": { + "nativeToken": "", + "networkType": "", + "networkUrl": "", + "showNetworkOnboarding": false, + }, + "switchedNetwork": { + "networkStatus": false, + "networkUrl": "", + }, + }, + "notification": { + "notification": { + "notificationsSettings": {}, + }, + "notifications": [], + }, + "onboarding": { + "events": [], + }, + "privacy": {}, + "rpcEvents": { + "signingEvent": { + "eventStage": "idle", + "rpcName": "", + }, + }, + "sdk": { + "approvedHosts": {}, + "connections": {}, + "dappConnections": {}, + "wc2Metadata": undefined, + }, + "security": { + "allowLoginWithRememberMe": false, + "automaticSecurityChecksEnabled": false, + "dataCollectionForMarketing": null, + "hasUserSelectedAutomaticSecurityCheckOption": false, + "isAutomaticSecurityChecksModalOpen": false, + "isNFTAutoDetectionModalViewed": false, + }, + "settings": { + "basicFunctionalityEnabled": true, + "hideZeroBalanceTokens": false, + "lockTime": 30000, + "primaryCurrency": "ETH", + "searchEngine": "DuckDuckGo", + "useBlockieIcon": true, + }, + "signatureRequest": "object", + "smartTransactions": { + "optInModalAppVersionSeen": null, + }, + "swaps": "object", + "transaction": "object", + "transactionMetrics": "object", + "user": { + "ambiguousAddressEntries": "object", + "appTheme": "os", + "backUpSeedphraseVisible": false, + "gasEducationCarouselSeen": false, + "initialScreen": "", + "isAuthChecked": false, + "loadingMsg": "", + "loadingSet": false, + "passwordSet": true, + "protectWalletModalVisible": false, + "seedphraseBackedUp": true, + "userLoggedIn": true, + }, + "wizard": { + "step": 1, + }, +} +`; diff --git a/app/util/sentry/utils.js b/app/util/sentry/utils.js index 598e79ea1d6..353e839970f 100644 --- a/app/util/sentry/utils.js +++ b/app/util/sentry/utils.js @@ -6,6 +6,213 @@ import DefaultPreference from 'react-native-default-preference'; import { regex } from '../regex'; import { AGREED, METRICS_OPT_IN } from '../../constants/storage'; import { isTest } from '../test/utils'; +import { store } from '../../store'; + +/** + * This symbol matches all object properties when used in a mask + */ +export const AllProperties = Symbol('*'); + +// This describes the subset of background controller state attached to errors +// sent to Sentry These properties have some potential to be useful for +// debugging, and they do not contain any identifiable information. +export const sentryStateMask = { + accounts: true, + alert: true, + bookmarks: true, + browser: true, + collectibles: true, + engine: { + backgroundState: { + AccountTrackerController: { + [AllProperties]: false, + }, + AccountsController: { + internalAccounts: { + [AllProperties]: false, + }, + }, + AddressBookController: { + [AllProperties]: false, + }, + ApprovalController: { + [AllProperties]: false, + }, + AssetsContractController: {}, + CurrencyRateController: { + currencyRates: true, + currentCurrency: true, + }, + GasFeeController: { + estimatedGasFeeTimeBounds: true, + gasEstimateType: true, + gasFeeEstimates: true, + gasFeeEstimatesByChainId: true, + }, + KeyringController: { + isUnlocked: true, + }, + LoggingController: { + [AllProperties]: false, + }, + NetworkController: { + networksMetadata: true, + providerConfig: { + chainId: true, + id: true, + nickname: true, + ticker: true, + type: true, + }, + }, + NftController: { + [AllProperties]: false, + }, + PPOMController: { + storageMetadata: [], + versionInfo: [], + }, + PermissionController: { + [AllProperties]: false, + }, + PhishingController: {}, + PreferencesController: { + disabledRpcMethodPreferences: true, + featureFlags: true, + isIpfsGatewayEnabled: true, + displayNftMedia: true, + useNftDetection: true, + useTokenDetection: true, + useTransactionSimulations: true, + }, + SignatureController: { + unapprovedMsgCount: true, + unapprovedPersonalMsgCount: true, + unapprovedTypedMessagesCount: true, + }, + SmartTransactionsController: { + smartTransactionsState: { + fees: { + approvalTxFees: true, + tradeTxFees: true, + }, + liveness: true, + userOptIn: true, + userOptInV2: true, + }, + }, + SnapController: { + [AllProperties]: false, + }, + SnapInterface: { + [AllProperties]: false, + }, + SnapsRegistry: { + [AllProperties]: false, + }, + SubjectMetadataController: { + [AllProperties]: false, + }, + SwapsController: { + swapsState: { + customGasPrice: true, + customMaxFeePerGas: true, + customMaxGas: true, + customMaxPriorityFeePerGas: true, + errorKey: true, + fetchParams: true, + quotesLastFetched: true, + quotesPollingLimitEnabled: true, + routeState: true, + saveFetchedQuotes: true, + selectedAggId: true, + swapsFeatureFlags: true, + swapsFeatureIsLive: true, + swapsQuotePrefetchingRefreshTime: true, + swapsQuoteRefreshTime: true, + swapsStxBatchStatusRefreshTime: true, + swapsStxGetTransactionsRefreshTime: true, + swapsStxMaxFeeMultiplier: true, + swapsUserFeeLevel: true, + }, + }, + TokenDetectionController: { + [AllProperties]: false, + }, + TokenListController: { + preventPollingOnNetworkRestart: true, + tokensChainsCache: { + [AllProperties]: false, + }, + }, + TokenRatesController: { + [AllProperties]: false, + }, + TokensController: { + allDetectedTokens: { + [AllProperties]: false, + }, + allIgnoredTokens: { + [AllProperties]: false, + }, + allTokens: { + [AllProperties]: false, + }, + }, + TransactionController: { + [AllProperties]: false, + }, + AuthenticationController: { + [AllProperties]: false, + }, + NotificationServicesController: { + isCheckingAccountsPresence: false, + isFeatureAnnouncementsEnabled: false, + isFetchingMetamaskNotifications: false, + isMetamaskNotificationsFeatureSeen: false, + isNotificationServicesEnabled: false, + isUpdatingMetamaskNotifications: false, + isUpdatingMetamaskNotificationsAccount: [], + metamaskNotificationsList: [], + metamaskNotificationsReadList: [], + subscriptionAccountsSeen: [], + }, + UserStorageController: { + isProfileSyncingEnabled: true, + isProfileSyncingUpdateLoading: false, + }, + }, + }, + experimentalSettings: true, + infuraAvailability: true, + inpageProvider: true, + legalNotices: true, + modals: true, + navigation: true, + networkOnboarded: true, + notification: true, + onboarding: true, + privacy: true, + rpcEvents: true, + sdk: true, + security: true, + settings: true, + smartTransactions: true, + user: { + appTheme: true, + backUpSeedphraseVisible: true, + gasEducationCarouselSeen: true, + initialScreen: true, + isAuthChecked: true, + loadingMsg: true, + loadingSet: true, + passwordSet: true, + protectWalletModalVisible: true, + seedphraseBackedUp: true, + userLoggedIn: true, + }, + wizard: true, +}; const METAMASK_ENVIRONMENT = process.env['METAMASK_ENVIRONMENT'] || 'local'; // eslint-disable-line dot-notation const METAMASK_BUILD_TYPE = process.env['METAMASK_BUILD_TYPE'] || 'main'; // eslint-disable-line dot-notation @@ -117,6 +324,53 @@ function removeSES(report) { } } +/** + * Return a "masked" copy of the given object. The returned object includes + * only the properties present in the mask. + * + * The mask is an object that mirrors the structure of the given object, except + * the only values are `true`, `false, a sub-mask, or the 'AllProperties" + * symbol. `true` implies the property should be included, and `false` will + * exclude it. A sub-mask implies the property should be further masked + * according to that sub-mask. The "AllProperties" symbol is used for objects + * with dynamic keys, and applies a rule (either `true`, `false`, or a + * sub-mask`) to every property in that object. + * + * If a property is excluded, its type is included instead. + * + * @param {object} objectToMask - The object to mask + * @param {{[key: string]: object | boolean}} mask - The mask to apply to the object + * @returns {object} - The masked object + */ +export function maskObject(objectToMask, mask = {}) { + if (!objectToMask) return {}; + let maskAllProperties = false; + if (Object.keys(mask).includes(AllProperties)) { + if (Object.keys(mask).length > 1) { + throw new Error('AllProperties mask key does not support sibling keys'); + } + maskAllProperties = true; + } + + return Object.keys(objectToMask).reduce((maskedObject, key) => { + const maskKey = maskAllProperties ? mask[AllProperties] : mask[key]; + const shouldPrintValue = maskKey === true; + const shouldIterateSubMask = + Boolean(maskKey) && typeof maskKey === 'object'; + const shouldPrintType = maskKey === undefined || maskKey === false; + if (shouldPrintValue) { + maskedObject[key] = objectToMask[key]; + } else if (shouldIterateSubMask) { + maskedObject[key] = maskObject(objectToMask[key], maskKey); + } else if (shouldPrintType) { + // Since typeof null is object, it is more valuable to us having the null instead of object + maskedObject[key] = + objectToMask[key] === null ? 'null' : typeof objectToMask[key]; + } + return maskedObject; + }, {}); +} + function rewriteReport(report) { try { // filter out SES from error stack trace @@ -134,6 +388,10 @@ function rewriteReport(report) { removeDeviceTimezone(report); // remove device name removeDeviceName(report); + + const appState = store?.getState(); + const maskedState = maskObject(appState, sentryStateMask); + report.contexts.appState = maskedState; } catch (err) { console.error('ENTER ERROR OF REPORT ', err); throw err; diff --git a/app/util/sentry/utils.test.ts b/app/util/sentry/utils.test.ts index 7b75edd20eb..59a49207651 100644 --- a/app/util/sentry/utils.test.ts +++ b/app/util/sentry/utils.test.ts @@ -4,7 +4,13 @@ import { deriveSentryEnvironment, excludeEvents, captureSentryFeedback, + maskObject, + sentryStateMask, + AllProperties, } from './utils'; +import { DeepPartial } from '../test/renderWithProvider'; +import { RootState } from '../../reducers'; +import { NetworkStatus } from '@metamask/network-controller'; jest.mock('@sentry/react-native', () => ({ ...jest.requireActual('@sentry/react-native'), @@ -13,7 +19,7 @@ jest.mock('@sentry/react-native', () => ({ const mockedCaptureUserFeedback = jest.mocked(captureUserFeedback); describe('deriveSentryEnvironment', () => { - test('returns production-flask for non-dev production environment and flask build type', async () => { + it('returns production-flask for non-dev production environment and flask build type', async () => { const METAMASK_ENVIRONMENT = 'production'; const METAMASK_BUILD_TYPE = 'flask'; const isDev = false; @@ -26,7 +32,7 @@ describe('deriveSentryEnvironment', () => { expect(env).toBe('production-flask'); }); - test('returns local-flask for non-dev undefined environment and flask build type', async () => { + it('returns local-flask for non-dev undefined environment and flask build type', async () => { const METAMASK_BUILD_TYPE = 'flask'; const isDev = false; @@ -34,7 +40,7 @@ describe('deriveSentryEnvironment', () => { expect(env).toBe('local-flask'); }); - test('returns debug-flask for non-dev flask environment debug build type', async () => { + it('returns debug-flask for non-dev flask environment debug build type', async () => { const METAMASK_BUILD_TYPE = 'flask'; const METAMASK_ENVIRONMENT = 'debug'; const isDev = false; @@ -47,7 +53,7 @@ describe('deriveSentryEnvironment', () => { expect(env).toBe('debug-flask'); }); - test('returns local for non-dev local environment and undefined build type', async () => { + it('returns local for non-dev local environment and undefined build type', async () => { const isDev = false; const METAMASK_ENVIRONMENT = 'local'; @@ -55,14 +61,14 @@ describe('deriveSentryEnvironment', () => { expect(env).toBe('local'); }); - test('returns local for non-dev with both undefined environment and build type', async () => { + it('returns local for non-dev with both undefined environment and build type', async () => { const isDev = false; const env = deriveSentryEnvironment(isDev); expect(env).toBe('local'); }); - test('returns production for non-dev production environment and undefined build type', async () => { + it('returns production for non-dev production environment and undefined build type', async () => { const METAMASK_ENVIRONMENT = 'production'; const isDev = false; @@ -70,14 +76,14 @@ describe('deriveSentryEnvironment', () => { expect(env).toBe('production'); }); - test('returns development for dev environment', async () => { + it('returns development for dev environment', async () => { const isDev = true; const env = deriveSentryEnvironment(isDev, '', ''); expect(env).toBe('development'); }); - test('returns development for dev environment regardless of environment and build type', async () => { + it('returns development for dev environment regardless of environment and build type', async () => { const isDev = true; const METAMASK_ENVIRONMENT = 'production'; const METAMASK_BUILD_TYPE = 'flask'; @@ -90,26 +96,26 @@ describe('deriveSentryEnvironment', () => { expect(env).toBe('development'); }); - test('return performance event Route Change', async () => { + it('returns performance event Route Change', async () => { const event = { transaction: 'Route Change' }; const eventExcluded = excludeEvents(event); expect(eventExcluded).toBe(null); }); - test('return performance event anything', async () => { + it('returns performance event anything', async () => { const event = { transaction: 'Login' }; const eventExcluded = excludeEvents(event); expect(eventExcluded).toBe(event); }); - test('return performance event null if empty', async () => { + it('returns performance event null if empty', async () => { const eventExcluded = excludeEvents(null); expect(eventExcluded).toBe(null); }); }); describe('captureSentryFeedback', () => { - it('should capture Sentry user feedback', async () => { + it('captures Sentry user feedback', async () => { const mockSentryId = '123'; const mockComments = 'Comment'; const expectedUserFeedback: UserFeedback = { @@ -126,4 +132,493 @@ describe('captureSentryFeedback', () => { expectedUserFeedback, ); }); + + describe('maskObject', () => { + const rootState: DeepPartial = { + legalNotices: { + newPrivacyPolicyToastClickedOrClosed: true, + newPrivacyPolicyToastShownDate: null, + }, + collectibles: { favorites: {}, isNftFetchingProgress: false }, + engine: { + backgroundState: { + AccountTrackerController: { + accounts: { + '0x6312c98831D74754F86dd4936668A13B7e9bA411': { + balance: '0x0', + }, + }, + accountsByChainId: { + '0x1': { + '0x6312c98831D74754F86dd4936668A13B7e9bA411': { + balance: '0x0', + }, + }, + }, + }, + AccountsController: { + internalAccounts: { + accounts: { + '1be55f5b-eba9-41a7-a9ed-a6a8274aca27': { + address: '0x6312c98831d74754f86dd4936668a13b7e9ba411', + id: '1be55f5b-eba9-41a7-a9ed-a6a8274aca27', + metadata: { + importTime: 1720023898234, + keyring: { + type: 'HD Key Tree', + }, + lastSelected: 1720023898236, + name: 'Account 1', + }, + methods: [ + 'personal_sign', + 'eth_sign', + 'eth_signTransaction', + 'eth_signTypedData_v1', + 'eth_signTypedData_v3', + 'eth_signTypedData_v4', + ], + options: {}, + type: 'eip155:eoa', + }, + }, + selectedAccount: '1be55f5b-eba9-41a7-a9ed-a6a8274aca27', + }, + }, + AddressBookController: { + addressBook: {}, + }, + ApprovalController: { + approvalFlows: [], + pendingApprovalCount: 0, + pendingApprovals: {}, + }, + AssetsContractController: {}, + CurrencyRateController: { + currencyRates: { + ETH: { + conversionDate: 1720196397083, + conversionRate: 298514, + usdConversionRate: 298514, + }, + }, + currentCurrency: 'usd', + }, + GasFeeController: { + estimatedGasFeeTimeBounds: {}, + gasEstimateType: 'none', + gasFeeEstimates: {}, + gasFeeEstimatesByChainId: {}, + nonRPCGasFeeApisDisabled: false, + }, + KeyringController: { + isUnlocked: true, + keyrings: [ + { + accounts: ['0x6312c98831d74754f86dd4936668a13b7e9ba411'], + type: 'HD Key Tree', + }, + ], + vault: '{"cipher":""}', + }, + LoggingController: { + logs: {}, + }, + NetworkController: { + networkConfigurations: {}, + networksMetadata: { + mainnet: { + EIPS: { + 1559: true, + }, + status: NetworkStatus.Available, + }, + }, + providerConfig: { + chainId: '0x1', + ticker: 'ETH', + type: 'mainnet', + }, + selectedNetworkClientId: 'mainnet', + }, + NftController: { + allNftContracts: {}, + allNfts: {}, + ignoredNfts: [], + }, + NftDetectionController: {}, + PermissionController: undefined, + PreferencesController: { + disabledRpcMethodPreferences: { + eth_sign: false, + }, + displayNftMedia: true, + featureFlags: {}, + identities: { + '0x6312c98831D74754F86dd4936668A13B7e9bA411': { + address: '0x6312c98831D74754F86dd4936668A13B7e9bA411', + importTime: 1720023898223, + name: 'Account 1', + }, + }, + ipfsGateway: 'https://cloudflare-ipfs.com/ipfs/', + isIpfsGatewayEnabled: true, + isMultiAccountBalancesEnabled: true, + lostIdentities: {}, + securityAlertsEnabled: true, + selectedAddress: '0x6312c98831D74754F86dd4936668A13B7e9bA411', + showIncomingTransactions: { + '0x1': true, + '0x13881': true, + '0x38': true, + '0x5': true, + '0x504': true, + '0x505': true, + '0x507': true, + '0x61': true, + '0x64': true, + '0x89': true, + '0xa': true, + '0xa869': true, + '0xa86a': true, + '0xaa36a7': true, + '0xaa37dc': true, + '0xe704': true, + '0xe705': true, + '0xe708': true, + '0xfa': true, + '0xfa2': true, + }, + showTestNetworks: false, + smartTransactionsOptInStatus: false, + useNftDetection: true, + useSafeChainsListValidation: true, + useTokenDetection: true, + useTransactionSimulations: true, + }, + SmartTransactionsController: { + smartTransactionsState: { + fees: {}, + feesByChainId: { + '0x1': {}, + '0xaa36a7': {}, + }, + liveness: true, + livenessByChainId: { + '0x1': true, + '0xaa36a7': true, + }, + smartTransactions: { + '0x1': [], + }, + }, + }, + }, + }, + privacy: {}, + bookmarks: {}, + browser: { + activeTab: null, + favicons: [], + history: [], + tabs: [], + visitedDappsByHostname: {}, + whitelist: [], + }, + modals: { + collectibleContractModalVisible: false, + dappTransactionModalVisible: false, + networkModalVisible: false, + receiveAsset: undefined, + receiveModalVisible: false, + shouldNetworkSwitchPopToWallet: true, + signMessageModalVisible: true, + }, + settings: { + basicFunctionalityEnabled: true, + hideZeroBalanceTokens: false, + lockTime: 30000, + primaryCurrency: 'ETH', + searchEngine: 'DuckDuckGo', + useBlockieIcon: true, + }, + alert: { + autodismiss: null, + content: null, + data: null, + isVisible: false, + }, + transaction: { + assetType: undefined, + ensRecipient: undefined, + id: undefined, + nonce: undefined, + paymentRequest: undefined, + proposedNonce: undefined, + readableValue: undefined, + selectedAsset: {}, + symbol: undefined, + transaction: { + data: undefined, + from: undefined, + gas: undefined, + gasPrice: undefined, + maxFeePerGas: undefined, + maxPriorityFeePerGas: undefined, + to: undefined, + value: undefined, + }, + transactionFromName: undefined, + transactionTo: undefined, + transactionToName: undefined, + transactionValue: undefined, + type: undefined, + warningGasPriceHigh: undefined, + }, + smartTransactions: { + optInModalAppVersionSeen: null, + }, + user: { + ambiguousAddressEntries: {}, + appTheme: 'os', + backUpSeedphraseVisible: false, + gasEducationCarouselSeen: false, + initialScreen: '', + isAuthChecked: false, + loadingMsg: '', + loadingSet: false, + passwordSet: true, + protectWalletModalVisible: false, + seedphraseBackedUp: true, + userLoggedIn: true, + }, + wizard: { + step: 1, + }, + onboarding: { + events: [], + }, + notification: { + notification: { + notificationsSettings: {}, + }, + notifications: [], + }, + swaps: { + '0x1': { + isLive: true, + }, + featureFlags: undefined, + hasOnboarded: true, + isLive: true, + }, + fiatOrders: { + activationKeys: [], + authenticationUrls: [], + customOrderIds: [], + getStartedAgg: false, + getStartedSell: false, + networks: [], + orders: [], + selectedPaymentMethodAgg: null, + selectedRegionAgg: null, + }, + infuraAvailability: { + isBlocked: false, + }, + navigation: { + currentBottomNavRoute: 'Wallet', + currentRoute: 'Login', + }, + networkOnboarded: { + networkOnboardedState: {}, + networkState: { + nativeToken: '', + networkType: '', + networkUrl: '', + showNetworkOnboarding: false, + }, + switchedNetwork: { + networkStatus: false, + networkUrl: '', + }, + }, + security: { + allowLoginWithRememberMe: false, + automaticSecurityChecksEnabled: false, + dataCollectionForMarketing: null, + hasUserSelectedAutomaticSecurityCheckOption: false, + isAutomaticSecurityChecksModalOpen: false, + isNFTAutoDetectionModalViewed: false, + }, + signatureRequest: { + securityAlertResponse: undefined, + }, + sdk: { + connections: {}, + approvedHosts: {}, + dappConnections: {}, + wc2Metadata: undefined, + }, + experimentalSettings: { + securityAlertsEnabled: true, + }, + rpcEvents: { + signingEvent: { + eventStage: 'idle', + rpcName: '', + }, + }, + accounts: { + reloadAccounts: false, + }, + inpageProvider: { + networkId: '1', + }, + transactionMetrics: { + metricsByTransactionId: {}, + }, + }; + + it('masks initial root state fixture', () => { + const maskedState = maskObject(rootState, sentryStateMask); + + expect(maskedState).toMatchSnapshot(); + }); + it('handles undefined mask', () => { + const maskedState = maskObject(rootState, undefined); + expect(maskedState).toEqual({ + accounts: 'object', + alert: 'object', + bookmarks: 'object', + browser: 'object', + collectibles: 'object', + engine: 'object', + experimentalSettings: 'object', + fiatOrders: 'object', + infuraAvailability: 'object', + inpageProvider: 'object', + legalNotices: 'object', + modals: 'object', + navigation: 'object', + networkOnboarded: 'object', + notification: 'object', + onboarding: 'object', + privacy: 'object', + rpcEvents: 'object', + sdk: 'object', + security: 'object', + settings: 'object', + signatureRequest: 'object', + smartTransactions: 'object', + swaps: 'object', + transaction: 'object', + transactionMetrics: 'object', + user: 'object', + wizard: 'object', + }); + }); + it('handles empty rootState', () => { + const maskedState = maskObject({}, sentryStateMask); + expect(maskedState).toEqual({}); + }); + it('handles rootState with more keys than what is defined in the mask', () => { + const maskedState = maskObject(rootState, { legalNotices: true }); + expect(maskedState).toEqual({ + legalNotices: { + newPrivacyPolicyToastClickedOrClosed: true, + newPrivacyPolicyToastShownDate: null, + }, + accounts: 'object', + alert: 'object', + bookmarks: 'object', + browser: 'object', + collectibles: 'object', + engine: 'object', + experimentalSettings: 'object', + fiatOrders: 'object', + infuraAvailability: 'object', + inpageProvider: 'object', + modals: 'object', + navigation: 'object', + networkOnboarded: 'object', + notification: 'object', + onboarding: 'object', + privacy: 'object', + rpcEvents: 'object', + sdk: 'object', + security: 'object', + settings: 'object', + signatureRequest: 'object', + smartTransactions: 'object', + swaps: 'object', + transaction: 'object', + transactionMetrics: 'object', + user: 'object', + wizard: 'object', + }); + }); + it('handles submask with { [AllProperties]: false, enabled: true }', () => { + const submask = { + [AllProperties]: false, + enabled: true, + }; + const maskedState = maskObject(rootState, submask); + expect(maskedState).toEqual({ + accounts: 'object', + alert: 'object', + bookmarks: 'object', + browser: 'object', + collectibles: 'object', + engine: 'object', + experimentalSettings: 'object', + fiatOrders: 'object', + infuraAvailability: 'object', + inpageProvider: 'object', + legalNotices: 'object', + modals: 'object', + navigation: 'object', + networkOnboarded: 'object', + notification: 'object', + onboarding: 'object', + privacy: 'object', + rpcEvents: 'object', + sdk: 'object', + security: 'object', + settings: 'object', + signatureRequest: 'object', + smartTransactions: 'object', + swaps: 'object', + transaction: 'object', + transactionMetrics: 'object', + user: 'object', + wizard: 'object', + }); + }); + }); + + it('handle root state with value null and mask false', () => { + const submask = { + SnapsController: { + [AllProperties]: false, + }, + }; + const maskedState = maskObject( + { + SnapsController: { + enabled: undefined, + data: null, + exampleObj: {}, + }, + }, + submask, + ); + expect(maskedState).toEqual({ + SnapsController: { + enabled: 'undefined', + data: 'null', + exampleObj: 'object', + }, + }); + }); }); diff --git a/app/util/test/initial-background-state.json b/app/util/test/initial-background-state.json index 0334ae604cc..94d866b69b4 100644 --- a/app/util/test/initial-background-state.json +++ b/app/util/test/initial-background-state.json @@ -1,12 +1,7 @@ { "KeyringController": { "isUnlocked": false, - "keyrings": [], - "vault": { - "cipher": "mock-cipher", - "iv": "mock-iv", - "lib": "original" - } + "keyrings": [] }, "LoggingController": { "logs": {} @@ -51,7 +46,6 @@ "storageMetadata": [], "versionInfo": [] }, - "CurrencyRateController": { "currentCurrency": "usd", "currencyRates": { @@ -70,7 +64,10 @@ "ticker": "ETH" }, "networksMetadata": { - "mainnet": { "EIPS": {}, "status": "unknown" } + "mainnet": { + "EIPS": {}, + "status": "unknown" + } }, "selectedNetworkClientId": "mainnet" }, @@ -136,7 +133,28 @@ }, "SmartTransactionsController": { "smartTransactionsState": { - "liveness": true + "smartTransactions": {}, + "userOptIn": false, + "userOptInV2": false, + "fees": { + "approvalTxFees": false, + "tradeTxFees": false + }, + "liveness": true, + "livenessByChainId": { + "mainnet": true, + "sepolia": true + }, + "feesByChainId": { + "mainnet": { + "approvalTxFees": false, + "tradeTxFees": false + }, + "sepolia": { + "approvalTxFees": false, + "tradeTxFees": false + } + } } }, "SnapController": { @@ -221,5 +239,24 @@ "unapprovedMsgCount": 0, "unapprovedPersonalMsgCount": 0, "unapprovedTypedMessagesCount": 0 + }, + "AuthenticationController": { + "isSignedIn": false + }, + "NotificationServicesController": { + "isCheckingAccountsPresence": false, + "isFeatureAnnouncementsEnabled": false, + "isFetchingMetamaskNotifications": false, + "isMetamaskNotificationsFeatureSeen": false, + "isNotificationServicesEnabled": false, + "isUpdatingMetamaskNotifications": false, + "isUpdatingMetamaskNotificationsAccount": [], + "metamaskNotificationsList": [], + "metamaskNotificationsReadList": [], + "subscriptionAccountsSeen": [] + }, + "UserStorageController": { + "isProfileSyncingEnabled": true, + "isProfileSyncingUpdateLoading": false } -} +} \ No newline at end of file diff --git a/app/util/test/initial-root-state.ts b/app/util/test/initial-root-state.ts index f39607964a7..d77178ba7bf 100644 --- a/app/util/test/initial-root-state.ts +++ b/app/util/test/initial-root-state.ts @@ -7,10 +7,10 @@ import { initialState as initialSmartTransactions } from '../../core/redux/slice import { initialState as transactionMetrics } from '../../core/redux/slices/transactionMetrics'; import initialBackgroundState from './initial-background-state.json'; -// Cast because TypeScript is incorrectly inferring the type of this JSON object -// TODO: Replace "any" with type -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const backgroundState: EngineState = initialBackgroundState as any; +// A cast is needed here because we use enums in some controllers, and TypeScript doesn't consider +// the string value of an enum as satisfying an enum type. +export const backgroundState: EngineState = + initialBackgroundState as unknown as EngineState; const initialRootState: RootState = { legalNotices: undefined, diff --git a/app/util/test/network-store.js b/app/util/test/network-store.js index b3870d9ed2a..1475cc40abe 100644 --- a/app/util/test/network-store.js +++ b/app/util/test/network-store.js @@ -46,18 +46,18 @@ class ReadOnlyNetworkStore { } // Async Storage - async getItem(key) { + async getString(key) { await this._initIfRequired(); const value = this._asyncState[key]; return value !== undefined ? value : null; } - async setItem(key, value) { + async set(key, value) { await this._initIfRequired(); this._asyncState[key] = value; } - async removeItem(key) { + async delete(key) { await this._initIfRequired(); delete this._asyncState[key]; } diff --git a/app/util/test/renderWithProvider.tsx b/app/util/test/renderWithProvider.tsx index 99591d09d64..44573945b44 100644 --- a/app/util/test/renderWithProvider.tsx +++ b/app/util/test/renderWithProvider.tsx @@ -15,11 +15,22 @@ import { import { mockTheme, ThemeContext } from '../theme'; import { Theme } from '../theme/models'; import configureStore from './configureStore'; +import { RootState } from '../../reducers'; +// DeepPartial is a generic type that recursively makes all properties of a given type T optional +export type DeepPartial = T extends (...args: unknown[]) => unknown + ? // If T is a function, return T as is. + T + : T extends (infer U)[] + ? // If T is an array, apply DeepPartial to its elements. + DeepPartial[] + : T extends object + ? // If T is an object, apply DeepPartial to each property of T. + { [K in keyof T]?: DeepPartial } + : // Otherwise, return T or undefined. + T | undefined; interface ProviderValues { - // TODO: Replace "any" with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - state?: Record; + state?: DeepPartial; theme?: Theme; } diff --git a/app/util/test/testSetup.js b/app/util/test/testSetup.js index 78c88376375..3b8b49ba068 100644 --- a/app/util/test/testSetup.js +++ b/app/util/test/testSetup.js @@ -20,6 +20,20 @@ jest.mock('react-native', () => { return originalModule; }); +/* + * NOTE: react-native-webview requires a jest mock starting on v12. + * More info on https://github.com/react-native-webview/react-native-webview/issues/2934 + */ +jest.mock('@metamask/react-native-webview', () => { + // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires + const { View } = require('react-native'); + const WebView = (props) => ; + + return { + WebView, + }; +}); + jest.mock('../../lib/snaps/preinstalled-snaps'); const mockFs = { diff --git a/app/util/transactions/index.js b/app/util/transactions/index.js index 670b57c606f..7ce75b038c7 100644 --- a/app/util/transactions/index.js +++ b/app/util/transactions/index.js @@ -217,7 +217,7 @@ export function isApprovalTransaction(data) { * Generates ERC20 approval data * * @param {object} opts - Object containing spender address, value and data - * @param {string} opts.spender - The address of the spender + * @param {string | null} opts.spender - The address of the spender * @param {string} opts.value - The amount of tokens to be approved or increased * @param {string} [opts.data] - The data of the transaction * @returns {String} - String containing the generated data, by default for approve method @@ -517,7 +517,7 @@ export function getEther(ticker) { * @param {object} config.addressBook - Object of address book entries * @param {string} config.chainId - network id * @param {string} config.toAddress - hex address of tx recipient - * @param {object} config.identities - object of identities + * @param {array} config.identities - array of accounts objects from AccountsController * @param {string} config.ensRecipient - name of ens recipient * @returns {string} - recipient name */ @@ -525,7 +525,7 @@ export function getTransactionToName({ addressBook, chainId, toAddress, - identities, + internalAccounts, ensRecipient, }) { if (ensRecipient) { @@ -535,11 +535,19 @@ export function getTransactionToName({ const networkAddressBook = addressBook[chainId]; const checksummedToAddress = toChecksumAddress(toAddress); + // Convert internalAccounts array to a map for quick lookup + const internalAccountsMap = internalAccounts.reduce((acc, account) => { + acc[toChecksumAddress(account.address)] = account; + return acc; + }, {}); + + const matchingAccount = internalAccountsMap[checksummedToAddress]; + const transactionToName = (networkAddressBook && networkAddressBook[checksummedToAddress] && networkAddressBook[checksummedToAddress].name) || - (identities[checksummedToAddress] && identities[checksummedToAddress].name); + (matchingAccount && matchingAccount.metadata.name); return transactionToName; } diff --git a/babel.config.js b/babel.config.js index 8029fd5651c..da1d15c592f 100644 --- a/babel.config.js +++ b/babel.config.js @@ -11,6 +11,14 @@ module.exports = { test: './node_modules/marked', plugins: [['@babel/plugin-transform-private-methods', { loose: true }]], }, + { + test: './node_modules/@metamask/profile-sync-controller', + plugins: [['@babel/plugin-transform-private-methods', { loose: true }]], + }, + { + test: './node_modules/@metamask/notification-services-controller', + plugins: [['@babel/plugin-transform-private-methods', { loose: true }]], + }, ], env: { production: { diff --git a/bitrise.yml b/bitrise.yml index b1ccc6af85c..c2b74ba7196 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -314,25 +314,6 @@ workflows: - issue_number: '$GITHUB_PR_NUMBER' - api_base_url: 'https://api.github.com' - update_comment_tag: '$GITHUB_PR_HASH' - - slack@3: - inputs: - - text: '${BITRISE_APP_TITLE} ${BITRISEIO_PIPELINE_TITLE} workflow notification' - - pretext: ':large_green_circle: *${BITRISEIO_PIPELINE_TITLE} succeeded!*' - - title: 'Commit #$COMMIT_SHORT_HASH $BRANCH_HEIGHT' - - title_link: https://github.com/${BITRISEIO_GIT_REPOSITORY_OWNER}/${BITRISEIO_GIT_REPOSITORY_SLUG}/commit/${COMMIT_SHORT_HASH} - - message: ${BITRISE_GIT_MESSAGE} - - fields: | - Branch|${BITRISE_GIT_BRANCH} - Workflow|${BITRISEIO_PIPELINE_TITLE} - Trigger|${WORKFLOW_TRIGGER} - Build|${BITRISE_BUILD_NUMBER} - - buttons: | - View app|${BITRISE_APP_URL} - View build|${BITRISEIO_PIPELINE_BUILD_URL} - View commit|https://github.com/${BITRISEIO_GIT_REPOSITORY_OWNER}/${BITRISEIO_GIT_REPOSITORY_SLUG}/commit/${COMMIT_SHORT_HASH} - - footer: 'Bitrise ${BITRISEIO_PIPELINE_TITLE} workflow notification' - - webhook_url: https://hooks.slack.com/services/${MM_SLACK_TOKEN}/${MM_SLACK_SECRET}/${MM_SLACK_ROOM} - # Send a Slack message when workflow fails notify_failure: before_run: @@ -372,27 +353,6 @@ workflows: - issue_number: '$GITHUB_PR_NUMBER' - api_base_url: 'https://api.github.com' - update_comment_tag: '$GITHUB_PR_HASH' - - slack@3: - is_always_run: true - run_if: .IsBuildFailed - inputs: - - text: '${BITRISE_APP_TITLE} ${BITRISE_TRIGGERED_WORKFLOW_TITLE} workflow notification' - - pretext_on_error: ':red_circle: *${BITRISE_TRIGGERED_WORKFLOW_TITLE} failed!*' - - title: 'Commit #$COMMIT_SHORT_HASH $BRANCH_HEIGHT' - - title_link: https://github.com/${BITRISEIO_GIT_REPOSITORY_OWNER}/${BITRISEIO_GIT_REPOSITORY_SLUG}/commit/${COMMIT_SHORT_HASH} - - message: ${BITRISE_GIT_MESSAGE} - - fields: | - Branch|${BITRISE_GIT_BRANCH} - Workflow|${BITRISE_TRIGGERED_WORKFLOW_TITLE} - Trigger|${WORKFLOW_TRIGGER} - Build|${BITRISE_BUILD_NUMBER} - - buttons: | - View app|${BITRISE_APP_URL} - View build|${BITRISE_BUILD_URL} - View commit|https://github.com/${BITRISEIO_GIT_REPOSITORY_OWNER}/${BITRISEIO_GIT_REPOSITORY_SLUG}/commit/${COMMIT_SHORT_HASH} - - footer: 'Bitrise ${BITRISE_TRIGGERED_WORKFLOW_TITLE} workflow notification' - - webhook_url: https://hooks.slack.com/services/${MM_SLACK_TOKEN}/${MM_SLACK_SECRET}/${MM_SLACK_ROOM} - # CI Steps ci_test: before_run: @@ -1471,16 +1431,16 @@ app: PROJECT_LOCATION_IOS: ios - opts: is_expand: false - VERSION_NAME: 7.24.4 + VERSION_NAME: 7.27.0 - opts: is_expand: false - VERSION_NUMBER: 1354 + VERSION_NUMBER: 1364 - opts: is_expand: false - FLASK_VERSION_NAME: 7.24.4 + FLASK_VERSION_NAME: 7.27.0 - opts: is_expand: false - FLASK_VERSION_NUMBER: 1354 + FLASK_VERSION_NUMBER: 1364 - opts: is_expand: false ANDROID_APK_LINK: '' diff --git a/e2e/fixtures/fixture-builder.js b/e2e/fixtures/fixture-builder.js index a39cce3ec0f..3e35dc36b61 100644 --- a/e2e/fixtures/fixture-builder.js +++ b/e2e/fixtures/fixture-builder.js @@ -2,7 +2,7 @@ import { getGanachePort } from './utils'; import { merge } from 'lodash'; -import { PopularNetworksList } from '../resources/networks.e2e'; +import { CustomNetworks, PopularNetworksList } from '../resources/networks.e2e'; const DAPP_URL = 'localhost'; /** @@ -208,11 +208,12 @@ class FixtureBuilder { AccountsController: { internalAccounts: { accounts: { - 1: { + '4d7a5e0b-b261-4aed-8126-43972b0fa0a1': { address: '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3', - id: '1', + id: '4d7a5e0b-b261-4aed-8126-43972b0fa0a1', metadata: { name: 'Account 1', + importTime: 1684232000456, keyring: { type: 'HD Key Tree', }, @@ -229,7 +230,7 @@ class FixtureBuilder { type: 'eip155:eoa', }, }, - selectedAccount: 1, + selectedAccount: '4d7a5e0b-b261-4aed-8126-43972b0fa0a1', }, }, PreferencesController: { @@ -719,6 +720,22 @@ class FixtureBuilder { return this; } + withSepoliaNetwork() { + const fixtures = this.fixture.state.engine.backgroundState; + + fixtures.NetworkController = { + isCustomNetwork: true, + providerConfig: { + type: 'rpc', + chainId: CustomNetworks.Sepolia.providerConfig.chainId, + rpcUrl: CustomNetworks.Sepolia.providerConfig.rpcTarget, + nickname: CustomNetworks.Sepolia.providerConfig.nickname, + ticker: CustomNetworks.Sepolia.providerConfig.ticker, + }, + }; + return this; + } + withPopularNetworks() { const fixtures = this.fixture.state.engine.backgroundState; const networkIDs = {}; // Object to store network configurations diff --git a/e2e/pages/Send/TransactionConfirmView.js b/e2e/pages/Send/TransactionConfirmView.js index 93ce056d05c..73c218a64e4 100644 --- a/e2e/pages/Send/TransactionConfirmView.js +++ b/e2e/pages/Send/TransactionConfirmView.js @@ -1,6 +1,5 @@ import Gestures from '../../utils/Gestures'; import Matchers from '../../utils/Matchers'; - import { EditGasViewSelectorsText, EditGasViewSelectorsIDs, @@ -40,7 +39,7 @@ class TransactionConfirmationView { } get transactionAmount() { - return Matchers.getElementByText( + return Matchers.getElementByID( TransactionConfirmViewSelectorsIDs.COMFIRM_TXN_AMOUNT, ); } @@ -73,7 +72,7 @@ class TransactionConfirmationView { } async tapConfirmButton() { - await Gestures.waitAndTap(await this.confirmButton); + await Gestures.waitAndTap(this.confirmButton); } async tapCancelButton() { @@ -81,6 +80,8 @@ class TransactionConfirmationView { } async tapEstimatedGasLink() { + await Gestures.swipe(this.transactionAmount, 'up', 'fast'); + await Gestures.waitAndTap(this.estimatedGasLink); } diff --git a/e2e/pages/Settings/SecurityAndPrivacy/SecurityAndPrivacyView.js b/e2e/pages/Settings/SecurityAndPrivacy/SecurityAndPrivacyView.js index ad71a190f6a..328a946c89b 100644 --- a/e2e/pages/Settings/SecurityAndPrivacy/SecurityAndPrivacyView.js +++ b/e2e/pages/Settings/SecurityAndPrivacy/SecurityAndPrivacyView.js @@ -13,6 +13,12 @@ class SecurityAndPrivacy { ); } + get revealSecretRecoveryPhraseButton() { + return Matchers.getElementByID( + SecurityPrivacyViewSelectorsIDs.REVEAL_SEED_BUTTON, + ); + } + get deleteWalletButton() { return device.getPlatform() === 'ios' ? Matchers.getElementByID(SECURITY_PRIVACY_DELETE_WALLET_BUTTON) @@ -70,6 +76,10 @@ class SecurityAndPrivacy { ); } + async tapRevealSecretRecoveryPhraseButton() { + await Gestures.waitAndTap(this.revealSecretRecoveryPhraseButton); + } + async tapChangePasswordButton() { await Gestures.waitAndTap(this.changePasswordButton); } diff --git a/e2e/pages/WalletView.js b/e2e/pages/WalletView.js deleted file mode 100644 index 4dcaa4df454..00000000000 --- a/e2e/pages/WalletView.js +++ /dev/null @@ -1,144 +0,0 @@ -import TestHelpers from '../helpers'; - -import { - IMPORT_NFT_BUTTON_ID, - IMPORT_TOKEN_BUTTON_ID, - NAVBAR_NETWORK_BUTTON, - NAVBAR_NETWORK_TEXT, - NFT_TAB_CONTAINER_ID, - WALLET_ACCOUNT_ICON, - WALLET_ACCOUNT_NAME_LABEL_INPUT, -} from '../../wdio/screen-objects/testIDs/Screens/WalletView.testIds'; -import { - WalletViewSelectorsIDs, - WalletViewSelectorsText, -} from '../selectors/wallet/WalletView.selectors'; -import { CommonSelectorsText } from '../selectors/Common.selectors'; -import Gestures from '../utils/Gestures'; -import Matchers from '../utils/Matchers'; - -class WalletView { - get container() { - return Matchers.getElementByID(WalletViewSelectorsIDs.WALLET_CONTAINER); - } - - get portfolioButton() { - return Matchers.getElementByID(WalletViewSelectorsIDs.PORTFOLIO_BUTTON); - } - - get tokenDetectionLinkButton() { - return Matchers.getElementByID( - WalletViewSelectorsIDs.WALLET_TOKEN_DETECTION_LINK_BUTTON, - ); - } - - async tapOKAlertButton() { - await TestHelpers.tapAlertWithButton(CommonSelectorsText.OK_ALERT_BUTTON); // system alert. - } - - async tapOnToken(token) { - await TestHelpers.waitAndTapText( - token || WalletViewSelectorsText.DEFAULT_TOKEN, - ); - } - - async tapIdenticon() { - await TestHelpers.waitAndTap(WALLET_ACCOUNT_ICON); - } - - async tapNetworksButtonOnNavBar() { - await TestHelpers.waitAndTap(NAVBAR_NETWORK_BUTTON); - } - - async isConnectedNetwork(value) { - await TestHelpers.checkIfHasText(NAVBAR_NETWORK_TEXT, value); - } - - async tapNftTab() { - await TestHelpers.tapByText(WalletViewSelectorsText.NFTS_TAB); - } - - async tapTokensTab() { - await TestHelpers.tapByText(WalletViewSelectorsText.TOKENS_TAB); - } - - async scrollDownOnNFTsTab() { - await TestHelpers.swipe(NFT_TAB_CONTAINER_ID, 'up', 'slow', 0.6); - } - - async scrollUpOnNFTsTab() { - await TestHelpers.swipe(NFT_TAB_CONTAINER_ID, 'down', 'slow', 0.6); - } - - async tapImportNFTButton() { - await TestHelpers.tap(IMPORT_NFT_BUTTON_ID); - } - - async tapImportTokensButton() { - await TestHelpers.delay(2000); - if (device.getPlatform() === 'android') { - await TestHelpers.tapByText(WalletViewSelectorsText.IMPORT_TOKENS); - } else { - await TestHelpers.tap(IMPORT_TOKEN_BUTTON_ID); - } - } - - async tapOnNFTInWallet(nftName) { - await TestHelpers.tapByText(nftName); - } - - async removeTokenFromWallet(token) { - await element(by.text(token)).longPress(); - await TestHelpers.tapByText(WalletViewSelectorsText.HIDE_TOKENS); - } - - async isVisible() { - if (!device.getPlatform() === 'android') { - // Check that we are on the wallet screen - await TestHelpers.checkIfExists(WalletViewSelectorsIDs.WALLET_CONTAINER); - } - } - - async isTokenVisibleInWallet(tokenName) { - await TestHelpers.checkIfElementByTextIsVisible(tokenName); - } - - async tokenIsNotVisibleInWallet(tokenName) { - await TestHelpers.checkIfElementWithTextIsNotVisible(tokenName); - } - - async isNFTVisibleInWallet(nftName) { - await TestHelpers.checkIfElementByTextIsVisible(nftName); - } - - async isNFTNameVisible(nftName) { - await TestHelpers.checkIfElementHasString( - WalletViewSelectorsIDs.NFT_CONTAINER, - nftName, - ); - } - - async isNetworkNameVisible(networkName) { - await TestHelpers.checkIfElementHasString( - WalletViewSelectorsIDs.NETWORK_NAME, - networkName, - ); - } - - async isAccountNameCorrect(accountName) { - await TestHelpers.checkIfElementHasString( - WALLET_ACCOUNT_NAME_LABEL_INPUT, - accountName, - ); - } - - async tapNewTokensFound() { - await Gestures.waitAndTap(this.tokenDetectionLinkButton); - } - - async tapPortfolio() { - await Gestures.waitAndTap(this.portfolioButton); - } -} - -export default new WalletView(); diff --git a/e2e/pages/modals/SecurityQuizModal.js b/e2e/pages/modals/SecurityQuizModal.js new file mode 100644 index 00000000000..1b2ff396d63 --- /dev/null +++ b/e2e/pages/modals/SecurityQuizModal.js @@ -0,0 +1,218 @@ +import { + SecurityQuizGetStartedModalSelectorsIDs, + SecurityQuizGetStartedModalSelectorsText, + SecurityQuestionOneModelSelectorsIDs, + SecurityQuizQuestionOneModalSelectorsText, + SecurityQuestionTwoModelSelectorsIDs, + SecurityQuizQuestionTwoModalSelectorsText, +} from '../../selectors/Modals/SecurityQuizModal.selectors'; +import Matchers from '../../utils/Matchers'; +import Gestures from '../../utils/Gestures'; + +class SecurityQuizModal { + get getStartedContainer() { + return Matchers.getElementByID( + SecurityQuizGetStartedModalSelectorsIDs.QUIZ_GET_STARTED_CONTAINER, + ); + } + + get getStartedDismiss() { + return Matchers.getElementByID( + SecurityQuizGetStartedModalSelectorsIDs.QUIZ_GET_STARTED_DISMISS, + ); + } + + get modalIntroduction() { + return Matchers.getElementByText( + SecurityQuizGetStartedModalSelectorsText.QUIZ_INTRODUCTION, + ); + } + + get getStartedButton() { + return Matchers.getElementByID( + SecurityQuizGetStartedModalSelectorsIDs.QUIZ_GET_STARTED_BUTTON, + ); + } + + get questionOneContainer() { + return Matchers.getElementByID( + SecurityQuestionOneModelSelectorsIDs.QUIZ_QUESTION_ONE_CONTAINER, + ); + } + + get questionOneDismiss() { + return Matchers.getElementByID( + SecurityQuestionOneModelSelectorsIDs.QUIZ_QUESTION_ONE_DISMISS, + ); + } + + get getQuizQuestionOne() { + return Matchers.getElementByText( + SecurityQuizQuestionOneModalSelectorsText.QUIZ_QUESTION_ONE, + ); + } + + get questionOneWrongAnswer() { + return Matchers.getElementByID( + SecurityQuestionOneModelSelectorsIDs.QUIZ_QUESTION_ONE_WRONG_ANSWER, + ); + } + + get questionOneWrongAnswerResponseTitle() { + return Matchers.getElementByText( + SecurityQuizQuestionOneModalSelectorsText.QUIZ_QUESTION_ONE_WRONG_ANSWER_RESPONSE_TITLE, + ); + } + + get questionOneWrongAnswerResponseDescription() { + return Matchers.getElementByText( + SecurityQuizQuestionOneModalSelectorsText.QUIZ_QUESTION_ONE_WRONG_ANSWER_RESPONSE_DESCRIPTION, + ); + } + + get questionOneWrongAnswerTryAgainButton() { + return Matchers.getElementByID( + SecurityQuestionOneModelSelectorsIDs.QUIZ_QUESTION_ONE_WRONG_ANSWER_TRY_AGAIN_BUTTON, + ); + } + + get questionOneRightAnswerButton() { + return Matchers.getElementByID( + SecurityQuestionOneModelSelectorsIDs.QUIZ_QUESTION_ONE_RIGHT_ANSWER, + ); + } + + get questionOneRightAnswerResponseDescription() { + return Matchers.getElementByID( + SecurityQuizQuestionOneModalSelectorsText.QUIZ_QUESTION_ONE_RIGHT_ANSWER_RESPONSE_DESCRIPTION, + ); + } + + get questionOneRightAnswerResponseTitle() { + return Matchers.getElementByText( + SecurityQuizQuestionOneModalSelectorsText.QUIZ_QUESTION_ONE_RIGHT_ANSWER_RESPONSE_TITLE, + ); + } + + get questionOneRightContinueButton() { + return Matchers.getElementByID( + SecurityQuestionOneModelSelectorsIDs.QUIZ_QUESTION_ONE_RIGHT_CONTINUE, + ); + } + + get questionTwoContainer() { + return Matchers.getElementByID( + SecurityQuestionTwoModelSelectorsIDs.QUIZ_QUESTION_TWO_CONTAINER, + ); + } + + get questionTwoDismiss() { + return Matchers.getElementByID( + SecurityQuestionTwoModelSelectorsIDs.QUIZ_QUESTION_TWO_DISMISS, + ); + } + + get getQuizQuestionTwo() { + return Matchers.getElementByText( + SecurityQuizQuestionTwoModalSelectorsText.QUIZ_QUESTION_TWO, + ); + } + + get questionTwoWrongAnswer() { + return Matchers.getElementByID( + SecurityQuestionTwoModelSelectorsIDs.QUIZ_QUESTION_TWO_WRONG_ANSWER, + ); + } + + get questionTwoWrongAnswerResponseTitle() { + return Matchers.getElementByText( + SecurityQuizQuestionTwoModalSelectorsText.QUIZ_QUESTION_TWO_WRONG_ANSWER_RESPONSE_TITLE, + ); + } + + get questionTwoWrongAnswerResponseDescription() { + return Matchers.getElementByText( + SecurityQuizQuestionTwoModalSelectorsText.QUIZ_QUESTION_TWO_WRONG_ANSWER_RESPONSE_DESCRIPTION, + ); + } + + get questionTwoWrongAnswerTryAgainButton() { + return Matchers.getElementByID( + SecurityQuestionTwoModelSelectorsIDs.QUIZ_QUESTION_TWO_WRONG_ANSWER_TRY_AGAIN_BUTTON, + ); + } + + get questionTwoRightAnswer() { + return Matchers.getElementByID( + SecurityQuestionTwoModelSelectorsIDs.QUIZ_QUESTION_TWO_RIGHT_ANSWER, + ); + } + + get questionTwoRightAnswerResponseTitle() { + return Matchers.getElementByText( + SecurityQuizQuestionTwoModalSelectorsText.QUIZ_QUESTION_TWO_RIGHT_ANSWER_RESPONSE_TITLE, + ); + } + + get questionTwoRightAnswerResponseDescription() { + return Matchers.getElementByText( + SecurityQuizQuestionTwoModalSelectorsText.QUIZ_QUESTION_TWO_RIGHT_ANSWER_RESPONSE_DESCRIPTION, + ); + } + + get questionTwoRightContinueButton() { + return Matchers.getElementByID( + SecurityQuestionTwoModelSelectorsIDs.QUIZ_QUESTION_TWO_RIGHT_CONTINUE, + ); + } + + async tapGetStartedDismiss() { + await Gestures.waitAndTap(this.getStartedDismiss); + } + + async tapGetStartedButton() { + await Gestures.waitAndTap(this.getStartedButton); + } + + async tapQuestionOneDismiss() { + await Gestures.waitAndTap(this.questionOneDismiss); + } + + async tapQuestionOneWrongAnswerButton() { + await Gestures.waitAndTap(this.questionOneWrongAnswer); + } + + async tapQuestionOneWrongAnswerTryAgainButton() { + await Gestures.waitAndTap(this.questionOneWrongAnswerTryAgainButton); + } + + async tapQuestionOneRightAnswerButton() { + await Gestures.waitAndTap(this.questionOneRightAnswerButton); + } + + async tapQuestionOneContinueButton() { + await Gestures.waitAndTap(this.questionOneRightContinueButton); + } + + async tapQuestionTwoDismiss() { + await Gestures.waitAndTap(this.questionTwoDismiss); + } + + async tapQuestionTwoWrongAnswerButton() { + await Gestures.waitAndTap(this.questionTwoWrongAnswer); + } + + async tapQuestionTwoWrongAnswerTryAgainButton() { + await Gestures.waitAndTap(this.questionTwoWrongAnswerTryAgainButton); + } + + async tapQuestionTwoRightAnswerButton() { + await Gestures.waitAndTap(this.questionTwoRightAnswer); + } + + async tapQuestionTwoContinueButton() { + await Gestures.waitAndTap(this.questionTwoRightContinueButton); + } +} + +export default new SecurityQuizModal(); diff --git a/e2e/pages/wallet/WalletView.js b/e2e/pages/wallet/WalletView.js new file mode 100644 index 00000000000..49d8cb692d7 --- /dev/null +++ b/e2e/pages/wallet/WalletView.js @@ -0,0 +1,145 @@ +import { + WalletViewSelectorsIDs, + WalletViewSelectorsText, +} from '../../selectors/wallet/WalletView.selectors'; +import { CommonSelectorsText } from '../../selectors/Common.selectors'; +import Gestures from '../../utils/Gestures'; +import Matchers from '../../utils/Matchers'; + +class WalletView { + get container() { + return Matchers.getElementByID(WalletViewSelectorsIDs.WALLET_CONTAINER); + } + + get portfolioButton() { + return Matchers.getElementByID(WalletViewSelectorsIDs.PORTFOLIO_BUTTON); + } + + get tokenDetectionLinkButton() { + return Matchers.getElementByID( + WalletViewSelectorsIDs.WALLET_TOKEN_DETECTION_LINK_BUTTON, + ); + } + + get okAlertButton() { + return Matchers.getElementByText(CommonSelectorsText.OK_ALERT_BUTTON); + } + + get accountIcon() { + return Matchers.getElementByID(WalletViewSelectorsIDs.ACCOUNT_ICON); + } + + get navbarNetworkText() { + return Matchers.getElementByID(WalletViewSelectorsIDs.NAVBAR_NETWORK_TEXT); + } + + get navbarNetworkButton() { + return Matchers.getElementByID( + WalletViewSelectorsIDs.NAVBAR_NETWORK_BUTTON, + ); + } + + get nftTab() { + return Matchers.getElementByText(WalletViewSelectorsText.NFTS_TAB); + } + + get nftTabContainer() { + return Matchers.getElementByID(WalletViewSelectorsIDs.NFT_TAB_CONTAINER); + } + + get importNFTButton() { + return Matchers.getElementByID(WalletViewSelectorsIDs.IMPORT_NFT_BUTTON); + } + + get importTokensButton() { + return device.getPlatform() === 'android' + ? Matchers.getElementByText(WalletViewSelectorsText.IMPORT_TOKENS) + : Matchers.getElementByID(WalletViewSelectorsIDs.IMPORT_TOKEN_BUTTON); + } + + get networkName() { + return Matchers.getElementByID(WalletViewSelectorsIDs.NETWORK_NAME); + } + + get totalBalance() { + return Matchers.getElementByID(WalletViewSelectorsIDs.TOTAL_BALANCE_TEXT); + } + + get accountName() { + return Matchers.getElementByID( + WalletViewSelectorsIDs.ACCOUNT_NAME_LABEL_TEXT, + ); + } + + get hideTokensLabel() { + return Matchers.getElementByText(WalletViewSelectorsText.HIDE_TOKENS); + } + + async tapOKAlertButton() { + await Gestures.waitAndTap(this.okAlertButton); + } + + async tapOnToken(token) { + const element = Matchers.getElementByText( + token || WalletViewSelectorsText.DEFAULT_TOKEN, + ); + await Gestures.waitAndTap(element); + } + + async tapIdenticon() { + await Gestures.waitAndTap(this.accountIcon); + } + + async tapNetworksButtonOnNavBar() { + await Gestures.waitAndTap(this.navbarNetworkButton); + } + + async tapNftTab() { + await Gestures.waitAndTap(this.nftTab); + } + + async scrollDownOnNFTsTab() { + await Gestures.swipe(this.nftTabContainer, 'up', 'slow', 0.6); + } + + async scrollUpOnNFTsTab() { + await Gestures.swipe(this.nftTabContainer, 'down', 'slow', 0.6); + } + + async tapImportNFTButton() { + await Gestures.waitAndTap(this.importNFTButton); + } + + async tapImportTokensButton() { + await Gestures.waitAndTap(this.importTokensButton); + } + + async tapOnNFTInWallet(nftName) { + const elem = Matchers.getElementByText(nftName); + await Gestures.waitAndTap(elem); + } + + async removeTokenFromWallet(token) { + const elem = Matchers.getElementByText(token); + await Gestures.tapAndLongPress(elem); + await Gestures.waitAndTap(this.hideTokensLabel); + } + + async tokenInWallet(tokenName) { + return Matchers.getElementByText(tokenName); + } + + async nftInWallet(nftName) { + return Matchers.getElementByText(nftName); + } + + async tapNewTokensFound() { + await Gestures.waitAndTap(this.tokenDetectionLinkButton); + } + + async tapPortfolio() { + await Gestures.waitAndTap(this.portfolioButton); + } +} + +export default new WalletView(); diff --git a/e2e/resources/networks.e2e.js b/e2e/resources/networks.e2e.js index 24e43f00a87..a348bded2e6 100644 --- a/e2e/resources/networks.e2e.js +++ b/e2e/resources/networks.e2e.js @@ -3,7 +3,6 @@ import { toHex } from '@metamask/controller-utils'; /* eslint-disable @typescript-eslint/no-require-imports, import/no-commonjs */ const InfuraKey = process.env.MM_INFURA_PROJECT_ID; const infuraProjectId = InfuraKey === 'null' ? '' : InfuraKey; -const TENDERLY_KEY = process.env.TENDERLY_NETWORK_ID; const PopularNetworksList = { Avalanche: { @@ -85,9 +84,9 @@ const CustomNetworks = { }, Sepolia: { providerConfig: { - type: 'mainnet', - chainId: '11155111', - rpcTarget: 'https://sepolia.infura.io/v3/', + type: 'rpc', + chainId: '0xaa36a7', + rpcTarget: `https://sepolia.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161`, nickname: 'Sepolia', ticker: 'SepoliaETH', }, @@ -98,7 +97,7 @@ const CustomNetworks = { providerConfig: { type: 'rpc', chainId: '0x1', - rpcUrl: `https://rpc.tenderly.co/fork/${TENDERLY_KEY}`, + rpcUrl: `https://rpc.tenderly.co/fork/bbfe5a2e-2426-4512-a5f8-46ce85fe9ad6`, nickname: 'Tenderly', ticker: 'ETH', }, diff --git a/e2e/selectors/Common.selectors.js b/e2e/selectors/Common.selectors.js index 797c8df89f2..1e98c4c559e 100644 --- a/e2e/selectors/Common.selectors.js +++ b/e2e/selectors/Common.selectors.js @@ -14,6 +14,7 @@ export const CommonSelectorsIDs = { FOX_SCREEN: 'fox-screen', NAVBAR_TITLE_NETWORKS_TEXT: 'navbar-title-networks', STATUS_CONFIRMED: 'status-confirmed-text', + ANDROID_PROGRESS_BAR: 'android.widget.ProgressBar', }; export const CommonSelectorsText = { diff --git a/e2e/selectors/Modals/AccountActionsModal.selectors.js b/e2e/selectors/Modals/AccountActionsModal.selectors.js new file mode 100644 index 00000000000..f958cf5dd77 --- /dev/null +++ b/e2e/selectors/Modals/AccountActionsModal.selectors.js @@ -0,0 +1,8 @@ +// eslint-disable-next-line import/prefer-default-export +export const AccountActionsModalSelectorsIDs = { + EDIT_ACCOUNT: 'edit-account-action', + VIEW_ETHERSCAN: 'view-etherscan-action', + SHARE_ADDRESS: 'share-address-action', + SHOW_PRIVATE_KEY: 'show-private-key-action', + REMOVE_HARDWARE_ACCOUNT: 'remove-hardward-account-action', +}; diff --git a/e2e/selectors/Modals/SecurityQuizModal.selectors.js b/e2e/selectors/Modals/SecurityQuizModal.selectors.js new file mode 100644 index 00000000000..4f0e3017227 --- /dev/null +++ b/e2e/selectors/Modals/SecurityQuizModal.selectors.js @@ -0,0 +1,55 @@ +import enContent from '../../../locales/languages/en.json'; + +export const SecurityQuizGetStartedModalSelectorsIDs = { + QUIZ_GET_STARTED_CONTAINER: 'quiz-get-started-modal', + QUIZ_GET_STARTED_BUTTON: 'quiz-get-started-button', + QUIZ_GET_STARTED_DISMISS: 'quiz-get-started-dismiss-button', +}; + +export const SecurityQuizGetStartedModalSelectorsText = { + QUIZ_INTRODUCTION: enContent.srp_security_quiz.introduction, +}; + +export const SecurityQuestionOneModelSelectorsIDs = { + QUIZ_QUESTION_ONE_CONTAINER: 'quiz-question-one-modal', + QUIZ_QUESTION_ONE_DISMISS: 'quiz-question-one-dismiss-button', + QUIZ_QUESTION_ONE_WRONG_ANSWER: 'quiz-question-one-wrong-answer', + QUIZ_QUESTION_ONE_RIGHT_ANSWER: 'quiz-question-one-right-answer', + QUIZ_QUESTION_ONE_WRONG_ANSWER_TRY_AGAIN_BUTTON: + 'quiz-question-one-wrong-answer-try-again', + QUIZ_QUESTION_ONE_RIGHT_CONTINUE: 'quiz-question-one-right-continue', +}; + +export const SecurityQuizQuestionOneModalSelectorsText = { + QUIZ_QUESTION_ONE: enContent.srp_security_quiz.question_one.question, + QUIZ_QUESTION_ONE_RIGHT_ANSWER_RESPONSE_DESCRIPTION: + enContent.srp_security_quiz.question_one.right_answer_description, + QUIZ_QUESTION_ONE_RIGHT_ANSWER_RESPONSE_TITLE: + enContent.srp_security_quiz.question_one.right_answer_title, + QUIZ_QUESTION_ONE_WRONG_ANSWER_RESPONSE_DESCRIPTION: + enContent.srp_security_quiz.question_one.wrong_answer_description, + QUIZ_QUESTION_ONE_WRONG_ANSWER_RESPONSE_TITLE: + enContent.srp_security_quiz.question_one.wrong_answer_title, +}; + +export const SecurityQuestionTwoModelSelectorsIDs = { + QUIZ_QUESTION_TWO_CONTAINER: 'quiz-question-two-modal', + QUIZ_QUESTION_TWO_DISMISS: 'quiz-question-two-dismiss-button', + QUIZ_QUESTION_TWO_WRONG_ANSWER: 'quiz-question-two-wrong-answer', + QUIZ_QUESTION_TWO_RIGHT_ANSWER: 'quiz-question-two-right-answer', + QUIZ_QUESTION_TWO_WRONG_ANSWER_TRY_AGAIN_BUTTON: + 'quiz-question-two-wrong-answer-try-again', + QUIZ_QUESTION_TWO_RIGHT_CONTINUE: 'quiz-question-two-right-continue', +}; + +export const SecurityQuizQuestionTwoModalSelectorsText = { + QUIZ_QUESTION_TWO: enContent.srp_security_quiz.question_two.question, + QUIZ_QUESTION_TWO_RIGHT_ANSWER_RESPONSE_DESCRIPTION: + enContent.srp_security_quiz.question_two.right_answer_description, + QUIZ_QUESTION_TWO_RIGHT_ANSWER_RESPONSE_TITLE: + enContent.srp_security_quiz.question_two.right_answer_title, + QUIZ_QUESTION_TWO_WRONG_ANSWER_RESPONSE_DESCRIPTION: + enContent.srp_security_quiz.question_two.wrong_answer_description, + QUIZ_QUESTION_TWO_WRONG_ANSWER_RESPONSE_TITLE: + enContent.srp_security_quiz.question_two.wrong_answer_title, +}; diff --git a/e2e/selectors/TransactionConfirmView.selectors.js b/e2e/selectors/TransactionConfirmView.selectors.js index e6cd712b7a4..2ca017253e5 100644 --- a/e2e/selectors/TransactionConfirmView.selectors.js +++ b/e2e/selectors/TransactionConfirmView.selectors.js @@ -1,6 +1,5 @@ import enContent from '../../locales/languages/en.json'; -// eslint-disable-next-line import/prefer-default-export export const TransactionConfirmViewSelectorsIDs = { COMFIRM_TXN_AMOUNT: 'confirm-txn-amount', TRANSACTION_VIEW_CONTAINER_ID: 'txn-confirm-screen', diff --git a/e2e/selectors/wallet/WalletView.selectors.js b/e2e/selectors/wallet/WalletView.selectors.js index 13641f781c1..add1b222bdc 100644 --- a/e2e/selectors/wallet/WalletView.selectors.js +++ b/e2e/selectors/wallet/WalletView.selectors.js @@ -10,6 +10,19 @@ export const WalletViewSelectorsIDs = { PORTFOLIO_BUTTON: 'portfolio-button', TOTAL_BALANCE_TEXT: 'total-balance-text', STAKE_BUTTON: 'stake-button', + IMPORT_NFT_BUTTON: 'import-collectible-button', + IMPORT_TOKEN_BUTTON: 'import-token-button', + NAVBAR_NETWORK_BUTTON: 'open-networks-button', + NAVBAR_NETWORK_TEXT: 'open-networks-text', + NFT_TAB_CONTAINER: 'collectible-contracts', + ACCOUNT_ICON: 'account-picker', + ACCOUNT_NAME_LABEL_INPUT: 'account-label-text-input', + ACCOUNT_NAME_LABEL_TEXT: 'account-label', + TOKENS_CONTAINER: 'tokens', + ACCOUNT_OVERVIEW: 'account-overview', + ACCOUNT_ACTIONS: 'main-wallet-account-actions', + ACCOUNT_COPY_BUTTON: 'wallet-account-copy-button', + ACCOUNT_ADDRESS: 'wallet-account-address', }; export const WalletViewSelectorsText = { diff --git a/e2e/specs/accounts/create-wallet-account.spec.js b/e2e/specs/accounts/create-wallet-account.spec.js index 431ffb86914..b13d5b2a07f 100644 --- a/e2e/specs/accounts/create-wallet-account.spec.js +++ b/e2e/specs/accounts/create-wallet-account.spec.js @@ -1,6 +1,6 @@ 'use strict'; import { SmokeAccounts } from '../../tags'; -import WalletView from '../../pages/WalletView'; +import WalletView from '../../pages/wallet/WalletView'; import { importWalletWithRecoveryPhrase } from '../../viewHelper'; import AccountListView from '../../pages/AccountListView'; diff --git a/e2e/specs/accounts/import-wallet-account.spec.js b/e2e/specs/accounts/import-wallet-account.spec.js index 381ff25ee85..b61ff03f3e6 100644 --- a/e2e/specs/accounts/import-wallet-account.spec.js +++ b/e2e/specs/accounts/import-wallet-account.spec.js @@ -1,9 +1,10 @@ 'use strict'; import { SmokeAccounts } from '../../tags'; -import WalletView from '../../pages/WalletView'; +import WalletView from '../../pages/wallet/WalletView'; import { importWalletWithRecoveryPhrase } from '../../viewHelper'; import AccountListView from '../../pages/AccountListView'; import ImportAccountView from '../../pages/ImportAccountView'; +import Assertions from '../../utils/Assertions'; describe(SmokeAccounts('Import account via private to wallet'), () => { // This key is for testing private key import only @@ -33,7 +34,10 @@ describe(SmokeAccounts('Import account via private to wallet'), () => { await ImportAccountView.isImportSuccessSreenVisible(); await ImportAccountView.tapCloseButtonOnImportSuccess(); await AccountListView.swipeToDimssAccountsModal(); - await WalletView.isVisible(); - await WalletView.isAccountNameCorrect('Account 3'); + await Assertions.checkIfVisible(WalletView.container); + await Assertions.checkIfElementNotToHaveText( + WalletView.accountName, + 'Account 1', + ); }); }); diff --git a/e2e/specs/accounts/secret_recovery_phrase_quiz.spec.js b/e2e/specs/accounts/secret_recovery_phrase_quiz.spec.js new file mode 100644 index 00000000000..fa8cf82654b --- /dev/null +++ b/e2e/specs/accounts/secret_recovery_phrase_quiz.spec.js @@ -0,0 +1,92 @@ +'use strict'; + +import { SmokeAccounts } from '../../tags.js'; +import TestHelpers from '../../helpers.js'; +import { loginToApp } from '../../viewHelper.js'; +import TabBarComponent from '../../pages/TabBarComponent.js'; +import SettingsView from '../../pages/Settings/SettingsView.js'; +import SecurityAndPrivacy from '../../pages/Settings/SecurityAndPrivacy/SecurityAndPrivacyView.js'; +import SecurityQuizModal from '../../pages/modals/SecurityQuizModal.js'; +import { + SecurityQuizQuestionOneModalSelectorsText, + SecurityQuizQuestionTwoModalSelectorsText, +} from '../../selectors/Modals/SecurityQuizModal.selectors'; +import { + withFixtures, + defaultGanacheOptions, +} from '../../fixtures/fixture-helper'; +import FixtureBuilder from '../../fixtures/fixture-builder.js'; +import Assertions from '../../utils/Assertions'; + +describe(SmokeAccounts('Secret Recovery Phrase Quiz'), () => { + beforeAll(async () => { + await TestHelpers.reverseServerPort(); + }); + + it('completes quiz after correcting wrong answers', async () => { + await withFixtures( + { + fixture: new FixtureBuilder().withGanacheNetwork().build(), + restartDevice: true, + ganacheOptions: defaultGanacheOptions, + }, + async () => { + await loginToApp(); + + // Navigate to Reveal Secret Recovery Phrase screen + await TabBarComponent.tapSettings(); + await SettingsView.tapSecurityAndPrivacy(); + await SecurityAndPrivacy.tapRevealSecretRecoveryPhraseButton(); + + // Start the quiz + await SecurityQuizModal.tapGetStartedButton(); + + // Question 1 + await Assertions.checkIfVisible(SecurityQuizModal.getQuizQuestionOne); + await SecurityQuizModal.tapQuestionOneWrongAnswerButton(); + await Assertions.checkIfTextIsDisplayed( + SecurityQuizQuestionOneModalSelectorsText.QUIZ_QUESTION_ONE_WRONG_ANSWER_RESPONSE_TITLE, + ); + await Assertions.checkIfTextIsDisplayed( + SecurityQuizQuestionOneModalSelectorsText.QUIZ_QUESTION_ONE_WRONG_ANSWER_RESPONSE_DESCRIPTION, + ); + await SecurityQuizModal.tapQuestionOneWrongAnswerTryAgainButton(); + await Assertions.checkIfVisible(SecurityQuizModal.getQuizQuestionOne); + await SecurityQuizModal.tapQuestionOneRightAnswerButton(); + await Assertions.checkIfTextIsDisplayed( + SecurityQuizQuestionOneModalSelectorsText.QUIZ_QUESTION_ONE_RIGHT_ANSWER_RESPONSE_TITLE, + ); + await Assertions.checkIfTextIsDisplayed( + SecurityQuizQuestionOneModalSelectorsText.QUIZ_QUESTION_ONE_RIGHT_ANSWER_RESPONSE_DESCRIPTION, + ); + await SecurityQuizModal.tapQuestionOneContinueButton(); + await Assertions.checkIfNotVisible( + SecurityQuizModal.questionOneRightContinueButton, + ); + + // // // Question 2 + await Assertions.checkIfVisible(SecurityQuizModal.getQuizQuestionTwo); + await SecurityQuizModal.tapQuestionTwoWrongAnswerButton(); + await Assertions.checkIfTextIsDisplayed( + SecurityQuizQuestionTwoModalSelectorsText.QUIZ_QUESTION_TWO_WRONG_ANSWER_RESPONSE_TITLE, + ); + await Assertions.checkIfTextIsDisplayed( + SecurityQuizQuestionTwoModalSelectorsText.QUIZ_QUESTION_TWO_WRONG_ANSWER_RESPONSE_DESCRIPTION, + ); + await SecurityQuizModal.tapQuestionTwoWrongAnswerTryAgainButton(); + await Assertions.checkIfVisible(SecurityQuizModal.getQuizQuestionTwo); + await SecurityQuizModal.tapQuestionTwoRightAnswerButton(); + await Assertions.checkIfTextIsDisplayed( + SecurityQuizQuestionTwoModalSelectorsText.QUIZ_QUESTION_TWO_RIGHT_ANSWER_RESPONSE_TITLE, + ); + await Assertions.checkIfTextIsDisplayed( + SecurityQuizQuestionTwoModalSelectorsText.QUIZ_QUESTION_TWO_RIGHT_ANSWER_RESPONSE_DESCRIPTION, + ); + await SecurityQuizModal.tapQuestionTwoContinueButton(); + await Assertions.checkIfNotVisible( + SecurityQuizModal.questionTwoRightContinueButton, + ); + }, + ); + }); +}); diff --git a/e2e/specs/assets/import-tokens.spec.js b/e2e/specs/assets/import-tokens.spec.js index f85b160f879..511a9adf1d9 100644 --- a/e2e/specs/assets/import-tokens.spec.js +++ b/e2e/specs/assets/import-tokens.spec.js @@ -1,7 +1,7 @@ 'use strict'; import { SmokeAssets } from '../../tags'; import TestHelpers from '../../helpers'; -import WalletView from '../../pages/WalletView'; +import WalletView from '../../pages/wallet/WalletView'; import ImportTokensView from '../../pages/ImportTokensView'; import FixtureBuilder from '../../fixtures/fixture-builder'; import { @@ -13,6 +13,7 @@ import { getFixturesServerPort } from '../../fixtures/utils'; import FixtureServer from '../../fixtures/fixture-server'; import { loginToApp } from '../../viewHelper'; import ConfirmAddAssetView from '../../pages/ConfirmAddAsset'; +import Assertions from '../../utils/Assertions'; const fixtureServer = new FixtureServer(); @@ -46,9 +47,9 @@ describe(SmokeAssets('Import Tokens'), () => { await ConfirmAddAssetView.tapOnConfirmButton(); - await WalletView.isVisible(); + await Assertions.checkIfVisible(WalletView.container); await TestHelpers.delay(8000); // to prevent flakey behavior in bitrise - await WalletView.isTokenVisibleInWallet('0 SNX'); + await Assertions.checkIfVisible(WalletView.tokenInWallet('0 SNX')); }); it('should cancel add a token via token autocomplete', async () => { @@ -72,6 +73,6 @@ describe(SmokeAssets('Import Tokens'), () => { it('should hide token from Wallet view', async () => { await WalletView.removeTokenFromWallet('0 SNX'); await TestHelpers.delay(1500); - await WalletView.tokenIsNotVisibleInWallet('SNX'); + await Assertions.checkIfNotVisible(WalletView.tokenInWallet('SNX')); }); }); diff --git a/e2e/specs/assets/nft-detection-modal.spec.js b/e2e/specs/assets/nft-detection-modal.spec.js index 77893af2d3d..fe6cb9d0b21 100644 --- a/e2e/specs/assets/nft-detection-modal.spec.js +++ b/e2e/specs/assets/nft-detection-modal.spec.js @@ -1,5 +1,5 @@ 'use strict'; -import WalletView from '../../pages/WalletView'; +import WalletView from '../../pages/wallet/WalletView'; import { loginToApp } from '../../viewHelper'; import FixtureBuilder from '../../fixtures/fixture-builder'; import { @@ -44,7 +44,7 @@ describe(SmokeAssets('NFT Detection Modal'), () => { await Assertions.checkIfVisible(NftDetectionModal.container); await NftDetectionModal.tapCancelButton(); // Check that we are on the wallet screen - await WalletView.isVisible(); + await Assertions.checkIfVisible(WalletView.container); // Go to NFTs tab and check that the banner is visible await WalletView.tapNftTab(); @@ -80,7 +80,7 @@ describe(SmokeAssets('NFT Detection Modal'), () => { await Assertions.checkIfVisible(NftDetectionModal.container); await NftDetectionModal.tapAllowButton(); // Check that we are on the wallet screen - await WalletView.isVisible(); + await Assertions.checkIfVisible(WalletView.container); // Go to NFTs tab and check that the banner is NOT visible await WalletView.tapNftTab(); diff --git a/e2e/specs/assets/token-detection-import-all.spec.js b/e2e/specs/assets/token-detection-import-all.spec.js index b13a2c42593..b9b400055db 100644 --- a/e2e/specs/assets/token-detection-import-all.spec.js +++ b/e2e/specs/assets/token-detection-import-all.spec.js @@ -1,38 +1,44 @@ 'use strict'; -import { importWalletWithRecoveryPhrase } from '../../viewHelper'; +import { loginToApp } from '../../viewHelper'; import { SmokeAssets } from '../../tags'; -import WalletView from '../../pages/WalletView'; +import WalletView from '../../pages/wallet/WalletView'; import DetectedTokensView from '../../pages/wallet/DetectedTokensView'; import Assertions from '../../utils/Assertions'; +import TestHelpers from '../../helpers'; +import FixtureBuilder from '../../fixtures/fixture-builder'; +import { withFixtures } from '../../fixtures/fixture-helper'; describe(SmokeAssets('Import all tokens detected'), () => { beforeAll(async () => { - await device.launchApp(); - }); - - it('should import wallet and go to the wallet view', async () => { - await importWalletWithRecoveryPhrase(); + jest.setTimeout(150000); + await TestHelpers.reverseServerPort(); }); it('should import all tokens detected', async () => { - await WalletView.tapNewTokensFound(); - await DetectedTokensView.tapImport(); - }); - - it('should land on wallet view after tokens detected', async () => { - await WalletView.isVisible(); - }); + await withFixtures( + { + fixture: new FixtureBuilder().build(), + restartDevice: true, + }, + async () => { + await loginToApp(); + await WalletView.tapNewTokensFound(); + await DetectedTokensView.tapImport(); + await Assertions.checkIfVisible(WalletView.container); - it('should show toast alert for tokens imported', async () => { - try { - await Assertions.checkIfTextIsDisplayed('Imported Tokens', 6000); - await Assertions.checkIfTextIsDisplayed( - 'Successfully imported WETH', - 6000, - ); - } catch (e) { - // eslint-disable-next-line no-console - console.log(`Toast message is slow to appear or did not appear: ${e}`); - } + try { + await Assertions.checkIfTextIsDisplayed('Imported Tokens', 6000); + await Assertions.checkIfTextIsDisplayed( + 'Successfully imported WETH', + 6000, + ); + } catch (e) { + // eslint-disable-next-line no-console + console.log( + `Toast message is slow to appear or did not appear: ${e}`, + ); + } + }, + ); }); }); diff --git a/e2e/specs/confirmations/advanced-gas-fees.spec.js b/e2e/specs/confirmations/advanced-gas-fees.spec.js index 480bb443f23..415179891b8 100644 --- a/e2e/specs/confirmations/advanced-gas-fees.spec.js +++ b/e2e/specs/confirmations/advanced-gas-fees.spec.js @@ -1,6 +1,6 @@ 'use strict'; import { SmokeConfirmations } from '../../tags'; -import WalletView from '../../pages/WalletView'; +import WalletView from '../../pages/wallet/WalletView'; import AmountView from '../../pages/Send/AmountView'; import SendView from '../../pages/Send/SendView'; import TransactionConfirmationView from '../../pages/Send/TransactionConfirmView'; @@ -15,9 +15,6 @@ import WalletActionsModal from '../../pages/modals/WalletActionsModal'; import TestHelpers from '../../helpers'; import Assertions from '../../utils/Assertions'; import { AmountViewSelectorsText } from '../../selectors/SendFlow/AmountView.selectors'; -import NetworkListModal from '../../pages/modals/NetworkListModal'; -import NetworkEducationModal from '../../pages/modals/NetworkEducationModal'; -import { CustomNetworks } from '../../resources/networks.e2e'; const VALID_ADDRESS = '0xebe6CcB6B55e1d094d9c58980Bc10Fed69932cAb'; @@ -30,7 +27,7 @@ describe(SmokeConfirmations('Advanced Gas Fees and Priority Tests'), () => { it('should edit priority gas settings and send ETH', async () => { await withFixtures( { - fixture: new FixtureBuilder().withGanacheNetwork().build(), + fixture: new FixtureBuilder().withSepoliaNetwork().build(), restartDevice: true, ganacheOptions: defaultGanacheOptions, }, @@ -38,16 +35,7 @@ describe(SmokeConfirmations('Advanced Gas Fees and Priority Tests'), () => { await loginToApp(); // Check that we are on the wallet screen - await WalletView.isVisible(); - - await WalletView.tapNetworksButtonOnNavBar(); - await TestHelpers.delay(2000); - await NetworkListModal.changeNetworkTo( - CustomNetworks.Sepolia.providerConfig.nickname, - ); - await Assertions.checkIfVisible(NetworkEducationModal.container); - await NetworkEducationModal.tapGotItButton(); - await Assertions.checkIfNotVisible(NetworkEducationModal.container); + await Assertions.checkIfVisible(WalletView.container); //Tap send Icon await TestHelpers.delay(2000); @@ -68,13 +56,13 @@ describe(SmokeConfirmations('Advanced Gas Fees and Priority Tests'), () => { // Check that we are on the confirm view await Assertions.checkIfVisible( - await TransactionConfirmationView.transactionViewContainer, + TransactionConfirmationView.transactionViewContainer, ); // Check different gas options await TransactionConfirmationView.tapEstimatedGasLink(); await Assertions.checkIfVisible( - await TransactionConfirmationView.editPriorityFeeSheetContainer, + TransactionConfirmationView.editPriorityFeeSheetContainer, ); await TransactionConfirmationView.tapLowPriorityGasOption(); await TransactionConfirmationView.tapAdvancedOptionsPriorityGasOption(); @@ -86,13 +74,13 @@ describe(SmokeConfirmations('Advanced Gas Fees and Priority Tests'), () => { await TransactionConfirmationView.tapAdvancedOptionsPriorityGasOption(); await TransactionConfirmationView.tapMaxPriorityFeeSaveButton(); await Assertions.checkIfVisible( - await TransactionConfirmationView.transactionViewContainer, + TransactionConfirmationView.transactionViewContainer, ); // Tap on the send button await TransactionConfirmationView.tapConfirmButton(); // Check that we are on the wallet screen - await WalletView.isVisible(); + await Assertions.checkIfVisible(WalletView.container); }, ); }); diff --git a/e2e/specs/confirmations/approve-custom-erc20.spec.js b/e2e/specs/confirmations/approve-custom-erc20.spec.js index ec65fd3c22d..e149bb45ed2 100644 --- a/e2e/specs/confirmations/approve-custom-erc20.spec.js +++ b/e2e/specs/confirmations/approve-custom-erc20.spec.js @@ -55,7 +55,7 @@ describe(SmokeConfirmations('ERC20 tokens'), () => { await ContractApprovalModal.inputCustomAmount('2'); // Assert that custom token amount is shown - await Assertions.checkIfHasText( + await Assertions.checkIfElementToHaveText( ContractApprovalModal.approveTokenAmount, '2', ); diff --git a/e2e/specs/confirmations/increase-allowance-erc20.spec.js b/e2e/specs/confirmations/increase-allowance-erc20.spec.js index d69dc1be120..aa76f2f7a07 100644 --- a/e2e/specs/confirmations/increase-allowance-erc20.spec.js +++ b/e2e/specs/confirmations/increase-allowance-erc20.spec.js @@ -54,7 +54,7 @@ describe(SmokeConfirmations('ERC20 - Increase Allowance'), () => { await ContractApprovalModal.inputCustomAmount('2'); // Assert that custom token amount is shown - await Assertions.checkIfHasText( + await Assertions.checkIfElementToHaveText( ContractApprovalModal.approveTokenAmount, '2', ); diff --git a/e2e/specs/networks/add-custom-rpc.spec.js b/e2e/specs/networks/add-custom-rpc.spec.js index 51f490d9b1e..9082b97bbd8 100644 --- a/e2e/specs/networks/add-custom-rpc.spec.js +++ b/e2e/specs/networks/add-custom-rpc.spec.js @@ -2,7 +2,8 @@ import TestHelpers from '../../helpers'; import { Regression } from '../../tags'; import NetworkView from '../../pages/Settings/NetworksView'; -import WalletView from '../../pages/WalletView'; +import WalletView from '../../pages/wallet/WalletView'; +import ToastModal from '../../pages/modals/ToastModal'; import SettingsView from '../../pages/Settings/SettingsView'; import NetworkListModal from '../../pages/modals/NetworkListModal'; import NetworkEducationModal from '../../pages/modals/NetworkEducationModal'; @@ -78,10 +79,6 @@ describe(Regression('Custom RPC Tests'), () => { await NetworkApprovalModal.tapApproveButton(); await Assertions.checkIfVisible(NetworkAddedModal.switchNetwork); await NetworkAddedModal.tapSwitchToNetwork(); - await WalletView.isVisible(); - await WalletView.isNetworkNameVisible( - CustomNetworks.Gnosis.providerConfig.nickname, - ); }); it('should dismiss network education modal', async () => { @@ -92,6 +89,11 @@ describe(Regression('Custom RPC Tests'), () => { ); await NetworkEducationModal.tapGotItButton(); await Assertions.checkIfNotVisible(NetworkEducationModal.container); + await Assertions.checkIfVisible(WalletView.container); + await Assertions.checkIfElementToHaveText( + WalletView.navbarNetworkText, + CustomNetworks.Gnosis.providerConfig.nickname, + ); }); it('should validate that Gnosis is added to network list', async () => { @@ -118,11 +120,12 @@ describe(Regression('Custom RPC Tests'), () => { ); await NetworkEducationModal.tapGotItButton(); await Assertions.checkIfNotVisible(NetworkEducationModal.container); - await WalletView.isVisible(); + await Assertions.checkIfVisible(WalletView.container); }); it('should switch back to Gnosis', async () => { - await WalletView.isNetworkNameVisible( + await Assertions.checkIfElementToHaveText( + WalletView.navbarNetworkText, CustomNetworks.Sepolia.providerConfig.nickname, ); await WalletView.tapNetworksButtonOnNavBar(); @@ -133,15 +136,23 @@ describe(Regression('Custom RPC Tests'), () => { CustomNetworks.Gnosis.providerConfig.nickname, true, ); - await WalletView.isVisible(); - await WalletView.isNetworkNameVisible( + await Assertions.checkIfVisible(WalletView.container); + await Assertions.checkIfElementToHaveText( + WalletView.navbarNetworkText, CustomNetworks.Gnosis.providerConfig.nickname, ); await Assertions.checkIfNotVisible(NetworkEducationModal.container); + + try { + await Assertions.checkIfVisible(ToastModal.container); + await Assertions.checkIfNotVisible(ToastModal.container); + } catch { + // eslint-disable-next-line no-console + console.log('Toast is not visible'); + } }); it('should go to settings networks and remove xDai network', async () => { - await TestHelpers.delay(3000); await TabBarComponent.tapSettings(); await SettingsView.tapNetworks(); await Assertions.checkIfVisible(NetworkView.networkContainer); @@ -151,10 +162,20 @@ describe(Regression('Custom RPC Tests'), () => { await NetworkView.longPressToRemoveNetwork( CustomNetworks.Gnosis.providerConfig.nickname, ); // Tap on Gnosis to remove network - await TestHelpers.delay(3000); await NetworkEducationModal.tapGotItButton(); + + try { + await Assertions.checkIfVisible(ToastModal.container); + await Assertions.checkIfNotVisible(ToastModal.container); + } catch { + // eslint-disable-next-line no-console + console.log('Toast is not visible'); + } await TabBarComponent.tapWallet(); - await WalletView.isVisible(); - await WalletView.isNetworkNameVisible(MAINNET); + await Assertions.checkIfVisible(WalletView.container); + await Assertions.checkIfElementToHaveText( + WalletView.navbarNetworkText, + MAINNET, + ); }); }); diff --git a/e2e/specs/networks/connect-test-network.spec.js b/e2e/specs/networks/connect-test-network.spec.js index 4b483da9fb9..b5711acfa9e 100644 --- a/e2e/specs/networks/connect-test-network.spec.js +++ b/e2e/specs/networks/connect-test-network.spec.js @@ -1,6 +1,6 @@ import { Regression } from '../../tags'; import { loginToApp } from '../../viewHelper'; -import WalletView from '../../pages/WalletView'; +import WalletView from '../../pages/wallet/WalletView'; import NetworkListModal from '../../pages/modals/NetworkListModal'; import NetworkEducationModal from '../../pages/modals/NetworkEducationModal'; import Assertions from '../../utils/Assertions'; @@ -53,8 +53,9 @@ describe(Regression('Connect to a Test Network'), () => { ); await NetworkEducationModal.tapGotItButton(); await Assertions.checkIfNotVisible(NetworkEducationModal.container); - await WalletView.isVisible(); - await WalletView.isConnectedNetwork( + await Assertions.checkIfVisible(WalletView.container); + await Assertions.checkIfElementToHaveText( + WalletView.navbarNetworkText, CustomNetworks.Sepolia.providerConfig.nickname, ); }); @@ -75,13 +76,19 @@ describe(Regression('Connect to a Test Network'), () => { ); await NetworkEducationModal.tapGotItButton(); await Assertions.checkIfNotVisible(NetworkEducationModal.container); - await WalletView.isVisible(); - await WalletView.isConnectedNetwork(ETHEREUM); + await Assertions.checkIfVisible(WalletView.container); + await Assertions.checkIfElementToHaveText( + WalletView.navbarNetworkText, + ETHEREUM, + ); }); it('should toggle off the Test Network switch', async () => { await WalletView.tapNetworksButtonOnNavBar(); await Assertions.checkIfVisible(NetworkListModal.networkScroll); + await Assertions.checkIfTextIsDisplayed( + CustomNetworks.Sepolia.providerConfig.nickname, + ); await Assertions.checkIfToggleIsOn(NetworkListModal.testNetToggle); await NetworkListModal.tapTestNetworkSwitch(); await Assertions.checkIfToggleIsOff(NetworkListModal.testNetToggle); diff --git a/e2e/specs/onboarding/onboarding-wizard-opt-in.spec.js b/e2e/specs/onboarding/onboarding-wizard-opt-in.spec.js index 4ae17644ef0..9c393ffb29b 100644 --- a/e2e/specs/onboarding/onboarding-wizard-opt-in.spec.js +++ b/e2e/specs/onboarding/onboarding-wizard-opt-in.spec.js @@ -7,7 +7,7 @@ import OnboardingView from '../../pages/Onboarding/OnboardingView'; import OnboardingCarouselView from '../../pages/Onboarding/OnboardingCarouselView'; import MetaMetricsOptIn from '../../pages/Onboarding/MetaMetricsOptInView'; import OnboardingSuccessView from '../../pages/Onboarding/OnboardingSuccessView'; -import WalletView from '../../pages/WalletView'; +import WalletView from '../../pages/wallet/WalletView'; import EnableAutomaticSecurityChecksView from '../../pages/EnableAutomaticSecurityChecksView'; import SettingsView from '../../pages/Settings/SettingsView'; import SecurityAndPrivacy from '../../pages/Settings/SecurityAndPrivacy/SecurityAndPrivacyView'; @@ -55,8 +55,6 @@ describe( await SkipAccountSecurityModal.tapIUnderstandCheckBox(); await SkipAccountSecurityModal.tapSkipButton(); await OnboardingSuccessView.tapDone(); - - await WalletView.isVisible(); }); it('Should dismiss Automatic Security checks screen', async () => { @@ -118,7 +116,7 @@ describe( await ProtectYourWalletModal.tapRemindMeLaterButton(); await SkipAccountSecurityModal.tapIUnderstandCheckBox(); await SkipAccountSecurityModal.tapSkipButton(); - await WalletView.isVisible(); + await Assertions.checkIfVisible(WalletView.container); }); it('should check that metametrics is enabled in settings', async () => { @@ -142,7 +140,7 @@ describe( await TestHelpers.delay(4500); await LoginView.isVisible(); await LoginView.enterPassword(PASSWORD); - await WalletView.isVisible(); + await Assertions.checkIfVisible(WalletView.container); }); it('should dismiss the onboarding wizard after logging in', async () => { diff --git a/e2e/specs/permission-systems/permission-system-delete-wallet.spec.js b/e2e/specs/permission-systems/permission-system-delete-wallet.spec.js index 2ba00efa9a9..0686d0f76a9 100644 --- a/e2e/specs/permission-systems/permission-system-delete-wallet.spec.js +++ b/e2e/specs/permission-systems/permission-system-delete-wallet.spec.js @@ -4,7 +4,7 @@ import { Regression } from '../../tags'; import OnboardingView from '../../pages/Onboarding/OnboardingView'; import ProtectYourWalletView from '../../pages/Onboarding/ProtectYourWalletView'; import CreatePasswordView from '../../pages/Onboarding/CreatePasswordView'; -import WalletView from '../../pages/WalletView'; +import WalletView from '../../pages/wallet/WalletView'; import Browser from '../../pages/Browser/BrowserView'; import SettingsView from '../../pages/Settings/SettingsView'; import TabBarComponent from '../../pages/TabBarComponent'; @@ -89,7 +89,7 @@ describe( await SkipAccountSecurityModal.tapIUnderstandCheckBox(); await SkipAccountSecurityModal.tapSkipButton(); await OnboardingSuccessView.tapDone(); - await WalletView.isVisible(); + await Assertions.checkIfVisible(WalletView.container); await ProtectYourWalletModal.tapRemindMeLaterButton(); await SkipAccountSecurityModal.tapIUnderstandCheckBox(); await SkipAccountSecurityModal.tapSkipButton(); diff --git a/e2e/specs/quarantine/add-edit-custom-eth-mainnet.failing.js b/e2e/specs/quarantine/add-edit-custom-eth-mainnet.failing.js index 9a991067a83..a36a4dc5503 100644 --- a/e2e/specs/quarantine/add-edit-custom-eth-mainnet.failing.js +++ b/e2e/specs/quarantine/add-edit-custom-eth-mainnet.failing.js @@ -9,7 +9,7 @@ import CreatePasswordView from '../../pages/Onboarding/CreatePasswordView'; import OnboardingSuccessView from '../../pages/Onboarding/OnboardingSuccessView'; import EnableAutomaticSecurityChecksView from '../../pages/EnableAutomaticSecurityChecksView'; import SkipAccountSecurityModal from '../../pages/modals/SkipAccountSecurityModal'; -import WalletView from '../../pages/WalletView'; +import WalletView from '../../pages/wallet/WalletView'; import ProtectYourWalletView from '../../pages/Onboarding/ProtectYourWalletView'; import NetworksView from '../../pages/Settings/NetworksView'; import Accounts from '../../../wdio/helpers/Accounts'; @@ -71,7 +71,8 @@ describe(Regression('Add custom default ETH Mainnet'), () => { }); it('should show custom default ETH Mainnet as active', async () => { - await WalletView.isNetworkNameVisible( + await Assertions.checkIfElementToHaveText( + WalletView.navbarNetworkText, CustomNetworks.EthereumMainCustom.providerConfig.nickname, ); }); @@ -107,8 +108,9 @@ describe(Regression('Add custom default ETH Mainnet'), () => { CustomNetworks.EthereumMainCustom.providerConfig.rpcUrlAlt, ); await NetworksView.tapSave(); - await WalletView.isConnectedNetwork( - CustomNetworks.EthereumMainCustom.providerConfig.nickname, + await Assertions.checkIfElementToHaveText( + WalletView.navbarNetworkText, + CustomNetworks.Sepolia.providerConfig.nickname, ); }); @@ -118,8 +120,9 @@ describe(Regression('Add custom default ETH Mainnet'), () => { CustomNetworks.EthereumMainCustom.providerConfig.nickname, true, //setting this made this step work for iOS ); - await WalletView.isConnectedNetwork( - CustomNetworks.EthereumMainCustom.providerConfig.nickname, + await Assertions.checkIfElementToHaveText( + WalletView.navbarNetworkText, + CustomNetworks.Sepolia.providerConfig.nickname, ); }); }); diff --git a/e2e/specs/quarantine/contract-nickname.failing.js b/e2e/specs/quarantine/contract-nickname.failing.js index cb055fe407a..c670e028718 100644 --- a/e2e/specs/quarantine/contract-nickname.failing.js +++ b/e2e/specs/quarantine/contract-nickname.failing.js @@ -6,7 +6,7 @@ import OnboardingCarouselView from '../../pages/Onboarding/OnboardingCarouselVie import ContractNickNameView from '../../pages/ContractNickNameView'; import SendView from '../../pages/Send/SendView'; import MetaMetricsOptIn from '../../pages/Onboarding/MetaMetricsOptInView'; -import WalletView from '../../pages/WalletView'; +import WalletView from '../../pages/wallet/WalletView'; import EnableAutomaticSecurityChecksView from '../../pages/EnableAutomaticSecurityChecksView'; import LoginView from '../../pages/LoginView'; @@ -64,7 +64,7 @@ describe('Adding Contract Nickname', () => { await ImportWalletView.enterSecretRecoveryPhrase(validAccount.seedPhrase); await ImportWalletView.enterPassword(validAccount.password); await ImportWalletView.reEnterPassword(validAccount.password); - await WalletView.isVisible(); + await Assertions.checkIfVisible(WalletView.container); }); it('Should dismiss Automatic Security checks screen', async () => { @@ -103,8 +103,8 @@ describe('Adding Contract Nickname', () => { await NetworkListModal.changeNetworkTo( CustomNetworks.Sepolia.providerConfig.nickname, ); - - await WalletView.isNetworkNameVisible( + await Assertions.checkIfElementToHaveText( + WalletView.navbarNetworkText, CustomNetworks.Sepolia.providerConfig.nickname, ); await TestHelpers.delay(1500); @@ -138,7 +138,7 @@ describe('Adding Contract Nickname', () => { await LoginView.toggleRememberMe(); await LoginView.enterPassword(validAccount.password); - await WalletView.isVisible(); + await Assertions.checkIfVisible(WalletView.container); }); it('should deep link to the approval modal', async () => { @@ -174,7 +174,7 @@ describe('Adding Contract Nickname', () => { it('should verify contract does not appear in contacts view', async () => { // Check that we are on the wallet screen - await WalletView.isVisible(); + await Assertions.checkIfVisible(WalletView.container); await TabBarComponent.tapSettings(); await SettingsView.tapContacts(); diff --git a/e2e/specs/quarantine/deeplinks.failing.js b/e2e/specs/quarantine/deeplinks.failing.js index ea7211e06b1..0f6533a03f6 100644 --- a/e2e/specs/quarantine/deeplinks.failing.js +++ b/e2e/specs/quarantine/deeplinks.failing.js @@ -14,11 +14,12 @@ import TransactionConfirmationView from '../../pages/Send/TransactionConfirmView import SecurityAndPrivacy from '../../pages/Settings/SecurityAndPrivacy/SecurityAndPrivacyView'; -import WalletView from '../../pages/WalletView'; +import WalletView from '../../pages/wallet/WalletView'; import { importWalletWithRecoveryPhrase } from '../../viewHelper'; import Accounts from '../../../wdio/helpers/Accounts'; import TabBarComponent from '../../pages/TabBarComponent'; import Assertions from '../../utils/Assertions'; +import { PopularNetworksList } from '../../resources/networks.e2e'; //const BINANCE_RPC_URL = 'https://bsc-dataseed1.binance.org'; @@ -71,7 +72,7 @@ describe(Regression('Deep linking Tests'), () => { await LoginView.toggleRememberMe(); await LoginView.enterPassword(validAccount.password); - await WalletView.isVisible(); + await Assertions.checkIfVisible(WalletView.container); }); it('should deep link to Binance Smart Chain & show a network not found error message', async () => { @@ -87,7 +88,7 @@ describe(Regression('Deep linking Tests'), () => { await TabBarComponent.tapSettings(); await SettingsView.tapNetworks(); - await NetworkView.isNetworkViewVisible(); + await Assertions.checkIfVisible(NetworkView.networkContainer); }); it('should add BSC network', async () => { @@ -95,28 +96,32 @@ describe(Regression('Deep linking Tests'), () => { await TestHelpers.delay(3000); await NetworkView.tapAddNetworkButton(); - await NetworkView.isRpcViewVisible(); - await NetworkView.tapPopularNetworkByName('BNB Smart Chain'); + await Assertions.checkIfVisible(NetworkView.networkContainer); + await NetworkView.tapNetworkByName( + PopularNetworksList.BNB.providerConfig.nickname, + ); await Assertions.checkIfVisible(NetworkApprovalModal.container); await Assertions.checkIfElementToHaveText( NetworkApprovalModal.displayName, - 'BNB Smart Chain', + PopularNetworksList.BNB.providerConfig.nickname, ); await NetworkApprovalModal.tapApproveButton(); await Assertions.checkIfVisible(NetworkAddedModal.switchNetwork); await NetworkAddedModal.tapCloseButton(); - await NetworkView.isRpcViewVisible(); + await Assertions.checkIfVisible(NetworkView.networkContainer); }); it('should add polygon network', async () => { - await NetworkView.tapPopularNetworkByName('Polygon Mainnet'); + await NetworkView.tapNetworkByName( + PopularNetworksList.Polygon.providerConfig.nickname, + ); await Assertions.checkIfVisible(NetworkApprovalModal.container); await Assertions.checkIfElementToHaveText( NetworkApprovalModal.displayName, - 'Polygon Mainnet', + PopularNetworksList.Polygon.providerConfig.nickname, ); await NetworkApprovalModal.tapApproveButton(); @@ -125,49 +130,64 @@ describe(Regression('Deep linking Tests'), () => { await Assertions.checkIfVisible(NetworkAddedModal.switchNetwork); await NetworkAddedModal.tapSwitchToNetwork(); - await WalletView.isVisible(); - await WalletView.isNetworkNameVisible('Polygon Mainnet'); + await Assertions.checkIfVisible(WalletView.container); + await Assertions.checkIfElementToHaveText( + WalletView.navbarNetworkText, + PopularNetworksList.Polygon.providerConfig.nickname, + ); }); it('should deep link to the send flow on matic', async () => { await TestHelpers.openDeepLink(POLYGON_DEEPLINK_URL); //FIXME: this is failing on iOS simulator await TestHelpers.delay(4500); - await TransactionConfirmationView.isVisible(); - await TransactionConfirmationView.isNetworkNameVisible('Polygon Mainnet'); + await Assertions.checkIfVisible( + TransactionConfirmationView.transactionViewContainer, + ); + //TODO: Update isNetworkNameVisible method + //await TransactionConfirmationView.isNetworkNameVisible('Polygon Mainnet'); await TestHelpers.delay(1500); await TransactionConfirmationView.tapCancelButton(); }); it('should deep link to the send flow on BSC', async () => { await TestHelpers.openDeepLink(BINANCE_DEEPLINK_URL); await TestHelpers.delay(4500); - await TransactionConfirmationView.isVisible(); - await TransactionConfirmationView.isNetworkNameVisible('BNB Smart Chain'); + await Assertions.checkIfVisible( + TransactionConfirmationView.transactionViewContainer, + ); + //TODO: Update isNetworkNameVisible method + //await TransactionConfirmationView.isNetworkNameVisible('BNB Smart Chain'); }); it('should deep link to the send flow on Goerli and submit the transaction', async () => { await TestHelpers.openDeepLink(GOERLI_DEEPLINK_URL); await TestHelpers.delay(4500); - await TransactionConfirmationView.isVisible(); - await TransactionConfirmationView.isNetworkNameVisible( - 'Goerli Test Network', + await Assertions.checkIfVisible( + TransactionConfirmationView.transactionViewContainer, ); + //TODO: Update isNetworkNameVisible method + /*await TransactionConfirmationView.isNetworkNameVisible( + 'Goerli Test Network', + );*/ await Assertions.checkIfTextIsDisplayed('0.00001 GoerliETH'); // Tap on the Send CTA await TransactionConfirmationView.tapConfirmButton(); // Check that we are on the wallet screen - await WalletView.isVisible(); + await Assertions.checkIfVisible(WalletView.container); }); it('should deep link to the send flow on mainnet', async () => { await TestHelpers.openDeepLink(ETHEREUM_DEEPLINK_URL); await TestHelpers.delay(4500); - await TransactionConfirmationView.isVisible(); - await TransactionConfirmationView.isNetworkNameVisible( - 'Ethereum Main Network', + await Assertions.checkIfVisible( + TransactionConfirmationView.transactionViewContainer, ); + //TODO: Update isNetworkNameVisible method + /*await TransactionConfirmationView.isNetworkNameVisible( + 'Ethereum Main Network', + );*/ await TransactionConfirmationView.tapCancelButton(); }); diff --git a/e2e/specs/quarantine/import-nft.failing.js b/e2e/specs/quarantine/import-nft.failing.js index 68eca90dcf0..004ffef6158 100644 --- a/e2e/specs/quarantine/import-nft.failing.js +++ b/e2e/specs/quarantine/import-nft.failing.js @@ -1,7 +1,7 @@ 'use strict'; import { SmokeAssets } from '../../tags'; -import TestHelpers from '../../helpers'; -import WalletView from '../../pages/WalletView'; +import Assertions from '../../utils/Assertions'; +import WalletView from '../../pages/wallet/WalletView'; import AddCustomTokenView from '../../pages/AddCustomTokenView'; import { loginToApp } from '../../viewHelper'; import { @@ -48,13 +48,13 @@ describe(SmokeAssets('Import NFT'), () => { await AddCustomTokenView.isVisible(); await AddCustomTokenView.typeInNFTAddress(erc1155ContractAddress); await AddCustomTokenView.typeInNFTIdentifier('1'); - await WalletView.isVisible(); + await Assertions.checkIfVisible(WalletView.container); // Wait for asset to load - await TestHelpers.delay(3000); - await WalletView.isNFTVisibleInWallet('TestDappNFTs'); + await Assertions.checkIfVisible(WalletView.nftInWallet('TestDappNFTs')); // Tap on Collectible await WalletView.tapOnNFTInWallet('TestDappNFTs'); - await WalletView.isNFTNameVisible('TestDappNFTs #1'); + //TODO: isNFTNameVisible have been removed. Update it for valid implementations + //await WalletView.isNFTNameVisible('TestDappNFTs #1'); await WalletView.scrollUpOnNFTsTab(); }, ); diff --git a/e2e/specs/quarantine/permission-system-removing-imported-account.failing.js b/e2e/specs/quarantine/permission-system-removing-imported-account.failing.js index 242b7d3466e..e6088415059 100644 --- a/e2e/specs/quarantine/permission-system-removing-imported-account.failing.js +++ b/e2e/specs/quarantine/permission-system-removing-imported-account.failing.js @@ -1,7 +1,7 @@ 'use strict'; import TestHelpers from '../../helpers'; import { Regression } from '../../tags'; -import WalletView from '../../pages/WalletView'; +import WalletView from '../../pages/wallet/WalletView'; import ImportAccountView from '../../pages/ImportAccountView'; import TabBarComponent from '../../pages/TabBarComponent'; @@ -44,6 +44,7 @@ describe( it('should trigger connect modal in the test dapp', async () => { await TestHelpers.delay(3000); + //TODO: Create goToTestDappAndTapConnectButton method. await TestDApp.goToTestDappAndTapConnectButton(); }); @@ -88,7 +89,7 @@ describe( it('should navigate to wallet view', async () => { await TestHelpers.delay(1500); await TabBarComponent.tapWallet(); - await WalletView.isVisible(); + await Assertions.checkIfVisible(WalletView.container); }); it('should remove imported account', async () => { diff --git a/e2e/specs/settings/fiat-on-testnets.spec.js b/e2e/specs/settings/fiat-on-testnets.spec.js index 0b308dc8c88..a28448e2641 100644 --- a/e2e/specs/settings/fiat-on-testnets.spec.js +++ b/e2e/specs/settings/fiat-on-testnets.spec.js @@ -1,5 +1,5 @@ 'use strict'; -import { SmokeCore } from '../../tags'; +import { SmokeAssets } from '../../tags'; import SettingsView from '../../pages/Settings/SettingsView'; import TabBarComponent from '../../pages/TabBarComponent'; import { loginToApp } from '../../viewHelper'; @@ -7,18 +7,16 @@ import FixtureBuilder from '../../fixtures/fixture-builder'; import { withFixtures } from '../../fixtures/fixture-helper'; import { CustomNetworks } from '../../resources/networks.e2e'; import NetworkListModal from '../../pages/modals/NetworkListModal'; -import WalletView from '../../pages/WalletView'; +import WalletView from '../../pages/wallet/WalletView'; import NetworkEducationModal from '../../pages/modals/NetworkEducationModal'; import AdvancedSettingsView from '../../pages/Settings/AdvancedView'; import FiatOnTestnetsModal from '../../pages/modals/FiatOnTestnetsModal.js'; import Assertions from '../../utils/Assertions.js'; -import Matchers from '../../utils/Matchers.js'; import TestHelpers from '../../helpers.js'; -import { WalletViewSelectorsIDs } from '../../selectors/wallet/WalletView.selectors'; const SEPOLIA = CustomNetworks.Sepolia.providerConfig.nickname; -describe(SmokeCore('Fiat On Testnets Setting'), () => { +describe(SmokeAssets('Fiat On Testnets Setting'), () => { beforeEach(async () => { jest.setTimeout(150000); await TestHelpers.reverseServerPort(); @@ -39,8 +37,8 @@ describe(SmokeCore('Fiat On Testnets Setting'), () => { await NetworkEducationModal.tapGotItButton(); // Verify no fiat values displayed - await Assertions.checkIfHasText( - Matchers.getElementByID(WalletViewSelectorsIDs.TOTAL_BALANCE_TEXT), + await Assertions.checkIfElementToHaveText( + WalletView.totalBalance, '$0', ); @@ -57,7 +55,7 @@ describe(SmokeCore('Fiat On Testnets Setting'), () => { // Verify fiat values are displayed await TabBarComponent.tapWallet(); await Assertions.checkIfElementNotToHaveText( - Matchers.getElementByID(WalletViewSelectorsIDs.TOTAL_BALANCE_TEXT), + WalletView.totalBalance, '$0', ); }, diff --git a/e2e/specs/swaps/swap-action-smoke.spec.js b/e2e/specs/swaps/swap-action-smoke.spec.js index a79378e5030..08d198fc8e0 100644 --- a/e2e/specs/swaps/swap-action-smoke.spec.js +++ b/e2e/specs/swaps/swap-action-smoke.spec.js @@ -83,6 +83,7 @@ describe(SmokeSwaps('Swap from Actions'), () => { await SwapView.tapIUnderstandPriceWarning(); await SwapView.swipeToSwap(); await SwapView.waitForSwapToComplete(sourceTokenSymbol, destTokenSymbol); + await Assertions.checkIfVisible(TabBarComponent.tabBarActivityButton); await TabBarComponent.tapActivity(); await ActivitiesView.isVisible(); await ActivitiesView.tapOnSwapActivity( diff --git a/e2e/specs/swaps/swap-token-chart.spec.js b/e2e/specs/swaps/swap-token-chart.spec.js index 96ddd4aa070..844479e1759 100644 --- a/e2e/specs/swaps/swap-token-chart.spec.js +++ b/e2e/specs/swaps/swap-token-chart.spec.js @@ -4,7 +4,7 @@ import Onboarding from '../../pages/swaps/OnBoarding'; import QuoteView from '../../pages/swaps/QuoteView'; import SwapView from '../../pages/swaps/SwapView'; import TabBarComponent from '../../pages/TabBarComponent'; -import WalletView from '../../pages/WalletView'; +import WalletView from '../../pages/wallet/WalletView'; import TokenOverview from '../../pages/TokenOverview'; import FixtureBuilder from '../../fixtures/fixture-builder'; import { @@ -17,6 +17,7 @@ import TestHelpers from '../../helpers'; import FixtureServer from '../../fixtures/fixture-server'; import { getFixturesServerPort } from '../../fixtures/utils'; import { Regression } from '../../tags'; +import Assertions from '../../utils/Assertions'; const fixtureServer = new FixtureServer(); @@ -46,7 +47,7 @@ describe(Regression('Swap from Token view'), () => { it('should complete a USDC to DAI swap from the token chart', async () => { await TabBarComponent.tapWallet(); - await WalletView.isVisible(); + await Assertions.checkIfVisible(WalletView.container); await WalletView.tapOnToken('Ethereum'); await TokenOverview.isVisible(); await TokenOverview.tapSwapButton(); diff --git a/e2e/specs/swaps/token-details.spec.js b/e2e/specs/swaps/token-details.spec.js index c183d718382..66a74e9f51e 100644 --- a/e2e/specs/swaps/token-details.spec.js +++ b/e2e/specs/swaps/token-details.spec.js @@ -1,6 +1,6 @@ 'use strict'; import { SmokeSwaps } from '../../tags'; -import WalletView from '../../pages/WalletView'; +import WalletView from '../../pages/wallet/WalletView'; import TokenOverview from '../../pages/TokenOverview'; import { importWalletWithRecoveryPhrase, diff --git a/e2e/specs/wallet/portfolio-connect-account.spec.js b/e2e/specs/wallet/portfolio-connect-account.spec.js index 719b8d42bab..d66af7a65ee 100644 --- a/e2e/specs/wallet/portfolio-connect-account.spec.js +++ b/e2e/specs/wallet/portfolio-connect-account.spec.js @@ -9,7 +9,7 @@ import { } from '../../fixtures/fixture-helper'; import FixtureBuilder from '../../fixtures/fixture-builder'; import TestHelpers from '../../helpers'; -import WalletView from '../../pages/WalletView'; +import WalletView from '../../pages/wallet/WalletView'; import { getFixturesServerPort } from '../../fixtures/utils'; import FixtureServer from '../../fixtures/fixture-server'; import BrowserView from '../../pages/Browser/BrowserView'; @@ -57,10 +57,10 @@ describe(SmokeCore('Connect account to Portfolio'), () => { }); it('should not open additional browser tabs to portfolio', async () => { - await Assertions.checkIfHasText(BrowserView.tabsNumber, '1'); + await Assertions.checkIfElementToHaveText(BrowserView.tabsNumber, '1'); await TabBarComponent.tapWallet(); await WalletView.tapPortfolio(); await BrowserView.waitForBrowserPageToLoad(); - await Assertions.checkIfHasText(BrowserView.tabsNumber, '1'); + await Assertions.checkIfElementToHaveText(BrowserView.tabsNumber, '1'); }); }); diff --git a/e2e/specs/wallet/request-token-flow.spec.js b/e2e/specs/wallet/request-token-flow.spec.js index 36e647c59ee..fc783dc8045 100644 --- a/e2e/specs/wallet/request-token-flow.spec.js +++ b/e2e/specs/wallet/request-token-flow.spec.js @@ -14,7 +14,7 @@ import { } from '../../fixtures/fixture-helper'; import FixtureBuilder from '../../fixtures/fixture-builder'; import TestHelpers from '../../helpers'; -import WalletView from '../../pages/WalletView'; +import WalletView from '../../pages/wallet/WalletView'; import { getFixturesServerPort } from '../../fixtures/utils'; import FixtureServer from '../../fixtures/fixture-server'; import Assertions from '../../utils/Assertions'; @@ -45,7 +45,7 @@ describe(SmokeCore('Request Token Flow with Unprotected Wallet'), () => { it('should request asset from Action button', async () => { await loginToApp(); - await WalletView.isVisible(); + await Assertions.checkIfVisible(WalletView.container); await TabBarComponent.tapActions(); await WalletActionsModal.tapReceiveButton(); await RequestPaymentModal.tapRequestPaymentButton(); diff --git a/e2e/specs/wallet/send-ERC-token.spec.js b/e2e/specs/wallet/send-ERC-token.spec.js index 38ed65cf729..baf4aacd48e 100644 --- a/e2e/specs/wallet/send-ERC-token.spec.js +++ b/e2e/specs/wallet/send-ERC-token.spec.js @@ -1,7 +1,7 @@ 'use strict'; import { SmokeCore } from '../../tags'; import TestHelpers from '../../helpers'; -import WalletView from '../../pages/WalletView'; +import WalletView from '../../pages/wallet/WalletView'; import NetworkEducationModal from '../../pages/modals/NetworkEducationModal'; import AddCustomTokenView from '../../pages/AddCustomTokenView'; import AmountView from '../../pages/Send/AmountView'; @@ -56,7 +56,7 @@ describe(SmokeCore('Send ERC Token'), () => { await TestHelpers.delay(500); await ConfirmAddAssetView.isVisible(); await ConfirmAddAssetView.tapOnConfirmButton(); - await WalletView.isVisible(); + await Assertions.checkIfVisible(WalletView.container); }); it('should send token to address via asset overview screen', async () => { diff --git a/e2e/specs/wallet/start-exploring.spec.js b/e2e/specs/wallet/start-exploring.spec.js index ecf1be6ee73..e42fab7aaa8 100644 --- a/e2e/specs/wallet/start-exploring.spec.js +++ b/e2e/specs/wallet/start-exploring.spec.js @@ -6,7 +6,6 @@ import OnboardingCarouselView from '../../pages/Onboarding/OnboardingCarouselVie import ProtectYourWalletView from '../../pages/Onboarding/ProtectYourWalletView'; import CreatePasswordView from '../../pages/Onboarding/CreatePasswordView'; import MetaMetricsOptIn from '../../pages/Onboarding/MetaMetricsOptInView'; -import WalletView from '../../pages/WalletView'; import OnboardingSuccessView from '../../pages/Onboarding/OnboardingSuccessView'; import EnableAutomaticSecurityChecksView from '../../pages/EnableAutomaticSecurityChecksView'; import Browser from '../../pages/Browser/BrowserView'; @@ -16,7 +15,6 @@ import WhatsNewModal from '../../pages/modals/WhatsNewModal'; import { acceptTermOfUse } from '../../viewHelper'; import Assertions from '../../utils/Assertions'; -const ACCOUNT = 'Test Account One'; const PASSWORD = '12345678'; describe(SmokeCore('Start Exploring'), () => { @@ -52,7 +50,6 @@ describe(SmokeCore('Start Exploring'), () => { await CreatePasswordView.tapIUnderstandCheckBox(); await CreatePasswordView.enterPassword(PASSWORD); await CreatePasswordView.reEnterPassword(PASSWORD); - // await CreatePasswordView.tapCreatePasswordButton(); }); it('Should skip backup check', async () => { @@ -61,7 +58,6 @@ describe(SmokeCore('Start Exploring'), () => { await ProtectYourWalletView.tapOnRemindMeLaterButton(); await SkipAccountSecurityModal.tapIUnderstandCheckBox(); await SkipAccountSecurityModal.tapSkipButton(); - await WalletView.isVisible(); }); it('Should skip onboarding success screen', async () => { @@ -86,7 +82,6 @@ describe(SmokeCore('Start Exploring'), () => { await Assertions.checkIfVisible(OnboardingWizardModal.stepThreeContainer); // await WalletView.editAccountName(ACCOUNT); await OnboardingWizardModal.tapGotItButton(); - await WalletView.isAccountNameCorrect(ACCOUNT); // Ensure step 4 is shown correctly await Assertions.checkIfVisible(OnboardingWizardModal.stepFourContainer); await OnboardingWizardModal.tapGotItButton(); diff --git a/e2e/utils/Assertions.js b/e2e/utils/Assertions.js index 3b19d651385..e3b15781d21 100644 --- a/e2e/utils/Assertions.js +++ b/e2e/utils/Assertions.js @@ -114,20 +114,6 @@ class Assertions { .withTimeout(timeout); } - /** - * Check if an element with the specified ID has the specified text. - * @param {Promise} elementId - The ID of the element to check. - * @param {string} text - The expected text content. - * @param {number} timeout - Timeout in milliseconds. - */ - static async checkIfHasText(elementId, text, timeout = TIMEOUT) { - // rename this. checkIfELEMENTHasText makes it clear - - return waitFor(await elementId) - .toHaveText(text) - .withTimeout(timeout); - } - /** * Check if the toggle with the specified ID is in the "on" state. * @param {Promise} elementID - The ID of the toggle element. diff --git a/e2e/utils/Matchers.js b/e2e/utils/Matchers.js index 413a67c3433..e9522b6d2a1 100644 --- a/e2e/utils/Matchers.js +++ b/e2e/utils/Matchers.js @@ -73,15 +73,29 @@ class Matchers { return element(by.id(childElement).withAncestor(by.id(parentElement))); } + /** + * Get Native WebView instance by elementId + * + * Because Android Webview might have more that one WebView instance present on the main activity, the correct element + * is select based on its parent element id. + * @param {string} elementId The web ID of the browser webview + * @returns {Detox.WebViewElement} WebView element + */ + static getWebViewByID(elementId) { + return device.getPlatform() === 'ios' + ? web(by.id(elementId)) + : web(by.type('android.webkit.WebView').withAncestor(by.id(elementId))); + } + /** * Get element by web ID. * - * * @param {string} webviewID - The web ID of the inner element to locate within the webview - * @param {string} innerID - The web ID of the browser webview + * @param {string} webviewID - The web ID of the inner element to locate within the webview + * @param {string} innerID - The web ID of the browser webview * @return {Promise} Resolves to the located element */ static async getElementByWebID(webviewID, innerID) { - const myWebView = web(by.id(webviewID)); + const myWebView = this.getWebViewByID(webviewID); return myWebView.element(by.web.id(innerID)); } @@ -101,15 +115,11 @@ class Matchers { * Get element by XPath. * @param {string} webviewID - The web ID of the browser webview * @param {string} xpath - XPath expression to locate the element - * @param {number} index - index to locate the webview (iOS only) * @return {Promise} - Resolves to the located element */ - static async getElementByXPath(webviewID, xpath, index = 0) { - const myWebView = - device.getPlatform() === 'ios' - ? web(by.id(webviewID)).atIndex(index) - : web(by.id(webviewID)); - return myWebView.element(by.web.xpath(xpath)).atIndex(0); + static async getElementByXPath(webviewID, xpath) { + const myWebView = this.getWebViewByID(webviewID); + return myWebView.element(by.web.xpath(xpath)); } /** * Get element by href. diff --git a/e2e/viewHelper.js b/e2e/viewHelper.js index 7cc2de054b6..8e1340bc222 100644 --- a/e2e/viewHelper.js +++ b/e2e/viewHelper.js @@ -11,7 +11,7 @@ import OnboardingCarouselView from './pages/Onboarding/OnboardingCarouselView'; import OnboardingWizardModal from './pages/modals/OnboardingWizardModal'; import ExperienceEnhancerModal from './pages/modals/ExperienceEnhancerModal'; import SettingsView from './pages/Settings/SettingsView'; -import WalletView from './pages/WalletView'; +import WalletView from './pages/wallet/WalletView'; import WhatsNewModal from './pages/modals/WhatsNewModal'; import Accounts from '../wdio/helpers/Accounts'; import SkipAccountSecurityModal from './pages/modals/SkipAccountSecurityModal'; @@ -153,7 +153,7 @@ export const CreateNewWallet = async () => { await SkipAccountSecurityModal.tapIUnderstandCheckBox(); await SkipAccountSecurityModal.tapSkipButton(); await device.enableSynchronization(); - await WalletView.isVisible(); + await Assertions.checkIfVisible(WalletView.container); //'Should dismiss Automatic Security checks screen' await TestHelpers.delay(3500); @@ -208,10 +208,20 @@ export const switchToSepoliaNetwork = async () => { await NetworkListModal.changeNetworkTo( CustomNetworks.Sepolia.providerConfig.nickname, ); - await WalletView.isNetworkNameVisible( + await Assertions.checkIfVisible(NetworkEducationModal.container); + await Assertions.checkIfElementToHaveText( + NetworkEducationModal.networkName, CustomNetworks.Sepolia.providerConfig.nickname, ); await NetworkEducationModal.tapGotItButton(); + await Assertions.checkIfNotVisible(NetworkEducationModal.container); + try { + await Assertions.checkIfVisible(ToastModal.container); + await Assertions.checkIfNotVisible(ToastModal.container); + } catch { + // eslint-disable-next-line no-console + console.log('Toast is not visible'); + } }; export const loginToApp = async () => { diff --git a/firebase.json b/firebase.json new file mode 100644 index 00000000000..d40454d68c7 --- /dev/null +++ b/firebase.json @@ -0,0 +1,9 @@ +{ + "react-native": { + "analytics_auto_collection_enabled": false, + "messaging_auto_init_enabled": false, + "messaging_ios_auto_register_for_remote_messages": true, + "android_task_executor_maximum_pool_size": 10, + "android_task_executor_keep_alive_seconds": 3 + } +} diff --git a/index.js b/index.js index 69379d6fe6b..23bf2b3975a 100644 --- a/index.js +++ b/index.js @@ -22,7 +22,7 @@ import { name } from './app.json'; import { isTest } from './app/util/test/utils.js'; import NotificationManager from './app/core/NotificationManager'; -import { isNotificationsFeatureEnabled } from './app/util/notifications/methods'; +import { isNotificationsFeatureEnabled } from './app/util/notifications'; // List of warnings that we're ignoring diff --git a/ios/GoogleService-Info.plist b/ios/GoogleService-Info.plist new file mode 100644 index 00000000000..8410def31e2 --- /dev/null +++ b/ios/GoogleService-Info.plist @@ -0,0 +1,26 @@ + + + + + API_KEY + $(FCM_CONFIG_API_KEY) + GCM_SENDER_ID + $(FCM_CONFIG_MESSAGING_SENDER_ID) + PLIST_VERSION + 1 + BUNDLE_ID + $(PRODUCT_BUNDLE_IDENTIFIER) + PROJECT_ID + $(FCM_CONFIG_PROJECT_ID) + STORAGE_BUCKET + $(FCM_CONFIG_STORAGE_BUCKET) + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + $(FCM_CONFIG_APP_ID) + + diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index e6c756b9576..1ef73d8d65e 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -592,6 +592,7 @@ 802588CED3FC487A5D5263F0 /* [CP] Copy Pods Resources */, C77EFC60F19570875F8DB3BD /* [CP] Embed Pods Frameworks */, 00DD1BFF1BD5951E006B06BC /* Bundle JS Code & Upload Sentry Files */, + 9F2FDF243A79F1A3A790828C /* [CP-User] [RNFB] Core Configuration */, ); buildRules = ( ); @@ -616,6 +617,7 @@ 2EF2828D2B0FF86900D7B4B1 /* [CP] Copy Pods Resources */, 2EF2828E2B0FF86900D7B4B1 /* [CP] Embed Pods Frameworks */, 2EF282892B0FF86900D7B4B1 /* Bundle JS Code & Upload Sentry Files */, + 7DCEC09F2EFA897359942504 /* [CP-User] [RNFB] Core Configuration */, ); buildRules = ( ); @@ -640,6 +642,7 @@ 8EF10BB14629809332947E5D /* [CP] Copy Pods Resources */, EB3465C9579A347237ADD532 /* [CP] Embed Pods Frameworks */, B339FF2F289ABD70001B89FB /* Bundle JS Code & Upload Sentry Files */, + 13E0EBB030DB9498ACF206AC /* [CP-User] [RNFB] Core Configuration */, ); buildRules = ( ); @@ -860,6 +863,19 @@ shellPath = /bin/sh; shellScript = "# Define script\nBUNDLE_AND_UPLOAD_TO_SENTRY=\"../scripts/ios/bundle-js-and-sentry-upload.sh\"\n\n# Give permissions to script\nchmod +x $BUNDLE_AND_UPLOAD_TO_SENTRY\n\n# Run script\n$BUNDLE_AND_UPLOAD_TO_SENTRY\n"; }; + 13E0EBB030DB9498ACF206AC /* [CP-User] [RNFB] Core Configuration */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)", + ); + name = "[CP-User] [RNFB] Core Configuration"; + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "#!/usr/bin/env bash\n#\n# Copyright (c) 2016-present Invertase Limited & Contributors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this library except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##########################################################################\n##########################################################################\n#\n# NOTE THAT IF YOU CHANGE THIS FILE YOU MUST RUN pod install AFTERWARDS\n#\n# This file is installed as an Xcode build script in the project file\n# by cocoapods, and you will not see your changes until you pod install\n#\n##########################################################################\n##########################################################################\n\nset -e\n\n_MAX_LOOKUPS=2;\n_SEARCH_RESULT=''\n_RN_ROOT_EXISTS=''\n_CURRENT_LOOKUPS=1\n_JSON_ROOT=\"'react-native'\"\n_JSON_FILE_NAME='firebase.json'\n_JSON_OUTPUT_BASE64='e30=' # { }\n_CURRENT_SEARCH_DIR=${PROJECT_DIR}\n_PLIST_BUDDY=/usr/libexec/PlistBuddy\n_TARGET_PLIST=\"${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}\"\n_DSYM_PLIST=\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist\"\n\n# plist arrays\n_PLIST_ENTRY_KEYS=()\n_PLIST_ENTRY_TYPES=()\n_PLIST_ENTRY_VALUES=()\n\nfunction setPlistValue {\n echo \"info: setting plist entry '$1' of type '$2' in file '$4'\"\n ${_PLIST_BUDDY} -c \"Add :$1 $2 '$3'\" $4 || echo \"info: '$1' already exists\"\n}\n\nfunction getFirebaseJsonKeyValue () {\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n ruby -Ku -e \"require 'rubygems';require 'json'; output=JSON.parse('$1'); puts output[$_JSON_ROOT]['$2']\"\n else\n echo \"\"\n fi;\n}\n\nfunction jsonBoolToYesNo () {\n if [[ $1 == \"false\" ]]; then\n echo \"NO\"\n elif [[ $1 == \"true\" ]]; then\n echo \"YES\"\n else echo \"NO\"\n fi\n}\n\necho \"info: -> RNFB build script started\"\necho \"info: 1) Locating ${_JSON_FILE_NAME} file:\"\n\nif [[ -z ${_CURRENT_SEARCH_DIR} ]]; then\n _CURRENT_SEARCH_DIR=$(pwd)\nfi;\n\nwhile true; do\n _CURRENT_SEARCH_DIR=$(dirname \"$_CURRENT_SEARCH_DIR\")\n if [[ \"$_CURRENT_SEARCH_DIR\" == \"/\" ]] || [[ ${_CURRENT_LOOKUPS} -gt ${_MAX_LOOKUPS} ]]; then break; fi;\n echo \"info: ($_CURRENT_LOOKUPS of $_MAX_LOOKUPS) Searching in '$_CURRENT_SEARCH_DIR' for a ${_JSON_FILE_NAME} file.\"\n _SEARCH_RESULT=$(find \"$_CURRENT_SEARCH_DIR\" -maxdepth 2 -name ${_JSON_FILE_NAME} -print | /usr/bin/head -n 1)\n if [[ ${_SEARCH_RESULT} ]]; then\n echo \"info: ${_JSON_FILE_NAME} found at $_SEARCH_RESULT\"\n break;\n fi;\n _CURRENT_LOOKUPS=$((_CURRENT_LOOKUPS+1))\ndone\n\nif [[ ${_SEARCH_RESULT} ]]; then\n _JSON_OUTPUT_RAW=$(cat \"${_SEARCH_RESULT}\")\n _RN_ROOT_EXISTS=$(ruby -Ku -e \"require 'rubygems';require 'json'; output=JSON.parse('$_JSON_OUTPUT_RAW'); puts output[$_JSON_ROOT]\" || echo '')\n\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n if ! python3 --version >/dev/null 2>&1; then echo \"python3 not found, firebase.json file processing error.\" && exit 1; fi\n _JSON_OUTPUT_BASE64=$(python3 -c 'import json,sys,base64;print(base64.b64encode(bytes(json.dumps(json.loads(open('\"'${_SEARCH_RESULT}'\"', '\"'rb'\"').read())['${_JSON_ROOT}']), '\"'utf-8'\"')).decode())' || echo \"e30=\")\n fi\n\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n\n # config.app_data_collection_default_enabled\n _APP_DATA_COLLECTION_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_data_collection_default_enabled\")\n if [[ $_APP_DATA_COLLECTION_ENABLED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseDataCollectionDefaultEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_DATA_COLLECTION_ENABLED\")\")\n fi\n\n # config.analytics_auto_collection_enabled\n _ANALYTICS_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_auto_collection_enabled\")\n if [[ $_ANALYTICS_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_COLLECTION\")\")\n fi\n\n # config.analytics_collection_deactivated\n _ANALYTICS_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_collection_deactivated\")\n if [[ $_ANALYTICS_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_DEACTIVATED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_DEACTIVATED\")\")\n fi\n\n # config.analytics_idfv_collection_enabled\n _ANALYTICS_IDFV_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_idfv_collection_enabled\")\n if [[ $_ANALYTICS_IDFV_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_IDFV_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_IDFV_COLLECTION\")\")\n fi\n\n # config.analytics_default_allow_analytics_storage\n _ANALYTICS_STORAGE=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_analytics_storage\")\n if [[ $_ANALYTICS_STORAGE ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_ANALYTICS_STORAGE\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_STORAGE\")\")\n fi\n\n # config.analytics_default_allow_ad_storage\n _ANALYTICS_AD_STORAGE=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_storage\")\n if [[ $_ANALYTICS_AD_STORAGE ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_STORAGE\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AD_STORAGE\")\")\n fi\n\n # config.analytics_default_allow_ad_user_data\n _ANALYTICS_AD_USER_DATA=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_user_data\")\n if [[ $_ANALYTICS_AD_USER_DATA ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_USER_DATA\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AD_USER_DATA\")\")\n fi\n\n # config.analytics_default_allow_ad_personalization_signals\n _ANALYTICS_PERSONALIZATION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_personalization_signals\")\n if [[ $_ANALYTICS_PERSONALIZATION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_PERSONALIZATION_SIGNALS\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_PERSONALIZATION\")\")\n fi\n\n # config.analytics_registration_with_ad_network_enabled\n _ANALYTICS_REGISTRATION_WITH_AD_NETWORK=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"google_analytics_registration_with_ad_network_enabled\")\n if [[ $_ANALYTICS_REGISTRATION_WITH_AD_NETWORK ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_REGISTRATION_WITH_AD_NETWORK_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_REGISTRATION_WITH_AD_NETWORK\")\")\n fi\n\n # config.google_analytics_automatic_screen_reporting_enabled\n _ANALYTICS_AUTO_SCREEN_REPORTING=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"google_analytics_automatic_screen_reporting_enabled\")\n if [[ $_ANALYTICS_AUTO_SCREEN_REPORTING ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseAutomaticScreenReportingEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_SCREEN_REPORTING\")\")\n fi\n\n # config.perf_auto_collection_enabled\n _PERF_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_auto_collection_enabled\")\n if [[ $_PERF_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_enabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_AUTO_COLLECTION\")\")\n fi\n\n # config.perf_collection_deactivated\n _PERF_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_collection_deactivated\")\n if [[ $_PERF_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_deactivated\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_DEACTIVATED\")\")\n fi\n\n # config.messaging_auto_init_enabled\n _MESSAGING_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"messaging_auto_init_enabled\")\n if [[ $_MESSAGING_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseMessagingAutoInitEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_MESSAGING_AUTO_INIT\")\")\n fi\n\n # config.in_app_messaging_auto_colllection_enabled\n _FIAM_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"in_app_messaging_auto_collection_enabled\")\n if [[ $_FIAM_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseInAppMessagingAutomaticDataCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_FIAM_AUTO_INIT\")\")\n fi\n\n # config.app_check_token_auto_refresh\n _APP_CHECK_TOKEN_AUTO_REFRESH=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_check_token_auto_refresh\")\n if [[ $_APP_CHECK_TOKEN_AUTO_REFRESH ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseAppCheckTokenAutoRefreshEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_CHECK_TOKEN_AUTO_REFRESH\")\")\n fi\n\n # config.crashlytics_disable_auto_disabler - undocumented for now - mainly for debugging, document if becomes useful\n _CRASHLYTICS_AUTO_DISABLE_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"crashlytics_disable_auto_disabler\")\n if [[ $_CRASHLYTICS_AUTO_DISABLE_ENABLED == \"true\" ]]; then\n echo \"Disabled Crashlytics auto disabler.\" # do nothing\n else\n _PLIST_ENTRY_KEYS+=(\"FirebaseCrashlyticsCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"NO\")\n fi\nelse\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n echo \"warning: A firebase.json file was not found, whilst this file is optional it is recommended to include it to configure firebase services in React Native Firebase.\"\nfi;\n\necho \"info: 2) Injecting Info.plist entries: \"\n\n# Log out the keys we're adding\nfor i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n echo \" -> $i) ${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\"\ndone\n\nfor plist in \"${_TARGET_PLIST}\" \"${_DSYM_PLIST}\" ; do\n if [[ -f \"${plist}\" ]]; then\n\n # paths with spaces break the call to setPlistValue. temporarily modify\n # the shell internal field separator variable (IFS), which normally\n # includes spaces, to consist only of line breaks\n oldifs=$IFS\n IFS=\"\n\"\n\n for i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n setPlistValue \"${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\" \"${plist}\"\n done\n\n # restore the original internal field separator value\n IFS=$oldifs\n else\n echo \"warning: A Info.plist build output file was not found (${plist})\"\n fi\ndone\n\necho \"info: <- RNFB build script finished\"\n"; + }; 15FDD86321B76696006B7C35 /* Override xcconfig files */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -991,6 +1007,19 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; + 7DCEC09F2EFA897359942504 /* [CP-User] [RNFB] Core Configuration */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)", + ); + name = "[CP-User] [RNFB] Core Configuration"; + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "#!/usr/bin/env bash\n#\n# Copyright (c) 2016-present Invertase Limited & Contributors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this library except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##########################################################################\n##########################################################################\n#\n# NOTE THAT IF YOU CHANGE THIS FILE YOU MUST RUN pod install AFTERWARDS\n#\n# This file is installed as an Xcode build script in the project file\n# by cocoapods, and you will not see your changes until you pod install\n#\n##########################################################################\n##########################################################################\n\nset -e\n\n_MAX_LOOKUPS=2;\n_SEARCH_RESULT=''\n_RN_ROOT_EXISTS=''\n_CURRENT_LOOKUPS=1\n_JSON_ROOT=\"'react-native'\"\n_JSON_FILE_NAME='firebase.json'\n_JSON_OUTPUT_BASE64='e30=' # { }\n_CURRENT_SEARCH_DIR=${PROJECT_DIR}\n_PLIST_BUDDY=/usr/libexec/PlistBuddy\n_TARGET_PLIST=\"${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}\"\n_DSYM_PLIST=\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist\"\n\n# plist arrays\n_PLIST_ENTRY_KEYS=()\n_PLIST_ENTRY_TYPES=()\n_PLIST_ENTRY_VALUES=()\n\nfunction setPlistValue {\n echo \"info: setting plist entry '$1' of type '$2' in file '$4'\"\n ${_PLIST_BUDDY} -c \"Add :$1 $2 '$3'\" $4 || echo \"info: '$1' already exists\"\n}\n\nfunction getFirebaseJsonKeyValue () {\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n ruby -Ku -e \"require 'rubygems';require 'json'; output=JSON.parse('$1'); puts output[$_JSON_ROOT]['$2']\"\n else\n echo \"\"\n fi;\n}\n\nfunction jsonBoolToYesNo () {\n if [[ $1 == \"false\" ]]; then\n echo \"NO\"\n elif [[ $1 == \"true\" ]]; then\n echo \"YES\"\n else echo \"NO\"\n fi\n}\n\necho \"info: -> RNFB build script started\"\necho \"info: 1) Locating ${_JSON_FILE_NAME} file:\"\n\nif [[ -z ${_CURRENT_SEARCH_DIR} ]]; then\n _CURRENT_SEARCH_DIR=$(pwd)\nfi;\n\nwhile true; do\n _CURRENT_SEARCH_DIR=$(dirname \"$_CURRENT_SEARCH_DIR\")\n if [[ \"$_CURRENT_SEARCH_DIR\" == \"/\" ]] || [[ ${_CURRENT_LOOKUPS} -gt ${_MAX_LOOKUPS} ]]; then break; fi;\n echo \"info: ($_CURRENT_LOOKUPS of $_MAX_LOOKUPS) Searching in '$_CURRENT_SEARCH_DIR' for a ${_JSON_FILE_NAME} file.\"\n _SEARCH_RESULT=$(find \"$_CURRENT_SEARCH_DIR\" -maxdepth 2 -name ${_JSON_FILE_NAME} -print | /usr/bin/head -n 1)\n if [[ ${_SEARCH_RESULT} ]]; then\n echo \"info: ${_JSON_FILE_NAME} found at $_SEARCH_RESULT\"\n break;\n fi;\n _CURRENT_LOOKUPS=$((_CURRENT_LOOKUPS+1))\ndone\n\nif [[ ${_SEARCH_RESULT} ]]; then\n _JSON_OUTPUT_RAW=$(cat \"${_SEARCH_RESULT}\")\n _RN_ROOT_EXISTS=$(ruby -Ku -e \"require 'rubygems';require 'json'; output=JSON.parse('$_JSON_OUTPUT_RAW'); puts output[$_JSON_ROOT]\" || echo '')\n\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n if ! python3 --version >/dev/null 2>&1; then echo \"python3 not found, firebase.json file processing error.\" && exit 1; fi\n _JSON_OUTPUT_BASE64=$(python3 -c 'import json,sys,base64;print(base64.b64encode(bytes(json.dumps(json.loads(open('\"'${_SEARCH_RESULT}'\"', '\"'rb'\"').read())['${_JSON_ROOT}']), '\"'utf-8'\"')).decode())' || echo \"e30=\")\n fi\n\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n\n # config.app_data_collection_default_enabled\n _APP_DATA_COLLECTION_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_data_collection_default_enabled\")\n if [[ $_APP_DATA_COLLECTION_ENABLED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseDataCollectionDefaultEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_DATA_COLLECTION_ENABLED\")\")\n fi\n\n # config.analytics_auto_collection_enabled\n _ANALYTICS_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_auto_collection_enabled\")\n if [[ $_ANALYTICS_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_COLLECTION\")\")\n fi\n\n # config.analytics_collection_deactivated\n _ANALYTICS_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_collection_deactivated\")\n if [[ $_ANALYTICS_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_DEACTIVATED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_DEACTIVATED\")\")\n fi\n\n # config.analytics_idfv_collection_enabled\n _ANALYTICS_IDFV_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_idfv_collection_enabled\")\n if [[ $_ANALYTICS_IDFV_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_IDFV_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_IDFV_COLLECTION\")\")\n fi\n\n # config.analytics_default_allow_analytics_storage\n _ANALYTICS_STORAGE=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_analytics_storage\")\n if [[ $_ANALYTICS_STORAGE ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_ANALYTICS_STORAGE\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_STORAGE\")\")\n fi\n\n # config.analytics_default_allow_ad_storage\n _ANALYTICS_AD_STORAGE=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_storage\")\n if [[ $_ANALYTICS_AD_STORAGE ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_STORAGE\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AD_STORAGE\")\")\n fi\n\n # config.analytics_default_allow_ad_user_data\n _ANALYTICS_AD_USER_DATA=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_user_data\")\n if [[ $_ANALYTICS_AD_USER_DATA ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_USER_DATA\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AD_USER_DATA\")\")\n fi\n\n # config.analytics_default_allow_ad_personalization_signals\n _ANALYTICS_PERSONALIZATION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_personalization_signals\")\n if [[ $_ANALYTICS_PERSONALIZATION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_PERSONALIZATION_SIGNALS\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_PERSONALIZATION\")\")\n fi\n\n # config.analytics_registration_with_ad_network_enabled\n _ANALYTICS_REGISTRATION_WITH_AD_NETWORK=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"google_analytics_registration_with_ad_network_enabled\")\n if [[ $_ANALYTICS_REGISTRATION_WITH_AD_NETWORK ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_REGISTRATION_WITH_AD_NETWORK_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_REGISTRATION_WITH_AD_NETWORK\")\")\n fi\n\n # config.google_analytics_automatic_screen_reporting_enabled\n _ANALYTICS_AUTO_SCREEN_REPORTING=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"google_analytics_automatic_screen_reporting_enabled\")\n if [[ $_ANALYTICS_AUTO_SCREEN_REPORTING ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseAutomaticScreenReportingEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_SCREEN_REPORTING\")\")\n fi\n\n # config.perf_auto_collection_enabled\n _PERF_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_auto_collection_enabled\")\n if [[ $_PERF_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_enabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_AUTO_COLLECTION\")\")\n fi\n\n # config.perf_collection_deactivated\n _PERF_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_collection_deactivated\")\n if [[ $_PERF_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_deactivated\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_DEACTIVATED\")\")\n fi\n\n # config.messaging_auto_init_enabled\n _MESSAGING_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"messaging_auto_init_enabled\")\n if [[ $_MESSAGING_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseMessagingAutoInitEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_MESSAGING_AUTO_INIT\")\")\n fi\n\n # config.in_app_messaging_auto_colllection_enabled\n _FIAM_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"in_app_messaging_auto_collection_enabled\")\n if [[ $_FIAM_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseInAppMessagingAutomaticDataCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_FIAM_AUTO_INIT\")\")\n fi\n\n # config.app_check_token_auto_refresh\n _APP_CHECK_TOKEN_AUTO_REFRESH=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_check_token_auto_refresh\")\n if [[ $_APP_CHECK_TOKEN_AUTO_REFRESH ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseAppCheckTokenAutoRefreshEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_CHECK_TOKEN_AUTO_REFRESH\")\")\n fi\n\n # config.crashlytics_disable_auto_disabler - undocumented for now - mainly for debugging, document if becomes useful\n _CRASHLYTICS_AUTO_DISABLE_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"crashlytics_disable_auto_disabler\")\n if [[ $_CRASHLYTICS_AUTO_DISABLE_ENABLED == \"true\" ]]; then\n echo \"Disabled Crashlytics auto disabler.\" # do nothing\n else\n _PLIST_ENTRY_KEYS+=(\"FirebaseCrashlyticsCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"NO\")\n fi\nelse\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n echo \"warning: A firebase.json file was not found, whilst this file is optional it is recommended to include it to configure firebase services in React Native Firebase.\"\nfi;\n\necho \"info: 2) Injecting Info.plist entries: \"\n\n# Log out the keys we're adding\nfor i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n echo \" -> $i) ${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\"\ndone\n\nfor plist in \"${_TARGET_PLIST}\" \"${_DSYM_PLIST}\" ; do\n if [[ -f \"${plist}\" ]]; then\n\n # paths with spaces break the call to setPlistValue. temporarily modify\n # the shell internal field separator variable (IFS), which normally\n # includes spaces, to consist only of line breaks\n oldifs=$IFS\n IFS=\"\n\"\n\n for i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n setPlistValue \"${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\" \"${plist}\"\n done\n\n # restore the original internal field separator value\n IFS=$oldifs\n else\n echo \"warning: A Info.plist build output file was not found (${plist})\"\n fi\ndone\n\necho \"info: <- RNFB build script finished\"\n"; + }; 802588CED3FC487A5D5263F0 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -1047,6 +1076,19 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; + 9F2FDF243A79F1A3A790828C /* [CP-User] [RNFB] Core Configuration */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)", + ); + name = "[CP-User] [RNFB] Core Configuration"; + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "#!/usr/bin/env bash\n#\n# Copyright (c) 2016-present Invertase Limited & Contributors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this library except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##########################################################################\n##########################################################################\n#\n# NOTE THAT IF YOU CHANGE THIS FILE YOU MUST RUN pod install AFTERWARDS\n#\n# This file is installed as an Xcode build script in the project file\n# by cocoapods, and you will not see your changes until you pod install\n#\n##########################################################################\n##########################################################################\n\nset -e\n\n_MAX_LOOKUPS=2;\n_SEARCH_RESULT=''\n_RN_ROOT_EXISTS=''\n_CURRENT_LOOKUPS=1\n_JSON_ROOT=\"'react-native'\"\n_JSON_FILE_NAME='firebase.json'\n_JSON_OUTPUT_BASE64='e30=' # { }\n_CURRENT_SEARCH_DIR=${PROJECT_DIR}\n_PLIST_BUDDY=/usr/libexec/PlistBuddy\n_TARGET_PLIST=\"${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}\"\n_DSYM_PLIST=\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist\"\n\n# plist arrays\n_PLIST_ENTRY_KEYS=()\n_PLIST_ENTRY_TYPES=()\n_PLIST_ENTRY_VALUES=()\n\nfunction setPlistValue {\n echo \"info: setting plist entry '$1' of type '$2' in file '$4'\"\n ${_PLIST_BUDDY} -c \"Add :$1 $2 '$3'\" $4 || echo \"info: '$1' already exists\"\n}\n\nfunction getFirebaseJsonKeyValue () {\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n ruby -Ku -e \"require 'rubygems';require 'json'; output=JSON.parse('$1'); puts output[$_JSON_ROOT]['$2']\"\n else\n echo \"\"\n fi;\n}\n\nfunction jsonBoolToYesNo () {\n if [[ $1 == \"false\" ]]; then\n echo \"NO\"\n elif [[ $1 == \"true\" ]]; then\n echo \"YES\"\n else echo \"NO\"\n fi\n}\n\necho \"info: -> RNFB build script started\"\necho \"info: 1) Locating ${_JSON_FILE_NAME} file:\"\n\nif [[ -z ${_CURRENT_SEARCH_DIR} ]]; then\n _CURRENT_SEARCH_DIR=$(pwd)\nfi;\n\nwhile true; do\n _CURRENT_SEARCH_DIR=$(dirname \"$_CURRENT_SEARCH_DIR\")\n if [[ \"$_CURRENT_SEARCH_DIR\" == \"/\" ]] || [[ ${_CURRENT_LOOKUPS} -gt ${_MAX_LOOKUPS} ]]; then break; fi;\n echo \"info: ($_CURRENT_LOOKUPS of $_MAX_LOOKUPS) Searching in '$_CURRENT_SEARCH_DIR' for a ${_JSON_FILE_NAME} file.\"\n _SEARCH_RESULT=$(find \"$_CURRENT_SEARCH_DIR\" -maxdepth 2 -name ${_JSON_FILE_NAME} -print | /usr/bin/head -n 1)\n if [[ ${_SEARCH_RESULT} ]]; then\n echo \"info: ${_JSON_FILE_NAME} found at $_SEARCH_RESULT\"\n break;\n fi;\n _CURRENT_LOOKUPS=$((_CURRENT_LOOKUPS+1))\ndone\n\nif [[ ${_SEARCH_RESULT} ]]; then\n _JSON_OUTPUT_RAW=$(cat \"${_SEARCH_RESULT}\")\n _RN_ROOT_EXISTS=$(ruby -Ku -e \"require 'rubygems';require 'json'; output=JSON.parse('$_JSON_OUTPUT_RAW'); puts output[$_JSON_ROOT]\" || echo '')\n\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n if ! python3 --version >/dev/null 2>&1; then echo \"python3 not found, firebase.json file processing error.\" && exit 1; fi\n _JSON_OUTPUT_BASE64=$(python3 -c 'import json,sys,base64;print(base64.b64encode(bytes(json.dumps(json.loads(open('\"'${_SEARCH_RESULT}'\"', '\"'rb'\"').read())['${_JSON_ROOT}']), '\"'utf-8'\"')).decode())' || echo \"e30=\")\n fi\n\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n\n # config.app_data_collection_default_enabled\n _APP_DATA_COLLECTION_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_data_collection_default_enabled\")\n if [[ $_APP_DATA_COLLECTION_ENABLED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseDataCollectionDefaultEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_DATA_COLLECTION_ENABLED\")\")\n fi\n\n # config.analytics_auto_collection_enabled\n _ANALYTICS_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_auto_collection_enabled\")\n if [[ $_ANALYTICS_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_COLLECTION\")\")\n fi\n\n # config.analytics_collection_deactivated\n _ANALYTICS_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_collection_deactivated\")\n if [[ $_ANALYTICS_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_DEACTIVATED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_DEACTIVATED\")\")\n fi\n\n # config.analytics_idfv_collection_enabled\n _ANALYTICS_IDFV_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_idfv_collection_enabled\")\n if [[ $_ANALYTICS_IDFV_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_IDFV_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_IDFV_COLLECTION\")\")\n fi\n\n # config.analytics_default_allow_analytics_storage\n _ANALYTICS_STORAGE=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_analytics_storage\")\n if [[ $_ANALYTICS_STORAGE ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_ANALYTICS_STORAGE\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_STORAGE\")\")\n fi\n\n # config.analytics_default_allow_ad_storage\n _ANALYTICS_AD_STORAGE=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_storage\")\n if [[ $_ANALYTICS_AD_STORAGE ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_STORAGE\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AD_STORAGE\")\")\n fi\n\n # config.analytics_default_allow_ad_user_data\n _ANALYTICS_AD_USER_DATA=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_user_data\")\n if [[ $_ANALYTICS_AD_USER_DATA ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_USER_DATA\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AD_USER_DATA\")\")\n fi\n\n # config.analytics_default_allow_ad_personalization_signals\n _ANALYTICS_PERSONALIZATION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_personalization_signals\")\n if [[ $_ANALYTICS_PERSONALIZATION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_PERSONALIZATION_SIGNALS\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_PERSONALIZATION\")\")\n fi\n\n # config.analytics_registration_with_ad_network_enabled\n _ANALYTICS_REGISTRATION_WITH_AD_NETWORK=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"google_analytics_registration_with_ad_network_enabled\")\n if [[ $_ANALYTICS_REGISTRATION_WITH_AD_NETWORK ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_REGISTRATION_WITH_AD_NETWORK_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_REGISTRATION_WITH_AD_NETWORK\")\")\n fi\n\n # config.google_analytics_automatic_screen_reporting_enabled\n _ANALYTICS_AUTO_SCREEN_REPORTING=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"google_analytics_automatic_screen_reporting_enabled\")\n if [[ $_ANALYTICS_AUTO_SCREEN_REPORTING ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseAutomaticScreenReportingEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_SCREEN_REPORTING\")\")\n fi\n\n # config.perf_auto_collection_enabled\n _PERF_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_auto_collection_enabled\")\n if [[ $_PERF_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_enabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_AUTO_COLLECTION\")\")\n fi\n\n # config.perf_collection_deactivated\n _PERF_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_collection_deactivated\")\n if [[ $_PERF_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_deactivated\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_DEACTIVATED\")\")\n fi\n\n # config.messaging_auto_init_enabled\n _MESSAGING_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"messaging_auto_init_enabled\")\n if [[ $_MESSAGING_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseMessagingAutoInitEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_MESSAGING_AUTO_INIT\")\")\n fi\n\n # config.in_app_messaging_auto_colllection_enabled\n _FIAM_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"in_app_messaging_auto_collection_enabled\")\n if [[ $_FIAM_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseInAppMessagingAutomaticDataCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_FIAM_AUTO_INIT\")\")\n fi\n\n # config.app_check_token_auto_refresh\n _APP_CHECK_TOKEN_AUTO_REFRESH=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_check_token_auto_refresh\")\n if [[ $_APP_CHECK_TOKEN_AUTO_REFRESH ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseAppCheckTokenAutoRefreshEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_CHECK_TOKEN_AUTO_REFRESH\")\")\n fi\n\n # config.crashlytics_disable_auto_disabler - undocumented for now - mainly for debugging, document if becomes useful\n _CRASHLYTICS_AUTO_DISABLE_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"crashlytics_disable_auto_disabler\")\n if [[ $_CRASHLYTICS_AUTO_DISABLE_ENABLED == \"true\" ]]; then\n echo \"Disabled Crashlytics auto disabler.\" # do nothing\n else\n _PLIST_ENTRY_KEYS+=(\"FirebaseCrashlyticsCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"NO\")\n fi\nelse\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n echo \"warning: A firebase.json file was not found, whilst this file is optional it is recommended to include it to configure firebase services in React Native Firebase.\"\nfi;\n\necho \"info: 2) Injecting Info.plist entries: \"\n\n# Log out the keys we're adding\nfor i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n echo \" -> $i) ${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\"\ndone\n\nfor plist in \"${_TARGET_PLIST}\" \"${_DSYM_PLIST}\" ; do\n if [[ -f \"${plist}\" ]]; then\n\n # paths with spaces break the call to setPlistValue. temporarily modify\n # the shell internal field separator variable (IFS), which normally\n # includes spaces, to consist only of line breaks\n oldifs=$IFS\n IFS=\"\n\"\n\n for i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n setPlistValue \"${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\" \"${plist}\"\n done\n\n # restore the original internal field separator value\n IFS=$oldifs\n else\n echo \"warning: A Info.plist build output file was not found (${plist})\"\n fi\ndone\n\necho \"info: <- RNFB build script finished\"\n"; + }; B339FF00289ABD70001B89FB /* Override xcconfig files */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -1207,7 +1249,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMaskDebug.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1354; + CURRENT_PROJECT_VERSION = 1364; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1244,7 +1286,7 @@ "${inherited}", ); LLVM_LTO = YES; - MARKETING_VERSION = 7.24.4; + MARKETING_VERSION = 7.27.0; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = ( @@ -1272,7 +1314,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1354; + CURRENT_PROJECT_VERSION = 1364; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1307,7 +1349,7 @@ "${inherited}", ); LLVM_LTO = YES; - MARKETING_VERSION = 7.24.4; + MARKETING_VERSION = 7.27.0; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = ( @@ -1335,7 +1377,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMaskDebug.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1354; + CURRENT_PROJECT_VERSION = 1364; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1368,7 +1410,7 @@ ); LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift$(inherited)"; LLVM_LTO = YES; - MARKETING_VERSION = 7.24.4; + MARKETING_VERSION = 7.27.0; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = ( @@ -1396,7 +1438,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1354; + CURRENT_PROJECT_VERSION = 1364; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1427,7 +1469,7 @@ ); LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift$(inherited)"; LLVM_LTO = YES; - MARKETING_VERSION = 7.24.4; + MARKETING_VERSION = 7.27.0; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = ( @@ -1554,7 +1596,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMaskDebug.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1354; + CURRENT_PROJECT_VERSION = 1364; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 48XVW22RCG; @@ -1591,7 +1633,7 @@ "\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"", ); LLVM_LTO = YES; - MARKETING_VERSION = 7.24.4; + MARKETING_VERSION = 7.27.0; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = ( "$(inherited)", @@ -1622,7 +1664,7 @@ CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1354; + CURRENT_PROJECT_VERSION = 1364; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 48XVW22RCG; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG; @@ -1657,7 +1699,7 @@ "\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"", ); LLVM_LTO = YES; - MARKETING_VERSION = 7.24.4; + MARKETING_VERSION = 7.27.0; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = ( "$(inherited)", diff --git a/ios/Podfile b/ios/Podfile index c292007f659..9cf62d5125c 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -10,6 +10,7 @@ linkage = ENV['USE_FRAMEWORKS'] if linkage != nil Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green use_frameworks! :linkage => linkage.to_sym + $RNFirebaseAsStaticFramework = true end # react_native_post_install: diff --git a/ios/Podfile.lock b/ios/Podfile.lock old mode 100755 new mode 100644 index f3c1ddfaecb..a016584ce50 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -14,15 +14,18 @@ PODS: - React-Core (= 0.71.15) - React-jsi (= 0.71.15) - ReactCommon/turbomodule/core (= 0.71.15) - - Firebase (10.24.0): - - Firebase/Core (= 10.24.0) - - Firebase/Core (10.24.0): + - Firebase (10.27.0): + - Firebase/Core (= 10.27.0) + - Firebase/Core (10.27.0): - Firebase/CoreOnly - - FirebaseAnalytics (~> 10.24.0) - - Firebase/CoreOnly (10.24.0): - - FirebaseCore (= 10.24.0) - - FirebaseAnalytics (10.24.0): - - FirebaseAnalytics/AdIdSupport (= 10.24.0) + - FirebaseAnalytics (~> 10.27.0) + - Firebase/CoreOnly (10.27.0): + - FirebaseCore (= 10.27.0) + - Firebase/Messaging (10.27.0): + - Firebase/CoreOnly + - FirebaseMessaging (~> 10.27.0) + - FirebaseAnalytics (10.27.0): + - FirebaseAnalytics/AdIdSupport (= 10.27.0) - FirebaseCore (~> 10.0) - FirebaseInstallations (~> 10.0) - GoogleUtilities/AppDelegateSwizzler (~> 7.11) @@ -30,26 +33,37 @@ PODS: - GoogleUtilities/Network (~> 7.11) - "GoogleUtilities/NSData+zlib (~> 7.11)" - nanopb (< 2.30911.0, >= 2.30908.0) - - FirebaseAnalytics/AdIdSupport (10.24.0): + - FirebaseAnalytics/AdIdSupport (10.27.0): - FirebaseCore (~> 10.0) - FirebaseInstallations (~> 10.0) - - GoogleAppMeasurement (= 10.24.0) + - GoogleAppMeasurement (= 10.27.0) - GoogleUtilities/AppDelegateSwizzler (~> 7.11) - GoogleUtilities/MethodSwizzler (~> 7.11) - GoogleUtilities/Network (~> 7.11) - "GoogleUtilities/NSData+zlib (~> 7.11)" - nanopb (< 2.30911.0, >= 2.30908.0) - - FirebaseCore (10.24.0): + - FirebaseCore (10.27.0): - FirebaseCoreInternal (~> 10.0) - GoogleUtilities/Environment (~> 7.12) - GoogleUtilities/Logger (~> 7.12) - - FirebaseCoreInternal (10.24.0): + - FirebaseCoreExtension (10.28.0): + - FirebaseCore (~> 10.0) + - FirebaseCoreInternal (10.28.0): - "GoogleUtilities/NSData+zlib (~> 7.8)" - - FirebaseInstallations (10.24.0): + - FirebaseInstallations (10.28.0): - FirebaseCore (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/UserDefaults (~> 7.8) - PromisesObjC (~> 2.1) + - FirebaseMessaging (10.27.0): + - FirebaseCore (~> 10.0) + - FirebaseInstallations (~> 10.0) + - GoogleDataTransport (~> 9.3) + - GoogleUtilities/AppDelegateSwizzler (~> 7.8) + - GoogleUtilities/Environment (~> 7.8) + - GoogleUtilities/Reachability (~> 7.8) + - GoogleUtilities/UserDefaults (~> 7.8) + - nanopb (< 2.30911.0, >= 2.30908.0) - Flipper (0.125.0): - Flipper-Folly (~> 2.6) - Flipper-RSocket (~> 1.4) @@ -113,80 +127,84 @@ PODS: - FlipperKit/FlipperKitNetworkPlugin - fmt (6.2.1) - glog (0.3.5) - - GoogleAppMeasurement (10.24.0): - - GoogleAppMeasurement/AdIdSupport (= 10.24.0) + - GoogleAppMeasurement (10.27.0): + - GoogleAppMeasurement/AdIdSupport (= 10.27.0) - GoogleUtilities/AppDelegateSwizzler (~> 7.11) - GoogleUtilities/MethodSwizzler (~> 7.11) - GoogleUtilities/Network (~> 7.11) - "GoogleUtilities/NSData+zlib (~> 7.11)" - nanopb (< 2.30911.0, >= 2.30908.0) - - GoogleAppMeasurement/AdIdSupport (10.24.0): - - GoogleAppMeasurement/WithoutAdIdSupport (= 10.24.0) + - GoogleAppMeasurement/AdIdSupport (10.27.0): + - GoogleAppMeasurement/WithoutAdIdSupport (= 10.27.0) - GoogleUtilities/AppDelegateSwizzler (~> 7.11) - GoogleUtilities/MethodSwizzler (~> 7.11) - GoogleUtilities/Network (~> 7.11) - "GoogleUtilities/NSData+zlib (~> 7.11)" - nanopb (< 2.30911.0, >= 2.30908.0) - - GoogleAppMeasurement/WithoutAdIdSupport (10.24.0): + - GoogleAppMeasurement/WithoutAdIdSupport (10.27.0): - GoogleUtilities/AppDelegateSwizzler (~> 7.11) - GoogleUtilities/MethodSwizzler (~> 7.11) - GoogleUtilities/Network (~> 7.11) - "GoogleUtilities/NSData+zlib (~> 7.11)" - nanopb (< 2.30911.0, >= 2.30908.0) - - GoogleUtilities (7.13.0): - - GoogleUtilities/AppDelegateSwizzler (= 7.13.0) - - GoogleUtilities/Environment (= 7.13.0) - - GoogleUtilities/ISASwizzler (= 7.13.0) - - GoogleUtilities/Logger (= 7.13.0) - - GoogleUtilities/MethodSwizzler (= 7.13.0) - - GoogleUtilities/Network (= 7.13.0) - - "GoogleUtilities/NSData+zlib (= 7.13.0)" - - GoogleUtilities/Privacy (= 7.13.0) - - GoogleUtilities/Reachability (= 7.13.0) - - GoogleUtilities/SwizzlerTestHelpers (= 7.13.0) - - GoogleUtilities/UserDefaults (= 7.13.0) - - GoogleUtilities/AppDelegateSwizzler (7.13.0): + - GoogleDataTransport (9.4.1): + - GoogleUtilities/Environment (~> 7.7) + - nanopb (< 2.30911.0, >= 2.30908.0) + - PromisesObjC (< 3.0, >= 1.2) + - GoogleUtilities (7.13.3): + - GoogleUtilities/AppDelegateSwizzler (= 7.13.3) + - GoogleUtilities/Environment (= 7.13.3) + - GoogleUtilities/ISASwizzler (= 7.13.3) + - GoogleUtilities/Logger (= 7.13.3) + - GoogleUtilities/MethodSwizzler (= 7.13.3) + - GoogleUtilities/Network (= 7.13.3) + - "GoogleUtilities/NSData+zlib (= 7.13.3)" + - GoogleUtilities/Privacy (= 7.13.3) + - GoogleUtilities/Reachability (= 7.13.3) + - GoogleUtilities/SwizzlerTestHelpers (= 7.13.3) + - GoogleUtilities/UserDefaults (= 7.13.3) + - GoogleUtilities/AppDelegateSwizzler (7.13.3): - GoogleUtilities/Environment - GoogleUtilities/Logger - GoogleUtilities/Network - GoogleUtilities/Privacy - - GoogleUtilities/Environment (7.13.0): + - GoogleUtilities/Environment (7.13.3): - GoogleUtilities/Privacy - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/ISASwizzler (7.13.0): + - GoogleUtilities/ISASwizzler (7.13.3): - GoogleUtilities/Privacy - - GoogleUtilities/Logger (7.13.0): + - GoogleUtilities/Logger (7.13.3): - GoogleUtilities/Environment - GoogleUtilities/Privacy - - GoogleUtilities/MethodSwizzler (7.13.0): + - GoogleUtilities/MethodSwizzler (7.13.3): - GoogleUtilities/Logger - GoogleUtilities/Privacy - - GoogleUtilities/Network (7.13.0): + - GoogleUtilities/Network (7.13.3): - GoogleUtilities/Logger - "GoogleUtilities/NSData+zlib" - GoogleUtilities/Privacy - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (7.13.0)": + - "GoogleUtilities/NSData+zlib (7.13.3)": - GoogleUtilities/Privacy - - GoogleUtilities/Privacy (7.13.0) - - GoogleUtilities/Reachability (7.13.0): + - GoogleUtilities/Privacy (7.13.3) + - GoogleUtilities/Reachability (7.13.3): - GoogleUtilities/Logger - GoogleUtilities/Privacy - - GoogleUtilities/SwizzlerTestHelpers (7.13.0): + - GoogleUtilities/SwizzlerTestHelpers (7.13.3): - GoogleUtilities/MethodSwizzler - - GoogleUtilities/UserDefaults (7.13.0): + - GoogleUtilities/UserDefaults (7.13.3): - GoogleUtilities/Logger - GoogleUtilities/Privacy - - GZIP (1.3.1) + - GZIP (1.3.2) - GzipSwift (5.1.1) - libevent (2.1.12) - lottie-ios (3.4.1) - lottie-react-native (5.1.5): - lottie-ios (~> 3.4.0) - React-Core - - MMKV (1.3.3): - - MMKVCore (~> 1.3.3) - - MMKVCore (1.3.3) + - MMKV (1.3.5): + - MMKVCore (~> 1.3.5) + - MMKVCore (1.3.5) - MultiplatformBleAdapter (0.2.0) - nanopb (2.30910.0): - nanopb/decode (= 2.30910.0) @@ -500,7 +518,7 @@ PODS: - React-Core - react-native-view-shot (3.1.2): - React - - react-native-webview (11.13.0): + - react-native-webview-mm (14.0.2): - React-Core - React-perflogger (0.71.15) - React-RCTActionSheet (0.71.15): @@ -607,6 +625,14 @@ PODS: - React - RNDeviceInfo (9.0.2): - React-Core + - RNFBApp (20.1.0): + - Firebase/CoreOnly (= 10.27.0) + - React-Core + - RNFBMessaging (20.1.0): + - Firebase/Messaging (= 10.27.0) + - FirebaseCoreExtension + - React-Core + - RNFBApp - RNFS (2.20.0): - React-Core - RNGestureHandler (1.10.3): @@ -762,7 +788,7 @@ DEPENDENCIES: - "react-native-splash-screen (from `../node_modules/@metamask/react-native-splash-screen`)" - react-native-video (from `../node_modules/react-native-video`) - react-native-view-shot (from `../node_modules/react-native-view-shot`) - - react-native-webview (from `../node_modules/react-native-webview`) + - "react-native-webview-mm (from `../node_modules/@metamask/react-native-webview`)" - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`) - React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`) @@ -786,6 +812,8 @@ DEPENDENCIES: - "RNDateTimePicker (from `../node_modules/@react-native-community/datetimepicker`)" - RNDefaultPreference (from `../node_modules/react-native-default-preference`) - RNDeviceInfo (from `../node_modules/react-native-device-info`) + - "RNFBApp (from `../node_modules/@react-native-firebase/app`)" + - "RNFBMessaging (from `../node_modules/@react-native-firebase/messaging`)" - RNFS (from `../node_modules/react-native-fs`) - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) - RNI18n (from `../node_modules/react-native-i18n`) @@ -814,8 +842,10 @@ SPEC REPOS: - Firebase - FirebaseAnalytics - FirebaseCore + - FirebaseCoreExtension - FirebaseCoreInternal - FirebaseInstallations + - FirebaseMessaging - Flipper - Flipper-Boost-iOSX - Flipper-DoubleConversion @@ -827,6 +857,7 @@ SPEC REPOS: - FlipperKit - fmt - GoogleAppMeasurement + - GoogleDataTransport - GoogleUtilities - GZIP - GzipSwift @@ -939,8 +970,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-video" react-native-view-shot: :path: "../node_modules/react-native-view-shot" - react-native-webview: - :path: "../node_modules/react-native-webview" + react-native-webview-mm: + :path: "../node_modules/@metamask/react-native-webview" React-perflogger: :path: "../node_modules/react-native/ReactCommon/reactperflogger" React-RCTActionSheet: @@ -987,6 +1018,10 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-default-preference" RNDeviceInfo: :path: "../node_modules/react-native-device-info" + RNFBApp: + :path: "../node_modules/@react-native-firebase/app" + RNFBMessaging: + :path: "../node_modules/@react-native-firebase/messaging" RNFS: :path: "../node_modules/react-native-fs" RNGestureHandler: @@ -1035,11 +1070,13 @@ SPEC CHECKSUMS: DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 FBLazyVector: d06bbe89e3a89ee90c4deab1c84bf306ffa5ed37 FBReactNativeSpec: d5d9871fe5c4b61787a3aed4f9e5529908e22069 - Firebase: 91fefd38712feb9186ea8996af6cbdef41473442 - FirebaseAnalytics: b5efc493eb0f40ec560b04a472e3e1a15d39ca13 - FirebaseCore: 11dc8a16dfb7c5e3c3f45ba0e191a33ac4f50894 - FirebaseCoreInternal: bcb5acffd4ea05e12a783ecf835f2210ce3dc6af - FirebaseInstallations: 8f581fca6478a50705d2bd2abd66d306e0f5736e + Firebase: 26b040b20866a55f55eb3611b9fcf3ae64816b86 + FirebaseAnalytics: f9211b719db260cc91aebee8bb539cb367d0dfd1 + FirebaseCore: a2b95ae4ce7c83ceecfbbbe3b6f1cddc7415a808 + FirebaseCoreExtension: f63147b723e2a700fe0f34ec6fb7f358d6fe83e0 + FirebaseCoreInternal: 58d07f1362fddeb0feb6a857d1d1d1c5e558e698 + FirebaseInstallations: 60c1d3bc1beef809fd1ad1189a8057a040c59f2e + FirebaseMessaging: 585984d0a1df120617eb10b44cad8968b859815e Flipper: 26fc4b7382499f1281eb8cb921e5c3ad6de91fe0 Flipper-Boost-iOSX: fd1e2b8cbef7e662a122412d7ac5f5bea715403c Flipper-DoubleConversion: 2dc99b02f658daf147069aad9dbd29d8feb06d30 @@ -1051,15 +1088,16 @@ SPEC CHECKSUMS: FlipperKit: cbdee19bdd4e7f05472a66ce290f1b729ba3cb86 fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b - GoogleAppMeasurement: f3abf08495ef2cba7829f15318c373b8d9226491 - GoogleUtilities: d053d902a8edaa9904e1bd00c37535385b8ed152 - GZIP: e6922ed5bdd1d77d84589d50821ac34ea0c38d4b + GoogleAppMeasurement: f65fc137531af9ad647f1c0a42f3b6a4d3a98049 + GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a + GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 + GZIP: 3c0abf794bfce8c7cb34ea05a1837752416c8868 GzipSwift: 893f3e48e597a1a4f62fafcb6514220fcf8287fa libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 lottie-ios: 016449b5d8be0c3dcbcfa0a9988469999cd04c5d lottie-react-native: 3e722c63015fdb9c27638b0a77969fc412067c18 - MMKV: f902fb6719da13c2ab0965233d8963a59416f911 - MMKVCore: d26e4d3edd5cb8588c2569222cbd8be4231374e9 + MMKV: 506311d0494023c2f7e0b62cc1f31b7370fa3cfb + MMKVCore: 9e2e5fd529b64a9fe15f1a7afb3d73b2e27b4db9 MultiplatformBleAdapter: b1fddd0d499b96b607e00f0faa8e60648343dc1d nanopb: 438bc412db1928dac798aa6fd75726007be04262 OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c @@ -1104,7 +1142,7 @@ SPEC CHECKSUMS: react-native-splash-screen: 49a7160705f32169d27ab8dff9dda53331592412 react-native-video: c26780b224543c62d5e1b2a7244a5cd1b50e8253 react-native-view-shot: 4475fde003fe8a210053d1f98fb9e06c1d834e1c - react-native-webview: 133a6a5149f963259646e710b4545c67ef35d7c9 + react-native-webview-mm: c7df943491b1c6d2c48238778c09a4d05cd2b322 React-perflogger: 0cc42978a483a47f3696171dac2e7033936fc82d React-RCTActionSheet: ea922b476d24f6d40b8e02ac3228412bd3637468 React-RCTAnimation: 7be2c148398eaa5beac950b2b5ec7102389ec3ad @@ -1128,6 +1166,8 @@ SPEC CHECKSUMS: RNDateTimePicker: b722c23030b744763cf51d80fc22f354653f65e0 RNDefaultPreference: 2f8d6d54230edbd78708ada8d63bb275e5a8415b RNDeviceInfo: 1e3f62b9ec32f7754fac60bd06b8f8a27124e7f0 + RNFBApp: 1ae7462cddf74a49df206d3418bc0170f8fa53e5 + RNFBMessaging: 85f661b9f16e2b081e6809ef63d3daa4458b9042 RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: a479ebd5ed4221a810967000735517df0d2db211 RNI18n: e2f7e76389fcc6e84f2c8733ea89b92502351fd8 @@ -1152,6 +1192,6 @@ SPEC CHECKSUMS: Yoga: 68c9c592c3e80ec37ff28db20eedb13d84aae5df YogaKit: f782866e155069a2cca2517aafea43200b01fd5a -PODFILE CHECKSUM: 9fbad5d11f3fd3d334c7bd651b28783ab9300999 +PODFILE CHECKSUM: 4e83bdef1bf5a5eb84c707eaa0b97f811ea0b6d5 COCOAPODS: 1.15.2 diff --git a/jest.config.js b/jest.config.js index 2cbdf35c327..20f340f51d5 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,5 +1,4 @@ process.env.TZ = 'America/Toronto'; -process.env.MM_BLOCKAID_UI_ENABLED = 'true'; process.env.SEGMENT_DELETE_API_SOURCE_ID = 'testSourceId'; process.env.SEGMENT_REGULATIONS_ENDPOINT = 'TestRegulationsEndpoint'; @@ -31,6 +30,7 @@ const config = { '/app/util/testUtils/', '/app/lib/ppom/ppom.html.js', '/app/lib/ppom/blockaid-version.js', + '/app/core/InpageBridgeWeb3.js', ], coverageReporters: ['text-summary', 'lcov'], coverageDirectory: '/tests/coverage', @@ -38,6 +38,7 @@ const config = { moduleNameMapper: { '\\.svg': '/app/__mocks__/svgMock.js', '\\.png': '/app/__mocks__/pngMock.js', + '\\webview/index.html': '/app/__mocks__/htmlMock.ts', }, // Disable jest cache cache: false, diff --git a/locales/languages/de.json b/locales/languages/de.json index 8fdbf4e4760..d511acd16f0 100644 --- a/locales/languages/de.json +++ b/locales/languages/de.json @@ -5,8 +5,6 @@ "deceptive_request_title": "Dies ist eine betrügerische Anfrage.", "failed_title": "Anfrage ist möglicherweise nicht sicher", "failed_description": "Aufgrund eines Fehlers wurde diese Anfrage vom Sicherheitsanbieter nicht überprüft. Gehen Sie mit Bedacht vor.", - "loading_title": "Suche nach Sicherheitsbenachrichtigungen ...", - "loading_complete_title": "Führen Sie immer Ihre eigene Due-Diligence-Prüfung durch, bevor Sie Anfragen genehmigen.", "malicious_domain_description": "Sie haben es mit einer bösartigen Domain zu tun. Wenn Sie diese Anfrage genehmigen, könnten Sie Ihre Assets verlieren.", "other_description": "Wenn Sie diese Anfrage genehmigen, könnten Sie Ihre Assets verlieren.", "raw_signature_farming_description": "Wenn Sie diese Anfrage genehmigen, könnten Sie Ihre Assets verlieren.", @@ -613,6 +611,10 @@ "remove_account_message": "Möchten Sie dieses Konto wirklich entfernen?", "no": "Nein", "yes_remove_it": "Ja, entfernen", + "remove_hardware_account": "Remove hardware account", + "remove_hw_account_alert_description": "Are you sure you want to remove this hardware wallet account? You’ll have to resync your hardware wallet if you want to use this account again with MetaMask Mobile.", + "remove_account_alert_remove_btn": "Remove", + "remove_account_alert_cancel_btn": "Nevermind", "accounts_title": "Konten", "connect_account_title": "Konto verbinden", "connect_accounts_title": "Konten verbinden", @@ -668,8 +670,7 @@ "hint_text": "Scannen Sie Ihre Keystone-Wallet, um", "purpose_connect": "verbinden", "purpose_sign": "die Transaktion zu bestätigen", - "select_accounts": "Ein Konto auswählen", - "please_wait": "Bitte warten" + "select_accounts": "Ein Konto auswählen" }, "data_collection_modal": { "accept": "Okay", @@ -682,6 +683,11 @@ "forget": "Dieses Gerät entfernen" }, "app_settings": { + "enabling_notifications": "Enabling notifications...", + "disabling_notifications": "Disabling notifications...", + "enabling_profile_sync": "Enabling profile syncing...", + "disabling_profile_sync": "Disabling profile syncing...", + "notifications_dismiss_modal": "Dismiss", "title": "Einstellungen", "current_conversion": "Basiswährung", "current_language": "Aktuelle Sprache", @@ -805,6 +811,7 @@ "security_desc": "Datenschutzeinstellungen, MetaMetrics, privater Schlüssel und geheime Wiederherstellungsphrase für die Wallet", "networks_title": "Netzwerke", "networks_default_title": "Standardnetzwerk", + "network_delete": "Wenn Sie dieses Netzwerk löschen, müssen Sie es erneut hinzufügen, um Ihre Assets in diesem Netzwerk anzuzeigen", "networks_default_cta": "Dieses Netzwerk verwenden", "networks_desc": "Benutzerdefinierte RPC-Netzwerke hinzufügen und verwalten", "network_name_label": "Netzwerkname", @@ -820,6 +827,7 @@ "network_other_networks": "Sonstige Netzwerke", "network_rpc_networks": "RPC-Netzwerke", "network_add_network": "Netzwerk hinzufügen", + "network_add_custom_network": "Benutzerdefiniertes Netzwerk hinzufügen", "network_add": "Hinzufügen", "network_save": "Speichern", "remove_network_title": "Möchten Sie dieses Netzwerk entfernen?", @@ -994,7 +1002,7 @@ "token_nft_ens_subheading": "Automatische Token-, NFT- und ENS-Erkennung", "security_check_subheading": "Sicherheitsprüfungen", "symbol_required": "Symbol ist erforderlich.", - "blockaid_desc": "Wahrung der Privatsphäre – es werden keine Daten an Dritte weitergegeben. Verfügbar auf Arbitrum, Avalanche, BNB Chain, Ethereum Mainnet, Optimism, Polygon, Sepolia und Base.", + "blockaid_desc": "Diese Funktion warnt Sie durch die aktive Überprüfung von Transaktions- und Signaturanfragen vor bösartigen Aktivitäten.", "security_alerts": "Sicherheitsbenachrichtigungen", "security_alerts_desc": "Diese Funktion warnt Sie vor bösartigen Aktivitäten, indem sie Ihre Transaktions- und Signaturanfragen lokal überprüft. Führen Sie immer Ihre eigene Prüfung durch, bevor Sie Anfragen genehmigen. Es gibt keine Garantie dafür, dass diese Funktion alle bösartigen Aktivitäten erkennt. Mit der Aktivierung dieser Funktion erklären Sie sich mit den Nutzungsbedingungen des Anbieters einverstanden.", "smart_transactions_opt_in_heading": "Smart Transactions", @@ -1004,7 +1012,7 @@ "simulation_details_description": "Schalten Sie dies ein, um die Kontostandänderungen von Transaktionen zu schätzen, bevor Sie sie bestätigen. Dies ist keine Garantie für das endgültige Ergebnis Ihrer Transaktionen. ", "simulation_details_learn_more": "Erfahren Sie mehr.", "aes_crypto_test_form_title": "AES Crypto – Testformular", - "aes_crypto_test_form_description": "Abschnitt, der ausschließlich für E2E-Tests entwickelt wurde. Wenn dies in Ihrer App sichtbar ist, melden Sie dies bitte dem MetaMask-Support." + "aes_crypto_test_form_description": "Abschnitt, der ausschließlich für E2E-Tests entwickelt wurde. Wenn dies in Ihrer App sichtbar ist, melden Sie es bitte dem MetaMask-Support." }, "aes_crypto_test_form": { "generate_random_salt": "Zufällige Salt generieren", @@ -1031,6 +1039,7 @@ "cancel": "Stornieren", "loading": "Verbindung mit MetaMask wird hergestellt ...", "unkown_dapp": "DAPP-Name nicht verfügbar", + "unknown": "Unbekannt", "no_connections": "Keine Verbindungen", "no_connections_desc": "Wenn Sie ein Konto mit einer Website oder einer App verbinden, wird es hier angezeigt." }, @@ -1411,7 +1420,7 @@ "token": "Token", "nft": "NFT", "you_trust_this_site": "Durch Genehmigungserteilung gewähren Sie dem folgenden Drittanbieter Zugriff auf Ihre Gelder.", - "you_trust_this_third_party": "Dies gestattet es Dritten, ohne weitere Benachrichtigung auf Ihre NFTs zuzugreifen und diese zu übertragen, bis Sie dieses Zugriffsrecht widerrufen.", + "you_trust_this_third_party": "Dies gestattet Dritten, ohne weitere Benachrichtigung auf Ihre NFTs zuzugreifen und diese zu übertragen, bis Sie dieses Zugriffsrecht widerrufen.", "you_trust_this_address": "Vertrauen Sie dieser Adresse? Wenn Sie diese Genehmigung erteilen, gewähren Sie dieser Adresse Zugriff auf Ihre Gelder.", "edit_permission": "Genehmigung bearbeiten", "edit": "Bearbeiten", @@ -1654,6 +1663,7 @@ "network_currency_symbol": "Währungssymbol", "network_block_explorer_url": "Block-Explorer-URL", "search": "Suche nach einem zuvor hinzugefügten Netzwerk", + "search-short": "Suchen", "add": "Hinzufügen", "cancel": "Stornieren", "approve": "Genehmigen", @@ -1661,7 +1671,9 @@ "malicious_network_warning": "Ein betrügerischer Netzwerkanbieter kann bezüglich des Status der Blockchain täuschen und Ihre Netzwerkaktivitäten aufzeichnen. Fügen Sie nur vertrauenswürdige benutzerdefinierte Netzwerke hinzu.", "security_link": "https://support.metamask.io/networks-and-sidechains/managing-networks/user-guide-custom-networks-and-sidechains/", "network_warning_title": "Netzwerkinformationen", + "additional_network_information_title": "Zusätzliche Netzwerkinformationen", "network_warning_desc": "Diese Netzwerkverbindung ist von Dritten abhängig. Diese Verbindung kann unzuverlässig sein oder es Dritten ermöglichen, Aktivitäten zu verfolgen.", + "additonial_network_information_desc": "Einige dieser Netzwerke werden von Dritten betrieben. Die Verbindungen können weniger zuverlässig sein oder Dritten die Verfolgung von Aktivitäten ermöglichen.", "learn_more": "Mehr erfahren", "learn_more_url": "https://support.metamask.io/networks-and-sidechains/managing-networks/the-risks-of-connecting-to-an-unknown-network/", "switch_network": "Zu Netzwerk wechseln", @@ -1950,7 +1962,7 @@ "received": "Empfangen", "unstaked": "Unstaked", "to": "An", - "rate": "Preis (einschließlich Gebühren)", + "rate": "Rate (fees included)", "unstaking_requested": "Unstaking angefordert", "stake_completed": "Stake vervollständigt", "withdrawal_completed": "Abhebung abgeschlossen", @@ -1960,6 +1972,49 @@ "swap_completed": "Getauscht {{from}} für {{to}}", "swap": "Geswappt", "sent": "An {{address}} gesendet", + "menu_item_title": { + "metamask_swap_completed": "Swapped {{symbol1}} for {{symbol2}}", + "erc20_sent": "Sent to {{address}}", + "erc20_received": "Received from {{address}}", + "eth_sent": "Sent to {{address}}", + "eth_received": "Received from {{address}}", + "erc721_sent": "Sent NFT to {{address}}", + "erc1155_sent": "Sent NFT to {{address}}", + "erc721_received": "Received NFT from {{address}}", + "erc1155_received": "Received NFT from {{address}}", + "rocketpool_stake_completed": "Staked", + "rocketpool_unstake_completed": "Unstaking complete", + "lido_stake_completed": "Staked", + "lido_withdrawal_requested": "Unstaking requested", + "lido_withdrawal_completed": "Unstaking complete", + "lido_stake_ready_to_be_withdrawn": "Withdrawal requested" + }, + "menu_item_description": { + "lido_withdrawal_requested": "Your request to unstake {{amount}} {{symbol}} has been sent", + "lido_stake_ready_to_be_withdrawn": "You can now withdraw your unstaked {{symbol}}" + }, + "modal": { + "title_sent": "Sent {{symbol}}", + "title_received": "Received {{symbol}}", + "title_unstake_requested": "Unstaking requested", + "title_untake_ready": "Withdrawl ready", + "title_stake": "Staked {{symbol}}", + "title_unstake_completed": "Unstaking completed", + "title_swapped": "Swapped {{symbol1}} to {{symbol2}}", + "label_address_to": "To", + "label_address_from": "From", + "label_address_to_you": "To (You)", + "label_address_from_you": "From (You)", + "label_asset": "Asset", + "label_account": "Account", + "label_unstaking_requested": "Unstaking Requested", + "label_unstake_ready": "Withdrawl ready", + "label_staked": "Staked", + "label_received": "Received", + "label_unstaking_confirmed": "Unstaking Confirmed", + "label_swapped": "Swapped", + "label_to": "To" + }, "received_from": "Erhalten von {{address}}", "nft_sent": "NFT an {{address}} gesendet", "erc721_sent": "NFT an {{address}} gesendet", @@ -1993,6 +2048,8 @@ "received_message": "Zum Anzeigen dieser Transaktion tippen", "received_payment_message": "Sie haben {{amount}} DAI empfangen.", "prompt_title": "Push-Benachrichtigungen aktivieren", + "notifications_enabled_error_title": "Something went wrong", + "notifications_enabled_error_desc": "We couldn't enable notifications. Please try again later.", "prompt_desc": "Aktivieren Sie Benachrichtigungen, damit MetaMask Ihnen Bescheid geben kann, wenn Sie ETH erhalten oder wenn Ihre Transaktionen bestätigt werden.", "prompt_ok": "Ja", "prompt_cancel": "Nein, danke!", @@ -2017,6 +2074,7 @@ "1": "Wallet", "2": "Ankündigungen" }, + "copied_to_clipboard": "Copied to clipboard", "address_copied_to_clipboard": "Adresse in die Zwischenablage kopiert", "transaction_id_copied_to_clipboard": "Transaktions-ID in die Zwischenablage kopiert", "activation_card": { @@ -2627,7 +2685,7 @@ "save_seedphrase_3": "und installieren Sie die App neu. Achtung: Sie können Ihre Wallet ohne ihre geheime Wiederherstellungsphrase NICHT wiederherstellen.", "copied_clipboard": "In die Zwischenablage kopiert", "ok": "OK", - "cancel": "Stornieren", + "cancel": "Abbrechen", "send": "Senden" }, "whats_new": { @@ -2648,7 +2706,7 @@ "header": "Verbesserter Transaktionsschutz", "description_1": "Erzielen Sie mit Smart Transactions höhere Erfolgsraten, einen Frontrunning-Schutz und eine bessere Transparenz.", "description_2": "Nur auf Ethereum verfügbar. Sie können diese Funktion jederzeit in den Einstellungen aktivieren oder deaktivieren.", - "secondary_button": "In Einstellungen verwalten", + "no_thanks": "No thanks", "primary_button": "Aktivieren", "learn_more": "Erfahren Sie mehr.", "benefit_1_1": "Erfolgsrate:", @@ -2659,7 +2717,7 @@ "benefit_3_2": "Updates" }, "transaction_simulation": { - "title": "Geschätzte Kontostandänderungen", + "title": "Geschätzte Saldoänderungen", "description_1": "Jetzt können Sie das mögliche Ergebnis Ihrer Transaktionen sehen, bevor Sie sie tätigen!", "description_2": "Dies ist nur eine Simulation, wir können also nicht für das Endergebnis garantieren. Sie können dies jederzeit unter Einstellungen > Sicherheit und Datenschutz ausschalten." } @@ -2847,6 +2905,15 @@ "enable_remember_me": "„Angemeldet bleiben“ aktivieren", "enable_remember_me_description": "Wenn „Angemeldet bleiben“ aktiviert ist, kann jeder, der Zugriff zu Ihrem Handy hat, auf Ihr MetaMask-Konto zugreifen." }, + "profile_sync": { + "title": "Profile Sync", + "disable_warning": "If you turn off profile sync, you won’t be able to receive notifications.", + "enable_description": "Creates a profile that MetaMask uses to sync some settings among your devices. This is required to get notifications. ", + "enable_description2": " to learn more.", + "enable_privacy_link": "Learn how we protect your privacy", + "bottomSheetTurnOn": "Turning on Profile Sync", + "bottomSheetTurnOff": "Turning off Profile Sync" + }, "turn_off_remember_me": { "title": "Geben Sie Ihr Passwort ein, um „Angemeldet bleiben“ zu deaktivieren.", "placeholder": "Passwort", @@ -3069,7 +3136,7 @@ }, "install_snap": { "title": "Verbindungsanfrage", - "description": "{{origin}} möchte {{snap}} herunterladen und sich mit diesem verbinden. Stellen Sie sicher, dass Sie den Autoren vertrauen, bevor Sie fortfahren.", + "description": "{{origin}} möchte {{snap}} verwenden.", "permissions_request_title": "Genehmigungsanfrage", "permissions_request_description": "{{origin}} möchte {{snap}} installieren, das die folgenden Genehmigungen anfordert.", "approve_permissions": "Genehmigen", @@ -3118,5 +3185,8 @@ "title": "Geschätzte Änderungen", "tooltip_description": "Die geschätzten Änderungen sind das, was passieren könnte, wenn Sie diese Transaktion durchführen. Dies ist nur eine Prognostizierung, keine Garantie.", "total_fiat": "Gesamt = {{currency}}" + }, + "common": { + "please_wait": "Please wait" } -} +} \ No newline at end of file diff --git a/locales/languages/el.json b/locales/languages/el.json index bbf8287883f..a24e66e6f0c 100644 --- a/locales/languages/el.json +++ b/locales/languages/el.json @@ -5,8 +5,6 @@ "deceptive_request_title": "Πρόκειται για παραπλανητικό αίτημα", "failed_title": "Το αίτημα μπορεί να μην είναι ασφαλές", "failed_description": "Λόγω σφάλματος, αυτό το αίτημα δεν επαληθεύτηκε από τον πάροχο ασφαλείας. Προχωρήστε με προσοχή.", - "loading_title": "Έλεγχος για ειδοποιήσεις ασφαλείας...", - "loading_complete_title": "Δεν λαβαμε ειδοποιήσεις. Κάνετε πάντα τη δική σας ενδελεχή έρευνα προτού εγκρίνετε τα αιτήματα.", "malicious_domain_description": "Αλληλεπιδράτε με έναν κακόβουλο τομέα. Εάν εγκρίνετε αυτό το αίτημα, ενδέχεται να χάσετε τα περιουσιακά σας στοιχεία.", "other_description": "Εάν εγκρίνετε αυτό το αίτημα, ενδέχεται να χάσετε τα περιουσιακά σας στοιχεία.", "raw_signature_farming_description": "Εάν εγκρίνετε αυτό το αίτημα, ενδέχεται να χάσετε τα περιουσιακά σας στοιχεία.", @@ -613,6 +611,10 @@ "remove_account_message": "Θέλετε σίγουρα να αφαιρέσετε αυτόν τον λογαριασμό;", "no": "Όχι", "yes_remove_it": "Ναι, αφαιρέστε τον", + "remove_hardware_account": "Remove hardware account", + "remove_hw_account_alert_description": "Are you sure you want to remove this hardware wallet account? You’ll have to resync your hardware wallet if you want to use this account again with MetaMask Mobile.", + "remove_account_alert_remove_btn": "Remove", + "remove_account_alert_cancel_btn": "Nevermind", "accounts_title": "Λογαριασμοί", "connect_account_title": "Σύνδεση λογαριασμού", "connect_accounts_title": "Σύνδεση λογαριασμών", @@ -668,8 +670,7 @@ "hint_text": "Σαρώστε το Keystone πορτοφόλι σας για να ", "purpose_connect": "συνδεθείτε", "purpose_sign": "επιβεβαιώσετε τη συναλλαγή", - "select_accounts": "Επιλέξτε Λογαριασμό", - "please_wait": "Παρακαλούμε περιμένετε" + "select_accounts": "Επιλέξτε Λογαριασμό" }, "data_collection_modal": { "accept": "Εντάξει", @@ -682,6 +683,11 @@ "forget": "Αγνοήστε αυτή τη συσκευή" }, "app_settings": { + "enabling_notifications": "Enabling notifications...", + "disabling_notifications": "Disabling notifications...", + "enabling_profile_sync": "Enabling profile syncing...", + "disabling_profile_sync": "Disabling profile syncing...", + "notifications_dismiss_modal": "Dismiss", "title": "Ρυθμίσεις", "current_conversion": "Βασικό Νόμισμα", "current_language": "Τρέχουσα Γλώσσα", @@ -805,6 +811,7 @@ "security_desc": "Ρυθμίσεις απορρήτου, MetaMetrics, ιδιωτικό κλειδί και Μυστική Φράση Ανάκτησης πορτοφολιού", "networks_title": "Δίκτυα", "networks_default_title": "Προεπιλεγμένο Δίκτυο", + "network_delete": "Εάν διαγράψετε αυτό το δίκτυο, θα πρέπει να το προσθέσετε ξανά για να δείτε τα περιουσιακά σας στοιχεία σε αυτό το δίκτυο", "networks_default_cta": "Χρήση αυτού του δικτύου", "networks_desc": "Προσθέστε και επεξεργαστείτε προσαρμοσμένα δίκτυα RPC", "network_name_label": "Όνομα Δικτύου", @@ -820,6 +827,7 @@ "network_other_networks": "Άλλα Δίκτυα", "network_rpc_networks": "Δίκτυα RPC", "network_add_network": "Προσθήκη Δικτύου", + "network_add_custom_network": "Προσθήκη προσαρμοσμένου δικτύου", "network_add": "Προσθήκη", "network_save": "Αποθήκευση", "remove_network_title": "Θέλετε να αφαιρέσετε αυτό το δίκτυο;", @@ -994,7 +1002,7 @@ "token_nft_ens_subheading": "Αυτόματος εντοπισμός Tokens, NFS και ENS", "security_check_subheading": "Έλεγχοι ασφαλείας", "symbol_required": "Απαιτείται το σύμβολο.", - "blockaid_desc": "Διαφύλαξη της ιδιωτικής ζωής - δεν κοινοποιούνται δεδομένα σε τρίτους. Διατίθεται στα Arbitrum, Avalanche, BNB chain, Ethereum Mainnet, Optimism, Polygon, Sepolia και Base.", + "blockaid_desc": "Αυτή η λειτουργία σας προειδοποιεί για κακόβουλη δραστηριότητα, καθώς ελέγχει ενεργά τα αιτήματα συναλλαγών και υπογραφών.", "security_alerts": "Ειδοποιήσεις ασφαλείας", "security_alerts_desc": "Αυτή η λειτουργία σας προειδοποιεί για κακόβουλη δραστηριότητα, καθώς ελέγχει τοπικά τα αιτήματα συναλλαγών και υπογραφών σας. Πάντα να κάνετε τη δική σας επιμελή έρευνα προτού εγκρίνετε οποιαδήποτε αιτήματα. Δεν υπάρχει καμία εγγύηση ότι αυτή η λειτουργία θα εντοπίσει όλες τις κακόβουλες δραστηριότητες. Ενεργοποιώντας αυτή τη λειτουργία, συμφωνείτε με τους όρους χρήσης του παρόχου.", "smart_transactions_opt_in_heading": "Έξυπνες Συναλλαγές", @@ -1031,6 +1039,7 @@ "cancel": "Άκυρο", "loading": "Σύνδεση στο MetaMask...", "unkown_dapp": "Το όνομα DAPP δεν είναι διαθέσιμο", + "unknown": "Άγνωστο", "no_connections": "Χωρίς συνδέσεις", "no_connections_desc": "Εάν συνδέσετε έναν λογαριασμό σε έναν ιστότοπο ή μια εφαρμογή, θα τον δείτε εδώ." }, @@ -1654,6 +1663,7 @@ "network_currency_symbol": "Σύμβολο Νομίσματος", "network_block_explorer_url": "Διεύθυνση URL του Block Explorer", "search": "Αναζήτηση για δίκτυο που έχει ήδη προστεθεί", + "search-short": "Αναζήτηση", "add": "Προσθήκη", "cancel": "Άκυρο", "approve": "Έγκριση", @@ -1661,7 +1671,9 @@ "malicious_network_warning": "Ένας κακόβουλος πάροχος δικτύου μπορεί να πει ψέματα για την κατάσταση του blockchain και να καταγράψει τη δραστηριότητα του δικτύου σας. Να προσθέτετε μόνο προσαρμοσμένα δίκτυα που εμπιστεύεστε.", "security_link": "https://support.metamask.io/networks-and-sidechains/managing-networks/user-guide-custom-networks-and-sidechains/", "network_warning_title": "Πληροφορίες Δικτύου", + "additional_network_information_title": "Πρόσθετες πληροφορίες δικτύων", "network_warning_desc": "Αυτή η σύνδεση δικτύου βασίζεται σε τρίτους. H σύνδεση ενδέχεται να είναι λιγότερο αξιόπιστη ή να επιτρέπει σε τρίτους να παρακολουθούν τη δραστηριότητα.", + "additonial_network_information_desc": "Ορισμένα από αυτά τα δίκτυα βασίζονται σε τρίτους. Οι συνδέσεις μπορεί να είναι λιγότερο αξιόπιστες ή να επιτρέπουν σε τρίτους να παρακολουθούν τη δραστηριότητα.", "learn_more": "Μάθετε περισσότερα", "learn_more_url": "https://support.metamask.io/networks-and-sidechains/managing-networks/the-risks-of-connecting-to-an-unknown-network/", "switch_network": "Εναλλαγή σε δίκτυο", @@ -1950,7 +1962,7 @@ "received": "Ελήφθη", "unstaked": "Ακύρωση πονταρίσματος", "to": "Προς", - "rate": "Ποσό (περιλαμβάνονται οι χρεώσεις)", + "rate": "Rate (fees included)", "unstaking_requested": "Αίτημα ακύρωσης πονταρίσματος", "stake_completed": "Ολοκλήρωση του πονταρίσματος", "withdrawal_completed": "Η ανάληψη ολοκληρώθηκε", @@ -1960,6 +1972,49 @@ "swap_completed": "Ανταλλαγή {{from}} για {{to}}", "swap": "Ανταλλαγή", "sent": "Στάλθηκε σε {{address}}", + "menu_item_title": { + "metamask_swap_completed": "Swapped {{symbol1}} for {{symbol2}}", + "erc20_sent": "Sent to {{address}}", + "erc20_received": "Received from {{address}}", + "eth_sent": "Sent to {{address}}", + "eth_received": "Received from {{address}}", + "erc721_sent": "Sent NFT to {{address}}", + "erc1155_sent": "Sent NFT to {{address}}", + "erc721_received": "Received NFT from {{address}}", + "erc1155_received": "Received NFT from {{address}}", + "rocketpool_stake_completed": "Staked", + "rocketpool_unstake_completed": "Unstaking complete", + "lido_stake_completed": "Staked", + "lido_withdrawal_requested": "Unstaking requested", + "lido_withdrawal_completed": "Unstaking complete", + "lido_stake_ready_to_be_withdrawn": "Withdrawal requested" + }, + "menu_item_description": { + "lido_withdrawal_requested": "Your request to unstake {{amount}} {{symbol}} has been sent", + "lido_stake_ready_to_be_withdrawn": "You can now withdraw your unstaked {{symbol}}" + }, + "modal": { + "title_sent": "Sent {{symbol}}", + "title_received": "Received {{symbol}}", + "title_unstake_requested": "Unstaking requested", + "title_untake_ready": "Withdrawl ready", + "title_stake": "Staked {{symbol}}", + "title_unstake_completed": "Unstaking completed", + "title_swapped": "Swapped {{symbol1}} to {{symbol2}}", + "label_address_to": "To", + "label_address_from": "From", + "label_address_to_you": "To (You)", + "label_address_from_you": "From (You)", + "label_asset": "Asset", + "label_account": "Account", + "label_unstaking_requested": "Unstaking Requested", + "label_unstake_ready": "Withdrawl ready", + "label_staked": "Staked", + "label_received": "Received", + "label_unstaking_confirmed": "Unstaking Confirmed", + "label_swapped": "Swapped", + "label_to": "To" + }, "received_from": "Ελήφθη από {{address}}", "nft_sent": "Αποστολή NFT σε {{address}}", "erc721_sent": "Αποστολή NFT σε {{address}}", @@ -1993,6 +2048,8 @@ "received_message": "Πατήστε για να δείτε τη συναλλαγή", "received_payment_message": "Λάβατε {{amount}} DAI", "prompt_title": "Ενεργοποίηση των Ειδοποιήσεων Ώθησης", + "notifications_enabled_error_title": "Something went wrong", + "notifications_enabled_error_desc": "We couldn't enable notifications. Please try again later.", "prompt_desc": "Ενεργοποιήστε τις ειδοποιήσεις, ώστε το MetaMask να σας ενημερώνει όταν έχετε λάβει ETH ή όταν οι συναλλαγές σας έχουν επιβεβαιωθεί.", "prompt_ok": "Ναι", "prompt_cancel": "Όχι, ευχαριστώ", @@ -2017,6 +2074,7 @@ "1": "Πορτοφόλι", "2": "Ανακοινώσεις" }, + "copied_to_clipboard": "Copied to clipboard", "address_copied_to_clipboard": "Αντιγραφή διεύθυνσης στο πρόχειρο", "transaction_id_copied_to_clipboard": "Αντιγραφή του αναγνωριστικού συναλλαγής στο πρόχειρο", "activation_card": { @@ -2648,7 +2706,7 @@ "header": "Ενισχυμένη Προστασία Συναλλαγών", "description_1": "Ξεκλειδώστε υψηλότερα ποσοστά επιτυχίας, προστασία εκ των προτέρων και καλύτερη ορατότητα με τις Έξυπνες Συναλλαγές.", "description_2": "Διατίθεται μόνο στο Ethereum. Ενεργοποίηση ή απενεργοποίηση ανά πάσα στιγμή στις ρυθμίσεις.", - "secondary_button": "Διαχείριση στις ρυθμίσεις", + "no_thanks": "No thanks", "primary_button": "Ενεργοποίηση", "learn_more": "Μάθετε περισσότερα.", "benefit_1_1": "99,5% ποσοστό", @@ -2847,6 +2905,15 @@ "enable_remember_me": "Ενεργοποιήστε την λειτουργία «Να με θυμάστε»", "enable_remember_me_description": "Όταν η λειτουργία «Να με θυμάστε» είναι ενεργοποιημένη, οποιοσδήποτε έχει πρόσβαση στο τηλέφωνό σας μπορεί να αποκτήσει πρόσβαση στον λογαριασμό σας στο MetaMask." }, + "profile_sync": { + "title": "Profile Sync", + "disable_warning": "If you turn off profile sync, you won’t be able to receive notifications.", + "enable_description": "Creates a profile that MetaMask uses to sync some settings among your devices. This is required to get notifications. ", + "enable_description2": " to learn more.", + "enable_privacy_link": "Learn how we protect your privacy", + "bottomSheetTurnOn": "Turning on Profile Sync", + "bottomSheetTurnOff": "Turning off Profile Sync" + }, "turn_off_remember_me": { "title": "Εισαγάγετε τον κωδικό πρόσβασής σας για να απενεργοποιήσετε την λειτουργία «Να με θυμάστε»", "placeholder": "Κωδικός πρόσβασης", @@ -3069,7 +3136,7 @@ }, "install_snap": { "title": "Αίτημα σύνδεσης", - "description": "Το {{origin}} θέλει να κατεβάσει και να συνδεθεί με το {{snap}}. Βεβαιωθείτε ότι εμπιστεύεστε τους συντάκτες πριν προχωρήσετε.", + "description": "Το {{origin}} θέλει να χρησιμοποιήσει το {{snap}}.", "permissions_request_title": "Αίτημα αδειών", "permissions_request_description": "Το {{origin}} θέλει να εγκαταστήσει το {{snap}}, το οποίο ζητά τις ακόλουθες άδειες.", "approve_permissions": "Έγκριση", @@ -3118,5 +3185,8 @@ "title": "Εκτιμώμενες αλλαγές", "tooltip_description": "Οι εκτιμώμενες αλλαγές είναι αυτές που μπορεί να συμβούν αν προχωρήσετε σε αυτή τη συναλλαγή. Πρόκειται απλώς για μια πρόβλεψη, δεν αποτελεί εγγύηση.", "total_fiat": "Σύνολο = {{currency}}" + }, + "common": { + "please_wait": "Please wait" } -} +} \ No newline at end of file diff --git a/locales/languages/en.json b/locales/languages/en.json index c78ad3a84ca..9259a9aaa86 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -5,8 +5,6 @@ "deceptive_request_title": "This is a deceptive request", "failed_title": "Request may not be safe", "failed_description": "Because of an error, this request was not verified by the security provider. Proceed with caution.", - "loading_title": "Checking for security alerts...", - "loading_complete_title": "No alerts received. Always do your own due diligence before approving requests.", "malicious_domain_description": "You're interacting with a malicious domain. If you approve this request, you might lose your assets.", "other_description": "If you approve this request, you might lose your assets.", "raw_signature_farming_description": "If you approve this request, you might lose your assets.", @@ -613,6 +611,10 @@ "remove_account_message": "Do you really want to remove this account?", "no": "No", "yes_remove_it": "Yes, remove it", + "remove_hardware_account": "Remove hardware account", + "remove_hw_account_alert_description": "Are you sure you want to remove this hardware wallet account? You’ll have to resync your hardware wallet if you want to use this account again with MetaMask Mobile.", + "remove_account_alert_remove_btn": "Remove", + "remove_account_alert_cancel_btn": "Nevermind", "accounts_title": "Accounts", "connect_account_title": "Connect account", "connect_accounts_title": "Connect accounts", @@ -668,8 +670,7 @@ "hint_text": "Scan your Keystone wallet to ", "purpose_connect": "connect", "purpose_sign": "confirm the transaction", - "select_accounts": "Select an Account", - "please_wait": "Please wait" + "select_accounts": "Select an Account" }, "data_collection_modal": { "accept": "Okay", @@ -682,6 +683,11 @@ "forget": "Forget this device" }, "app_settings": { + "enabling_notifications": "Enabling notifications...", + "disabling_notifications": "Disabling notifications...", + "enabling_profile_sync": "Enabling profile syncing...", + "disabling_profile_sync": "Disabling profile syncing...", + "notifications_dismiss_modal": "Dismiss", "title": "Settings", "current_conversion": "Base Currency", "current_language": "Current Language", @@ -728,7 +734,7 @@ "protect_cta": "Protect", "protect_title": "Protect your wallet", "video_failed": "Video Failed to Load.", - "protect_desc": "Protect your wallet by saving your Secret Recovery Phrase in various places like on a piece of paper, password manager and/or the cloud.", + "protect_desc": "Protect your wallet by saving your Secret Recovery Phrase in a place only you can access and that you will not forget. Be sure to keep it offline for optimal safety.", "protect_desc_no_backup": "This is your wallet’s 12 word phrase. This phrase can be used to take control of all your current and future accounts, including the ability to send away funds in your wallet. Keep this phrase stored safely, DO NOT share it with anyone. MetaMask cannot help you recover this key.", "learn_more": "Learn more.", "seedphrase_not_backed_up": "Important! Secret Recovery Phrase not backed up", @@ -996,7 +1002,7 @@ "token_nft_ens_subheading": "Token, NFT, and ENS autodetection", "security_check_subheading": "Security checks", "symbol_required": "Symbol is required.", - "blockaid_desc": "Privacy preserving - no data is shared with third parties. Available on Arbitrum, Avalanche, BNB chain, Ethereum Mainnet, Optimism, Polygon, Sepolia and Base.", + "blockaid_desc": "This feature alerts you to malicious activity by actively reviewing transaction and signature requests.", "security_alerts": "Security alerts", "security_alerts_desc": "This feature alerts you to malicious activity by locally reviewing your transaction and signature requests. Always do your own due diligence before approving any requests. There's no guarantee that this feature will detect all malicious activity. By enabling this feature you agree to the provider's terms of use.", "smart_transactions_opt_in_heading": "Smart Transactions", @@ -1414,7 +1420,7 @@ "token": "token", "nft": "NFT", "you_trust_this_site": "By granting permission, you're allowing the following third party to access your funds.", - "you_trust_this_third_party" : "This allows a third party to access and transfer your NFTs without further notice until you revoke its access.", + "you_trust_this_third_party": "This allows a third party to access and transfer your NFTs without further notice until you revoke its access.", "you_trust_this_address": "Do you trust this address? By granting this permission, you're allowing this address to access your funds.", "edit_permission": "Edit permission", "edit": "Edit", @@ -1659,6 +1665,7 @@ "search": "Search for previously added network", "search-short": "Search", "add": "Add", + "continue": "Continue", "cancel": "Cancel", "approve": "Approve", "edit_network_details": "Edit network details", @@ -1956,7 +1963,7 @@ "received": "Received", "unstaked": "Unstaked", "to": "To", - "rate": "Rate(fees included)", + "rate": "Rate (fees included)", "unstaking_requested": "Unstaking requested", "stake_completed": "Stake completed", "withdrawal_completed": "Withdrawal completed", @@ -1966,6 +1973,49 @@ "swap_completed": "Swapped {{from}} for {{to}}", "swap": "Swapped", "sent": "Sent to {{address}}", + "menu_item_title": { + "metamask_swap_completed": "Swapped {{symbol1}} for {{symbol2}}", + "erc20_sent": "Sent to {{address}}", + "erc20_received": "Received from {{address}}", + "eth_sent": "Sent to {{address}}", + "eth_received": "Received from {{address}}", + "erc721_sent": "Sent NFT to {{address}}", + "erc1155_sent": "Sent NFT to {{address}}", + "erc721_received": "Received NFT from {{address}}", + "erc1155_received": "Received NFT from {{address}}", + "rocketpool_stake_completed": "Staked", + "rocketpool_unstake_completed": "Unstaking complete", + "lido_stake_completed": "Staked", + "lido_withdrawal_requested": "Unstaking requested", + "lido_withdrawal_completed": "Unstaking complete", + "lido_stake_ready_to_be_withdrawn": "Withdrawal requested" + }, + "menu_item_description": { + "lido_withdrawal_requested": "Your request to unstake {{amount}} {{symbol}} has been sent", + "lido_stake_ready_to_be_withdrawn": "You can now withdraw your unstaked {{symbol}}" + }, + "modal": { + "title_sent": "Sent {{symbol}}", + "title_received": "Received {{symbol}}", + "title_unstake_requested": "Unstaking requested", + "title_untake_ready": "Withdrawl ready", + "title_stake": "Staked {{symbol}}", + "title_unstake_completed": "Unstaking completed", + "title_swapped": "Swapped {{symbol1}} to {{symbol2}}", + "label_address_to": "To", + "label_address_from": "From", + "label_address_to_you": "To (You)", + "label_address_from_you": "From (You)", + "label_asset": "Asset", + "label_account": "Account", + "label_unstaking_requested": "Unstaking Requested", + "label_unstake_ready": "Withdrawl ready", + "label_staked": "Staked", + "label_received": "Received", + "label_unstaking_confirmed": "Unstaking Confirmed", + "label_swapped": "Swapped", + "label_to": "To" + }, "received_from": "Received from {{address}}", "nft_sent": "Sent NFT to {{address}}", "erc721_sent": "Sent NFT to {{address}}", @@ -1999,6 +2049,8 @@ "received_message": "Tap to view this transaction", "received_payment_message": "You received {{amount}} DAI", "prompt_title": "Enable Push Notifications", + "notifications_enabled_error_title": "Something went wrong", + "notifications_enabled_error_desc": "We couldn't enable notifications. Please try again later.", "prompt_desc": "Enable notifications so MetaMask can let you know when you've received ETH or when your transactions have been confirmed.", "prompt_ok": "Yes", "prompt_cancel": "No, thanks", @@ -2023,6 +2075,7 @@ "1": "Wallet", "2": "Annoucements" }, + "copied_to_clipboard": "Copied to clipboard", "address_copied_to_clipboard": "Address copied to clipboard", "transaction_id_copied_to_clipboard": "Transaction ID copied to clipboard", "activation_card": { @@ -2654,7 +2707,7 @@ "header": "Enhanced Transaction Protection", "description_1": "Unlock higher success rates, frontrunning protection, and better visibility with Smart Transactions.", "description_2": "Only available on Ethereum. Enable or disable any time in settings.", - "secondary_button": "Manage in settings", + "no_thanks": "No thanks", "primary_button": "Enable", "learn_more": "Learn more.", "benefit_1_1": "99.5% success", @@ -2853,6 +2906,15 @@ "enable_remember_me": "Turn on Remember me", "enable_remember_me_description": "When Remember me is on, anyone with access to your phone can access your MetaMask account." }, + "profile_sync": { + "title": "Profile Sync", + "disable_warning": "If you turn off profile sync, you won’t be able to receive notifications.", + "enable_description": "Creates a profile that MetaMask uses to sync some settings among your devices. This is required to get notifications. ", + "enable_description2": " to learn more.", + "enable_privacy_link": "Learn how we protect your privacy", + "bottomSheetTurnOn": "Turning on Profile Sync", + "bottomSheetTurnOff": "Turning off Profile Sync" + }, "turn_off_remember_me": { "title": "Enter your password to turn off Remember me", "placeholder": "Password", @@ -3124,5 +3186,8 @@ "title": "Estimated changes", "tooltip_description": "Estimated changes are what might happen if you go through with this transaction. This is just a prediction, not a guarantee.", "total_fiat": "Total = {{currency}}" + }, + "common": { + "please_wait": "Please wait" } } diff --git a/locales/languages/es.json b/locales/languages/es.json index ae339b025fb..793556e3353 100644 --- a/locales/languages/es.json +++ b/locales/languages/es.json @@ -5,8 +5,6 @@ "deceptive_request_title": "Esta es una solicitud engañosa", "failed_title": "Es posible que la solicitud no sea segura", "failed_description": "Debido a un error, el proveedor de seguridad no verificó esta solicitud. Proceda con precaución.", - "loading_title": "Comprobando alertas de seguridad...", - "loading_complete_title": "No se recibió ninguna. Siempre asegúrese de hacer su propia diligencia debida antes de aprobar cualquier solicitud.", "malicious_domain_description": "Está interactuando con un dominio malicioso. Si aprueba esta solicitud, podría perder sus activos.", "other_description": "Si aprueba esta solicitud, podría perder sus activos.", "raw_signature_farming_description": "Si aprueba esta solicitud, podría perder sus activos.", @@ -613,6 +611,10 @@ "remove_account_message": "¿Está seguro de que quiere quitar esta cuenta?", "no": "No", "yes_remove_it": "Sí, deseo quitarla", + "remove_hardware_account": "Remove hardware account", + "remove_hw_account_alert_description": "Are you sure you want to remove this hardware wallet account? You’ll have to resync your hardware wallet if you want to use this account again with MetaMask Mobile.", + "remove_account_alert_remove_btn": "Remove", + "remove_account_alert_cancel_btn": "Nevermind", "accounts_title": "Cuentas", "connect_account_title": "Conectar cuenta", "connect_accounts_title": "Conectar cuentas", @@ -668,8 +670,7 @@ "hint_text": "Escanee su monedero Keystone para ", "purpose_connect": "conectar", "purpose_sign": "confirmar la transacción", - "select_accounts": "Seleccionar una cuenta", - "please_wait": "Espere, por favor" + "select_accounts": "Seleccionar una cuenta" }, "data_collection_modal": { "accept": "De acuerdo", @@ -682,6 +683,11 @@ "forget": "Olvidar este dispositivo" }, "app_settings": { + "enabling_notifications": "Enabling notifications...", + "disabling_notifications": "Disabling notifications...", + "enabling_profile_sync": "Enabling profile syncing...", + "disabling_profile_sync": "Disabling profile syncing...", + "notifications_dismiss_modal": "Dismiss", "title": "Configuración", "current_conversion": "Moneda base", "current_language": "Idioma actual", @@ -805,6 +811,7 @@ "security_desc": "Configuración de privacidad, MetaMetrics, clave privada y frase secreta de recuperación del monedero", "networks_title": "Redes", "networks_default_title": "Red predeterminada", + "network_delete": "Si elimina esta red, deberá volver a agregarla para ver sus activos en esta red", "networks_default_cta": "Usar esta red", "networks_desc": "Agregar y editar redes RPC personalizadas", "network_name_label": "Nombre de la red", @@ -820,6 +827,7 @@ "network_other_networks": "Otras redes", "network_rpc_networks": "Redes RPC", "network_add_network": "Agregar red", + "network_add_custom_network": "Agregar una red personalizada", "network_add": "Agregar", "network_save": "Guardar", "remove_network_title": "¿Quiere quitar esta red?", @@ -994,7 +1002,7 @@ "token_nft_ens_subheading": "Detección automática de tokens, NFT y ENS", "security_check_subheading": "Verificaciones de seguridad", "symbol_required": "Se requiere símbolo.", - "blockaid_desc": "Preservación de la privacidad: no se comparten datos con terceros. Disponible en Arbitrum, Avalanche, BNB Chain, la red principal de Ethereum, Optimism, Polygon, Sepolia y Base.", + "blockaid_desc": "Esta función le alerta sobre actividades maliciosas al revisar activamente las solicitudes de transacciones y firmas.", "security_alerts": "Alertas de seguridad", "security_alerts_desc": "Esta función le alerta sobre actividad maliciosa al revisar localmente sus solicitudes de transacción y firma. Haga siempre su propia diligencia debida antes de aprobar cualquier solicitud. No hay garantía de que esta función detecte toda la actividad maliciosa. Al habilitar esta función, acepta los términos de uso del proveedor.", "smart_transactions_opt_in_heading": "Transacciones inteligentes", @@ -1031,6 +1039,7 @@ "cancel": "Cancelar", "loading": "Conectándose a MetaMask...", "unkown_dapp": "Nombre de DAPP no disponible", + "unknown": "Desconocido", "no_connections": "Sin conexiones", "no_connections_desc": "Si conecta una cuenta a un sitio o una aplicación, lo verá aquí." }, @@ -1654,6 +1663,7 @@ "network_currency_symbol": "Símbolo de moneda", "network_block_explorer_url": "Dirección URL del explorador de bloques", "search": "Buscar una red agregada anteriormente", + "search-short": "Buscar", "add": "Agregar", "cancel": "Cancelar", "approve": "Aprobar", @@ -1661,7 +1671,9 @@ "malicious_network_warning": "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.", "security_link": "https://support.metamask.io/networks-and-sidechains/managing-networks/user-guide-custom-networks-and-sidechains/", "network_warning_title": "Información de red", + "additional_network_information_title": "Información adicional sobre las redes", "network_warning_desc": "Esta conexión de red depende de terceros. Esta conexión puede ser menos confiable o permite que terceros rastreen la actividad.", + "additonial_network_information_desc": "Algunas de estas redes dependen de terceros. Las conexiones pueden ser menos confiables o permitir que terceros realicen un seguimiento de la actividad.", "learn_more": "Más información", "learn_more_url": "https://support.metamask.io/networks-and-sidechains/managing-networks/the-risks-of-connecting-to-an-unknown-network/", "switch_network": "Cambiar de red", @@ -1950,7 +1962,7 @@ "received": "Recibido", "unstaked": "Sin staking", "to": "Para", - "rate": "Tasa (tarifa incluida)", + "rate": "Rate (fees included)", "unstaking_requested": "Unstaking solicitado", "stake_completed": "Staking finalizado", "withdrawal_completed": "Retiro finalizado", @@ -1960,6 +1972,49 @@ "swap_completed": "Canjeado {{from}} por {{to}}", "swap": "Canjeado", "sent": "Enviado a {{address}}", + "menu_item_title": { + "metamask_swap_completed": "Swapped {{symbol1}} for {{symbol2}}", + "erc20_sent": "Sent to {{address}}", + "erc20_received": "Received from {{address}}", + "eth_sent": "Sent to {{address}}", + "eth_received": "Received from {{address}}", + "erc721_sent": "Sent NFT to {{address}}", + "erc1155_sent": "Sent NFT to {{address}}", + "erc721_received": "Received NFT from {{address}}", + "erc1155_received": "Received NFT from {{address}}", + "rocketpool_stake_completed": "Staked", + "rocketpool_unstake_completed": "Unstaking complete", + "lido_stake_completed": "Staked", + "lido_withdrawal_requested": "Unstaking requested", + "lido_withdrawal_completed": "Unstaking complete", + "lido_stake_ready_to_be_withdrawn": "Withdrawal requested" + }, + "menu_item_description": { + "lido_withdrawal_requested": "Your request to unstake {{amount}} {{symbol}} has been sent", + "lido_stake_ready_to_be_withdrawn": "You can now withdraw your unstaked {{symbol}}" + }, + "modal": { + "title_sent": "Sent {{symbol}}", + "title_received": "Received {{symbol}}", + "title_unstake_requested": "Unstaking requested", + "title_untake_ready": "Withdrawl ready", + "title_stake": "Staked {{symbol}}", + "title_unstake_completed": "Unstaking completed", + "title_swapped": "Swapped {{symbol1}} to {{symbol2}}", + "label_address_to": "To", + "label_address_from": "From", + "label_address_to_you": "To (You)", + "label_address_from_you": "From (You)", + "label_asset": "Asset", + "label_account": "Account", + "label_unstaking_requested": "Unstaking Requested", + "label_unstake_ready": "Withdrawl ready", + "label_staked": "Staked", + "label_received": "Received", + "label_unstaking_confirmed": "Unstaking Confirmed", + "label_swapped": "Swapped", + "label_to": "To" + }, "received_from": "Recibido desde {{address}}", "nft_sent": "NFT enviado a {{address}}", "erc721_sent": "NFT enviado a {{address}}", @@ -1993,6 +2048,8 @@ "received_message": "Presionar para ver esta transacción", "received_payment_message": "Recibió {{amount}} DAI", "prompt_title": "Habilitar notificaciones push", + "notifications_enabled_error_title": "Something went wrong", + "notifications_enabled_error_desc": "We couldn't enable notifications. Please try again later.", "prompt_desc": "Habilite las notificaciones para que MetaMask pueda informarle cuando ha recibido ETH o cuando se han confirmado sus transacciones.", "prompt_ok": "Sí", "prompt_cancel": "No, gracias", @@ -2017,6 +2074,7 @@ "1": "Monedero", "2": "Anuncios" }, + "copied_to_clipboard": "Copied to clipboard", "address_copied_to_clipboard": "Dirección copiada al portapapeles", "transaction_id_copied_to_clipboard": "ID de transacción copiado al portapapeles", "activation_card": { @@ -2648,7 +2706,7 @@ "header": "Protección mejorada de transacciones", "description_1": "Desbloquee índices de éxito más altos, protección contra frontrunning y mejor visibilidad con transacciones inteligentes.", "description_2": "Solo disponible en Ethereum. Active o desactive en cualquier momento en la configuración.", - "secondary_button": "Administrar en configuración", + "no_thanks": "No thanks", "primary_button": "Activar", "learn_more": "Más información.", "benefit_1_1": "99,5 % de índice", @@ -2847,6 +2905,15 @@ "enable_remember_me": "Activar Recordarme", "enable_remember_me_description": "Cuando Recordarme está activado, cualquier persona con acceso a su teléfono puede acceder a su cuenta de MetaMask." }, + "profile_sync": { + "title": "Profile Sync", + "disable_warning": "If you turn off profile sync, you won’t be able to receive notifications.", + "enable_description": "Creates a profile that MetaMask uses to sync some settings among your devices. This is required to get notifications. ", + "enable_description2": " to learn more.", + "enable_privacy_link": "Learn how we protect your privacy", + "bottomSheetTurnOn": "Turning on Profile Sync", + "bottomSheetTurnOff": "Turning off Profile Sync" + }, "turn_off_remember_me": { "title": "Ingrese su contraseña para desactivar Recordarme", "placeholder": "Contraseña", @@ -3069,7 +3136,7 @@ }, "install_snap": { "title": "Solicitud de conexión", - "description": "{{origin}} quiere descargar y conectarse con {{snap}}. Asegúrese de confiar en los autores antes de continuar.", + "description": "{{origin}} desea usar {{snap}}.", "permissions_request_title": "Solicitud de permiso", "permissions_request_description": "{{origin}} quiere instalar {{snap}}, que solicita los siguientes permisos.", "approve_permissions": "Aprobar", @@ -3118,5 +3185,8 @@ "title": "Cambios estimados", "tooltip_description": "Los cambios estimados son los que podrían producirse si sigue adelante con esta transacción. Esto es solo una predicción, no una garantía.", "total_fiat": "Total = {{currency}}" + }, + "common": { + "please_wait": "Please wait" } -} +} \ No newline at end of file diff --git a/locales/languages/fr.json b/locales/languages/fr.json index d6ce53cf281..4a052bd7452 100644 --- a/locales/languages/fr.json +++ b/locales/languages/fr.json @@ -5,8 +5,6 @@ "deceptive_request_title": "Cette demande est trompeuse", "failed_title": "Cette demande peut présenter des risques", "failed_description": "À cause d’une erreur, cette demande n’a pas été vérifiée par le fournisseur de services de sécurité. Veuillez agir avec prudence.", - "loading_title": "En train de vérifier s’il y a des alertes de sécurité…", - "loading_complete_title": "Aucune alerte n’a été reçue. Faites toujours preuve de diligence raisonnable avant d’approuver les demandes.", "malicious_domain_description": "Vous interagissez avec un domaine malveillant. Si vous approuvez cette demande, vous risquez de perdre vos actifs.", "other_description": "Si vous approuvez cette demande, vous risquez de perdre vos actifs.", "raw_signature_farming_description": "Si vous approuvez cette demande, vous risquez de perdre vos actifs.", @@ -613,6 +611,10 @@ "remove_account_message": "Souhaitez-vous vraiment supprimer ce compte ?", "no": "Non", "yes_remove_it": "Oui, supprimez-le", + "remove_hardware_account": "Remove hardware account", + "remove_hw_account_alert_description": "Are you sure you want to remove this hardware wallet account? You’ll have to resync your hardware wallet if you want to use this account again with MetaMask Mobile.", + "remove_account_alert_remove_btn": "Remove", + "remove_account_alert_cancel_btn": "Nevermind", "accounts_title": "Comptes", "connect_account_title": "Connecter le compte", "connect_accounts_title": "Connecter les comptes", @@ -668,8 +670,7 @@ "hint_text": "Scannez votre portefeuille Keystone pour ", "purpose_connect": "connexion", "purpose_sign": "confirmer la transaction", - "select_accounts": "Sélectionner un compte", - "please_wait": "Veuillez patienter" + "select_accounts": "Sélectionner un compte" }, "data_collection_modal": { "accept": "OK", @@ -682,6 +683,11 @@ "forget": "Oublier cet appareil" }, "app_settings": { + "enabling_notifications": "Enabling notifications...", + "disabling_notifications": "Disabling notifications...", + "enabling_profile_sync": "Enabling profile syncing...", + "disabling_profile_sync": "Disabling profile syncing...", + "notifications_dismiss_modal": "Dismiss", "title": "Paramètres", "current_conversion": "Devise de base", "current_language": "Langue actuelle", @@ -805,6 +811,7 @@ "security_desc": "Paramètres de confidentialité, MetaMetrics, clé privée et phrase secrète de récupération du portefeuille", "networks_title": "Réseaux", "networks_default_title": "Réseau par défaut", + "network_delete": "Si vous supprimez ce réseau, vous devrez l’ajouter à nouveau pour voir vos actifs sur ce réseau", "networks_default_cta": "Utiliser ce réseau", "networks_desc": "Ajouter et modifier des réseaux RPC personnalisés", "network_name_label": "Nom du réseau", @@ -820,6 +827,7 @@ "network_other_networks": "Autres réseaux", "network_rpc_networks": "Réseaux RPC", "network_add_network": "Ajouter un réseau", + "network_add_custom_network": "Ajouter un réseau personnalisé", "network_add": "Ajouter", "network_save": "Enregistrer", "remove_network_title": "Voulez-vous supprimer ce réseau ?", @@ -994,7 +1002,7 @@ "token_nft_ens_subheading": "Détection automatique des jetons, NFT et ENS", "security_check_subheading": "Contrôles de sécurité", "symbol_required": "Le symbole est requis.", - "blockaid_desc": "Protection de la vie privée : aucune donnée n’est partagée avec des tiers. Disponible sur Arbitrum, Avalanche, BNB chain, Optimism, Polygon, Sepolia, Base et le réseau principal Ethereum.", + "blockaid_desc": "Cette fonctionnalité vous avertit de toute activité malveillante en examinant activement les demandes de transaction et de signature.", "security_alerts": "Alertes de sécurité", "security_alerts_desc": "Cette fonctionnalité vous avertit de toute activité malveillante en examinant localement vos demandes de transaction et de signature. Vous devez faire preuve de diligence raisonnable avant d’approuver toute demande. Rien ne garantit que toutes les activités malveillantes seront détectées par cette fonctionnalité. En l’activant, vous acceptez les conditions d’utilisation du fournisseur.", "smart_transactions_opt_in_heading": "Transactions intelligentes", @@ -1031,6 +1039,7 @@ "cancel": "Annuler", "loading": "Connexion à MetaMask...", "unkown_dapp": "Nom de DAPP non disponible", + "unknown": "Inconnu", "no_connections": "Aucune connexion", "no_connections_desc": "Les comptes que vous connectez à un site ou à une application seront affichés ici." }, @@ -1654,6 +1663,7 @@ "network_currency_symbol": "Symbole de la devise", "network_block_explorer_url": "URL de l’explorateur de blocs", "search": "Chercher un réseau ajouté précédemment", + "search-short": "Rechercher", "add": "Ajouter", "cancel": "Annuler", "approve": "Approuver", @@ -1661,7 +1671,9 @@ "malicious_network_warning": "Un fournisseur de réseau malveillant peut mentir quant à l’état de la blockchain et enregistrer votre activité sur le réseau. N’ajoutez que des réseaux personnalisés auxquels vous faites confiance.", "security_link": "https://support.metamask.io/networks-and-sidechains/managing-networks/user-guide-custom-networks-and-sidechains/", "network_warning_title": "Informations sur le réseau", + "additional_network_information_title": "Informations supplémentaires sur les réseaux", "network_warning_desc": "Cette connexion réseau est assurée par des tiers. Elle peut être moins fiable ou permettre à des tiers de suivre l’activité des utilisateurs.", + "additonial_network_information_desc": "Certains de ces réseaux dépendent de services tiers. Ils peuvent être moins fiables ou permettre à des tiers de suivre l’activité des utilisateurs.", "learn_more": "En savoir plus", "learn_more_url": "https://support.metamask.io/networks-and-sidechains/managing-networks/the-risks-of-connecting-to-an-unknown-network/", "switch_network": "Changer de réseau", @@ -1950,7 +1962,7 @@ "received": "Reçu", "unstaked": "Déstaké", "to": "À", - "rate": "Taux (frais inclus)", + "rate": "Rate (fees included)", "unstaking_requested": "Demande d’annulation du staking", "stake_completed": "Stake terminé", "withdrawal_completed": "Retrait effectué", @@ -1960,6 +1972,49 @@ "swap_completed": "A échangé {{from}} contre {{to}}", "swap": "Échangé", "sent": "Envoyée à {{address}}", + "menu_item_title": { + "metamask_swap_completed": "Swapped {{symbol1}} for {{symbol2}}", + "erc20_sent": "Sent to {{address}}", + "erc20_received": "Received from {{address}}", + "eth_sent": "Sent to {{address}}", + "eth_received": "Received from {{address}}", + "erc721_sent": "Sent NFT to {{address}}", + "erc1155_sent": "Sent NFT to {{address}}", + "erc721_received": "Received NFT from {{address}}", + "erc1155_received": "Received NFT from {{address}}", + "rocketpool_stake_completed": "Staked", + "rocketpool_unstake_completed": "Unstaking complete", + "lido_stake_completed": "Staked", + "lido_withdrawal_requested": "Unstaking requested", + "lido_withdrawal_completed": "Unstaking complete", + "lido_stake_ready_to_be_withdrawn": "Withdrawal requested" + }, + "menu_item_description": { + "lido_withdrawal_requested": "Your request to unstake {{amount}} {{symbol}} has been sent", + "lido_stake_ready_to_be_withdrawn": "You can now withdraw your unstaked {{symbol}}" + }, + "modal": { + "title_sent": "Sent {{symbol}}", + "title_received": "Received {{symbol}}", + "title_unstake_requested": "Unstaking requested", + "title_untake_ready": "Withdrawl ready", + "title_stake": "Staked {{symbol}}", + "title_unstake_completed": "Unstaking completed", + "title_swapped": "Swapped {{symbol1}} to {{symbol2}}", + "label_address_to": "To", + "label_address_from": "From", + "label_address_to_you": "To (You)", + "label_address_from_you": "From (You)", + "label_asset": "Asset", + "label_account": "Account", + "label_unstaking_requested": "Unstaking Requested", + "label_unstake_ready": "Withdrawl ready", + "label_staked": "Staked", + "label_received": "Received", + "label_unstaking_confirmed": "Unstaking Confirmed", + "label_swapped": "Swapped", + "label_to": "To" + }, "received_from": "Reçue de {{address}}", "nft_sent": "A envoyé un NFT à {{address}}", "erc721_sent": "A envoyé un NFT à {{address}}", @@ -1993,6 +2048,8 @@ "received_message": "Appuyez pour voir cette transaction", "received_payment_message": "Vous avez reçu {{amount}} DAI", "prompt_title": "Activer les notifications Push", + "notifications_enabled_error_title": "Something went wrong", + "notifications_enabled_error_desc": "We couldn't enable notifications. Please try again later.", "prompt_desc": "Activez les notifications afin que MetaMask puisse vous informer lorsque vous recevez des ETH ou lorsque vos transactions sont confirmées.", "prompt_ok": "Oui", "prompt_cancel": "Non, merci.", @@ -2017,6 +2074,7 @@ "1": "Portefeuille", "2": "Annonces" }, + "copied_to_clipboard": "Copied to clipboard", "address_copied_to_clipboard": "Adresse copiée dans le presse-papiers", "transaction_id_copied_to_clipboard": "ID de transaction copié dans le presse-papiers", "activation_card": { @@ -2648,7 +2706,7 @@ "header": "Protection renforcée des transactions", "description_1": "Bénéficiez de taux de réussite plus élevés, d’une protection contre le « front running » et d’une meilleure visibilité grâce aux transactions intelligentes.", "description_2": "Disponible uniquement sur Ethereum. Vous pouvez activer ou désactiver cette option à tout moment dans les paramètres.", - "secondary_button": "Gérer dans les paramètres", + "no_thanks": "No thanks", "primary_button": "Activer", "learn_more": "En savoir plus.", "benefit_1_1": "Taux de réussite de", @@ -2847,6 +2905,15 @@ "enable_remember_me": "Activer l'option « Se souvenir de moi »", "enable_remember_me_description": "Lorsque l'option « Se souvenir de moi » est activée, toute personne ayant accès à votre téléphone peut accéder à votre compte MetaMask." }, + "profile_sync": { + "title": "Profile Sync", + "disable_warning": "If you turn off profile sync, you won’t be able to receive notifications.", + "enable_description": "Creates a profile that MetaMask uses to sync some settings among your devices. This is required to get notifications. ", + "enable_description2": " to learn more.", + "enable_privacy_link": "Learn how we protect your privacy", + "bottomSheetTurnOn": "Turning on Profile Sync", + "bottomSheetTurnOff": "Turning off Profile Sync" + }, "turn_off_remember_me": { "title": "Saisissez votre mot de passe pour désactiver l'option « Se souvenir de moi »", "placeholder": "Mot de passe", @@ -3069,7 +3136,7 @@ }, "install_snap": { "title": "Demande de connexion", - "description": "{{origin}} veut télécharger et se connecter avec {{snap}}. Veuillez vous assurer que vous pouvez faire confiance aux auteurs avant de continuer.", + "description": "{{origin}} souhaite utiliser {{snap}}.", "permissions_request_title": "Demande d’autorisation", "permissions_request_description": "{{origin}} veut installer {{snap}} qui demande les autorisations suivantes.", "approve_permissions": "Approuver", @@ -3118,5 +3185,8 @@ "title": "Changements estimés", "tooltip_description": "Les changements estimés représentent ce qui pourrait se produire si vous effectuez cette transaction. Il s’agit juste d’une estimation fournie à des fins d’information.", "total_fiat": "Total = {{currency}}" + }, + "common": { + "please_wait": "Please wait" } -} +} \ No newline at end of file diff --git a/locales/languages/hi.json b/locales/languages/hi.json index ffe90ef975e..a8654e1e920 100644 --- a/locales/languages/hi.json +++ b/locales/languages/hi.json @@ -5,8 +5,6 @@ "deceptive_request_title": "इस अनुरोध को धोखेबाजी के उद्देश्य से भेजा गया है", "failed_title": "हो सकता है कि अनुरोध सुरक्षित न हो", "failed_description": "किसी गड़बड़ी के कारण, इस अनुरोध को सिक्यूरिटी प्रोवाइडर द्वारा सत्यापित नहीं किया गया। सावधानी से आगे बढ़ें।", - "loading_title": "सुरक्षा एलर्ट की जाँच की जा रही है...", - "loading_complete_title": "कोई एलर्ट प्राप्त नहीं हुआ। किसी भी अनुरोध को एप्रूव करने से पहले हमेशा पूरी जांच-पड़ताल ज़रूर करें।", "malicious_domain_description": "आप एक बुरी नीयत वाले डोमेन से इंटरैक्ट कर रहे हैं। यदि आप इस रिक्वेस्ट को एप्रूव करते हैं, तो आप अपने सारे एसेट गंवा सकते हैं।", "other_description": "यदि आप इस अनुरोध को एप्रूव करते हैं, तो आप अपने सारे एसेट गंवा सकते हैं।", "raw_signature_farming_description": "यदि आप इस अनुरोध को एप्रूव करते हैं, तो आप अपने सारे एसेट गंवा सकते हैं।", @@ -613,6 +611,10 @@ "remove_account_message": "क्या आप वास्तव में इस अकाउंट को हटाना चाहते हैं?", "no": "नहीं", "yes_remove_it": "हां, इसे हटा दें", + "remove_hardware_account": "Remove hardware account", + "remove_hw_account_alert_description": "Are you sure you want to remove this hardware wallet account? You’ll have to resync your hardware wallet if you want to use this account again with MetaMask Mobile.", + "remove_account_alert_remove_btn": "Remove", + "remove_account_alert_cancel_btn": "Nevermind", "accounts_title": "खाता", "connect_account_title": "खाता कनेक्ट करें", "connect_accounts_title": "खातों को कनेक्ट करें", @@ -668,8 +670,7 @@ "hint_text": "अपना कीस्टोन वॉलेट स्कैन करें ", "purpose_connect": "कनेक्ट", "purpose_sign": "लेनदेन की पुष्टि करें", - "select_accounts": "किसी खाते का चयन करें", - "please_wait": "कृपया प्रतीक्षा करें" + "select_accounts": "किसी खाते का चयन करें" }, "data_collection_modal": { "accept": "ठीक है", @@ -682,6 +683,11 @@ "forget": "इस डिवाइस को भूल जाएं" }, "app_settings": { + "enabling_notifications": "Enabling notifications...", + "disabling_notifications": "Disabling notifications...", + "enabling_profile_sync": "Enabling profile syncing...", + "disabling_profile_sync": "Disabling profile syncing...", + "notifications_dismiss_modal": "Dismiss", "title": "सेटिंग", "current_conversion": "बेस करेंसी", "current_language": "वर्तमान भाषा", @@ -805,6 +811,7 @@ "security_desc": "गोपनीय सेटिंग, MetaMetrics, निजी की और वॉलेट सीक्रेट रिकवरी फ्रेज", "networks_title": "नेटवर्क", "networks_default_title": "डिफ़ॉल्ट नेटवर्क", + "network_delete": "अगर आप इस नेटवर्क को हटाते हैं, तो आपको इस नेटवर्क में अपने एसेट देखने के लिए इसे फिर से जोड़ना होगा", "networks_default_cta": "इस नेटवर्क का प्रयोग करें", "networks_desc": "कस्टम RPC नेटवर्क को जोड़ें और संपादित करें", "network_name_label": "नेटवर्क का नाम", @@ -820,6 +827,7 @@ "network_other_networks": "अन्य नेटवर्क", "network_rpc_networks": "RPC नेटवर्क", "network_add_network": "नेटवर्क जोड़ें", + "network_add_custom_network": "एक कस्टम नेटवर्क जोड़ें", "network_add": "जोड़ें", "network_save": "सहेजें", "remove_network_title": "क्या आप इस नेटवर्क को हटाना चाहते हैं?", @@ -994,7 +1002,7 @@ "token_nft_ens_subheading": "टोकन, NFT और ENS ऑटो-डिटेक्शन", "security_check_subheading": "सुरक्षा जाँचें", "symbol_required": "सिंबल की ज़रूरत है।", - "blockaid_desc": "गोपनीयता को सुरक्षित रखना - कोई भी डेटा थर्ड पार्टी के साथ साझा नहीं किया जाता है। Arbitrum, Avalanche, BNB chain, Ethereum Mainnet, Optimism, Polygon, Sepolia और Base पर उपलब्ध है।", + "blockaid_desc": "यह फीचर सक्रिय रूप से ट्रांसेक्शन और सिग्नेचर अनुरोधों की समीक्षा करके आपको बुरी नीयत वाली गतिविधि के प्रति एलर्ट करती है।", "security_alerts": "सुरक्षा एलर्ट", "security_alerts_desc": "यह सुविधा स्थानीय रूप से आपके ट्रांसेक्शन और हस्ताक्षर अनुरोधों की समीक्षा करके आपको बुरी नीयत वाली गतिविधि के प्रति एलर्ट करती है। किसी भी अनुरोध को मंजूरी देने से पहले हमेशा पूरी जांच-पड़ताल ज़रूर करें। इस बात की कोई गारंटी नहीं है कि यह सुविधा सभी बुरी नीयत वाली गतिविधि का पता लगा लेगी। इस सुविधा को सक्षम करके आप प्रदाता की उपयोग की शर्तों से सहमत होते हैं।", "smart_transactions_opt_in_heading": "स्मार्ट ट्रांसेक्शन", @@ -1031,6 +1039,7 @@ "cancel": "रद्द करें", "loading": "MetaMask से कनेक्ट हो रहा है...", "unkown_dapp": "DAPP नाम उपलब्ध नहीं है", + "unknown": "अज्ञात", "no_connections": "कोई कनेक्शन नहीं", "no_connections_desc": "यदि आप किसी अकाउंट को किसी साइट या ऐप से कनेक्ट करते हैं, तो वह आपको यहां दिखेगा।" }, @@ -1654,6 +1663,7 @@ "network_currency_symbol": "मुद्रा चिन्ह", "network_block_explorer_url": "एक्सप्लोरर URL को ब्लॉक करें", "search": "पहले जोड़े गए नेटवर्क की खोज करें", + "search-short": "ढूंढें", "add": "जोड़ें", "cancel": "रद्द करें", "approve": "स्वीकृति दें", @@ -1661,7 +1671,9 @@ "malicious_network_warning": "एक दुर्भावनापूर्ण नेटवर्क प्रदाता ब्लॉकचेन की स्थिति के बारे में झूठ बोल सकता है और आपकी नेटवर्क गतिविधि को रिकॉर्ड कर सकता है। केवल ऐसे कस्टम नेटवर्क जोड़ें जिन पर आपको भरोसा हो।", "security_link": "https://support.metamask.io/networks-and-sidechains/managing-networks/user-guide-custom-networks-and-sidechains/", "network_warning_title": "नेटवर्क संबंधी सूचना", + "additional_network_information_title": "अतिरिक्त नेटवर्क सूचना", "network_warning_desc": "यह नेटवर्क कनेक्शन थर्ड पार्टी पर निर्भर करता है। यह कनेक्शन कम विश्वसनीय हो सकता है या गतिविधि को ट्रैक करने के लिए तृतीय-पक्ष को सक्षम कर सकता है।", + "additonial_network_information_desc": "इनमें से कुछ नेटवर्क थर्ड पार्टीज़ पर निर्भर हैं। कनेक्शन कम विश्वसनीय हो सकते हैं या गतिविधि को ट्रैक करने के लिए थर्ड पार्टीज़ को चालू कर सकते हैं।", "learn_more": "अधिक जानें", "learn_more_url": "https://support.metamask.io/networks-and-sidechains/managing-networks/the-risks-of-connecting-to-an-unknown-network/", "switch_network": "नेटवर्क पर स्विच करें", @@ -1950,7 +1962,7 @@ "received": "प्राप्त किया गया", "unstaked": "अनस्टेक किया गया", "to": "प्रति", - "rate": "दर (शुल्क शामिल है)", + "rate": "Rate (fees included)", "unstaking_requested": "अनस्टेकिंग का अनुरोध किया गया", "stake_completed": "स्टेक पूरा हुआ", "withdrawal_completed": "विदड्रॉवल पूरा हुआ", @@ -1960,6 +1972,49 @@ "swap_completed": "{{from}} को {{to}} के लिए स्वैप किया गया", "swap": "स्वैप किया गया", "sent": "{{address}} पर भेजा गया", + "menu_item_title": { + "metamask_swap_completed": "Swapped {{symbol1}} for {{symbol2}}", + "erc20_sent": "Sent to {{address}}", + "erc20_received": "Received from {{address}}", + "eth_sent": "Sent to {{address}}", + "eth_received": "Received from {{address}}", + "erc721_sent": "Sent NFT to {{address}}", + "erc1155_sent": "Sent NFT to {{address}}", + "erc721_received": "Received NFT from {{address}}", + "erc1155_received": "Received NFT from {{address}}", + "rocketpool_stake_completed": "Staked", + "rocketpool_unstake_completed": "Unstaking complete", + "lido_stake_completed": "Staked", + "lido_withdrawal_requested": "Unstaking requested", + "lido_withdrawal_completed": "Unstaking complete", + "lido_stake_ready_to_be_withdrawn": "Withdrawal requested" + }, + "menu_item_description": { + "lido_withdrawal_requested": "Your request to unstake {{amount}} {{symbol}} has been sent", + "lido_stake_ready_to_be_withdrawn": "You can now withdraw your unstaked {{symbol}}" + }, + "modal": { + "title_sent": "Sent {{symbol}}", + "title_received": "Received {{symbol}}", + "title_unstake_requested": "Unstaking requested", + "title_untake_ready": "Withdrawl ready", + "title_stake": "Staked {{symbol}}", + "title_unstake_completed": "Unstaking completed", + "title_swapped": "Swapped {{symbol1}} to {{symbol2}}", + "label_address_to": "To", + "label_address_from": "From", + "label_address_to_you": "To (You)", + "label_address_from_you": "From (You)", + "label_asset": "Asset", + "label_account": "Account", + "label_unstaking_requested": "Unstaking Requested", + "label_unstake_ready": "Withdrawl ready", + "label_staked": "Staked", + "label_received": "Received", + "label_unstaking_confirmed": "Unstaking Confirmed", + "label_swapped": "Swapped", + "label_to": "To" + }, "received_from": "{{address}} से प्राप्त हुआ", "nft_sent": "NFT को {{address}} पर भेजा गया", "erc721_sent": "NFT को {{address}} पर भेजा गया", @@ -1993,6 +2048,8 @@ "received_message": "इस लेन-देन को देखने के लिए टैप करें", "received_payment_message": "आपने {{amount}} DAI प्राप्त किया", "prompt_title": "पुश नोटिफिकेशन को सक्षम करें", + "notifications_enabled_error_title": "Something went wrong", + "notifications_enabled_error_desc": "We couldn't enable notifications. Please try again later.", "prompt_desc": "नोटिफिकेशन को सक्षम करें जिससे MetaMask आपको बता सके कि आपने कब ETH प्राप्त किया है या आपके लेन-देन की पुष्टि कब हुई है।", "prompt_ok": "हां", "prompt_cancel": "नहीं, धन्यवाद", @@ -2017,6 +2074,7 @@ "1": "वॉलेट", "2": "घोषणाएं" }, + "copied_to_clipboard": "Copied to clipboard", "address_copied_to_clipboard": "एड्रेस क्लिपबोर्ड पर कॉपी किया गया", "transaction_id_copied_to_clipboard": "ट्रांसेक्शन आईडी क्लिपबोर्ड पर कॉपी किया गया", "activation_card": { @@ -2648,7 +2706,7 @@ "header": "उन्नत ट्रांसेक्शन सुरक्षा", "description_1": "स्मार्ट ट्रांसेक्शन के साथ उच्च सफलता दर, फ्रंटरनिंग सुरक्षा और बेहतर दृश्यता अनलॉक करें।", "description_2": "केवल Ethereum पर उपलब्ध है। सेटिंग्स में किसी भी समय चालू करें या बंद करें।", - "secondary_button": "सेटिंग्स में मैनेज करें", + "no_thanks": "No thanks", "primary_button": "चालू करें", "learn_more": "अधिक जानें।", "benefit_1_1": "99.5% सफलता", @@ -2847,6 +2905,15 @@ "enable_remember_me": "रिमेंबर मी चालू करें", "enable_remember_me_description": "जब रिमेंबर मी चालू रहेगा, आपका फोन एक्सेस करने वाला कोई भी व्यक्ति आपके MetaMask अकाउंट को एक्सेस कर सकता है।" }, + "profile_sync": { + "title": "Profile Sync", + "disable_warning": "If you turn off profile sync, you won’t be able to receive notifications.", + "enable_description": "Creates a profile that MetaMask uses to sync some settings among your devices. This is required to get notifications. ", + "enable_description2": " to learn more.", + "enable_privacy_link": "Learn how we protect your privacy", + "bottomSheetTurnOn": "Turning on Profile Sync", + "bottomSheetTurnOff": "Turning off Profile Sync" + }, "turn_off_remember_me": { "title": "रिमेंबर मी को बंद करने के लिए अपना पासवर्ड दर्ज करें", "placeholder": "पासवर्ड", @@ -3069,7 +3136,7 @@ }, "install_snap": { "title": "कनेक्शन के लिए अनुरोध", - "description": "{{origin}} {{snap}} को डाउनलोड करना चाहता है और उससे कनेक्ट करना चाहता है। आगे बढ़ने से पहले पक्का कर लें कि आप ऑथर्स पर भरोसा करते हैं।", + "description": "{{origin}} {{snap}} का उपयोग करना चाहता है।", "permissions_request_title": "अनुमति के लिए अनुरोध", "permissions_request_description": "{{origin}} {{snap}} को इंस्टॉल करना चाहता है, जो नीचे बताई अनुमतियों के लिए अनुरोध कर रहा है।", "approve_permissions": "एप्रूव करें", @@ -3118,5 +3185,8 @@ "title": "अनुमानित बदलाव", "tooltip_description": "अगर आप यह ट्रांसेक्शन करते हैं तो अनुमानित परिवर्तन हो सकते हैं। यह सिर्फ एक प्रेडिक्शन है, कोई गारंटी नहीं।", "total_fiat": "कुल = {{currency}}" + }, + "common": { + "please_wait": "Please wait" } -} +} \ No newline at end of file diff --git a/locales/languages/id.json b/locales/languages/id.json index 835cb3453c3..94f5b8ad484 100644 --- a/locales/languages/id.json +++ b/locales/languages/id.json @@ -5,8 +5,6 @@ "deceptive_request_title": "Ini adalah permintaan tipuan", "failed_title": "Permintaan mungkin tidak aman", "failed_description": "Karena terjadi kesalahan, permintaan ini tidak diverifikasi oleh penyedia keamanan. Lanjutkan dengan hati-hati.", - "loading_title": "Memeriksa peringatan keamanan...", - "loading_complete_title": "Tidak menerima peringatan. Selalu lakukan uji tuntas sendiri sebelum menyetujui permintaan.", "malicious_domain_description": "Anda berinteraksi dengan domain berbahaya. Jika Anda menyetujui permintaan ini, aset Anda kemungkinan akan hilang.", "other_description": "Jika Anda menyetujui permintaan ini, aset Anda kemungkinan akan hilang.", "raw_signature_farming_description": "Jika Anda menyetujui permintaan ini, aset Anda kemungkinan akan hilang.", @@ -613,6 +611,10 @@ "remove_account_message": "Yakin ingin menghapus akun ini?", "no": "Tidak", "yes_remove_it": "Ya, hapus", + "remove_hardware_account": "Remove hardware account", + "remove_hw_account_alert_description": "Are you sure you want to remove this hardware wallet account? You’ll have to resync your hardware wallet if you want to use this account again with MetaMask Mobile.", + "remove_account_alert_remove_btn": "Remove", + "remove_account_alert_cancel_btn": "Nevermind", "accounts_title": "Akun", "connect_account_title": "Hubungkan akun", "connect_accounts_title": "Hubungkan akun", @@ -668,8 +670,7 @@ "hint_text": "Pindai dompet Keystone Anda ke ", "purpose_connect": "hubungkan", "purpose_sign": "konfirmasikan transaksi", - "select_accounts": "Pilih Akun", - "please_wait": "Harap tunggu" + "select_accounts": "Pilih Akun" }, "data_collection_modal": { "accept": "Oke", @@ -682,6 +683,11 @@ "forget": "Lupakan perangkat ini" }, "app_settings": { + "enabling_notifications": "Enabling notifications...", + "disabling_notifications": "Disabling notifications...", + "enabling_profile_sync": "Enabling profile syncing...", + "disabling_profile_sync": "Disabling profile syncing...", + "notifications_dismiss_modal": "Dismiss", "title": "Pengaturan", "current_conversion": "Mata Uang Dasar", "current_language": "Bahasa Saat ini", @@ -805,6 +811,7 @@ "security_desc": "Pengaturan privasi, MetaMetrics, kunci pribadi, dan Frasa Pemulihan Rahasia dompet", "networks_title": "Jaringan", "networks_default_title": "Jaringan Default", + "network_delete": "Jika menghapus jaringan ini, Anda harus menambahkannya lagi untuk melihat aset di jaringan ini", "networks_default_cta": "Gunakan jaringan ini", "networks_desc": "Tambahkan dan edit jaringan RPC khusus", "network_name_label": "Nama Jaringan", @@ -820,6 +827,7 @@ "network_other_networks": "Jaringan Lain", "network_rpc_networks": "Jaringan RPC", "network_add_network": "Tambahkan Jaringan", + "network_add_custom_network": "Tambahkan jaringan khusus", "network_add": "Tambahkan", "network_save": "Simpan", "remove_network_title": "Apakah Anda ingin menghapus jaringan ini?", @@ -994,7 +1002,7 @@ "token_nft_ens_subheading": "Autodeteksi Token, NFT, dan ENS", "security_check_subheading": "Pemeriksaan keamanan", "symbol_required": "Simbol wajib diisi.", - "blockaid_desc": "Menjaga privasi - tidak ada data yang dibagikan kepada pihak ketiga. Tersedia di Arbitrum, Avalanche, BNB chain, Mainnet Ethereum, Optimism, Polygon, Sepolia, dan Base.", + "blockaid_desc": "Fitur ini memperingatkan Anda tentang aktivitas jahat dengan meninjau permintaan transaksi dan tanda tangan secara aktif.", "security_alerts": "Peringatan keamanan", "security_alerts_desc": "Fitur ini memperingatkan Anda tentang aktivitas berbahaya dengan meninjau permintaan transaksi dan tanda tangan secara lokal. Selalu lakukan uji tuntas sendiri sebelum menyetujui permintaan. Tidak ada jaminan bahwa fitur ini akan mendeteksi semua aktivitas berbahaya. Dengan mengaktifkan fitur ini, Anda menyetujui persyaratan penggunaan penyedia.", "smart_transactions_opt_in_heading": "Transaksi Pintar", @@ -1031,6 +1039,7 @@ "cancel": "Batal", "loading": "Menghubungkan ke MetaMask...", "unkown_dapp": "Nama Aplikasi Terdesentralisasi (DAPP) tidak tersedia", + "unknown": "Tidak dikenal", "no_connections": "Tidak ada koneksi", "no_connections_desc": "Jika Anda menghubungkan akun ke situs atau aplikasi, Anda akan melihatnya di sini." }, @@ -1654,6 +1663,7 @@ "network_currency_symbol": "Simbol Mata Uang", "network_block_explorer_url": "URL Block Explorer", "search": "Cari jaringan yang ditambahkan sebelumnya", + "search-short": "Cari", "add": "Tambahkan", "cancel": "Batal", "approve": "Setujui", @@ -1661,7 +1671,9 @@ "malicious_network_warning": "Penyedia jaringan jahat dapat berbohong tentang status blockchain dan merekam aktivitas jaringan Anda. Hanya tambahkan jaringan kustom yang Anda percayai.", "security_link": "https://support.metamask.io/networks-and-sidechains/managing-networks/user-guide-custom-networks-and-sidechains/", "network_warning_title": "Informasi Jaringan", + "additional_network_information_title": "Informasi Jaringan Tambahan", "network_warning_desc": "Koneksi jaringan ini mengandalkan pihak ketiga. Koneksi ini mungkin kurang bisa diandalkan atau memungkinkan pihak ketiga melacak aktivitas.", + "additonial_network_information_desc": "Beberapa jaringan ini mengandalkan pihak ketiga. Koneksi ini kurang dapat diandalkan atau memungkinkan pihak ketiga melacak aktivitas.", "learn_more": "Pelajari selengkapnya", "learn_more_url": "https://support.metamask.io/networks-and-sidechains/managing-networks/the-risks-of-connecting-to-an-unknown-network/", "switch_network": "Beralih ke jaringan", @@ -1950,7 +1962,7 @@ "received": "Diterima", "unstaked": "Stake dibatalkan", "to": "Ke", - "rate": "Tarif (termasuk biaya)", + "rate": "Rate (fees included)", "unstaking_requested": "Permintaan pembatalan stake", "stake_completed": "Stake selesai", "withdrawal_completed": "Penarikan selesai", @@ -1960,6 +1972,49 @@ "swap_completed": "Swap {{from}} dengan {{to}}", "swap": "Ditukar", "sent": "Dikirim ke {{address}}", + "menu_item_title": { + "metamask_swap_completed": "Swapped {{symbol1}} for {{symbol2}}", + "erc20_sent": "Sent to {{address}}", + "erc20_received": "Received from {{address}}", + "eth_sent": "Sent to {{address}}", + "eth_received": "Received from {{address}}", + "erc721_sent": "Sent NFT to {{address}}", + "erc1155_sent": "Sent NFT to {{address}}", + "erc721_received": "Received NFT from {{address}}", + "erc1155_received": "Received NFT from {{address}}", + "rocketpool_stake_completed": "Staked", + "rocketpool_unstake_completed": "Unstaking complete", + "lido_stake_completed": "Staked", + "lido_withdrawal_requested": "Unstaking requested", + "lido_withdrawal_completed": "Unstaking complete", + "lido_stake_ready_to_be_withdrawn": "Withdrawal requested" + }, + "menu_item_description": { + "lido_withdrawal_requested": "Your request to unstake {{amount}} {{symbol}} has been sent", + "lido_stake_ready_to_be_withdrawn": "You can now withdraw your unstaked {{symbol}}" + }, + "modal": { + "title_sent": "Sent {{symbol}}", + "title_received": "Received {{symbol}}", + "title_unstake_requested": "Unstaking requested", + "title_untake_ready": "Withdrawl ready", + "title_stake": "Staked {{symbol}}", + "title_unstake_completed": "Unstaking completed", + "title_swapped": "Swapped {{symbol1}} to {{symbol2}}", + "label_address_to": "To", + "label_address_from": "From", + "label_address_to_you": "To (You)", + "label_address_from_you": "From (You)", + "label_asset": "Asset", + "label_account": "Account", + "label_unstaking_requested": "Unstaking Requested", + "label_unstake_ready": "Withdrawl ready", + "label_staked": "Staked", + "label_received": "Received", + "label_unstaking_confirmed": "Unstaking Confirmed", + "label_swapped": "Swapped", + "label_to": "To" + }, "received_from": "Diterima dari {{address}}", "nft_sent": "Mengirim NFT ke {{address}}", "erc721_sent": "Mengirim NFT ke {{address}}", @@ -1993,6 +2048,8 @@ "received_message": "Ketuk untuk melihat transaksi ini", "received_payment_message": "Anda menerima {{amount}} DAI", "prompt_title": "Aktifkan Pemberitahuan Push", + "notifications_enabled_error_title": "Something went wrong", + "notifications_enabled_error_desc": "We couldn't enable notifications. Please try again later.", "prompt_desc": "Aktifkan pemberitahuan agar MetaMask dapat memberitahukan saat Anda menerima ETH atau saat transaksi Anda telah dikonfirmasi.", "prompt_ok": "Ya", "prompt_cancel": "Tidak, terima kasih", @@ -2017,6 +2074,7 @@ "1": "Dompet", "2": "Pengumuman" }, + "copied_to_clipboard": "Copied to clipboard", "address_copied_to_clipboard": "Alamat disalin ke papan klip", "transaction_id_copied_to_clipboard": "ID Transaksi disalin ke papan klip", "activation_card": { @@ -2648,7 +2706,7 @@ "header": "Peningkatan Perlindungan Transaksi", "description_1": "Raih tingkat keberhasilan yang lebih tinggi, perlindungan frontrunning, dan visibilitas yang lebih baik dengan Transaksi Pintar.", "description_2": "Hanya tersedia di Ethereum. Aktifkan atau nonaktifkan setiap saat di pengaturan.", - "secondary_button": "Kelola di pengaturan", + "no_thanks": "No thanks", "primary_button": "Aktifkan", "learn_more": "Selengkapnya.", "benefit_1_1": "Keberhasilan 99,5%", @@ -2847,6 +2905,15 @@ "enable_remember_me": "Aktifkan Ingatkan saya", "enable_remember_me_description": "Saat Ingatkan saya aktif, siapa pun yang memiliki akses ke ponsel Anda dapat mengakses akun MetaMask Anda." }, + "profile_sync": { + "title": "Profile Sync", + "disable_warning": "If you turn off profile sync, you won’t be able to receive notifications.", + "enable_description": "Creates a profile that MetaMask uses to sync some settings among your devices. This is required to get notifications. ", + "enable_description2": " to learn more.", + "enable_privacy_link": "Learn how we protect your privacy", + "bottomSheetTurnOn": "Turning on Profile Sync", + "bottomSheetTurnOff": "Turning off Profile Sync" + }, "turn_off_remember_me": { "title": "Masukkan kata sandi Anda untuk menonaktifkan Ingatkan saya", "placeholder": "Kata sandi", @@ -3069,7 +3136,7 @@ }, "install_snap": { "title": "Permintaan koneksi", - "description": "{{origin}} ingin mengunduh dan terhubung dengan {{snap}}. Pastikan Anda memercayai pembuatnya sebelum melanjutkan.", + "description": "{{origin}} ingin menggunakan {{snap}}.", "permissions_request_title": "Permintaan izin", "permissions_request_description": "{{origin}} ingin menginstal {{snap}}, yang meminta izin berikut.", "approve_permissions": "Setujui", @@ -3118,5 +3185,8 @@ "title": "Estimasi perubahan", "tooltip_description": "Estimasi perubahan merupakan hal yang mungkin terjadi jika Anda melakukan transaksi ini. Ini hanyalah prediksi, bukan jaminan.", "total_fiat": "Total = {{currency}}" + }, + "common": { + "please_wait": "Please wait" } -} +} \ No newline at end of file diff --git a/locales/languages/ja.json b/locales/languages/ja.json index 631dcfc7ebf..9cdf27aec48 100644 --- a/locales/languages/ja.json +++ b/locales/languages/ja.json @@ -5,8 +5,6 @@ "deceptive_request_title": "これは虚偽のリクエストです", "failed_title": "リクエストが安全でない可能性があります", "failed_description": "エラーが発生したため、このリクエストはセキュリティプロバイダーにより確認されませんでした。慎重に進めてください。", - "loading_title": "セキュリティアラートを確認中...", - "loading_complete_title": "アラートはありません。要求を承認する前に、必ず独自のデューデリジェンスを行ってください。", "malicious_domain_description": "悪質なドメインとやり取りしています。このリクエストを承認すると、資産を失う可能性があります。", "other_description": "このリクエストを承認すると、資産を失う可能性があります。", "raw_signature_farming_description": "このリクエストを承認すると、資産を失う可能性があります。", @@ -613,6 +611,10 @@ "remove_account_message": "このアカウントを削除してよろしいですか?", "no": "いいえ", "yes_remove_it": "はい、削除します", + "remove_hardware_account": "Remove hardware account", + "remove_hw_account_alert_description": "Are you sure you want to remove this hardware wallet account? You’ll have to resync your hardware wallet if you want to use this account again with MetaMask Mobile.", + "remove_account_alert_remove_btn": "Remove", + "remove_account_alert_cancel_btn": "Nevermind", "accounts_title": "アカウント", "connect_account_title": "アカウントの接続", "connect_accounts_title": "アカウントの接続", @@ -668,8 +670,7 @@ "hint_text": "Keystoneウォレットをスキャンして ", "purpose_connect": "接続", "purpose_sign": "トランザクションを確定", - "select_accounts": "アカウントを選択してください", - "please_wait": "お待ちください" + "select_accounts": "アカウントを選択してください" }, "data_collection_modal": { "accept": "OK", @@ -682,6 +683,11 @@ "forget": "このデバイスの登録を解除" }, "app_settings": { + "enabling_notifications": "Enabling notifications...", + "disabling_notifications": "Disabling notifications...", + "enabling_profile_sync": "Enabling profile syncing...", + "disabling_profile_sync": "Disabling profile syncing...", + "notifications_dismiss_modal": "Dismiss", "title": "設定", "current_conversion": "ベース通貨", "current_language": "現在の言語", @@ -805,6 +811,7 @@ "security_desc": "プライバシー設定、MetaMetrics、秘密鍵、ウォレットのシークレットリカバリーフレーズ", "networks_title": "ネットワーク", "networks_default_title": "デフォルトのネットワーク", + "network_delete": "このネットワークを削除した場合、このネットワーク内の資産を見るには、再度ネットワークの追加が必要になります。", "networks_default_cta": "このネットワークを使用", "networks_desc": "カスタムRPCネットワークの追加と編集", "network_name_label": "ネットワーク名", @@ -820,6 +827,7 @@ "network_other_networks": "他のネットワーク", "network_rpc_networks": "RPCネットワーク", "network_add_network": "ネットワークを追加", + "network_add_custom_network": "カスタムネットワークを追加", "network_add": "追加", "network_save": "保存", "remove_network_title": "このネットワークを削除しますか?", @@ -994,7 +1002,7 @@ "token_nft_ens_subheading": "トークン、NFT、ENSの自動検出", "security_check_subheading": "セキュリティチェック", "symbol_required": "シンボルが必要です", - "blockaid_desc": "プライバシーを保護 - サードパーティとデータが一切共有されません。Arbitrum、Avalanche、BNB Chain、イーサリアムメインネット、Optimism、Polygon、Sepolia、Baseで利用可能。", + "blockaid_desc": "この機能は、トランザクションと署名要求を能動的に確認し、悪質なアクティビティに関するアラートを発します。", "security_alerts": "セキュリティアラート", "security_alerts_desc": "この機能は、トランザクションと署名要求をローカルで確認することで、悪質な行為に関するアラートを発します。要求を承認する前に、必ず独自のデューデリジェンスを行ってください。この機能がすべての悪質な行為を検出するという保証はありません。この機能を有効にすることで、プロバイダーの利用規約に同意したものとみなされます。", "smart_transactions_opt_in_heading": "スマートトランザクション", @@ -1031,6 +1039,7 @@ "cancel": "キャンセル", "loading": "MetaMaskに接続しています...", "unkown_dapp": "DApp名が利用できません", + "unknown": "不明", "no_connections": "接続がありません", "no_connections_desc": "サイトまたはアプリにアカウントを接続すると、ここに表示されます。" }, @@ -1654,6 +1663,7 @@ "network_currency_symbol": "通貨記号", "network_block_explorer_url": "ブロックエクスプローラーのURL", "search": "以前追加されたネットワークを検索", + "search-short": "検索", "add": "追加", "cancel": "キャンセル", "approve": "承認", @@ -1661,7 +1671,9 @@ "malicious_network_warning": "悪意のあるネットワーク プロバイダーは、ブロックチェーンのステータスを偽り、ユーザーのネットワークアクティビティを記録することがあります。信頼するカスタムネットワークのみを追加してください。", "security_link": "https://support.metamask.io/networks-and-sidechains/managing-networks/user-guide-custom-networks-and-sidechains/", "network_warning_title": "ネットワーク情報", + "additional_network_information_title": "その他のネットワーク情報", "network_warning_desc": "このネットワーク接続は第三者に依存しているため、信頼性が低い可能性、または第三者がアクティビティをトラッキングできる可能性があります。", + "additonial_network_information_desc": "これらのネットワークの一部はサードパーティに依存しているため、接続の信頼性が低かったり、サードパーティによるアクティビティの追跡が可能になったりする可能性があります。", "learn_more": "詳細", "learn_more_url": "https://support.metamask.io/networks-and-sidechains/managing-networks/the-risks-of-connecting-to-an-unknown-network/", "switch_network": "ネットワークに切り替える", @@ -1950,7 +1962,7 @@ "received": "受け取りました", "unstaked": "ステーキングが解除されました", "to": "送り先", - "rate": "レート (手数料込み)", + "rate": "Rate (fees included)", "unstaking_requested": "ステーキングの解除がリクエストされました", "stake_completed": "ステーキングが完了しました", "withdrawal_completed": "出金が完了しました", @@ -1960,6 +1972,49 @@ "swap_completed": "{{from}}を{{to}}にスワップしました", "swap": "スワップしました", "sent": "{{address}}に送りました", + "menu_item_title": { + "metamask_swap_completed": "Swapped {{symbol1}} for {{symbol2}}", + "erc20_sent": "Sent to {{address}}", + "erc20_received": "Received from {{address}}", + "eth_sent": "Sent to {{address}}", + "eth_received": "Received from {{address}}", + "erc721_sent": "Sent NFT to {{address}}", + "erc1155_sent": "Sent NFT to {{address}}", + "erc721_received": "Received NFT from {{address}}", + "erc1155_received": "Received NFT from {{address}}", + "rocketpool_stake_completed": "Staked", + "rocketpool_unstake_completed": "Unstaking complete", + "lido_stake_completed": "Staked", + "lido_withdrawal_requested": "Unstaking requested", + "lido_withdrawal_completed": "Unstaking complete", + "lido_stake_ready_to_be_withdrawn": "Withdrawal requested" + }, + "menu_item_description": { + "lido_withdrawal_requested": "Your request to unstake {{amount}} {{symbol}} has been sent", + "lido_stake_ready_to_be_withdrawn": "You can now withdraw your unstaked {{symbol}}" + }, + "modal": { + "title_sent": "Sent {{symbol}}", + "title_received": "Received {{symbol}}", + "title_unstake_requested": "Unstaking requested", + "title_untake_ready": "Withdrawl ready", + "title_stake": "Staked {{symbol}}", + "title_unstake_completed": "Unstaking completed", + "title_swapped": "Swapped {{symbol1}} to {{symbol2}}", + "label_address_to": "To", + "label_address_from": "From", + "label_address_to_you": "To (You)", + "label_address_from_you": "From (You)", + "label_asset": "Asset", + "label_account": "Account", + "label_unstaking_requested": "Unstaking Requested", + "label_unstake_ready": "Withdrawl ready", + "label_staked": "Staked", + "label_received": "Received", + "label_unstaking_confirmed": "Unstaking Confirmed", + "label_swapped": "Swapped", + "label_to": "To" + }, "received_from": "{{address}}から受け取りました", "nft_sent": "{{address}}にNFTを送りました", "erc721_sent": "{{address}}にNFTを送りました", @@ -1993,6 +2048,8 @@ "received_message": "このトランザクションを表示するにはタップしてください", "received_payment_message": "{{amount}}DAI受け取りました", "prompt_title": "プッシュ通知を有効にする", + "notifications_enabled_error_title": "Something went wrong", + "notifications_enabled_error_desc": "We couldn't enable notifications. Please try again later.", "prompt_desc": "ETHを受け取ったり、トランザクションが確認された際にMetaMaskが通知できるよう、通知を有効にしてください。", "prompt_ok": "はい", "prompt_cancel": "いいえ、結構です", @@ -2017,6 +2074,7 @@ "1": "ウォレット", "2": "お知らせ" }, + "copied_to_clipboard": "Copied to clipboard", "address_copied_to_clipboard": "アドレスがクリップボードにコピーされました", "transaction_id_copied_to_clipboard": "トランザクションIDがクリップボードにコピーされました", "activation_card": { @@ -2648,7 +2706,7 @@ "header": "強化されたトランザクション保護", "description_1": "スマートトランザクションで、成功率を上げ、フロントランニングを防ぎ、可視性を高めましょう。", "description_2": "イーサリアムでのみご利用いただけ、いつでも設定で有効・無効を切り替えられます。", - "secondary_button": "設定で管理", + "no_thanks": "No thanks", "primary_button": "有効にする", "learn_more": "詳細。", "benefit_1_1": "99.5%の", @@ -2847,6 +2905,15 @@ "enable_remember_me": "認証情報の保存を有効にする", "enable_remember_me_description": "認証情報の保存を有効にすると、携帯にアクセスできる人は誰でもMetaMaskアカウントにアクセスできるようになります。" }, + "profile_sync": { + "title": "Profile Sync", + "disable_warning": "If you turn off profile sync, you won’t be able to receive notifications.", + "enable_description": "Creates a profile that MetaMask uses to sync some settings among your devices. This is required to get notifications. ", + "enable_description2": " to learn more.", + "enable_privacy_link": "Learn how we protect your privacy", + "bottomSheetTurnOn": "Turning on Profile Sync", + "bottomSheetTurnOff": "Turning off Profile Sync" + }, "turn_off_remember_me": { "title": "認証情報の保存を無効にするには、パスワードを入力してください", "placeholder": "パスワード", @@ -3069,7 +3136,7 @@ }, "install_snap": { "title": "接続の要求", - "description": "{{origin}}が{{snap}}のダウンロードと接続を要求しています。進める前に、作成者が信頼できることを確認してください。", + "description": "{{origin}}が{{snap}}の使用許可を求めています。", "permissions_request_title": "アクセス許可の要求", "permissions_request_description": "{{origin}}が{{snap}}のインストールを要求していて、このSnapは次のアクセス許可を要求しています。", "approve_permissions": "承認", @@ -3118,5 +3185,8 @@ "title": "予測される増減額", "tooltip_description": "予測される増減額は、このトランザクションを実行すると発生する可能性がある増減額です。これは単に予測に過ぎず、保証されたものではありません。", "total_fiat": "合計 = {{currency}}" + }, + "common": { + "please_wait": "Please wait" } -} +} \ No newline at end of file diff --git a/locales/languages/ko.json b/locales/languages/ko.json index ab9cefebaab..af85cc522aa 100644 --- a/locales/languages/ko.json +++ b/locales/languages/ko.json @@ -5,8 +5,6 @@ "deceptive_request_title": "사기성 요청입니다", "failed_title": "안전하지 않은 요청일 수 있습니다", "failed_description": "오류로 인해 보안업체에서 이 요청을 확인하지 못했습니다. 주의하여 진행하세요.", - "loading_title": "보안 경고 확인하는 중...", - "loading_complete_title": "수신한 경고가 없습니다. 요청을 승인하기 전에 항상 직접 확인하세요.", "malicious_domain_description": "악성 도메인과 인터렉션하고 있습니다. 이 요청을 승인하면 본인의 자산을 잃을 수도 있습니다.", "other_description": "이 요청을 승인하면, 자산을 잃을 수 있습니다.", "raw_signature_farming_description": "이 요청을 승인하면, 자산을 잃을 수 있습니다.", @@ -613,6 +611,10 @@ "remove_account_message": "해당 계정을 정말로 제거하시겠습니까?", "no": "아니오", "yes_remove_it": "네. 제거하겠습니다", + "remove_hardware_account": "Remove hardware account", + "remove_hw_account_alert_description": "Are you sure you want to remove this hardware wallet account? You’ll have to resync your hardware wallet if you want to use this account again with MetaMask Mobile.", + "remove_account_alert_remove_btn": "Remove", + "remove_account_alert_cancel_btn": "Nevermind", "accounts_title": "계정", "connect_account_title": "계정 연결", "connect_accounts_title": "계정 연결", @@ -668,8 +670,7 @@ "hint_text": "키스톤 지갑 스캔을 통해 ", "purpose_connect": "연결", "purpose_sign": "트랜잭션 컨펌", - "select_accounts": "계정 선택", - "please_wait": "잠시 기다리세요" + "select_accounts": "계정 선택" }, "data_collection_modal": { "accept": "확인", @@ -682,6 +683,11 @@ "forget": "이 기기 제거" }, "app_settings": { + "enabling_notifications": "Enabling notifications...", + "disabling_notifications": "Disabling notifications...", + "enabling_profile_sync": "Enabling profile syncing...", + "disabling_profile_sync": "Disabling profile syncing...", + "notifications_dismiss_modal": "Dismiss", "title": "설정", "current_conversion": "기준 통화", "current_language": "현재 언어", @@ -805,6 +811,7 @@ "security_desc": "개인정보 설정, 메타 매트릭스, 개인 키 및 지갑 비밀복구구문", "networks_title": "네트워크", "networks_default_title": "기본 네트워크", + "network_delete": "이 네트워크를 삭제하시면, 이 네트워크에 있는 자산을 볼 때 네트워크를 다시 추가해야 합니다", "networks_default_cta": "이 네트워크 사용", "networks_desc": "맞춤 RPC 네트워크 추가 및 편집", "network_name_label": "네트워크 이름", @@ -820,6 +827,7 @@ "network_other_networks": "다른 네트워크", "network_rpc_networks": "RPC 네트워크", "network_add_network": "네트워크 추가", + "network_add_custom_network": "사용자 지정 네트워크 추가", "network_add": "추가", "network_save": "저장", "remove_network_title": "해당 네트워크를 제거하시겠습니까?", @@ -994,7 +1002,7 @@ "token_nft_ens_subheading": "토큰, NFT 및 ENS 자동 감지", "security_check_subheading": "보안 점검", "symbol_required": "심볼은 필수입니다.", - "blockaid_desc": "개인정보 보호 - 제3자와 데이터를 공유하지 않습니다. Arbitrum, Avalanche, BNB chain, 이더리움 메인넷, Optimism, Polygon, Sepolia, Base에서 사용할 수 있습니다.", + "blockaid_desc": "이 기능은 트랜잭션과 서명 요청을 적극적으로 검토하여 악의적인 활동을 경고합니다.", "security_alerts": "보안 경고", "security_alerts_desc": "이 기능은 트랜잭션 및 서명 요청을 로컬에서 검토하여 악의적인 활동이 있는 경우 경고합니다. 요청을 승인하기 전에 항상 직접 검토하세요. 이 기능이 모든 악성 활동을 탐지하는 것은 아닙니다. 이 기능을 활성화하면 제공 업체의 이용 약관에 동의하는 것이 됩니다.", "smart_transactions_opt_in_heading": "스마트 트랜잭션", @@ -1031,6 +1039,7 @@ "cancel": "취소", "loading": "MetaMask 연결 중...", "unkown_dapp": "디앱 이름을 사용할 수 없습니다", + "unknown": "알 수 없음", "no_connections": "연결 없음", "no_connections_desc": "계정을 사이트나 앱에 연결하면 여기에 표시됩니다." }, @@ -1654,6 +1663,7 @@ "network_currency_symbol": "통화 기호", "network_block_explorer_url": "블록 탐색기 URL", "search": "이전에 추가된 네트워크 검색", + "search-short": "검색", "add": "추가", "cancel": "취소", "approve": "승인", @@ -1661,7 +1671,9 @@ "malicious_network_warning": "악성 네트워크 공급업체는 블록체인 상태를 거짓으로 보고하고 네트워크 활동을 기록할 수 있습니다. 신뢰하는 커스텀 네트워크만 추가하세요.", "security_link": "https://support.metamask.io/networks-and-sidechains/managing-networks/user-guide-custom-networks-and-sidechains/", "network_warning_title": "네트워크 정보", + "additional_network_information_title": "추가 네트워크 정보", "network_warning_desc": "이 네트워크 연결은 타사 서비스를 이용합니다. 연결의 신뢰성이 낮거나 타사가 활동을 추적할 수 있습니다.", + "additonial_network_information_desc": "이러한 네트워크 중 일부는 제삼자에 의존합니다. 이러한 연결은 안정성이 떨어지거나 제삼자가 활동을 추적할 수 있습니다.", "learn_more": "더 보기", "learn_more_url": "https://support.metamask.io/networks-and-sidechains/managing-networks/the-risks-of-connecting-to-an-unknown-network/", "switch_network": "네트워크 전환", @@ -1950,7 +1962,7 @@ "received": "받음", "unstaked": "언스테이킹됨", "to": "수신:", - "rate": "환율(수수료 포함)", + "rate": "Rate (fees included)", "unstaking_requested": "언스테이킹 요청 완료", "stake_completed": "스테이킹 완료", "withdrawal_completed": "인출 완료", @@ -1960,6 +1972,49 @@ "swap_completed": "{{from}}에서 {{to}}(으)로 스왑됨", "swap": "스왑됨", "sent": "{{address}}(으)로 보냄", + "menu_item_title": { + "metamask_swap_completed": "Swapped {{symbol1}} for {{symbol2}}", + "erc20_sent": "Sent to {{address}}", + "erc20_received": "Received from {{address}}", + "eth_sent": "Sent to {{address}}", + "eth_received": "Received from {{address}}", + "erc721_sent": "Sent NFT to {{address}}", + "erc1155_sent": "Sent NFT to {{address}}", + "erc721_received": "Received NFT from {{address}}", + "erc1155_received": "Received NFT from {{address}}", + "rocketpool_stake_completed": "Staked", + "rocketpool_unstake_completed": "Unstaking complete", + "lido_stake_completed": "Staked", + "lido_withdrawal_requested": "Unstaking requested", + "lido_withdrawal_completed": "Unstaking complete", + "lido_stake_ready_to_be_withdrawn": "Withdrawal requested" + }, + "menu_item_description": { + "lido_withdrawal_requested": "Your request to unstake {{amount}} {{symbol}} has been sent", + "lido_stake_ready_to_be_withdrawn": "You can now withdraw your unstaked {{symbol}}" + }, + "modal": { + "title_sent": "Sent {{symbol}}", + "title_received": "Received {{symbol}}", + "title_unstake_requested": "Unstaking requested", + "title_untake_ready": "Withdrawl ready", + "title_stake": "Staked {{symbol}}", + "title_unstake_completed": "Unstaking completed", + "title_swapped": "Swapped {{symbol1}} to {{symbol2}}", + "label_address_to": "To", + "label_address_from": "From", + "label_address_to_you": "To (You)", + "label_address_from_you": "From (You)", + "label_asset": "Asset", + "label_account": "Account", + "label_unstaking_requested": "Unstaking Requested", + "label_unstake_ready": "Withdrawl ready", + "label_staked": "Staked", + "label_received": "Received", + "label_unstaking_confirmed": "Unstaking Confirmed", + "label_swapped": "Swapped", + "label_to": "To" + }, "received_from": "{{address}}에서 받음", "nft_sent": "{{address}}(으)로 NFT 전송함", "erc721_sent": "{{address}}(으)로 NFT 전송함", @@ -1993,6 +2048,8 @@ "received_message": "눌러서 이 트랜잭션을 확인하세요", "received_payment_message": "{{amount}} DAI를 받았습니다", "prompt_title": "공지 알림 활성화", + "notifications_enabled_error_title": "Something went wrong", + "notifications_enabled_error_desc": "We couldn't enable notifications. Please try again later.", "prompt_desc": "공지 알림을 활성화해서 MetaMask가 언제 ETH를 받았는지 혹은 언제 트랜잭션이 컨펌되었는지 등의 알림을 받으세요.", "prompt_ok": "예", "prompt_cancel": "아니요, 괜찮습니다", @@ -2017,6 +2074,7 @@ "1": "지갑", "2": "공지" }, + "copied_to_clipboard": "Copied to clipboard", "address_copied_to_clipboard": "주소가 클립보드에 복사되었습니다", "transaction_id_copied_to_clipboard": "트랜잭션 ID를 클립보드에 복사했습니다", "activation_card": { @@ -2648,7 +2706,7 @@ "header": "트랜잭션 보호 강화", "description_1": "스마트 트랜잭션으로 선행거래를 방지하고 더 높은 성공률과 가시성을 확보하세요.", "description_2": "이더리움에서만 사용할 수 있습니다. 설정에서 언제든지 활성화하거나 비활성화할 수 있습니다.", - "secondary_button": "설정에서 관리", + "no_thanks": "No thanks", "primary_button": "활성화", "learn_more": "자세히 알아보세요.", "benefit_1_1": "99.5% 성공", @@ -2847,6 +2905,15 @@ "enable_remember_me": "로그인 정보 저장", "enable_remember_me_description": "로그인 정보를 저장하면 회원님의 휴대폰을 이용하는 모든 사람이 회원님의 MetaMask 계정에 액세스할 수 있습니다." }, + "profile_sync": { + "title": "Profile Sync", + "disable_warning": "If you turn off profile sync, you won’t be able to receive notifications.", + "enable_description": "Creates a profile that MetaMask uses to sync some settings among your devices. This is required to get notifications. ", + "enable_description2": " to learn more.", + "enable_privacy_link": "Learn how we protect your privacy", + "bottomSheetTurnOn": "Turning on Profile Sync", + "bottomSheetTurnOff": "Turning off Profile Sync" + }, "turn_off_remember_me": { "title": "로그인 정보 저장 기능을 끄려면 비밀번호를 입력하세요", "placeholder": "비밀번호", @@ -3069,7 +3136,7 @@ }, "install_snap": { "title": "연결 요청", - "description": "{{origin}}에서 {{snap}} Snap을 다운로드하고 연결하려고 합니다. 계속하기 전에 신뢰할 수 있는 개발자인지 확인하세요.", + "description": "{{origin}}이(가) {{snap}}을(를) 사용하고자 합니다.", "permissions_request_title": "승인 요청", "permissions_request_description": "{{origin}}에서 {{snap}} Snap을 설치하기 위하여 다음 권한을 요청합니다.", "approve_permissions": "승인", @@ -3118,5 +3185,8 @@ "title": "예상 변동 사항", "tooltip_description": "예상 변동 사항은 이 트랜잭션을 진행할 경우 발생하는 결과를 예측한 것입니다. 이는 예측일 뿐 결과를 보장하지는 않습니다.", "total_fiat": "합계 = {{currency}}" + }, + "common": { + "please_wait": "Please wait" } -} +} \ No newline at end of file diff --git a/locales/languages/pt.json b/locales/languages/pt.json index d552edbd391..741219fdaf8 100644 --- a/locales/languages/pt.json +++ b/locales/languages/pt.json @@ -5,8 +5,6 @@ "deceptive_request_title": "Essa solicitação é enganosa", "failed_title": "A solicitação pode não ser segura", "failed_description": "Em razão de um erro, essa solicitação não foi confirmada pelo provedor de segurança. Prossiga com cautela.", - "loading_title": "Verificando se há alertas de segurança...", - "loading_complete_title": "Nenhum alerta recebido. Sempre faça sua própria devida diligência antes de aprovar solicitações.", "malicious_domain_description": "Você está interagindo com um domínio mal-intencionado. Se você aprovar essa solicitação, poderá perder seus ativos.", "other_description": "Se você aprovar essa solicitação, poderá perder seus ativos.", "raw_signature_farming_description": "Se você aprovar essa solicitação, poderá perder seus ativos.", @@ -413,7 +411,7 @@ "no_tokens": "Você não tem nenhum token!", "no_nfts_yet": "Nenhum NFT até agora", "nfts_autodetection_title": "Detecção de NFTs", - "nfts_autodetection_desc": "Permite à MetaMask detectar e exibir automaticamente os NFTs em sua carteira.", + "nfts_autodetection_desc": "Permita que a MetaMask detecte e exiba automaticamente os NFTs em sua carteira.", "network_details_check": "A verificação dos dados da rede", "network_with_chain_id": "A rede com a ID de cadeia", "chain_list_returned_different_ticker_symbol": "O símbolo deste token não corresponde ao nome ou ID da cadeia inseridos para a rede. Muitos tokens populares apresentam símbolos semelhantes, que podem ser usados por golpistas para induzir você ao erro de enviar um token mais valioso em troca. Verifique todos os detalhes antes de continuar.", @@ -613,6 +611,10 @@ "remove_account_message": "Tem certeza de que deseja remover esta conta?", "no": "Não", "yes_remove_it": "Sim, remover", + "remove_hardware_account": "Remove hardware account", + "remove_hw_account_alert_description": "Are you sure you want to remove this hardware wallet account? You’ll have to resync your hardware wallet if you want to use this account again with MetaMask Mobile.", + "remove_account_alert_remove_btn": "Remove", + "remove_account_alert_cancel_btn": "Nevermind", "accounts_title": "Contas", "connect_account_title": "Conectar conta", "connect_accounts_title": "Conectar contas", @@ -668,8 +670,7 @@ "hint_text": "Leia sua carteira Keystone para ", "purpose_connect": "conectar", "purpose_sign": "confirmar a transação", - "select_accounts": "Selecione uma conta", - "please_wait": "Aguarde" + "select_accounts": "Selecione uma conta" }, "data_collection_modal": { "accept": "OK", @@ -682,6 +683,11 @@ "forget": "Esquecer este dispositivo" }, "app_settings": { + "enabling_notifications": "Enabling notifications...", + "disabling_notifications": "Disabling notifications...", + "enabling_profile_sync": "Enabling profile syncing...", + "disabling_profile_sync": "Disabling profile syncing...", + "notifications_dismiss_modal": "Dismiss", "title": "Configurações", "current_conversion": "Moeda base", "current_language": "Idioma atual", @@ -805,6 +811,7 @@ "security_desc": "Configurações de privacidade, MetaMetrics, chave privada e Frase de Recuperação Secreta da carteira", "networks_title": "Redes", "networks_default_title": "Rede padrão", + "network_delete": "Se você excluir esta rede, precisará adicioná-la de novo para visualizar seus ativos nela", "networks_default_cta": "Usar essa rede", "networks_desc": "Adicione e edite redes RPC personalizadas", "network_name_label": "Nome da rede", @@ -820,6 +827,7 @@ "network_other_networks": "Outras redes", "network_rpc_networks": "Redes RPC", "network_add_network": "Adicionar rede", + "network_add_custom_network": "Adicionar uma rede personalizada", "network_add": "Adicionar", "network_save": "Salvar", "remove_network_title": "Deseja remover essa rede?", @@ -994,7 +1002,7 @@ "token_nft_ens_subheading": "Detecção automática de tokens, NFTs e ENS", "security_check_subheading": "Verificações de segurança", "symbol_required": "O símbolo é obrigatório.", - "blockaid_desc": "Proteção de privacidade: nenhum dado é compartilhado com terceiros. Disponível em Arbitrum, Avalanche, BNB Chain, Mainnet da Ethereum, Optimism, Polygon, Sepolia e Base.", + "blockaid_desc": "Esse recurso alerta você sobre atividades mal-intencionadas analisando ativamente as solicitações de transações e assinaturas.", "security_alerts": "Alertas de segurança", "security_alerts_desc": "Esse recurso alerta sobre atividades mal-intencionadas por meio da análise local de solicitações de transações e assinaturas. Sempre realize sua própria devida diligência antes de aprovar solicitações. Não há garantia de que esse recurso detectará toda e qualquer atividade mal-intencionada. Ao ativar esse recurso, você concorda com os termos de uso do provedor.", "smart_transactions_opt_in_heading": "Transações inteligentes", @@ -1031,6 +1039,7 @@ "cancel": "Cancelar", "loading": "Conectando à MetaMask...", "unkown_dapp": "Nome do dapp indisponível", + "unknown": "Desconhecido", "no_connections": "Nenhuma conexão", "no_connections_desc": "Se conectar uma conta a um site ou app, você os verá aqui." }, @@ -1202,9 +1211,9 @@ "enable_nft-auto-detection": { "title": "Ativar detecção automática de NFTs", "description": "Permita que a MetaMask detecte e exiba seus NFTs com detecção automática. Você será capaz de:", - "immediateAccess": "Acessar suas NFTs imediatamente", + "immediateAccess": "Acessar seus NFTs imediatamente", "navigate": "Navegar tranquilo por seus ativos digitais", - "dive": "Acessar e começar a usar diretamente seus NFTs", + "dive": "Acessar diretamente e começar a usar seus NFTs", "allow": "Permitir", "notRightNow": "Agora não" }, @@ -1654,6 +1663,7 @@ "network_currency_symbol": "Símbolo da moeda", "network_block_explorer_url": "URL do explorador de blocos", "search": "Pesquisar rede adicionada anteriormente", + "search-short": "Pesquisar", "add": "Adicionar", "cancel": "Cancelar", "approve": "Aprovar", @@ -1661,7 +1671,9 @@ "malicious_network_warning": "Um provedor de rede mal-intencionado pode mentir sobre o estado da blockchain e registrar sua atividade na rede. Adicione apenas redes personalizadas em que você confia.", "security_link": "https://support.metamask.io/networks-and-sidechains/managing-networks/user-guide-custom-networks-and-sidechains/", "network_warning_title": "Informações da rede", + "additional_network_information_title": "Informações sobre redes adicionais", "network_warning_desc": "Essa conexão de rede depende de terceiros. Ela pode ser menos confiável ou permitir que eles rastreiem as atividades.", + "additonial_network_information_desc": "Algumas dessas redes dependem de terceiros. As conexões podem ser menos confiáveis ​​ou permitir que terceiros rastreiem atividades.", "learn_more": "Saiba mais", "learn_more_url": "https://support.metamask.io/networks-and-sidechains/managing-networks/the-risks-of-connecting-to-an-unknown-network/", "switch_network": "Alternar para rede", @@ -1950,7 +1962,7 @@ "received": "Recebido", "unstaked": "Retirado de staking", "to": "Para", - "rate": "Cotação (taxas incluídas)", + "rate": "Rate (fees included)", "unstaking_requested": "Retirada de staking solicitada", "stake_completed": "Staking concluído", "withdrawal_completed": "Saque concluído", @@ -1960,6 +1972,49 @@ "swap_completed": "{{from}} trocado por {{to}}", "swap": "Trocado", "sent": "Enviou para {{address}}", + "menu_item_title": { + "metamask_swap_completed": "Swapped {{symbol1}} for {{symbol2}}", + "erc20_sent": "Sent to {{address}}", + "erc20_received": "Received from {{address}}", + "eth_sent": "Sent to {{address}}", + "eth_received": "Received from {{address}}", + "erc721_sent": "Sent NFT to {{address}}", + "erc1155_sent": "Sent NFT to {{address}}", + "erc721_received": "Received NFT from {{address}}", + "erc1155_received": "Received NFT from {{address}}", + "rocketpool_stake_completed": "Staked", + "rocketpool_unstake_completed": "Unstaking complete", + "lido_stake_completed": "Staked", + "lido_withdrawal_requested": "Unstaking requested", + "lido_withdrawal_completed": "Unstaking complete", + "lido_stake_ready_to_be_withdrawn": "Withdrawal requested" + }, + "menu_item_description": { + "lido_withdrawal_requested": "Your request to unstake {{amount}} {{symbol}} has been sent", + "lido_stake_ready_to_be_withdrawn": "You can now withdraw your unstaked {{symbol}}" + }, + "modal": { + "title_sent": "Sent {{symbol}}", + "title_received": "Received {{symbol}}", + "title_unstake_requested": "Unstaking requested", + "title_untake_ready": "Withdrawl ready", + "title_stake": "Staked {{symbol}}", + "title_unstake_completed": "Unstaking completed", + "title_swapped": "Swapped {{symbol1}} to {{symbol2}}", + "label_address_to": "To", + "label_address_from": "From", + "label_address_to_you": "To (You)", + "label_address_from_you": "From (You)", + "label_asset": "Asset", + "label_account": "Account", + "label_unstaking_requested": "Unstaking Requested", + "label_unstake_ready": "Withdrawl ready", + "label_staked": "Staked", + "label_received": "Received", + "label_unstaking_confirmed": "Unstaking Confirmed", + "label_swapped": "Swapped", + "label_to": "To" + }, "received_from": "Recebido de {{address}}", "nft_sent": "Enviou NFT para {{address}}", "erc721_sent": "Enviou NFT para {{address}}", @@ -1993,6 +2048,8 @@ "received_message": "Toque para ver essa transação", "received_payment_message": "Você recebeu {{amount}} DAI", "prompt_title": "Ativar notificações push", + "notifications_enabled_error_title": "Something went wrong", + "notifications_enabled_error_desc": "We couldn't enable notifications. Please try again later.", "prompt_desc": "Ative as notificações para que a MetaMask informe quando você tiver recebido ETH ou quando suas transações forem confirmadas.", "prompt_ok": "Sim", "prompt_cancel": "Não, obrigado", @@ -2017,6 +2074,7 @@ "1": "Carteira", "2": "Comunicados" }, + "copied_to_clipboard": "Copied to clipboard", "address_copied_to_clipboard": "Endereço copiado para a área de transferência", "transaction_id_copied_to_clipboard": "ID da transação copiado para a área de transferência", "activation_card": { @@ -2618,10 +2676,10 @@ "submit_ticket_6": "aqui.", "submit_ticket_7": "Inclua a mensagem de erro e a captura de tela.", "submit_ticket_8": "Envie-nos um relatório de erros", - "submit_ticket_9": "Inclua detalhes sobre a ocorrência.", + "submit_ticket_9": "Inclua detalhes sobre o que aconteceu.", "bug_report_prompt_title": "Conte-nos o que aconteceu", "bug_report_prompt_description": "Adicione detalhes para que possamos descobrir o que deu errado.", - "bug_report_thanks": "Obrigado! Daremos uma olhada em breve.", + "bug_report_thanks": "Obrigado! Vamos verificar em breve.", "save_seedphrase_1": "Se o erro persistir,", "save_seedphrase_2": "salve sua Frase de Recuperação Secreta", "save_seedphrase_3": "e reinstale o app. Observação: você NÃO poderá restaurar sua carteira sem a Frase de Recuperação Secreta.", @@ -2648,7 +2706,7 @@ "header": "Proteção de transações aprimorada", "description_1": "Desbloqueie taxas de sucesso maiores, proteção contra front running e melhor visibilidade com as transações inteligentes.", "description_2": "Disponível somente na Ethereum. Ative ou desative a qualquer momento nas configurações.", - "secondary_button": "Gerenciar nas configurações", + "no_thanks": "No thanks", "primary_button": "Ativar", "learn_more": "Saiba mais.", "benefit_1_1": "99,5% de taxa de", @@ -2661,7 +2719,7 @@ "transaction_simulation": { "title": "Alterações de saldo estimadas", "description_1": "Agora você pode ver os possíveis resultados de suas transações antes de realizá-las!", - "description_2": "Isto é apenas uma simulação, portanto não podemos garantir o resultado. Você pode desativar isso quando quiser em Configurações > Segurança e privacidade." + "description_2": "Isto é apenas uma simulação, portanto não podemos garantir o resultado. Você pode desativar isso quando quiser em Configurações > Segurança e Privacidade." } }, "invalid_network": { @@ -2847,6 +2905,15 @@ "enable_remember_me": "Ativar \"Lembrar de mim\"", "enable_remember_me_description": "Quando \"Lembrar de mim\" estiver ativado, qualquer pessoa com acesso ao seu telefone poderá acessar sua conta da MetaMask." }, + "profile_sync": { + "title": "Profile Sync", + "disable_warning": "If you turn off profile sync, you won’t be able to receive notifications.", + "enable_description": "Creates a profile that MetaMask uses to sync some settings among your devices. This is required to get notifications. ", + "enable_description2": " to learn more.", + "enable_privacy_link": "Learn how we protect your privacy", + "bottomSheetTurnOn": "Turning on Profile Sync", + "bottomSheetTurnOff": "Turning off Profile Sync" + }, "turn_off_remember_me": { "title": "Insira sua senha para desativar \"Lembrar de mim\"", "placeholder": "Senha", @@ -3069,7 +3136,7 @@ }, "install_snap": { "title": "Solicitação de conexão", - "description": "{{origin}} deseja baixar e se conectar a {{snap}}. Certifique-se de que você confia nos autores antes de prosseguir.", + "description": "{{origin}} quer usar {{snap}}.", "permissions_request_title": "Solicitação de permissões", "permissions_request_description": "{{origin}} deseja instalar {{snap}}, que está solicitando as permissões a seguir.", "approve_permissions": "Aprovar", @@ -3118,5 +3185,8 @@ "title": "Alterações estimadas", "tooltip_description": "As alterações estimadas podem acontecer se você prosseguir com essa transação. Isso é apenas uma previsão, não uma garantia.", "total_fiat": "Total = {{currency}}" + }, + "common": { + "please_wait": "Please wait" } -} +} \ No newline at end of file diff --git a/locales/languages/ru.json b/locales/languages/ru.json index 22e0e680ebf..6b28b8406ea 100644 --- a/locales/languages/ru.json +++ b/locales/languages/ru.json @@ -5,8 +5,6 @@ "deceptive_request_title": "Это запрос с целью обмана", "failed_title": "Запрос может быть небезопасным", "failed_description": "Из-за ошибки этот запрос не был подтвержден поставщиком услуг безопасности. Действуйте осторожно.", - "loading_title": "Проверка оповещений безопасности...", - "loading_complete_title": "Оповещения не получены. Всегда проводите собственную комплексную проверку перед утверждением запросов.", "malicious_domain_description": "Вы взаимодействуете с вредоносным доменом. Если вы одобрите этот запрос, вы можете потерять свои активы.", "other_description": "Если вы одобрите этот запрос, вы можете потерять свои активы.", "raw_signature_farming_description": "Если вы одобрите этот запрос, вы можете потерять свои активы.", @@ -613,6 +611,10 @@ "remove_account_message": "Вы действительно хотите удалить этот счет?", "no": "Нет", "yes_remove_it": "Да, удалить", + "remove_hardware_account": "Remove hardware account", + "remove_hw_account_alert_description": "Are you sure you want to remove this hardware wallet account? You’ll have to resync your hardware wallet if you want to use this account again with MetaMask Mobile.", + "remove_account_alert_remove_btn": "Remove", + "remove_account_alert_cancel_btn": "Nevermind", "accounts_title": "Счета", "connect_account_title": "Подключить счет", "connect_accounts_title": "Подключить счета", @@ -668,8 +670,7 @@ "hint_text": "Отсканируйте свой кошелек Keystone, чтобы", "purpose_connect": "подключиться", "purpose_sign": "подтвердить транзакцию", - "select_accounts": "Выберите счет", - "please_wait": "Пожалуйста, подождите..." + "select_accounts": "Выберите счет" }, "data_collection_modal": { "accept": "Хорошо", @@ -682,6 +683,11 @@ "forget": "Забыть это устройство" }, "app_settings": { + "enabling_notifications": "Enabling notifications...", + "disabling_notifications": "Disabling notifications...", + "enabling_profile_sync": "Enabling profile syncing...", + "disabling_profile_sync": "Disabling profile syncing...", + "notifications_dismiss_modal": "Dismiss", "title": "Настройки", "current_conversion": "Базовая валюта", "current_language": "Текущий язык", @@ -805,6 +811,7 @@ "security_desc": "Настройки конфиденциальности, MetaMetrics, закрытый ключ и секретная фраза для восстановления кошелька", "networks_title": "Сети", "networks_default_title": "Сеть по умолчанию", + "network_delete": "Если вы удалите эту сеть, вам нужно будет добавить ее снова, чтобы просмотреть свои активы в этой сети", "networks_default_cta": "Использовать эту сеть", "networks_desc": "Добавляйте и редактируйте пользовательские сети RPC", "network_name_label": "Имя сети", @@ -820,6 +827,7 @@ "network_other_networks": "Другие сети", "network_rpc_networks": "RPC-сети", "network_add_network": "Добавить сеть", + "network_add_custom_network": "Добавить пользовательскую сеть", "network_add": "Добавить", "network_save": "Сохранить", "remove_network_title": "Вы хотите удалить эту сеть?", @@ -994,7 +1002,7 @@ "token_nft_ens_subheading": "Автоопределение токенов, NFT и ENS", "security_check_subheading": "Проверки безопасности", "symbol_required": "Требуется символ.", - "blockaid_desc": "Сохранение конфиденциальности – никакие данные не передаются третьим сторонам. Доступно в Arbitrum, Avalanche, BNB Chain, Мейн-нете Ethereum, Optimism, Polygon, Sepolia и Base.", + "blockaid_desc": "Эта функция предупреждает вас о вредоносной активности, активно проверяя транзакции и запросы на подпись.", "security_alerts": "Оповещения безопасности", "security_alerts_desc": "Эта функция предупреждает вас о вредоносной активности, проверяя запросы транзакций и подписей локально. Всегда проводите комплексную проверку перед утверждением каких-либо запросов. Нет никакой гарантии, что эта функция обнаружит всю вредоносную активность. Включая эту функцию, вы соглашаетесь с условиями использования поставщика.", "smart_transactions_opt_in_heading": "Умные транзакции", @@ -1031,6 +1039,7 @@ "cancel": "Отмена", "loading": "Подключение к MetaMask...", "unkown_dapp": "Имя DAPP недоступно", + "unknown": "Неизвестно", "no_connections": "Нет подключений", "no_connections_desc": "Если вы подключите счет к сайту или приложению, вы увидите его здесь." }, @@ -1654,6 +1663,7 @@ "network_currency_symbol": "Символ валюты", "network_block_explorer_url": "URL обозревателя блоков", "search": "Поиск ранее добавленной сети", + "search-short": "Поиск", "add": "Добавить", "cancel": "Отмена", "approve": "Одобрить", @@ -1661,7 +1671,9 @@ "malicious_network_warning": "Вредоносный сетевой провайдер может дезинформировать о состоянии блокчейна и записывать ваши действия в сети. Добавляйте только те пользовательские сети, которым доверяете.", "security_link": "https://support.metamask.io/networks-and-sidechains/managing-networks/user-guide-custom-networks-and-sidechains/", "network_warning_title": "Информация о сети", + "additional_network_information_title": "Дополнительная информация о сетях", "network_warning_desc": "Это сетевое подключение зависит от третьих сторон. Оно может быть менее надежным или позволять третьим лицам отслеживать активность.", + "additonial_network_information_desc": "Некоторые из этих сетей являются зависимыми от третьих сторон. Соединения могут быть менее надежными или позволять третьим сторонам отслеживать активность.", "learn_more": "Подробнее", "learn_more_url": "https://support.metamask.io/networks-and-sidechains/managing-networks/the-risks-of-connecting-to-an-unknown-network/", "switch_network": "Переключиться на сеть", @@ -1950,7 +1962,7 @@ "received": "Получено", "unstaked": "Стейкинг отменен", "to": "В", - "rate": "Курс (включая комиссии)", + "rate": "Rate (fees included)", "unstaking_requested": "Запрошена отмена стейкинга", "stake_completed": "Стейкинг завершен", "withdrawal_completed": "Вывод завершен", @@ -1960,6 +1972,49 @@ "swap_completed": "{{from}} обменянные на {{to}}", "swap": "Выполнен обмен", "sent": "Отправленные на {{address}}", + "menu_item_title": { + "metamask_swap_completed": "Swapped {{symbol1}} for {{symbol2}}", + "erc20_sent": "Sent to {{address}}", + "erc20_received": "Received from {{address}}", + "eth_sent": "Sent to {{address}}", + "eth_received": "Received from {{address}}", + "erc721_sent": "Sent NFT to {{address}}", + "erc1155_sent": "Sent NFT to {{address}}", + "erc721_received": "Received NFT from {{address}}", + "erc1155_received": "Received NFT from {{address}}", + "rocketpool_stake_completed": "Staked", + "rocketpool_unstake_completed": "Unstaking complete", + "lido_stake_completed": "Staked", + "lido_withdrawal_requested": "Unstaking requested", + "lido_withdrawal_completed": "Unstaking complete", + "lido_stake_ready_to_be_withdrawn": "Withdrawal requested" + }, + "menu_item_description": { + "lido_withdrawal_requested": "Your request to unstake {{amount}} {{symbol}} has been sent", + "lido_stake_ready_to_be_withdrawn": "You can now withdraw your unstaked {{symbol}}" + }, + "modal": { + "title_sent": "Sent {{symbol}}", + "title_received": "Received {{symbol}}", + "title_unstake_requested": "Unstaking requested", + "title_untake_ready": "Withdrawl ready", + "title_stake": "Staked {{symbol}}", + "title_unstake_completed": "Unstaking completed", + "title_swapped": "Swapped {{symbol1}} to {{symbol2}}", + "label_address_to": "To", + "label_address_from": "From", + "label_address_to_you": "To (You)", + "label_address_from_you": "From (You)", + "label_asset": "Asset", + "label_account": "Account", + "label_unstaking_requested": "Unstaking Requested", + "label_unstake_ready": "Withdrawl ready", + "label_staked": "Staked", + "label_received": "Received", + "label_unstaking_confirmed": "Unstaking Confirmed", + "label_swapped": "Swapped", + "label_to": "To" + }, "received_from": "Получено от {{address}}", "nft_sent": "Отправил(-а) NFT на {{address}}", "erc721_sent": "Отправил(-а) NFT на {{address}}", @@ -1993,6 +2048,8 @@ "received_message": "Нажмите для просмотра этой транзакции", "received_payment_message": "Вы получили {{amount}} DAI", "prompt_title": "Включить push-уведомления", + "notifications_enabled_error_title": "Something went wrong", + "notifications_enabled_error_desc": "We couldn't enable notifications. Please try again later.", "prompt_desc": "Включите уведомления, чтобы MetaMask мог сообщить вам о получении вами ETH или подтверждении ваших транзакций.", "prompt_ok": "Да", "prompt_cancel": "Нет, спасибо", @@ -2017,6 +2074,7 @@ "1": "Кошелек", "2": "Объявления" }, + "copied_to_clipboard": "Copied to clipboard", "address_copied_to_clipboard": "Адрес скопирован в буфер обмена", "transaction_id_copied_to_clipboard": "ID транзакции скопирован в буфер обмена", "activation_card": { @@ -2648,7 +2706,7 @@ "header": "Улучшенная защита транзакций", "description_1": "Откройте для себя более высокие коэффициенты успеха, передовую защиту и лучшую прозрачность с помощью смарт-транзакций.", "description_2": "Доступно только на Ethereum. Включайте или отключайте в любое время в настройках.", - "secondary_button": "Управляйте в настройках", + "no_thanks": "No thanks", "primary_button": "Включить", "learn_more": "Подробнее.", "benefit_1_1": "Показатель успеха:", @@ -2847,6 +2905,15 @@ "enable_remember_me": "Включить \"Запомнить меня\"", "enable_remember_me_description": "Когда функция \"Запомнить меня\" включена, любой, у кого есть доступ к вашему телефону, может получить доступ к вашему счету MetaMask." }, + "profile_sync": { + "title": "Profile Sync", + "disable_warning": "If you turn off profile sync, you won’t be able to receive notifications.", + "enable_description": "Creates a profile that MetaMask uses to sync some settings among your devices. This is required to get notifications. ", + "enable_description2": " to learn more.", + "enable_privacy_link": "Learn how we protect your privacy", + "bottomSheetTurnOn": "Turning on Profile Sync", + "bottomSheetTurnOff": "Turning off Profile Sync" + }, "turn_off_remember_me": { "title": "Введите пароль, чтобы отключить \"Запомнить меня\"", "placeholder": "Пароль", @@ -3069,7 +3136,7 @@ }, "install_snap": { "title": "Запрос подключения", - "description": "{{origin}} хочет загрузить и подключиться к {{snap}}. Прежде чем продолжить, убедитесь, что вы доверяете авторам.", + "description": "{{origin}} хочет использовать {{snap}}.", "permissions_request_title": "Запрос разрешений", "permissions_request_description": "{{origin}} хочет установить {{snap}}, который запрашивает следующие разрешения.", "approve_permissions": "Одобрить", @@ -3118,5 +3185,8 @@ "title": "Прогнозируемые изменения", "tooltip_description": "Прогнозируемые изменения — это то, что может произойти, если вы завершите эту транзакцию. Это всего лишь прогноз, а не гарантия.", "total_fiat": "Итого = {{currency}}" + }, + "common": { + "please_wait": "Please wait" } -} +} \ No newline at end of file diff --git a/locales/languages/tl.json b/locales/languages/tl.json index 42bf7fc95ec..ac5aebe7a2a 100644 --- a/locales/languages/tl.json +++ b/locales/languages/tl.json @@ -5,8 +5,6 @@ "deceptive_request_title": "Isa itong mapanlinlang na kahilingan", "failed_title": "Baka hindi ligtas ang kahilingan", "failed_description": "Dahil sa pagkakamali, ang kahilingang ito ay hindi na-verify ng tagapaglaan ng seguridad. Magpatuloy nang may pag-iingat.", - "loading_title": "Sinusuri ang mga alertong pangseguridad...", - "loading_complete_title": "Walang alerto na natanggap. Palaging gumawa ng sarili mong pagsusuri bago aprubahan ang mga kahilingan.", "malicious_domain_description": "Nakikipag-ugnayan ka sa isang mapaminsalang domain. Kung aaprubahan mo ang kahilingang ito, posibleng mawala sa iyo ang mga asset mo.", "other_description": "Kung aaprubahan mo ang kahilingang ito, posibleng mawala sa iyo ang mga asset mo.", "raw_signature_farming_description": "Kung aaprubahan mo ang kahilingang ito, posibleng mawala sa iyo ang mga asset mo.", @@ -613,6 +611,10 @@ "remove_account_message": "Gusto mo ba talagang alisin ang account na ito?", "no": "Hindi", "yes_remove_it": "Oo, alisin ito", + "remove_hardware_account": "Remove hardware account", + "remove_hw_account_alert_description": "Are you sure you want to remove this hardware wallet account? You’ll have to resync your hardware wallet if you want to use this account again with MetaMask Mobile.", + "remove_account_alert_remove_btn": "Remove", + "remove_account_alert_cancel_btn": "Nevermind", "accounts_title": "Mga Account", "connect_account_title": "Ikonekta ang account", "connect_accounts_title": "Ikonekta ang mga account", @@ -668,8 +670,7 @@ "hint_text": "I-scan ang iyong Keystone wallet sa ", "purpose_connect": "kumonekta", "purpose_sign": "kumpirmahin ang transaksyon", - "select_accounts": "Pumili ng Account", - "please_wait": "Mangyaring maghintay" + "select_accounts": "Pumili ng Account" }, "data_collection_modal": { "accept": "Okay", @@ -682,6 +683,11 @@ "forget": "Kalimutan ang device na ito" }, "app_settings": { + "enabling_notifications": "Enabling notifications...", + "disabling_notifications": "Disabling notifications...", + "enabling_profile_sync": "Enabling profile syncing...", + "disabling_profile_sync": "Disabling profile syncing...", + "notifications_dismiss_modal": "Dismiss", "title": "Mga Setting", "current_conversion": "Batayang Currency", "current_language": "Kasalukuyang Wika", @@ -805,6 +811,7 @@ "security_desc": "Mga setting ng pagkapribado, MetaMetrics, pribadong key at Lihim na Parirala sa Pagbawi ng wallet", "networks_title": "Mga Network", "networks_default_title": "Default na Network", + "network_delete": "Kapag tinanggal mo ang network na ito, kakailanganin mo itong idagdag muli para makita ang mga asset mo sa network na ito", "networks_default_cta": "Gamitin ang network na ito", "networks_desc": "Magdagdag at mag-edit ng mga custom na RPC network", "network_name_label": "Pangalan ng Network", @@ -820,6 +827,7 @@ "network_other_networks": "Iba Pang Network", "network_rpc_networks": "Mga RPC Network", "network_add_network": "Magdagdag ng Network", + "network_add_custom_network": "Magdagdag ng custom na network", "network_add": "Magdagdag", "network_save": "I-save", "remove_network_title": "Gusto mo bang alisin ang network na ito?", @@ -994,7 +1002,7 @@ "token_nft_ens_subheading": "Awtomatikong pagtuklas ng token, NFT at ENS", "security_check_subheading": "Pagsusuring panseguridad", "symbol_required": "Kailangan ang simbolo.", - "blockaid_desc": "Pagpapanatili ng privacy - walang data na ibinabahagi sa mga third party. Available sa Arbitrum, Avalanche, BNB chain, Ethereum Mainnet, Optimism, Polygon, Sepolia at Base.", + "blockaid_desc": "Ang tampok na ito ay nag-aalerto sa iyo ng masamang aktibidad sa pamamagitan ng aktibong pagsusuri sa mga transaksyon at paghiling ng pirma.", "security_alerts": "Mga alerto sa seguridad", "security_alerts_desc": "Inaalertuhan ka ng tampok na ito sa mga aktibidad na may masamang hangarin sa pamamagitan ng lokal na pagsusuri sa iyong mga transaksyon at kahilingan sa paglagda. Palaging gumawa ng sarili mong pag-iingat bago aprubahan ang anumang mga kahilingan. Walang garantiya na made-detect ng tampok na ito ang lahat ng aktibidad na may masamang hangarin. Sa pagpapagana sa tampok na ito, sumasang-ayon ka sa mga tuntunin ng paggamit ng provider.", "smart_transactions_opt_in_heading": "Mga Smart Transaction", @@ -1031,6 +1039,7 @@ "cancel": "Kanselahin", "loading": "Kumokonekta sa MetaMask...", "unkown_dapp": "Hindi available ang pangalan ng DAPP", + "unknown": "Hindi Alam", "no_connections": "Walang mga koneksyon", "no_connections_desc": "Kapag ikinonekta mo ang isang account sa isang site o app, makikita mo ito dito." }, @@ -1654,6 +1663,7 @@ "network_currency_symbol": "Simbolo ng Currency", "network_block_explorer_url": "URL ng Block Explorer", "search": "Maghanap ng mga dati nang idinagdag na network", + "search-short": "Maghanap", "add": "Idagdag", "cancel": "Kanselahin", "approve": "Aprubahan", @@ -1661,7 +1671,9 @@ "malicious_network_warning": "Ang isang mapaminsalang network provider ay maaaring magsinungaling tungkol sa estado ng blockchain at itala ang iyong aktibidad sa network. Magdagdag lang ng mga custom na network na pinagkakatiwalaan mo.", "security_link": "https://support.metamask.io/networks-and-sidechains/managing-networks/user-guide-custom-networks-and-sidechains/", "network_warning_title": "Impormasyon ng Network", + "additional_network_information_title": "Impormasyon sa mga Karagdagang Network", "network_warning_desc": "Ang koneksyon sa network na ito ay umaasa sa mga third party. Ang koneksyon na ito ay maaaring hindi gaanong maaasahan o binibigyang-daan ang mga third-party na subaybayan ang aktibidad.", + "additonial_network_information_desc": "Ang ilan sa mga network na ito ay umaasa sa mga third party. Ang mga koneksyon na ito ay maaaring hindi gaanong maaasahan o binibigyang-daan ang mga third-party na mag-track ng aktibidad.", "learn_more": "Matuto pa", "learn_more_url": "https://support.metamask.io/networks-and-sidechains/managing-networks/the-risks-of-connecting-to-an-unknown-network/", "switch_network": "Lumipat sa network", @@ -1950,7 +1962,7 @@ "received": "Natanggap", "unstaked": "Na-unstake", "to": "Sa/Kay", - "rate": "Halaga (kasama ang bayarin)", + "rate": "Rate (fees included)", "unstaking_requested": "Hiniling ang pag-unstake", "stake_completed": "Nakumpleto ang pag-stake", "withdrawal_completed": "Nakumpleto ang pag-withdraw", @@ -1960,6 +1972,49 @@ "swap_completed": "I-swap ang {{from}} sa {{to}}", "swap": "Nai-swap", "sent": "Ipinadala sa {{address}}", + "menu_item_title": { + "metamask_swap_completed": "Swapped {{symbol1}} for {{symbol2}}", + "erc20_sent": "Sent to {{address}}", + "erc20_received": "Received from {{address}}", + "eth_sent": "Sent to {{address}}", + "eth_received": "Received from {{address}}", + "erc721_sent": "Sent NFT to {{address}}", + "erc1155_sent": "Sent NFT to {{address}}", + "erc721_received": "Received NFT from {{address}}", + "erc1155_received": "Received NFT from {{address}}", + "rocketpool_stake_completed": "Staked", + "rocketpool_unstake_completed": "Unstaking complete", + "lido_stake_completed": "Staked", + "lido_withdrawal_requested": "Unstaking requested", + "lido_withdrawal_completed": "Unstaking complete", + "lido_stake_ready_to_be_withdrawn": "Withdrawal requested" + }, + "menu_item_description": { + "lido_withdrawal_requested": "Your request to unstake {{amount}} {{symbol}} has been sent", + "lido_stake_ready_to_be_withdrawn": "You can now withdraw your unstaked {{symbol}}" + }, + "modal": { + "title_sent": "Sent {{symbol}}", + "title_received": "Received {{symbol}}", + "title_unstake_requested": "Unstaking requested", + "title_untake_ready": "Withdrawl ready", + "title_stake": "Staked {{symbol}}", + "title_unstake_completed": "Unstaking completed", + "title_swapped": "Swapped {{symbol1}} to {{symbol2}}", + "label_address_to": "To", + "label_address_from": "From", + "label_address_to_you": "To (You)", + "label_address_from_you": "From (You)", + "label_asset": "Asset", + "label_account": "Account", + "label_unstaking_requested": "Unstaking Requested", + "label_unstake_ready": "Withdrawl ready", + "label_staked": "Staked", + "label_received": "Received", + "label_unstaking_confirmed": "Unstaking Confirmed", + "label_swapped": "Swapped", + "label_to": "To" + }, "received_from": "Natanggap mula sa {{address}}", "nft_sent": "Ipinadala ang NFT sa {{address}}", "erc721_sent": "Ipinadala ang NFT sa {{address}}", @@ -1993,6 +2048,8 @@ "received_message": "Mag-tap para tingnan ang transaksyong ito", "received_payment_message": "Nakatanggap ka ng {{amount}} DAI", "prompt_title": "Paganahin ang Mga Push Notification", + "notifications_enabled_error_title": "Something went wrong", + "notifications_enabled_error_desc": "We couldn't enable notifications. Please try again later.", "prompt_desc": "Paganahin ang mga notification para maipaalam sa iyo ng MetaMask kapag nakatanggap ka ng ETH o kapag nakumpirma na ang iyong mga transaksyon.", "prompt_ok": "Oo", "prompt_cancel": "Huwag na lang", @@ -2017,6 +2074,7 @@ "1": "Wallet", "2": "Mga anunsiyo" }, + "copied_to_clipboard": "Copied to clipboard", "address_copied_to_clipboard": "Nakopya ang address sa clipboard", "transaction_id_copied_to_clipboard": "Nakopya ang ID ng transaksyon sa clipboard", "activation_card": { @@ -2648,7 +2706,7 @@ "header": "Pinahusay na Proteksyon sa Transaksyon", "description_1": "I-unlock ang mas mataas na tiyansa ng tagumpay, proteksyon sa frontrunning, at mas mahusay na visibility sa mga Smart Transaction.", "description_2": "Available lamang sa Ethereum. I-enable o i-disable anumang oras sa mga setting.", - "secondary_button": "Pamahalaan sa mga setting", + "no_thanks": "No thanks", "primary_button": "I-enable", "learn_more": "Matuto pa.", "benefit_1_1": "99.5% tagumpay", @@ -2847,6 +2905,15 @@ "enable_remember_me": "I-on ang Tandaan ako", "enable_remember_me_description": "Kapag naka-on ang tandaan ako, sinumang may access sa iyong telepono ay makaka-access sa iyong MetaMask account." }, + "profile_sync": { + "title": "Profile Sync", + "disable_warning": "If you turn off profile sync, you won’t be able to receive notifications.", + "enable_description": "Creates a profile that MetaMask uses to sync some settings among your devices. This is required to get notifications. ", + "enable_description2": " to learn more.", + "enable_privacy_link": "Learn how we protect your privacy", + "bottomSheetTurnOn": "Turning on Profile Sync", + "bottomSheetTurnOff": "Turning off Profile Sync" + }, "turn_off_remember_me": { "title": "Ilagay ang iyong password para i-off ang Tandaan ako", "placeholder": "Password", @@ -3069,7 +3136,7 @@ }, "install_snap": { "title": "Kahilingan sa koneksyon", - "description": "Gusto ng {{origin}} na i-download ang {{snap}} at kumonekta rito. Siguraduhing pinagkakatiwalaan mo ang mga author bago ka magpatuloy.", + "description": "{{origin}} ay nais gamitin ang {{snap}}.", "permissions_request_title": "Kahilingan sa mga pahintulot", "permissions_request_description": "Gusto ng {{origin}} na i-install ang {{snap}}, na humihiling ng mga sumusunod na pahintulot.", "approve_permissions": "Aprubahan", @@ -3118,5 +3185,8 @@ "title": "Tinatayang mga pagbabago", "tooltip_description": "Ang tinatayang mga pagbabago ay ang maaaring mangyari kung magpapatuloy ka sa transaksyon ito. Ito ay isang hula lamang, hindi isang garantiya.", "total_fiat": "Kabuuan = {{currency}}" + }, + "common": { + "please_wait": "Please wait" } -} +} \ No newline at end of file diff --git a/locales/languages/tr.json b/locales/languages/tr.json index 03f6f8e8234..3a0c477f5dd 100644 --- a/locales/languages/tr.json +++ b/locales/languages/tr.json @@ -5,8 +5,6 @@ "deceptive_request_title": "Bu, aldatıcı bir taleptir", "failed_title": "Talep güvenli olmayabilir", "failed_description": "Bu talep bir hatadan dolayı güvenlik sağlayıcısı tarafından doğrulanmadı. Dikkatli bir şekilde ilerleyin.", - "loading_title": "Güvenlik uyarıları kontrol ediliyor...", - "loading_complete_title": "Herhangi bir uyarı alınmadı. Talepleri onaylamadan önce kendiniz her zaman gerekli özeni gösterin.", "malicious_domain_description": "Kötü niyetli bir alanla etkileşimde bulunuyorsunuz. Bu talebi onaylarsanız varlıklarınızı kaybedebilirsiniz.", "other_description": "Bu talebi onaylarsanız varlıklarınızı kaybedebilirsiniz.", "raw_signature_farming_description": "Bu talebi onaylarsanız varlıklarınızı kaybedebilirsiniz.", @@ -613,6 +611,10 @@ "remove_account_message": "Bu hesabı gerçekten kaldırmak istiyor musunuz?", "no": "Hayır", "yes_remove_it": "Evet, kaldır", + "remove_hardware_account": "Remove hardware account", + "remove_hw_account_alert_description": "Are you sure you want to remove this hardware wallet account? You’ll have to resync your hardware wallet if you want to use this account again with MetaMask Mobile.", + "remove_account_alert_remove_btn": "Remove", + "remove_account_alert_cancel_btn": "Nevermind", "accounts_title": "Hesaplar", "connect_account_title": "Hesabı bağla", "connect_accounts_title": "Hesapları bağla", @@ -668,8 +670,7 @@ "hint_text": "İşlemi onaylamak için ", "purpose_connect": "bağlan", "purpose_sign": "kilit cüzdanınızı okutun", - "select_accounts": "Hesap Seç", - "please_wait": "Lütfen bekleyin" + "select_accounts": "Hesap Seç" }, "data_collection_modal": { "accept": "Tamam", @@ -682,6 +683,11 @@ "forget": "Bu cihazı unut" }, "app_settings": { + "enabling_notifications": "Enabling notifications...", + "disabling_notifications": "Disabling notifications...", + "enabling_profile_sync": "Enabling profile syncing...", + "disabling_profile_sync": "Disabling profile syncing...", + "notifications_dismiss_modal": "Dismiss", "title": "Ayarlar", "current_conversion": "Temel Para Birimi", "current_language": "Geçerli Dil", @@ -805,6 +811,7 @@ "security_desc": "Gizlilik ayarları, MetaMetrics, özel anahtar ve cüzdan Gizli Kurtarma İfadesi", "networks_title": "Ağlar", "networks_default_title": "Varsayılan Ağ", + "network_delete": "Bu ağı silerseniz bu ağdaki varlıklarınızı görüntülemek için bu ağı tekrar eklemeniz gerekir", "networks_default_cta": "Bu ağı kullan", "networks_desc": "Kişisel RPC ağı ekle ve düzenle", "network_name_label": "Ağ Adı", @@ -820,6 +827,7 @@ "network_other_networks": "Diğer Ağlar", "network_rpc_networks": "RPC Ağları", "network_add_network": "Ağ Ekle", + "network_add_custom_network": "Özel bir ağ ekle", "network_add": "Ekle", "network_save": "Kaydet", "remove_network_title": "Bu ağı kaldırmak istiyor musunuz?", @@ -994,7 +1002,7 @@ "token_nft_ens_subheading": "Token, NFT ve ENS otomatik algılama", "security_check_subheading": "Güvenlik kontrolleri", "symbol_required": "Sembol gerekli.", - "blockaid_desc": "Gizlilik koruması - Hiçbir veri üçüncü taraflarla paylaşılmaz. Arbitrum, Avalanche, BNB chain, Ethereum Ana Ağı, Optimism, Polygon, Sepolia ve Base için sunulur.", + "blockaid_desc": "Bu özellik, işlem ve imza taleplerini aktif bir şekilde inceleyerek kötü amaçlı aktivite konusunda sizi uyarır.", "security_alerts": "Güvenlik uyarıları", "security_alerts_desc": "Bu özellik, işlem ve imza taleplerinizi yerel olarak incelerken gizliliğinizi koruyarak Ethereum Ana Ağındaki kötü amaçlı aktivitelere karşı sizi uyarır. Talepleri onaylamadan önce her zaman gereken özeni kendiniz gösterin. Bu özelliğin tüm kötü amaçlı faaliyetleri algılayacağına dair herhangi bir garanti bulunmamaktadır. Bu özelliği etkinleştirerek sağlayıcının kullanım koşullarını kabul etmiş olursunuz.", "smart_transactions_opt_in_heading": "Akıllı İşlemler", @@ -1004,7 +1012,7 @@ "simulation_details_description": "İşlemleri onaylamadan önce işlemlerin neden olacağı bakiye değişikliklerini tahmin etmek için bunu açın. İşlemlerinizin nihai sonucunu garanti etmez. ", "simulation_details_learn_more": "Daha fazla bilgi edinin.", "aes_crypto_test_form_title": "AES Crypto - Test Biçimi", - "aes_crypto_test_form_description": "Kısım özel olarak E2E testi için geliştirilmiştir. Bu sizin uygulamanızda görünür ise lütfen MetaMask destek bölümüne bildirin." + "aes_crypto_test_form_description": "Bu kısım özel olarak E2E testi için geliştirilmiştir. Bu, uygulamanızda görünüyorsa lütfen MetaMask destek bölümüne bildirin." }, "aes_crypto_test_form": { "generate_random_salt": "Rastgele Tuz Oluştur", @@ -1031,6 +1039,7 @@ "cancel": "İptal", "loading": "MetaMask'a bağlanılıyor...", "unkown_dapp": "Merkeziyetsiz uygulama adı kullanılamıyor", + "unknown": "Bilinmeyen", "no_connections": "Bağlantı yok", "no_connections_desc": "Bir siteye veya bir uygulamaya bağladığınız hesabı burada göreceksiniz." }, @@ -1406,12 +1415,12 @@ "approve": "Onayla", "allow_to_access": "Erişim izni verin:", "allow_to_address_access": "Bu adrese erişim izni verin:", - "allow_to_transfer_all": "Erişim ve transfer izni verilsin mi", + "allow_to_transfer_all": "Şunların tümüne erişim ve transfer izni verin:", "spend_cap": "Şunun için harcama üst limiti:", "token": "token", "nft": "NFT", "you_trust_this_site": "İzin vererek aşağıdaki üçüncü tarafın bakiyenize erişim sağlamasına izin verirsiniz.", - "you_trust_this_third_party": "Üçüncü bir tarafın, siz bu erişimi iptal edene kadar başka bildirim yapılmaksızın NFT'lerinize erişim sağlamasına ve onları transfer edebilmesine izin verir.", + "you_trust_this_third_party": "Üçüncü bir tarafın, siz bu erişimi iptal edene kadar başka bildirim yapılmaksızın NFT'lerinize erişim sağlamasına ve NFT'lerinizi transfer edebilmesine izin verir.", "you_trust_this_address": "Bu adrese güveniyor musunuz? Bu izni vererek bu adresin paranıza erişim sağlamasına izin veriyorsunuz.", "edit_permission": "İzni düzenle", "edit": "Düzenle", @@ -1654,6 +1663,7 @@ "network_currency_symbol": "Para Birimi Sembolü", "network_block_explorer_url": "Blok Gezgini URL Adresi", "search": "Önceden eklenmiş ağı ara", + "search-short": "Ara", "add": "Ekle", "cancel": "İptal et", "approve": "Onayla", @@ -1661,7 +1671,9 @@ "malicious_network_warning": "Kötü amaçlı bir ağ sağlayıcı blokzincirinin durumu hakkında yalan söyleyebilir ve ağ aktivitenizi kaydedebilir. Sadece güvendiğiniz özel ağları ekleyin.", "security_link": "https://support.metamask.io/networks-and-sidechains/managing-networks/user-guide-custom-networks-and-sidechains/", "network_warning_title": "Ağ Bilgileri", + "additional_network_information_title": "Diğer Ağ Bilgileri", "network_warning_desc": "Bu ağ bağlantısı üçüncü taraflara dayalıdır. Bu bağlantı daha az güvenilir olabilir ya da üçüncü tarafların aktiviteyi takip etmesini sağlayabilir.", + "additonial_network_information_desc": "Bu ağların bazıları üçüncü taraflara dayalıdır. Bağlantılar daha az güvenilir olabilir veya üçüncü tarafların aktiviteleri takip etmesine olanak sağlayabilir.", "learn_more": "Daha fazlasını öğrenin", "learn_more_url": "https://support.metamask.io/networks-and-sidechains/managing-networks/the-risks-of-connecting-to-an-unknown-network/", "switch_network": "Ağ değiştir", @@ -1950,7 +1962,7 @@ "received": "Alınan", "unstaked": "Unstake edildi", "to": "Alıcı", - "rate": "Oran (ücretler dahil)", + "rate": "Rate (fees included)", "unstaking_requested": "Unstake talep edildi", "stake_completed": "Stake tamamlandı", "withdrawal_completed": "Para çekme işlemi tamamlandı", @@ -1960,6 +1972,49 @@ "swap_completed": "{{from}} - {{to}} swap işlemi gerçekleştirildi", "swap": "Swap yapılan", "sent": "Alıcı: {{address}}", + "menu_item_title": { + "metamask_swap_completed": "Swapped {{symbol1}} for {{symbol2}}", + "erc20_sent": "Sent to {{address}}", + "erc20_received": "Received from {{address}}", + "eth_sent": "Sent to {{address}}", + "eth_received": "Received from {{address}}", + "erc721_sent": "Sent NFT to {{address}}", + "erc1155_sent": "Sent NFT to {{address}}", + "erc721_received": "Received NFT from {{address}}", + "erc1155_received": "Received NFT from {{address}}", + "rocketpool_stake_completed": "Staked", + "rocketpool_unstake_completed": "Unstaking complete", + "lido_stake_completed": "Staked", + "lido_withdrawal_requested": "Unstaking requested", + "lido_withdrawal_completed": "Unstaking complete", + "lido_stake_ready_to_be_withdrawn": "Withdrawal requested" + }, + "menu_item_description": { + "lido_withdrawal_requested": "Your request to unstake {{amount}} {{symbol}} has been sent", + "lido_stake_ready_to_be_withdrawn": "You can now withdraw your unstaked {{symbol}}" + }, + "modal": { + "title_sent": "Sent {{symbol}}", + "title_received": "Received {{symbol}}", + "title_unstake_requested": "Unstaking requested", + "title_untake_ready": "Withdrawl ready", + "title_stake": "Staked {{symbol}}", + "title_unstake_completed": "Unstaking completed", + "title_swapped": "Swapped {{symbol1}} to {{symbol2}}", + "label_address_to": "To", + "label_address_from": "From", + "label_address_to_you": "To (You)", + "label_address_from_you": "From (You)", + "label_asset": "Asset", + "label_account": "Account", + "label_unstaking_requested": "Unstaking Requested", + "label_unstake_ready": "Withdrawl ready", + "label_staked": "Staked", + "label_received": "Received", + "label_unstaking_confirmed": "Unstaking Confirmed", + "label_swapped": "Swapped", + "label_to": "To" + }, "received_from": "Gönderen: {{address}}", "nft_sent": "NFT alıcısı: {{address}}", "erc721_sent": "NFT alıcısı: {{address}}", @@ -1993,6 +2048,8 @@ "received_message": "Bu işlemi görüntülemek için dokunun", "received_payment_message": "{{amount}} DAI aldınız", "prompt_title": "Anlık Bildirimleri Etkinleştir", + "notifications_enabled_error_title": "Something went wrong", + "notifications_enabled_error_desc": "We couldn't enable notifications. Please try again later.", "prompt_desc": "MetaMask'ın ne zaman ETH aldığınızı veya işlemlerinizin ne zaman onaylandığını size bildirebilmesi için bildirimleri etkinleştirin.", "prompt_ok": "Evet", "prompt_cancel": "Hayır, teşekkürler", @@ -2017,6 +2074,7 @@ "1": "Cüzdan", "2": "Duyurular" }, + "copied_to_clipboard": "Copied to clipboard", "address_copied_to_clipboard": "Adres hafıza panosuna kopyalandı", "transaction_id_copied_to_clipboard": "İşlem numarası hafıza panosuna kopyalandı", "activation_card": { @@ -2617,8 +2675,8 @@ "submit_ticket_5": "Buradan", "submit_ticket_6": "bir sorgu gönderin.", "submit_ticket_7": "Lütfen hata mesajını ve ekran görüntüsünü dahil edin.", - "submit_ticket_8": "Bize bir hata raporu gönder", - "submit_ticket_9": "Lütfen ne olduğuyla ilgili ayrıntıları ekleyin.", + "submit_ticket_8": "Bize bir hata raporu gönderin", + "submit_ticket_9": "Lütfen sorunla ilgili ayrıntıları ekleyin.", "bug_report_prompt_title": "Ne olduğunu bize söyleyin", "bug_report_prompt_description": "Neyin ters gittiğini anlayabilmemiz için ayrıntıları ekleyin.", "bug_report_thanks": "Teşekkürler! Kısa bir süre sonra inceleyeceğiz.", @@ -2648,7 +2706,7 @@ "header": "İyileştirilmiş İşlem Koruması", "description_1": "Akıllı İşlemler ile daha yüksek başarı oranlarının, arkadan çalıştırma korumasının ve daha iyi görünürlüğün kilidini açın.", "description_2": "Sadece Ethereum'da mevcuttur. Dilediğiniz zaman ayarlar kısmında etkinleştirin veya devre dışı bırakın.", - "secondary_button": "Ayarlar kısmında yönet", + "no_thanks": "No thanks", "primary_button": "Etkinleştir", "learn_more": "Daha fazla bilgi edinin.", "benefit_1_1": "%99,5 başarı oranı", @@ -2847,6 +2905,15 @@ "enable_remember_me": "Beni Hatırla özelliğini aç", "enable_remember_me_description": "Beni Hatırla özelliği açıldığında telefonunuza erişimi olan herkes MetaMask hesabınıza erişim sağlayabilir." }, + "profile_sync": { + "title": "Profile Sync", + "disable_warning": "If you turn off profile sync, you won’t be able to receive notifications.", + "enable_description": "Creates a profile that MetaMask uses to sync some settings among your devices. This is required to get notifications. ", + "enable_description2": " to learn more.", + "enable_privacy_link": "Learn how we protect your privacy", + "bottomSheetTurnOn": "Turning on Profile Sync", + "bottomSheetTurnOff": "Turning off Profile Sync" + }, "turn_off_remember_me": { "title": "Beni Hatırla özelliğini kapatmak için şifrenizi girin", "placeholder": "Şifre", @@ -3069,7 +3136,7 @@ }, "install_snap": { "title": "Bağlantı talebi", - "description": "{{origin}}, {{snap}} adlı snap'i indirerek bağlanmak istiyor. İlerlemeden önce yazarlarına güvendiğinizden emin olun.", + "description": "{{origin}}, {{snap}} kullanmak istiyor.", "permissions_request_title": "İzin talebi", "permissions_request_description": "{{origin}}, {{snap}} adlı snap'i yüklemek istiyor ve bu snap aşağıdaki izinleri talep ediyor.", "approve_permissions": "Onayla", @@ -3118,5 +3185,8 @@ "title": "Tahmini değişiklikler", "tooltip_description": "Tahmini değişiklikler bu işlemi gerçekleştirirseniz meydana gelebilecek değişikliklerdir. Bu bir garanti değil, sadece bir tahmindir.", "total_fiat": "Toplam = {{currency}}" + }, + "common": { + "please_wait": "Please wait" } -} +} \ No newline at end of file diff --git a/locales/languages/vi.json b/locales/languages/vi.json index 6d0afb42500..66a7a1d426f 100644 --- a/locales/languages/vi.json +++ b/locales/languages/vi.json @@ -5,8 +5,6 @@ "deceptive_request_title": "Đây là một yêu cầu lừa đảo", "failed_title": "Yêu cầu có thể không an toàn", "failed_description": "Do có lỗi, yêu cầu này đã không được nhà cung cấp dịch vụ bảo mật xác minh. Hãy thực hiện cẩn thận.", - "loading_title": "Đang kiểm tra cảnh báo bảo mật...", - "loading_complete_title": "Không nhận được cảnh báo nào. Luôn tự thẩm định trước khi chấp thuận các yêu cầu.", "malicious_domain_description": "Bạn đang tương tác với một tên miền độc hại. Nếu bạn chấp thuận yêu cầu này, bạn có thể mất tài sản của mình.", "other_description": "Nếu bạn chấp thuận yêu cầu này, bạn có thể mất tài sản của mình.", "raw_signature_farming_description": "Nếu bạn chấp thuận yêu cầu này, bạn có thể mất tài sản của mình.", @@ -613,6 +611,10 @@ "remove_account_message": "Bạn có thật sự muốn gỡ bỏ tài khoản này?", "no": "Không", "yes_remove_it": "Có, hãy gỡ bỏ nó", + "remove_hardware_account": "Remove hardware account", + "remove_hw_account_alert_description": "Are you sure you want to remove this hardware wallet account? You’ll have to resync your hardware wallet if you want to use this account again with MetaMask Mobile.", + "remove_account_alert_remove_btn": "Remove", + "remove_account_alert_cancel_btn": "Nevermind", "accounts_title": "Tài khoản", "connect_account_title": "Kết nối tài khoản", "connect_accounts_title": "Kết nối tài khoản", @@ -668,8 +670,7 @@ "hint_text": "Quét ví Keystone của bạn để ", "purpose_connect": "kết nối", "purpose_sign": "xác nhận giao dịch", - "select_accounts": "Chọn một tài khoản", - "please_wait": "Vui lòng chờ" + "select_accounts": "Chọn một tài khoản" }, "data_collection_modal": { "accept": "Đồng ý", @@ -682,6 +683,11 @@ "forget": "Quên thiết bị này" }, "app_settings": { + "enabling_notifications": "Enabling notifications...", + "disabling_notifications": "Disabling notifications...", + "enabling_profile_sync": "Enabling profile syncing...", + "disabling_profile_sync": "Disabling profile syncing...", + "notifications_dismiss_modal": "Dismiss", "title": "Cài đặt", "current_conversion": "Tiền tệ cơ bản", "current_language": "Ngôn ngữ hiện tại", @@ -805,6 +811,7 @@ "security_desc": "Cài đặt quyền riêng tư, MetaMetrics, khoá riêng tư và Cụm từ khôi phục bí mật của ví", "networks_title": "Mạng", "networks_default_title": "Mạng mặc định", + "network_delete": "Nếu xóa mạng này, bạn sẽ cần thêm lại mạng này để xem các tài sản của mình trong mạng này", "networks_default_cta": "Sử dụng mạng này", "networks_desc": "Thêm và sửa mạng RPC tùy chỉnh", "network_name_label": "Tên mạng", @@ -820,6 +827,7 @@ "network_other_networks": "Mạng khác", "network_rpc_networks": "Mạng RPC", "network_add_network": "Thêm mạng", + "network_add_custom_network": "Thêm mạng tùy chỉnh", "network_add": "Thêm", "network_save": "Lưu", "remove_network_title": "Bạn có muốn gỡ bỏ mạng này?", @@ -994,7 +1002,7 @@ "token_nft_ens_subheading": "Tự động phát hiện Token, NFT và ENS", "security_check_subheading": "Kiểm tra bảo mật", "symbol_required": "Ký hiệu là bắt buộc.", - "blockaid_desc": "Bảo vệ quyền riêng tư - không có dữ liệu nào được chia sẻ với các bên thứ ba. Có sẵn trên Arbitrum, Avalanche, BNB chain, Ethereum Mainnet, Optimism, Polygon, Sepolia và Base.", + "blockaid_desc": "Tính năng này sẽ cảnh báo bạn khi có hoạt động độc hại bằng cách chủ động xem xét các yêu cầu giao dịch và chữ ký.", "security_alerts": "Cảnh báo bảo mật", "security_alerts_desc": "Tính năng này sẽ cảnh báo bạn về hoạt động độc hại bằng cách xem xét cục bộ các yêu cầu giao dịch và chữ ký của bạn. Hãy luôn tự mình thực hiện quy trình thẩm định trước khi chấp thuận bất kỳ yêu cầu nào. Không có gì đảm bảo rằng tính năng này sẽ phát hiện được tất cả các hoạt động độc hại. Bằng cách bật tính năng này, bạn đồng ý với các điều khoản sử dụng của nhà cung cấp.", "smart_transactions_opt_in_heading": "Giao dịch thông minh", @@ -1031,6 +1039,7 @@ "cancel": "Hủy", "loading": "Đang kết nối với MetaMask...", "unkown_dapp": "Tên DAPP không khả dụng", + "unknown": "Không xác định", "no_connections": "Không có kết nối", "no_connections_desc": "Nếu bạn kết nối tài khoản với một trang web hoặc một ứng dụng, bạn sẽ thấy tài khoản đó ở đây." }, @@ -1654,6 +1663,7 @@ "network_currency_symbol": "Ký hiệu tiền tệ", "network_block_explorer_url": "URL của Trình khám phá khối", "search": "Tìm kiếm mạng đã thêm trước đây", + "search-short": "Tìm kiếm", "add": "Thêm", "cancel": "Hủy", "approve": "Chấp thuận", @@ -1661,7 +1671,9 @@ "malicious_network_warning": "Một nhà cung cấp mạng độc hại có thể nói dối về trạng thái của chuỗi khối và ghi lại hoạt động của bạn trên mạng. Chỉ thêm các mạng tùy chỉnh mà bạn tin tưởng.", "security_link": "https://support.metamask.io/networks-and-sidechains/managing-networks/user-guide-custom-networks-and-sidechains/", "network_warning_title": "Thông tin mạng", + "additional_network_information_title": "Thông tin mạng bổ sung", "network_warning_desc": "Kết nối mạng này dựa vào các bên thứ ba. Kết nối này có thể kém tin cậy hơn hoặc cho phép các bên thứ ba theo dõi hoạt động.", + "additonial_network_information_desc": "Một vài mạng trong số này phụ thuộc vào bên thứ ba. Kết nối có thể kém tin cậy hơn hoặc cho phép bên thứ ba theo dõi hoạt động.", "learn_more": "Tìm hiểu thêm", "learn_more_url": "https://support.metamask.io/networks-and-sidechains/managing-networks/the-risks-of-connecting-to-an-unknown-network/", "switch_network": "Chuyển sang mạng", @@ -1950,7 +1962,7 @@ "received": "Đã nhận", "unstaked": "Đã hủy ký gửi", "to": "Đến", - "rate": "Tỷ giá (đã bao gồm phí)", + "rate": "Rate (fees included)", "unstaking_requested": "Đã yêu cầu hủy ký gửi", "stake_completed": "Ký gửi đã hoàn tất", "withdrawal_completed": "Rút tiền đã hoàn tất", @@ -1960,6 +1972,49 @@ "swap_completed": "Đã hoán đổi {{from}} thành {{to}}", "swap": "Đã hoán đổi", "sent": "Đã gửi đến {{address}}", + "menu_item_title": { + "metamask_swap_completed": "Swapped {{symbol1}} for {{symbol2}}", + "erc20_sent": "Sent to {{address}}", + "erc20_received": "Received from {{address}}", + "eth_sent": "Sent to {{address}}", + "eth_received": "Received from {{address}}", + "erc721_sent": "Sent NFT to {{address}}", + "erc1155_sent": "Sent NFT to {{address}}", + "erc721_received": "Received NFT from {{address}}", + "erc1155_received": "Received NFT from {{address}}", + "rocketpool_stake_completed": "Staked", + "rocketpool_unstake_completed": "Unstaking complete", + "lido_stake_completed": "Staked", + "lido_withdrawal_requested": "Unstaking requested", + "lido_withdrawal_completed": "Unstaking complete", + "lido_stake_ready_to_be_withdrawn": "Withdrawal requested" + }, + "menu_item_description": { + "lido_withdrawal_requested": "Your request to unstake {{amount}} {{symbol}} has been sent", + "lido_stake_ready_to_be_withdrawn": "You can now withdraw your unstaked {{symbol}}" + }, + "modal": { + "title_sent": "Sent {{symbol}}", + "title_received": "Received {{symbol}}", + "title_unstake_requested": "Unstaking requested", + "title_untake_ready": "Withdrawl ready", + "title_stake": "Staked {{symbol}}", + "title_unstake_completed": "Unstaking completed", + "title_swapped": "Swapped {{symbol1}} to {{symbol2}}", + "label_address_to": "To", + "label_address_from": "From", + "label_address_to_you": "To (You)", + "label_address_from_you": "From (You)", + "label_asset": "Asset", + "label_account": "Account", + "label_unstaking_requested": "Unstaking Requested", + "label_unstake_ready": "Withdrawl ready", + "label_staked": "Staked", + "label_received": "Received", + "label_unstaking_confirmed": "Unstaking Confirmed", + "label_swapped": "Swapped", + "label_to": "To" + }, "received_from": "Đã nhận được từ {{address}}", "nft_sent": "Đã gửi NFT đến {{address}}", "erc721_sent": "Đã gửi NFT đến {{address}}", @@ -1993,6 +2048,8 @@ "received_message": "Chạm để xem giao dịch này", "received_payment_message": "Bạn đã nhận {{amount}} DAI", "prompt_title": "Bật thông báo đẩy", + "notifications_enabled_error_title": "Something went wrong", + "notifications_enabled_error_desc": "We couldn't enable notifications. Please try again later.", "prompt_desc": "Bật thông báo để MetaMask có thể thông báo cho bạn biết khi bạn nhận ETH hoặc khi giao dịch của bạn được xác nhận.", "prompt_ok": "Có", "prompt_cancel": "Không, cảm ơn", @@ -2017,6 +2074,7 @@ "1": "Ví", "2": "Thông báo" }, + "copied_to_clipboard": "Copied to clipboard", "address_copied_to_clipboard": "Đã sao chép địa chỉ vào bộ nhớ đệm", "transaction_id_copied_to_clipboard": "Đã sao chép ID giao dịch vào bộ nhớ đệm", "activation_card": { @@ -2648,7 +2706,7 @@ "header": "Tăng cường bảo vệ giao dịch", "description_1": "Đạt tỷ lệ thành công cao hơn, bảo vệ chống hành vi lợi dụng thông tin biết trước và khả năng hiển thị tốt hơn với Giao dịch thông minh.", "description_2": "Chỉ có sẵn trên Ethereum. Có thể bật/tắt bất cứ lúc nào trong phần Cài đặt.", - "secondary_button": "Quản lý trong phần cài đặt", + "no_thanks": "No thanks", "primary_button": "Bật", "learn_more": "Tìm hiểu thêm.", "benefit_1_1": "Tỷ lệ thành công", @@ -2847,6 +2905,15 @@ "enable_remember_me": "Bật Ghi nhớ", "enable_remember_me_description": "Khi bật Ghi nhớ, bất kỳ ai có quyền truy cập vào điện thoại của bạn đều có thể truy cập vào tài khoản MetaMask của bạn." }, + "profile_sync": { + "title": "Profile Sync", + "disable_warning": "If you turn off profile sync, you won’t be able to receive notifications.", + "enable_description": "Creates a profile that MetaMask uses to sync some settings among your devices. This is required to get notifications. ", + "enable_description2": " to learn more.", + "enable_privacy_link": "Learn how we protect your privacy", + "bottomSheetTurnOn": "Turning on Profile Sync", + "bottomSheetTurnOff": "Turning off Profile Sync" + }, "turn_off_remember_me": { "title": "Nhập mật khẩu để tắt Ghi nhớ", "placeholder": "Mật khẩu", @@ -3069,7 +3136,7 @@ }, "install_snap": { "title": "Yêu cầu kết nối", - "description": "{{origin}} muốn tải xuống và kết nối với {{snap}}. Đảm bảo bạn tin tưởng các tác giả trước khi tiếp tục.", + "description": "{{origin}} muốn sử dụng {{snap}}.", "permissions_request_title": "Yêu cầu cấp quyền", "permissions_request_description": "{{origin}} muốn cài đặt {{snap}}, điều này sẽ yêu cầu các quyền sau.", "approve_permissions": "Chấp thuận", @@ -3118,5 +3185,8 @@ "title": "Thay đổi ước tính", "tooltip_description": "Thay đổi ước tính là những gì có thể xảy ra nếu bạn thực hiện giao dịch này. Đây chỉ là dự đoán, không phải là đảm bảo.", "total_fiat": "Tổng cộng = {{currency}}" + }, + "common": { + "please_wait": "Please wait" } -} +} \ No newline at end of file diff --git a/locales/languages/zh.json b/locales/languages/zh.json index 008e1ec3bdd..cdafb2a56f9 100644 --- a/locales/languages/zh.json +++ b/locales/languages/zh.json @@ -5,8 +5,6 @@ "deceptive_request_title": "此请求属欺骗性质", "failed_title": "此请求可能不安全", "failed_description": "由于出现错误,安全服务提供商未验证此请求。请谨慎操作。", - "loading_title": "正在检查安全提醒......", - "loading_complete_title": "未收到提醒。在批准请求之前,请务必自行作出审慎调查。", "malicious_domain_description": "您正在与恶意网域交互。如果您批准此请求,可能会遭受资产损失。", "other_description": "如果您批准此请求,可能会遭受资产损失。", "raw_signature_farming_description": "如果您批准此请求,可能会有资产损失。", @@ -613,6 +611,10 @@ "remove_account_message": "是否确实想要删除此账户?", "no": "否", "yes_remove_it": "是,删除它", + "remove_hardware_account": "Remove hardware account", + "remove_hw_account_alert_description": "Are you sure you want to remove this hardware wallet account? You’ll have to resync your hardware wallet if you want to use this account again with MetaMask Mobile.", + "remove_account_alert_remove_btn": "Remove", + "remove_account_alert_cancel_btn": "Nevermind", "accounts_title": "账户", "connect_account_title": "连接账户", "connect_accounts_title": "连接账户", @@ -668,8 +670,7 @@ "hint_text": "扫描您的Keystone钱包以", "purpose_connect": "连接", "purpose_sign": "确认交易", - "select_accounts": "选择账户", - "please_wait": "请稍候" + "select_accounts": "选择账户" }, "data_collection_modal": { "accept": "好的", @@ -682,6 +683,11 @@ "forget": "忽略此设备" }, "app_settings": { + "enabling_notifications": "Enabling notifications...", + "disabling_notifications": "Disabling notifications...", + "enabling_profile_sync": "Enabling profile syncing...", + "disabling_profile_sync": "Disabling profile syncing...", + "notifications_dismiss_modal": "Dismiss", "title": "设置", "current_conversion": "基础货币", "current_language": "当前语言", @@ -805,6 +811,7 @@ "security_desc": "隐私设置、MetaMetrics、私钥和钱包私钥助记词", "networks_title": "网络", "networks_default_title": "默认网络", + "network_delete": "如果您删除此网络,则需要再次添加此网络才能查看您在其中的资产", "networks_default_cta": "使用此网络", "networks_desc": "添加并编辑自定义 RPC 网络", "network_name_label": "网络名称", @@ -820,6 +827,7 @@ "network_other_networks": "其他网络", "network_rpc_networks": "RPC 网络", "network_add_network": "添加网络", + "network_add_custom_network": "添加自定义网络", "network_add": "添加", "network_save": "保存", "remove_network_title": "是否要删除此网络?", @@ -994,7 +1002,7 @@ "token_nft_ens_subheading": "代币、NFT 和 ENS(Ethereum 域名服务)自动检测", "security_check_subheading": "安全检查", "symbol_required": "需要符号。", - "blockaid_desc": "隐私保护 - 不会与第三方共享任何数据。适用于 Arbitrum、Avalanche、BNB Chain、以太坊主网、Optimism、Polygon、Sepolia 和 Base。", + "blockaid_desc": "此功能通过主动审查交易和签名请求向您发出恶意活动提醒。", "security_alerts": "安全提醒", "security_alerts_desc": "此功能通过本地审查您的交易和签名请求来提醒您注意恶意活动。在批准任何请求之前,请务必自行进行审慎调查。无法保证此功能能够检测到所有恶意活动。启用此功能即表示您同意提供商的使用条款。", "smart_transactions_opt_in_heading": "智能交易", @@ -1031,6 +1039,7 @@ "cancel": "取消", "loading": "正在连接MetaMask…...", "unkown_dapp": "去中心化应用(DAPP)名称不可用", + "unknown": "未知", "no_connections": "无连接", "no_connections_desc": "如果您将账户连接到网站或应用程序,您可在此处查看。" }, @@ -1654,6 +1663,7 @@ "network_currency_symbol": "货币符号", "network_block_explorer_url": "区块浏览器URL", "search": "搜索以前添加的网络", + "search-short": "搜索", "add": "添加", "cancel": "取消", "approve": "批准", @@ -1661,7 +1671,9 @@ "malicious_network_warning": "恶意网络提供商可能会谎报区块链的状态,并记录您的网络活动。仅添加您信任的自定义网络。", "security_link": "https://support.metamask.io/networks-and-sidechains/managing-networks/user-guide-custom-networks-and-sidechains/", "network_warning_title": "网络信息", + "additional_network_information_title": "其他网络信息", "network_warning_desc": "此网络连接依赖于第三方。这种连接可靠程度可能会较低,或会使第三方能够跟踪活动。", + "additonial_network_information_desc": "这些网络中的其中一些依赖于第三方。此连接可能不太可靠,或使第三方可进行活动跟踪。", "learn_more": "了解详情", "learn_more_url": "https://support.metamask.io/networks-and-sidechains/managing-networks/the-risks-of-connecting-to-an-unknown-network/", "switch_network": "切换到网络", @@ -1950,7 +1962,7 @@ "received": "已收到", "unstaked": "已解除质押", "to": "至", - "rate": "价格(包括费用)", + "rate": "Rate (fees included)", "unstaking_requested": "已请求解除质押", "stake_completed": "质押已完成", "withdrawal_completed": "提取已完成", @@ -1960,6 +1972,49 @@ "swap_completed": "已用 {{from}} 兑换 {{to}}", "swap": "已兑换", "sent": "已发送至 {{address}}", + "menu_item_title": { + "metamask_swap_completed": "Swapped {{symbol1}} for {{symbol2}}", + "erc20_sent": "Sent to {{address}}", + "erc20_received": "Received from {{address}}", + "eth_sent": "Sent to {{address}}", + "eth_received": "Received from {{address}}", + "erc721_sent": "Sent NFT to {{address}}", + "erc1155_sent": "Sent NFT to {{address}}", + "erc721_received": "Received NFT from {{address}}", + "erc1155_received": "Received NFT from {{address}}", + "rocketpool_stake_completed": "Staked", + "rocketpool_unstake_completed": "Unstaking complete", + "lido_stake_completed": "Staked", + "lido_withdrawal_requested": "Unstaking requested", + "lido_withdrawal_completed": "Unstaking complete", + "lido_stake_ready_to_be_withdrawn": "Withdrawal requested" + }, + "menu_item_description": { + "lido_withdrawal_requested": "Your request to unstake {{amount}} {{symbol}} has been sent", + "lido_stake_ready_to_be_withdrawn": "You can now withdraw your unstaked {{symbol}}" + }, + "modal": { + "title_sent": "Sent {{symbol}}", + "title_received": "Received {{symbol}}", + "title_unstake_requested": "Unstaking requested", + "title_untake_ready": "Withdrawl ready", + "title_stake": "Staked {{symbol}}", + "title_unstake_completed": "Unstaking completed", + "title_swapped": "Swapped {{symbol1}} to {{symbol2}}", + "label_address_to": "To", + "label_address_from": "From", + "label_address_to_you": "To (You)", + "label_address_from_you": "From (You)", + "label_asset": "Asset", + "label_account": "Account", + "label_unstaking_requested": "Unstaking Requested", + "label_unstake_ready": "Withdrawl ready", + "label_staked": "Staked", + "label_received": "Received", + "label_unstaking_confirmed": "Unstaking Confirmed", + "label_swapped": "Swapped", + "label_to": "To" + }, "received_from": "已从 {{address}} 收到", "nft_sent": "已发送 NFT 至 {{address}}", "erc721_sent": "已发送 NFT 至 {{address}}", @@ -1993,6 +2048,8 @@ "received_message": "点按以查看此交易", "received_payment_message": "您已收到 {{amount}} DAI", "prompt_title": "启用推送通知", + "notifications_enabled_error_title": "Something went wrong", + "notifications_enabled_error_desc": "We couldn't enable notifications. Please try again later.", "prompt_desc": "启用通知,以便在您收到 ETH 或您的交易被确认时,MetaMask 能够让您知道。", "prompt_ok": "是", "prompt_cancel": "不,谢谢", @@ -2017,6 +2074,7 @@ "1": "钱包", "2": "公告" }, + "copied_to_clipboard": "Copied to clipboard", "address_copied_to_clipboard": "地址已复制到剪贴板", "transaction_id_copied_to_clipboard": "交易 ID 已复制到剪贴板", "activation_card": { @@ -2648,7 +2706,7 @@ "header": "增强型交易保护", "description_1": "通过智能交易解锁更高的成功率、抢先交易保护和更高的透明度。", "description_2": "仅适用于以太坊。可随时在设置中启用或禁用。", - "secondary_button": "在设置中管理", + "no_thanks": "No thanks", "primary_button": "启用", "learn_more": "了解更多。", "benefit_1_1": "99.5% 的成功", @@ -2847,6 +2905,15 @@ "enable_remember_me": "打开“记住我”", "enable_remember_me_description": "在“记住我”功能打开时,任何有访问您手机权限的人都可以访问您的 MetaMask 账户。" }, + "profile_sync": { + "title": "Profile Sync", + "disable_warning": "If you turn off profile sync, you won’t be able to receive notifications.", + "enable_description": "Creates a profile that MetaMask uses to sync some settings among your devices. This is required to get notifications. ", + "enable_description2": " to learn more.", + "enable_privacy_link": "Learn how we protect your privacy", + "bottomSheetTurnOn": "Turning on Profile Sync", + "bottomSheetTurnOff": "Turning off Profile Sync" + }, "turn_off_remember_me": { "title": "输入密码以关闭“记住我”", "placeholder": "密码", @@ -3069,7 +3136,7 @@ }, "install_snap": { "title": "连接请求", - "description": "{{origin}}想要下载并连接{{snap}}。继续之前,请确保您信任作者。", + "description": "{{origin}} 想要使用 {{snap}}。", "permissions_request_title": "许可请求", "permissions_request_description": "{{origin}}想要安装{{snap}},这是在请求以下许可。", "approve_permissions": "批准", @@ -3118,5 +3185,8 @@ "title": "预计变化", "tooltip_description": "预计变化是指您完成该交易可能发生的变化。这只是一个预测,而不是保证。", "total_fiat": "总计 = {{currency}}" + }, + "common": { + "please_wait": "Please wait" } -} +} \ No newline at end of file diff --git a/metro.transform.js b/metro.transform.js index 0fc4f1dc32f..63c7cc5a4de 100644 --- a/metro.transform.js +++ b/metro.transform.js @@ -19,7 +19,7 @@ const availableFeatures = new Set([ 'beta', ]); -const mainFeatureSet = new Set(['preinstalled-snaps']); +const mainFeatureSet = new Set([]); const flaskFeatureSet = new Set([ 'flask', 'preinstalled-snaps', diff --git a/package.json b/package.json index 552e94579b0..afe7350e335 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "metamask", - "version": "7.24.4", + "version": "7.27.0", "private": true, "scripts": { "audit:ci": "./scripts/yarn-audit.sh", @@ -78,9 +78,10 @@ "test:merge-coverage": "nyc report --temp-dir ./tests/coverage --report-dir ./tests/merged-coverage/ --reporter json --reporter text --reporter lcovonly", "test:validate-coverage": "nyc check-coverage --nycrc-path ./coverage-thresholds.json -t ./tests/merged-coverage/", "update-changelog": "./scripts/auto-changelog.sh", + "changeset-changelog": "wrap () { node ./scripts/generate-rc-commits.js \"$@\" && ./scripts/changelog-csv.sh }; wrap ", "prestorybook": "rnstl", "deduplicate": "yarn yarn-deduplicate && yarn install", - "create-release": "./scripts/set-versions.sh && yarn update-changelog", + "set-version": "./scripts/set-versions.sh", "add-release-label-to-pr-and-linked-issues": "ts-node ./.github/scripts/add-release-label-to-pr-and-linked-issues.ts", "add-team-label-to-pr": "ts-node ./.github/scripts/add-team-label-to-pr.ts", "run-bitrise-e2e-check": "ts-node ./.github/scripts/bitrise/run-bitrise-e2e-check.ts", @@ -126,6 +127,8 @@ "**/babel-runtime/regenerator-runtime": "^0.13.8", "redux-persist-filesystem-storage/react-native-blob-util": "^0.19.9", "xmldom": "npm:@xmldom/xmldom@0.7.13", + "@storybook/react-native/chokidar/braces": "^3.0.3", + "lint-staged/micromatch/braces": "^3.0.3", "@metamask/metamask-eth-abis": "3.1.1", "**/@walletconnect/socket-transport/ws": "^7.5.10", "**/@ethersproject/providers/ws": "^7.5.10", @@ -135,8 +138,7 @@ "socket.io-client/engine.io-client/ws": "^8.17.1" }, "dependencies": { - "@consensys/ledgerhq-metamask-keyring": "0.0.9", - "@consensys/on-ramp-sdk": "1.27.1", + "@consensys/on-ramp-sdk": "1.28.1", "@eth-optimism/contracts": "0.0.0-2021919175625", "@ethereumjs/tx": "^3.2.1", "@ethersproject/abi": "^5.7.0", @@ -153,26 +155,29 @@ "@metamask/contract-metadata": "^2.1.0", "@metamask/controller-utils": "^10.0.0", "@metamask/design-tokens": "^4.0.0", + "@metamask/eth-ledger-bridge-keyring": "^4.1.0", "@metamask/eth-sig-util": "^7.0.2", "@metamask/etherscan-link": "^2.0.0", - "@metamask/gas-fee-controller": "^15.1.2", + "@metamask/gas-fee-controller": "^18.0.0", "@metamask/key-tree": "^9.0.0", "@metamask/keyring-api": "^6.0.0", "@metamask/keyring-controller": "^16.0.0", "@metamask/logging-controller": "^3.0.0", "@metamask/message-signing-snap": "^0.3.3", "@metamask/network-controller": "^18.1.0", + "@metamask/notification-services-controller": "^0.1.1", "@metamask/permission-controller": "^9.0.0", "@metamask/phishing-controller": "^9.0.0", "@metamask/post-message-stream": "^8.0.0", - "@metamask/ppom-validator": "0.31.0", + "@metamask/ppom-validator": "0.32.0", "@metamask/preferences-controller": "^11.0.0", + "@metamask/profile-sync-controller": "^0.1.3", "@metamask/react-native-actionsheet": "2.4.2", - "@metamask/react-native-animated-fox": "^2.1.0", "@metamask/react-native-button": "^3.0.0", "@metamask/react-native-payments": "^2.0.0", "@metamask/react-native-search-api": "1.0.1", "@metamask/react-native-splash-screen": "^3.2.0", + "@metamask/react-native-webview": "^14.0.2", "@metamask/rpc-errors": "^6.2.1", "@metamask/scure-bip39": "^2.1.0", "@metamask/sdk-communication-layer": "^0.20.2", @@ -180,11 +185,12 @@ "@metamask/slip44": "3.1.0", "@metamask/smart-transactions-controller": "10.1.1", "@metamask/snaps-controllers": "^9.2.0", + "@metamask/snaps-execution-environments": "^6.5.0", "@metamask/snaps-rpc-methods": "^9.1.4", "@metamask/snaps-sdk": "^6.0.0", "@metamask/snaps-utils": "^7.7.0", "@metamask/swappable-obj-proxy": "^2.1.0", - "@metamask/swaps-controller": "^9.0.0", + "@metamask/swaps-controller": "^9.0.1", "@metamask/transaction-controller": "^13.0.0", "@metamask/utils": "^8.1.0", "@ngraveio/bc-ur": "^1.1.6", @@ -195,6 +201,8 @@ "@react-native-community/checkbox": "^0.5.12", "@react-native-community/netinfo": "6.0.0", "@react-native-cookies/cookies": "^6.2.1", + "@react-native-firebase/app": "^20.1.0", + "@react-native-firebase/messaging": "^20.1.0", "@react-native-masked-view/masked-view": "^0.2.6", "@react-native-picker/picker": "^2.2.1", "@react-navigation/bottom-tabs": "^5.11.11", @@ -212,7 +220,7 @@ "@walletconnect/core": "2.13.0", "@walletconnect/jsonrpc-types": "^1.0.2", "@walletconnect/react-native-compat": "2.13.0", - "@walletconnect/se-sdk": "1.8.0", + "@walletconnect/se-sdk": "1.8.1", "@walletconnect/utils": "2.13.0", "@xmldom/xmldom": "^0.8.10", "appium-adb": "^9.11.4", @@ -326,7 +334,6 @@ "react-native-vector-icons": "6.4.2", "react-native-video": "5.2.1", "react-native-view-shot": "^3.1.2", - "react-native-webview": "11.13.0", "react-native-webview-invoke": "^0.6.2", "react-redux": "^8.1.3", "readable-stream": "2.3.7", @@ -370,6 +377,7 @@ "@metamask/object-multiplex": "^1.1.0", "@metamask/providers": "^13.1.0", "@metamask/test-dapp": "^8.9.0", + "@octokit/rest": "^21.0.0", "@open-rpc/mock-server": "^1.7.5", "@open-rpc/schema-utils-js": "^1.16.2", "@open-rpc/test-coverage": "^2.2.2", @@ -447,7 +455,7 @@ "execa": "^8.0.1", "fbjs-scripts": "^3.0.1", "fs-extra": "^10.1.0", - "ganache": "^7.7.7", + "ganache": "^7.9.2", "husky": "1.3.1", "improved-yarn-audit": "^3.0.0", "jest": "^29.7.0", @@ -476,6 +484,7 @@ "regenerator-runtime": "0.13.9", "rn-nodeify": "10.3.0", "serve-handler": "^6.1.5", + "simple-git": "^3.22.0", "ts-node": "^10.5.0", "typescript": "~4.8.4", "wdio-cucumberjs-json-reporter": "^4.4.3", @@ -568,7 +577,9 @@ "react-native-svg-asset-plugin>sharp": true, "ts-node>@swc/core": false, "@metamask/sdk-communication-layer>bufferutil": false, - "@metamask/sdk-communication-layer>utf-8-validate": false + "@metamask/sdk-communication-layer>utf-8-validate": false, + "detox>ws>bufferutil": false, + "@metamask/notification-services-controller>firebase>@firebase/firestore>@grpc/proto-loader>protobufjs": false } }, "packageManager": "yarn@1.22.22" diff --git a/patches/@metamask+assets-controllers+30.0.0.patch b/patches/@metamask+assets-controllers+30.0.0.patch index dc9b7fdfeba..90cad62e460 100644 --- a/patches/@metamask+assets-controllers+30.0.0.patch +++ b/patches/@metamask+assets-controllers+30.0.0.patch @@ -24,7 +24,7 @@ index 0dc70ec..461a210 100644 }; return acc; diff --git a/node_modules/@metamask/assets-controllers/dist/chunk-FMZML3V5.js b/node_modules/@metamask/assets-controllers/dist/chunk-FMZML3V5.js -index ee6155c..3b33a57 100644 +index ee6155c..addfe1e 100644 --- a/node_modules/@metamask/assets-controllers/dist/chunk-FMZML3V5.js +++ b/node_modules/@metamask/assets-controllers/dist/chunk-FMZML3V5.js @@ -1,12 +1,14 @@ @@ -38,7 +38,7 @@ index ee6155c..3b33a57 100644 var _controllerutils = require('@metamask/controller-utils'); var _pollingcontroller = require('@metamask/polling-controller'); var DEFAULT_INTERVAL = 18e4; -+var supportedNftDetectionNetworks = [_controllerutils.ChainId.mainnet]; ++var supportedNftDetectionNetworks = [_controllerutils.ChainId.mainnet, _controllerutils.ChainId['linea-mainnet']]; +var inProcessNftFetchingUpdates; var BlockaidResultType = /* @__PURE__ */ ((BlockaidResultType2) => { BlockaidResultType2["Benign"] = "Benign"; diff --git a/patches/@metamask+gas-fee-controller+15.1.2.patch b/patches/@metamask+gas-fee-controller+15.1.2.patch deleted file mode 100644 index fcde1422a66..00000000000 --- a/patches/@metamask+gas-fee-controller+15.1.2.patch +++ /dev/null @@ -1,373 +0,0 @@ -diff --git a/node_modules/@metamask/gas-fee-controller/dist/GasFeeController.js b/node_modules/@metamask/gas-fee-controller/dist/GasFeeController.js -index 089bba2..97efb17 100644 ---- a/node_modules/@metamask/gas-fee-controller/dist/GasFeeController.js -+++ b/node_modules/@metamask/gas-fee-controller/dist/GasFeeController.js -@@ -10,5 +10,5 @@ require('./chunk-Q2YPK5SL.js'); - - - --exports.GAS_API_BASE_URL = _chunkH5WHAYLIjs.GAS_API_BASE_URL; exports.GAS_ESTIMATE_TYPES = _chunkH5WHAYLIjs.GAS_ESTIMATE_TYPES; exports.GasFeeController = _chunkH5WHAYLIjs.GasFeeController; exports.default = _chunkH5WHAYLIjs.GasFeeController_default; -+exports.LEGACY_GAS_PRICES_API_URL = _chunkH5WHAYLIjs.LEGACY_GAS_PRICES_API_URL; exports.GAS_ESTIMATE_TYPES = _chunkH5WHAYLIjs.GAS_ESTIMATE_TYPES; exports.GasFeeController = _chunkH5WHAYLIjs.GasFeeController; exports.default = _chunkH5WHAYLIjs.GasFeeController_default; - //# sourceMappingURL=GasFeeController.js.map -diff --git a/node_modules/@metamask/gas-fee-controller/dist/GasFeeController.mjs b/node_modules/@metamask/gas-fee-controller/dist/GasFeeController.mjs -index 14ab557..815b104 100644 ---- a/node_modules/@metamask/gas-fee-controller/dist/GasFeeController.mjs -+++ b/node_modules/@metamask/gas-fee-controller/dist/GasFeeController.mjs -@@ -1,12 +1,12 @@ - import { -- GAS_API_BASE_URL, -+ LEGACY_GAS_PRICES_API_URL, - GAS_ESTIMATE_TYPES, - GasFeeController, - GasFeeController_default - } from "./chunk-BEVZS3YV.mjs"; - import "./chunk-KORLXV32.mjs"; - export { -- GAS_API_BASE_URL, -+ LEGACY_GAS_PRICES_API_URL, - GAS_ESTIMATE_TYPES, - GasFeeController, - GasFeeController_default as default -diff --git a/node_modules/@metamask/gas-fee-controller/dist/chunk-BEVZS3YV.mjs b/node_modules/@metamask/gas-fee-controller/dist/chunk-BEVZS3YV.mjs -index 3be5db0..13ffc77 100644 ---- a/node_modules/@metamask/gas-fee-controller/dist/chunk-BEVZS3YV.mjs -+++ b/node_modules/@metamask/gas-fee-controller/dist/chunk-BEVZS3YV.mjs -@@ -18,13 +18,14 @@ import { - import EthQuery from "@metamask/eth-query"; - import { StaticIntervalPollingController } from "@metamask/polling-controller"; - import { v1 as random } from "uuid"; --var GAS_API_BASE_URL = "https://gas.api.infura.io"; -+var LEGACY_GAS_PRICES_API_URL = `https://api.metaswap.codefi.network/gasPrices`;; - var GAS_ESTIMATE_TYPES = { - FEE_MARKET: "fee-market", - LEGACY: "legacy", - ETH_GASPRICE: "eth_gasPrice", - NONE: "none" - }; -+ - var metadata = { - gasFeeEstimatesByChainId: { - persist: true, -@@ -94,8 +95,8 @@ var GasFeeController = class extends StaticIntervalPollingController { - this.getCurrentNetworkLegacyGasAPICompatibility = getCurrentNetworkLegacyGasAPICompatibility; - this.getCurrentAccountEIP1559Compatibility = getCurrentAccountEIP1559Compatibility; - __privateSet(this, _getProvider, getProvider); -- this.EIP1559APIEndpoint = `${GAS_API_BASE_URL}/networks//suggestedGasFees`; -- this.legacyAPIEndpoint = `${GAS_API_BASE_URL}/networks//gasPrices`; -+ this.EIP1559APIEndpoint = `${LEGACY_GAS_PRICES_API_URL}/networks//suggestedGasFees`; -+ this.legacyAPIEndpoint = `${LEGACY_GAS_PRICES_API_URL}/networks//gasPrices`; - this.clientId = clientId; - this.infuraAPIKey = infuraAPIKey; - this.ethQuery = new EthQuery(__privateGet(this, _getProvider).call(this)); -@@ -388,7 +389,7 @@ async function getEstimatesUsingProvider(request) { - - export { - determineGasFeeCalculations, -- GAS_API_BASE_URL, -+ LEGACY_GAS_PRICES_API_URL, - GAS_ESTIMATE_TYPES, - GasFeeController, - GasFeeController_default -diff --git a/node_modules/@metamask/gas-fee-controller/dist/chunk-BEVZS3YV.mjs.map b/node_modules/@metamask/gas-fee-controller/dist/chunk-BEVZS3YV.mjs.map -index fc90025..ce90988 100644 ---- a/node_modules/@metamask/gas-fee-controller/dist/chunk-BEVZS3YV.mjs.map -+++ b/node_modules/@metamask/gas-fee-controller/dist/chunk-BEVZS3YV.mjs.map -@@ -1 +1 @@ --{"version":3,"sources":["../src/GasFeeController.ts","../src/determineGasFeeCalculations.ts"],"sourcesContent":["import type {\n ControllerGetStateAction,\n ControllerStateChangeEvent,\n RestrictedControllerMessenger,\n} from '@metamask/base-controller';\nimport {\n convertHexToDecimal,\n safelyExecute,\n toHex,\n} from '@metamask/controller-utils';\nimport EthQuery from '@metamask/eth-query';\nimport type {\n NetworkClientId,\n NetworkControllerGetEIP1559CompatibilityAction,\n NetworkControllerGetNetworkClientByIdAction,\n NetworkControllerGetStateAction,\n NetworkControllerNetworkDidChangeEvent,\n NetworkState,\n ProviderProxy,\n} from '@metamask/network-controller';\nimport { StaticIntervalPollingController } from '@metamask/polling-controller';\nimport type { Hex } from '@metamask/utils';\nimport { v1 as random } from 'uuid';\n\nimport determineGasFeeCalculations from './determineGasFeeCalculations';\nimport {\n calculateTimeEstimate,\n fetchGasEstimates,\n fetchLegacyGasPriceEstimates,\n fetchEthGasPriceEstimate,\n} from './gas-util';\n\nexport const GAS_API_BASE_URL = 'https://gas.api.infura.io';\n\nexport type unknownString = 'unknown';\n\n// Fee Market describes the way gas is set after the london hardfork, and was\n// defined by EIP-1559.\nexport type FeeMarketEstimateType = 'fee-market';\n// Legacy describes gasPrice estimates from before london hardfork, when the\n// user is connected to mainnet and are presented with fast/average/slow\n// estimate levels to choose from.\nexport type LegacyEstimateType = 'legacy';\n// EthGasPrice describes a gasPrice estimate received from eth_gasPrice. Post\n// london this value should only be used for legacy type transactions when on\n// networks that support EIP-1559. This type of estimate is the most accurate\n// to display on custom networks that don't support EIP-1559.\nexport type EthGasPriceEstimateType = 'eth_gasPrice';\n// NoEstimate describes the state of the controller before receiving its first\n// estimate.\nexport type NoEstimateType = 'none';\n\n/**\n * Indicates which type of gasEstimate the controller is currently returning.\n * This is useful as a way of asserting that the shape of gasEstimates matches\n * expectations. NONE is a special case indicating that no previous gasEstimate\n * has been fetched.\n */\nexport const GAS_ESTIMATE_TYPES = {\n FEE_MARKET: 'fee-market' as FeeMarketEstimateType,\n LEGACY: 'legacy' as LegacyEstimateType,\n ETH_GASPRICE: 'eth_gasPrice' as EthGasPriceEstimateType,\n NONE: 'none' as NoEstimateType,\n};\n\nexport type GasEstimateType =\n | FeeMarketEstimateType\n | EthGasPriceEstimateType\n | LegacyEstimateType\n | NoEstimateType;\n\nexport type EstimatedGasFeeTimeBounds = {\n lowerTimeBound: number | null;\n upperTimeBound: number | unknownString;\n};\n\n/**\n * @type EthGasPriceEstimate\n *\n * A single gas price estimate for networks and accounts that don't support EIP-1559\n * This estimate comes from eth_gasPrice but is converted to dec gwei to match other\n * return values\n * @property gasPrice - A GWEI dec string\n */\n\nexport type EthGasPriceEstimate = {\n gasPrice: string;\n};\n\n/**\n * @type LegacyGasPriceEstimate\n *\n * A set of gas price estimates for networks and accounts that don't support EIP-1559\n * These estimates include low, medium and high all as strings representing gwei in\n * decimal format.\n * @property high - gasPrice, in decimal gwei string format, suggested for fast inclusion\n * @property medium - gasPrice, in decimal gwei string format, suggested for avg inclusion\n * @property low - gasPrice, in decimal gwei string format, suggested for slow inclusion\n */\nexport type LegacyGasPriceEstimate = {\n high: string;\n medium: string;\n low: string;\n};\n\n/**\n * @type Eip1559GasFee\n *\n * Data necessary to provide an estimate of a gas fee with a specific tip\n * @property minWaitTimeEstimate - The fastest the transaction will take, in milliseconds\n * @property maxWaitTimeEstimate - The slowest the transaction will take, in milliseconds\n * @property suggestedMaxPriorityFeePerGas - A suggested \"tip\", a GWEI hex number\n * @property suggestedMaxFeePerGas - A suggested max fee, the most a user will pay. a GWEI hex number\n */\nexport type Eip1559GasFee = {\n minWaitTimeEstimate: number; // a time duration in milliseconds\n maxWaitTimeEstimate: number; // a time duration in milliseconds\n suggestedMaxPriorityFeePerGas: string; // a GWEI decimal number\n suggestedMaxFeePerGas: string; // a GWEI decimal number\n};\n\n/**\n * @type GasFeeEstimates\n *\n * Data necessary to provide multiple GasFee estimates, and supporting information, to the user\n * @property low - A GasFee for a minimum necessary combination of tip and maxFee\n * @property medium - A GasFee for a recommended combination of tip and maxFee\n * @property high - A GasFee for a high combination of tip and maxFee\n * @property estimatedBaseFee - An estimate of what the base fee will be for the pending/next block. A GWEI dec number\n * @property networkCongestion - A normalized number that can be used to gauge the congestion\n * level of the network, with 0 meaning not congested and 1 meaning extremely congested\n */\nexport type GasFeeEstimates = SourcedGasFeeEstimates | FallbackGasFeeEstimates;\n\ntype SourcedGasFeeEstimates = {\n low: Eip1559GasFee;\n medium: Eip1559GasFee;\n high: Eip1559GasFee;\n estimatedBaseFee: string;\n historicalBaseFeeRange: [string, string];\n baseFeeTrend: 'up' | 'down' | 'level';\n latestPriorityFeeRange: [string, string];\n historicalPriorityFeeRange: [string, string];\n priorityFeeTrend: 'up' | 'down' | 'level';\n networkCongestion: number;\n};\n\ntype FallbackGasFeeEstimates = {\n low: Eip1559GasFee;\n medium: Eip1559GasFee;\n high: Eip1559GasFee;\n estimatedBaseFee: string;\n historicalBaseFeeRange: null;\n baseFeeTrend: null;\n latestPriorityFeeRange: null;\n historicalPriorityFeeRange: null;\n priorityFeeTrend: null;\n networkCongestion: null;\n};\n\nconst metadata = {\n gasFeeEstimatesByChainId: {\n persist: true,\n anonymous: false,\n },\n gasFeeEstimates: { persist: true, anonymous: false },\n estimatedGasFeeTimeBounds: { persist: true, anonymous: false },\n gasEstimateType: { persist: true, anonymous: false },\n nonRPCGasFeeApisDisabled: { persist: true, anonymous: false },\n};\n\nexport type GasFeeStateEthGasPrice = {\n gasFeeEstimates: EthGasPriceEstimate;\n estimatedGasFeeTimeBounds: Record;\n gasEstimateType: EthGasPriceEstimateType;\n};\n\nexport type GasFeeStateFeeMarket = {\n gasFeeEstimates: GasFeeEstimates;\n estimatedGasFeeTimeBounds: EstimatedGasFeeTimeBounds | Record;\n gasEstimateType: FeeMarketEstimateType;\n};\n\nexport type GasFeeStateLegacy = {\n gasFeeEstimates: LegacyGasPriceEstimate;\n estimatedGasFeeTimeBounds: Record;\n gasEstimateType: LegacyEstimateType;\n};\n\nexport type GasFeeStateNoEstimates = {\n gasFeeEstimates: Record;\n estimatedGasFeeTimeBounds: Record;\n gasEstimateType: NoEstimateType;\n};\n\nexport type FetchGasFeeEstimateOptions = {\n shouldUpdateState?: boolean;\n networkClientId?: NetworkClientId;\n};\n\n/**\n * @type GasFeeState\n *\n * Gas Fee controller state\n * @property gasFeeEstimates - Gas fee estimate data based on new EIP-1559 properties\n * @property estimatedGasFeeTimeBounds - Estimates representing the minimum and maximum\n */\nexport type SingleChainGasFeeState =\n | GasFeeStateEthGasPrice\n | GasFeeStateFeeMarket\n | GasFeeStateLegacy\n | GasFeeStateNoEstimates;\n\nexport type GasFeeEstimatesByChainId = {\n gasFeeEstimatesByChainId?: Record;\n};\n\nexport type GasFeeState = GasFeeEstimatesByChainId &\n SingleChainGasFeeState & {\n nonRPCGasFeeApisDisabled?: boolean;\n };\n\nconst name = 'GasFeeController';\n\nexport type GasFeeStateChange = ControllerStateChangeEvent<\n typeof name,\n GasFeeState\n>;\n\nexport type GetGasFeeState = ControllerGetStateAction;\n\nexport type GasFeeControllerActions = GetGasFeeState;\n\nexport type GasFeeControllerEvents = GasFeeStateChange;\n\ntype AllowedActions =\n | NetworkControllerGetStateAction\n | NetworkControllerGetNetworkClientByIdAction\n | NetworkControllerGetEIP1559CompatibilityAction;\n\ntype GasFeeMessenger = RestrictedControllerMessenger<\n typeof name,\n GasFeeControllerActions | AllowedActions,\n GasFeeControllerEvents | NetworkControllerNetworkDidChangeEvent,\n AllowedActions['type'],\n NetworkControllerNetworkDidChangeEvent['type']\n>;\n\nconst defaultState: GasFeeState = {\n gasFeeEstimatesByChainId: {},\n gasFeeEstimates: {},\n estimatedGasFeeTimeBounds: {},\n gasEstimateType: GAS_ESTIMATE_TYPES.NONE,\n nonRPCGasFeeApisDisabled: false,\n};\n\n/**\n * Controller that retrieves gas fee estimate data and polls for updated data on a set interval\n */\nexport class GasFeeController extends StaticIntervalPollingController<\n typeof name,\n GasFeeState,\n GasFeeMessenger\n> {\n private intervalId?: ReturnType;\n\n private readonly intervalDelay;\n\n private readonly pollTokens: Set;\n\n private readonly legacyAPIEndpoint: string;\n\n private readonly EIP1559APIEndpoint: string;\n\n private readonly getCurrentNetworkEIP1559Compatibility;\n\n private readonly getCurrentNetworkLegacyGasAPICompatibility;\n\n private readonly getCurrentAccountEIP1559Compatibility;\n\n private readonly infuraAPIKey: string;\n\n private currentChainId;\n\n private ethQuery?: EthQuery;\n\n private readonly clientId?: string;\n\n #getProvider: () => ProviderProxy;\n\n /**\n * Creates a GasFeeController instance.\n *\n * @param options - The controller options.\n * @param options.interval - The time in milliseconds to wait between polls.\n * @param options.messenger - The controller messenger.\n * @param options.state - The initial state.\n * @param options.getCurrentNetworkEIP1559Compatibility - Determines whether or not the current\n * network is EIP-1559 compatible.\n * @param options.getCurrentNetworkLegacyGasAPICompatibility - Determines whether or not the\n * current network is compatible with the legacy gas price API.\n * @param options.getCurrentAccountEIP1559Compatibility - Determines whether or not the current\n * account is EIP-1559 compatible.\n * @param options.getChainId - Returns the current chain ID.\n * @param options.getProvider - Returns a network provider for the current network.\n * @param options.onNetworkDidChange - A function for registering an event handler for the\n * network state change event.\n * @param options.clientId - The client ID used to identify to the gas estimation API who is\n * asking for estimates.\n * @param options.infuraAPIKey - The Infura API key used for infura API requests.\n */\n constructor({\n interval = 15000,\n messenger,\n state,\n getCurrentNetworkEIP1559Compatibility,\n getCurrentAccountEIP1559Compatibility,\n getChainId,\n getCurrentNetworkLegacyGasAPICompatibility,\n getProvider,\n onNetworkDidChange,\n clientId,\n infuraAPIKey,\n }: {\n interval?: number;\n messenger: GasFeeMessenger;\n state?: GasFeeState;\n getCurrentNetworkEIP1559Compatibility: () => Promise;\n getCurrentNetworkLegacyGasAPICompatibility: () => boolean;\n getCurrentAccountEIP1559Compatibility?: () => boolean;\n getChainId?: () => Hex;\n getProvider: () => ProviderProxy;\n onNetworkDidChange?: (listener: (state: NetworkState) => void) => void;\n clientId?: string;\n infuraAPIKey: string;\n }) {\n super({\n name,\n metadata,\n messenger,\n state: { ...defaultState, ...state },\n });\n this.intervalDelay = interval;\n this.setIntervalLength(interval);\n this.pollTokens = new Set();\n this.getCurrentNetworkEIP1559Compatibility =\n getCurrentNetworkEIP1559Compatibility;\n this.getCurrentNetworkLegacyGasAPICompatibility =\n getCurrentNetworkLegacyGasAPICompatibility;\n this.getCurrentAccountEIP1559Compatibility =\n getCurrentAccountEIP1559Compatibility;\n this.#getProvider = getProvider;\n this.EIP1559APIEndpoint = `${GAS_API_BASE_URL}/networks//suggestedGasFees`;\n this.legacyAPIEndpoint = `${GAS_API_BASE_URL}/networks//gasPrices`;\n this.clientId = clientId;\n this.infuraAPIKey = infuraAPIKey;\n\n this.ethQuery = new EthQuery(this.#getProvider());\n\n if (onNetworkDidChange && getChainId) {\n this.currentChainId = getChainId();\n onNetworkDidChange(async (networkControllerState) => {\n await this.#onNetworkControllerDidChange(networkControllerState);\n });\n } else {\n this.currentChainId = this.messagingSystem.call(\n 'NetworkController:getState',\n ).providerConfig.chainId;\n this.messagingSystem.subscribe(\n 'NetworkController:networkDidChange',\n async (networkControllerState) => {\n await this.#onNetworkControllerDidChange(networkControllerState);\n },\n );\n }\n }\n\n async resetPolling() {\n if (this.pollTokens.size !== 0) {\n const tokens = Array.from(this.pollTokens);\n this.stopPolling();\n await this.getGasFeeEstimatesAndStartPolling(tokens[0]);\n tokens.slice(1).forEach((token) => {\n this.pollTokens.add(token);\n });\n }\n }\n\n async fetchGasFeeEstimates(options?: FetchGasFeeEstimateOptions) {\n return await this._fetchGasFeeEstimateData(options);\n }\n\n async getGasFeeEstimatesAndStartPolling(\n pollToken: string | undefined,\n ): Promise {\n const _pollToken = pollToken || random();\n\n this.pollTokens.add(_pollToken);\n\n if (this.pollTokens.size === 1) {\n await this._fetchGasFeeEstimateData();\n this._poll();\n }\n\n return _pollToken;\n }\n\n /**\n * Gets and sets gasFeeEstimates in state.\n *\n * @param options - The gas fee estimate options.\n * @param options.shouldUpdateState - Determines whether the state should be updated with the\n * updated gas estimates.\n * @returns The gas fee estimates.\n */\n async _fetchGasFeeEstimateData(\n options: FetchGasFeeEstimateOptions = {},\n ): Promise {\n const { shouldUpdateState = true, networkClientId } = options;\n\n let ethQuery,\n isEIP1559Compatible,\n isLegacyGasAPICompatible,\n decimalChainId: number;\n\n if (networkClientId !== undefined) {\n const networkClient = this.messagingSystem.call(\n 'NetworkController:getNetworkClientById',\n networkClientId,\n );\n isLegacyGasAPICompatible = networkClient.configuration.chainId === '0x38';\n\n decimalChainId = convertHexToDecimal(networkClient.configuration.chainId);\n\n try {\n const result = await this.messagingSystem.call(\n 'NetworkController:getEIP1559Compatibility',\n networkClientId,\n );\n isEIP1559Compatible = result || false;\n } catch {\n isEIP1559Compatible = false;\n }\n ethQuery = new EthQuery(networkClient.provider);\n }\n\n ethQuery ??= this.ethQuery;\n\n isLegacyGasAPICompatible ??=\n this.getCurrentNetworkLegacyGasAPICompatibility();\n\n decimalChainId ??= convertHexToDecimal(this.currentChainId);\n\n try {\n isEIP1559Compatible ??= await this.getEIP1559Compatibility();\n } catch (e) {\n console.error(e);\n isEIP1559Compatible ??= false;\n }\n\n const gasFeeCalculations = await determineGasFeeCalculations({\n isEIP1559Compatible,\n isLegacyGasAPICompatible,\n fetchGasEstimates,\n fetchGasEstimatesUrl: this.EIP1559APIEndpoint.replace(\n '',\n `${decimalChainId}`,\n ),\n fetchLegacyGasPriceEstimates,\n fetchLegacyGasPriceEstimatesUrl: this.legacyAPIEndpoint.replace(\n '',\n `${decimalChainId}`,\n ),\n fetchEthGasPriceEstimate,\n calculateTimeEstimate,\n clientId: this.clientId,\n ethQuery,\n infuraAPIKey: this.infuraAPIKey,\n nonRPCGasFeeApisDisabled: this.state.nonRPCGasFeeApisDisabled,\n });\n\n if (shouldUpdateState) {\n const chainId = toHex(decimalChainId);\n this.update((state) => {\n if (this.currentChainId === chainId) {\n state.gasFeeEstimates = gasFeeCalculations.gasFeeEstimates;\n state.estimatedGasFeeTimeBounds =\n gasFeeCalculations.estimatedGasFeeTimeBounds;\n state.gasEstimateType = gasFeeCalculations.gasEstimateType;\n }\n state.gasFeeEstimatesByChainId ??= {};\n state.gasFeeEstimatesByChainId[chainId] = {\n gasFeeEstimates: gasFeeCalculations.gasFeeEstimates,\n estimatedGasFeeTimeBounds:\n gasFeeCalculations.estimatedGasFeeTimeBounds,\n gasEstimateType: gasFeeCalculations.gasEstimateType,\n } as SingleChainGasFeeState;\n });\n }\n\n return gasFeeCalculations;\n }\n\n /**\n * Remove the poll token, and stop polling if the set of poll tokens is empty.\n *\n * @param pollToken - The poll token to disconnect.\n */\n disconnectPoller(pollToken: string) {\n this.pollTokens.delete(pollToken);\n if (this.pollTokens.size === 0) {\n this.stopPolling();\n }\n }\n\n stopPolling() {\n if (this.intervalId) {\n clearInterval(this.intervalId);\n }\n this.pollTokens.clear();\n this.resetState();\n }\n\n /**\n * Prepare to discard this controller.\n *\n * This stops any active polling.\n */\n override destroy() {\n super.destroy();\n this.stopPolling();\n }\n\n private _poll() {\n if (this.intervalId) {\n clearInterval(this.intervalId);\n }\n\n this.intervalId = setInterval(async () => {\n await safelyExecute(() => this._fetchGasFeeEstimateData());\n }, this.intervalDelay);\n }\n\n /**\n * Fetching token list from the Token Service API.\n *\n * @private\n * @param networkClientId - The ID of the network client triggering the fetch.\n * @returns A promise that resolves when this operation completes.\n */\n async _executePoll(networkClientId: string): Promise {\n await this._fetchGasFeeEstimateData({ networkClientId });\n }\n\n private resetState() {\n this.update(() => {\n return defaultState;\n });\n }\n\n private async getEIP1559Compatibility() {\n const currentNetworkIsEIP1559Compatible =\n await this.getCurrentNetworkEIP1559Compatibility();\n const currentAccountIsEIP1559Compatible =\n this.getCurrentAccountEIP1559Compatibility?.() ?? true;\n\n return (\n currentNetworkIsEIP1559Compatible && currentAccountIsEIP1559Compatible\n );\n }\n\n getTimeEstimate(\n maxPriorityFeePerGas: string,\n maxFeePerGas: string,\n ): EstimatedGasFeeTimeBounds | Record {\n if (\n !this.state.gasFeeEstimates ||\n this.state.gasEstimateType !== GAS_ESTIMATE_TYPES.FEE_MARKET\n ) {\n return {};\n }\n return calculateTimeEstimate(\n maxPriorityFeePerGas,\n maxFeePerGas,\n this.state.gasFeeEstimates,\n );\n }\n\n async #onNetworkControllerDidChange(networkControllerState: NetworkState) {\n const newChainId = networkControllerState.providerConfig.chainId;\n\n if (newChainId !== this.currentChainId) {\n this.ethQuery = new EthQuery(this.#getProvider());\n await this.resetPolling();\n\n this.currentChainId = newChainId;\n }\n }\n\n enableNonRPCGasFeeApis() {\n this.update((state) => {\n state.nonRPCGasFeeApisDisabled = false;\n });\n }\n\n disableNonRPCGasFeeApis() {\n this.update((state) => {\n state.nonRPCGasFeeApisDisabled = true;\n });\n }\n}\n\nexport default GasFeeController;\n","import type {\n EstimatedGasFeeTimeBounds,\n EthGasPriceEstimate,\n GasFeeEstimates,\n GasFeeState as GasFeeCalculations,\n LegacyGasPriceEstimate,\n} from './GasFeeController';\nimport { GAS_ESTIMATE_TYPES } from './GasFeeController';\n\ntype DetermineGasFeeCalculationsRequest = {\n isEIP1559Compatible: boolean;\n isLegacyGasAPICompatible: boolean;\n fetchGasEstimates: (\n url: string,\n infuraAPIKey: string,\n clientId?: string,\n ) => Promise;\n fetchGasEstimatesUrl: string;\n fetchLegacyGasPriceEstimates: (\n url: string,\n infuraAPIKey: string,\n clientId?: string,\n ) => Promise;\n fetchLegacyGasPriceEstimatesUrl: string;\n // TODO: Replace `any` with type\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n fetchEthGasPriceEstimate: (ethQuery: any) => Promise;\n calculateTimeEstimate: (\n maxPriorityFeePerGas: string,\n maxFeePerGas: string,\n gasFeeEstimates: GasFeeEstimates,\n ) => EstimatedGasFeeTimeBounds;\n clientId: string | undefined;\n // TODO: Replace `any` with type\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n ethQuery: any;\n infuraAPIKey: string;\n nonRPCGasFeeApisDisabled?: boolean;\n};\n\n/**\n * Obtains a set of max base and priority fee estimates along with time estimates so that we\n * can present them to users when they are sending transactions or making swaps.\n *\n * @param args - The arguments.\n * @param args.isEIP1559Compatible - Governs whether or not we can use an EIP-1559-only method to\n * produce estimates.\n * @param args.isLegacyGasAPICompatible - Governs whether or not we can use a non-EIP-1559 method to\n * produce estimates (for instance, testnets do not support estimates altogether).\n * @param args.fetchGasEstimates - A function that fetches gas estimates using an EIP-1559-specific\n * API.\n * @param args.fetchGasEstimatesUrl - The URL for the API we can use to obtain EIP-1559-specific\n * estimates.\n * @param args.fetchLegacyGasPriceEstimates - A function that fetches gas estimates using an\n * non-EIP-1559-specific API.\n * @param args.fetchLegacyGasPriceEstimatesUrl - The URL for the API we can use to obtain\n * non-EIP-1559-specific estimates.\n * @param args.fetchEthGasPriceEstimate - A function that fetches gas estimates using\n * `eth_gasPrice`.\n * @param args.calculateTimeEstimate - A function that determine time estimate bounds.\n * @param args.clientId - An identifier that an API can use to know who is asking for estimates.\n * @param args.ethQuery - An EthQuery instance we can use to talk to Ethereum directly.\n * @param args.infuraAPIKey - Infura API key to use for requests to Infura.\n * @param args.nonRPCGasFeeApisDisabled - Whether to disable requests to the legacyAPIEndpoint and the EIP1559APIEndpoint\n * @returns The gas fee calculations.\n */\nexport default async function determineGasFeeCalculations(\n args: DetermineGasFeeCalculationsRequest,\n): Promise {\n try {\n return await getEstimatesUsingFallbacks(args);\n } catch (error) {\n if (error instanceof Error) {\n throw new Error(\n `Gas fee/price estimation failed. Message: ${error.message}`,\n );\n }\n\n throw error;\n }\n}\n\n/**\n * Retrieve the gas fee estimates using a series of fallback mechanisms.\n * @param request - The request object.\n * @returns The gas fee estimates.\n */\nasync function getEstimatesUsingFallbacks(\n request: DetermineGasFeeCalculationsRequest,\n): Promise {\n const {\n isEIP1559Compatible,\n isLegacyGasAPICompatible,\n nonRPCGasFeeApisDisabled,\n } = request;\n\n try {\n if (isEIP1559Compatible && !nonRPCGasFeeApisDisabled) {\n return await getEstimatesUsingFeeMarketEndpoint(request);\n }\n\n if (isLegacyGasAPICompatible && !nonRPCGasFeeApisDisabled) {\n return await getEstimatesUsingLegacyEndpoint(request);\n }\n\n throw new Error('Main gas fee/price estimation failed. Use fallback');\n } catch {\n return await getEstimatesUsingProvider(request);\n }\n}\n\n/**\n * Retrieve gas fee estimates using the EIP-1559 endpoint of the gas API.\n * @param request - The request object.\n * @returns The gas fee estimates.\n */\nasync function getEstimatesUsingFeeMarketEndpoint(\n request: DetermineGasFeeCalculationsRequest,\n): Promise {\n const {\n fetchGasEstimates,\n fetchGasEstimatesUrl,\n infuraAPIKey,\n clientId,\n calculateTimeEstimate,\n } = request;\n\n const estimates = await fetchGasEstimates(\n fetchGasEstimatesUrl,\n infuraAPIKey,\n clientId,\n );\n\n const { suggestedMaxPriorityFeePerGas, suggestedMaxFeePerGas } =\n estimates.medium;\n\n const estimatedGasFeeTimeBounds = calculateTimeEstimate(\n suggestedMaxPriorityFeePerGas,\n suggestedMaxFeePerGas,\n estimates,\n );\n\n return {\n gasFeeEstimates: estimates,\n estimatedGasFeeTimeBounds,\n gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET,\n };\n}\n\n/**\n * Retrieve gas fee estimates using the legacy endpoint of the gas API.\n * @param request - The request object.\n * @returns The gas fee estimates.\n */\nasync function getEstimatesUsingLegacyEndpoint(\n request: DetermineGasFeeCalculationsRequest,\n): Promise {\n const {\n fetchLegacyGasPriceEstimates,\n fetchLegacyGasPriceEstimatesUrl,\n infuraAPIKey,\n clientId,\n } = request;\n\n const estimates = await fetchLegacyGasPriceEstimates(\n fetchLegacyGasPriceEstimatesUrl,\n infuraAPIKey,\n clientId,\n );\n\n return {\n gasFeeEstimates: estimates,\n estimatedGasFeeTimeBounds: {},\n gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,\n };\n}\n\n/**\n * Retrieve gas fee estimates using an `eth_gasPrice` call to the RPC provider.\n * @param request - The request object.\n * @returns The gas fee estimates.\n */\nasync function getEstimatesUsingProvider(\n request: DetermineGasFeeCalculationsRequest,\n): Promise {\n const { ethQuery, fetchEthGasPriceEstimate } = request;\n\n const estimates = await fetchEthGasPriceEstimate(ethQuery);\n\n return {\n gasFeeEstimates: estimates,\n estimatedGasFeeTimeBounds: {},\n gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE,\n };\n}\n"],"mappings":";;;;;;;;;;;;AAKA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,OAAO,cAAc;AAUrB,SAAS,uCAAuC;AAEhD,SAAS,MAAM,cAAc;AAUtB,IAAM,mBAAmB;AA0BzB,IAAM,qBAAqB;AAAA,EAChC,YAAY;AAAA,EACZ,QAAQ;AAAA,EACR,cAAc;AAAA,EACd,MAAM;AACR;AAiGA,IAAM,WAAW;AAAA,EACf,0BAA0B;AAAA,IACxB,SAAS;AAAA,IACT,WAAW;AAAA,EACb;AAAA,EACA,iBAAiB,EAAE,SAAS,MAAM,WAAW,MAAM;AAAA,EACnD,2BAA2B,EAAE,SAAS,MAAM,WAAW,MAAM;AAAA,EAC7D,iBAAiB,EAAE,SAAS,MAAM,WAAW,MAAM;AAAA,EACnD,0BAA0B,EAAE,SAAS,MAAM,WAAW,MAAM;AAC9D;AAqDA,IAAM,OAAO;AA0Bb,IAAM,eAA4B;AAAA,EAChC,0BAA0B,CAAC;AAAA,EAC3B,iBAAiB,CAAC;AAAA,EAClB,2BAA2B,CAAC;AAAA,EAC5B,iBAAiB,mBAAmB;AAAA,EACpC,0BAA0B;AAC5B;AA9PA;AAmQO,IAAM,mBAAN,cAA+B,gCAIpC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgDA,YAAY;AAAA,IACV,WAAW;AAAA,IACX;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,GAYG;AACD,UAAM;AAAA,MACJ;AAAA,MACA;AAAA,MACA;AAAA,MACA,OAAO,EAAE,GAAG,cAAc,GAAG,MAAM;AAAA,IACrC,CAAC;AAuPH,uBAAM;AA5SN;AAsDE,SAAK,gBAAgB;AACrB,SAAK,kBAAkB,QAAQ;AAC/B,SAAK,aAAa,oBAAI,IAAI;AAC1B,SAAK,wCACH;AACF,SAAK,6CACH;AACF,SAAK,wCACH;AACF,uBAAK,cAAe;AACpB,SAAK,qBAAqB,GAAG,gBAAgB;AAC7C,SAAK,oBAAoB,GAAG,gBAAgB;AAC5C,SAAK,WAAW;AAChB,SAAK,eAAe;AAEpB,SAAK,WAAW,IAAI,SAAS,mBAAK,cAAL,UAAmB;AAEhD,QAAI,sBAAsB,YAAY;AACpC,WAAK,iBAAiB,WAAW;AACjC,yBAAmB,OAAO,2BAA2B;AACnD,cAAM,sBAAK,gEAAL,WAAmC;AAAA,MAC3C,CAAC;AAAA,IACH,OAAO;AACL,WAAK,iBAAiB,KAAK,gBAAgB;AAAA,QACzC;AAAA,MACF,EAAE,eAAe;AACjB,WAAK,gBAAgB;AAAA,QACnB;AAAA,QACA,OAAO,2BAA2B;AAChC,gBAAM,sBAAK,gEAAL,WAAmC;AAAA,QAC3C;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,eAAe;AACnB,QAAI,KAAK,WAAW,SAAS,GAAG;AAC9B,YAAM,SAAS,MAAM,KAAK,KAAK,UAAU;AACzC,WAAK,YAAY;AACjB,YAAM,KAAK,kCAAkC,OAAO,CAAC,CAAC;AACtD,aAAO,MAAM,CAAC,EAAE,QAAQ,CAAC,UAAU;AACjC,aAAK,WAAW,IAAI,KAAK;AAAA,MAC3B,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAEA,MAAM,qBAAqB,SAAsC;AAC/D,WAAO,MAAM,KAAK,yBAAyB,OAAO;AAAA,EACpD;AAAA,EAEA,MAAM,kCACJ,WACiB;AACjB,UAAM,aAAa,aAAa,OAAO;AAEvC,SAAK,WAAW,IAAI,UAAU;AAE9B,QAAI,KAAK,WAAW,SAAS,GAAG;AAC9B,YAAM,KAAK,yBAAyB;AACpC,WAAK,MAAM;AAAA,IACb;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,yBACJ,UAAsC,CAAC,GACjB;AACtB,UAAM,EAAE,oBAAoB,MAAM,gBAAgB,IAAI;AAEtD,QAAI,UACF,qBACA,0BACA;AAEF,QAAI,oBAAoB,QAAW;AACjC,YAAM,gBAAgB,KAAK,gBAAgB;AAAA,QACzC;AAAA,QACA;AAAA,MACF;AACA,iCAA2B,cAAc,cAAc,YAAY;AAEnE,uBAAiB,oBAAoB,cAAc,cAAc,OAAO;AAExE,UAAI;AACF,cAAM,SAAS,MAAM,KAAK,gBAAgB;AAAA,UACxC;AAAA,UACA;AAAA,QACF;AACA,8BAAsB,UAAU;AAAA,MAClC,QAAQ;AACN,8BAAsB;AAAA,MACxB;AACA,iBAAW,IAAI,SAAS,cAAc,QAAQ;AAAA,IAChD;AAEA,4BAAa,KAAK;AAElB,4DACE,KAAK,2CAA2C;AAElD,wCAAmB,oBAAoB,KAAK,cAAc;AAE1D,QAAI;AACF,oDAAwB,MAAM,KAAK,wBAAwB;AAAA,IAC7D,SAAS,GAAG;AACV,cAAQ,MAAM,CAAC;AACf,oDAAwB;AAAA,IAC1B;AAEA,UAAM,qBAAqB,MAAM,4BAA4B;AAAA,MAC3D;AAAA,MACA;AAAA,MACA;AAAA,MACA,sBAAsB,KAAK,mBAAmB;AAAA,QAC5C;AAAA,QACA,GAAG,cAAc;AAAA,MACnB;AAAA,MACA;AAAA,MACA,iCAAiC,KAAK,kBAAkB;AAAA,QACtD;AAAA,QACA,GAAG,cAAc;AAAA,MACnB;AAAA,MACA;AAAA,MACA;AAAA,MACA,UAAU,KAAK;AAAA,MACf;AAAA,MACA,cAAc,KAAK;AAAA,MACnB,0BAA0B,KAAK,MAAM;AAAA,IACvC,CAAC;AAED,QAAI,mBAAmB;AACrB,YAAM,UAAU,MAAM,cAAc;AACpC,WAAK,OAAO,CAAC,UAAU;AACrB,YAAI,KAAK,mBAAmB,SAAS;AACnC,gBAAM,kBAAkB,mBAAmB;AAC3C,gBAAM,4BACJ,mBAAmB;AACrB,gBAAM,kBAAkB,mBAAmB;AAAA,QAC7C;AACA,cAAM,6BAAN,MAAM,2BAA6B,CAAC;AACpC,cAAM,yBAAyB,OAAO,IAAI;AAAA,UACxC,iBAAiB,mBAAmB;AAAA,UACpC,2BACE,mBAAmB;AAAA,UACrB,iBAAiB,mBAAmB;AAAA,QACtC;AAAA,MACF,CAAC;AAAA,IACH;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,iBAAiB,WAAmB;AAClC,SAAK,WAAW,OAAO,SAAS;AAChC,QAAI,KAAK,WAAW,SAAS,GAAG;AAC9B,WAAK,YAAY;AAAA,IACnB;AAAA,EACF;AAAA,EAEA,cAAc;AACZ,QAAI,KAAK,YAAY;AACnB,oBAAc,KAAK,UAAU;AAAA,IAC/B;AACA,SAAK,WAAW,MAAM;AACtB,SAAK,WAAW;AAAA,EAClB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOS,UAAU;AACjB,UAAM,QAAQ;AACd,SAAK,YAAY;AAAA,EACnB;AAAA,EAEQ,QAAQ;AACd,QAAI,KAAK,YAAY;AACnB,oBAAc,KAAK,UAAU;AAAA,IAC/B;AAEA,SAAK,aAAa,YAAY,YAAY;AACxC,YAAM,cAAc,MAAM,KAAK,yBAAyB,CAAC;AAAA,IAC3D,GAAG,KAAK,aAAa;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,aAAa,iBAAwC;AACzD,UAAM,KAAK,yBAAyB,EAAE,gBAAgB,CAAC;AAAA,EACzD;AAAA,EAEQ,aAAa;AACnB,SAAK,OAAO,MAAM;AAChB,aAAO;AAAA,IACT,CAAC;AAAA,EACH;AAAA,EAEA,MAAc,0BAA0B;AACtC,UAAM,oCACJ,MAAM,KAAK,sCAAsC;AACnD,UAAM,oCACJ,KAAK,wCAAwC,KAAK;AAEpD,WACE,qCAAqC;AAAA,EAEzC;AAAA,EAEA,gBACE,sBACA,cACmD;AACnD,QACE,CAAC,KAAK,MAAM,mBACZ,KAAK,MAAM,oBAAoB,mBAAmB,YAClD;AACA,aAAO,CAAC;AAAA,IACV;AACA,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA,KAAK,MAAM;AAAA,IACb;AAAA,EACF;AAAA,EAaA,yBAAyB;AACvB,SAAK,OAAO,CAAC,UAAU;AACrB,YAAM,2BAA2B;AAAA,IACnC,CAAC;AAAA,EACH;AAAA,EAEA,0BAA0B;AACxB,SAAK,OAAO,CAAC,UAAU;AACrB,YAAM,2BAA2B;AAAA,IACnC,CAAC;AAAA,EACH;AACF;AAlUE;AA4SM;AAAA,kCAA6B,eAAC,wBAAsC;AACxE,QAAM,aAAa,uBAAuB,eAAe;AAEzD,MAAI,eAAe,KAAK,gBAAgB;AACtC,SAAK,WAAW,IAAI,SAAS,mBAAK,cAAL,UAAmB;AAChD,UAAM,KAAK,aAAa;AAExB,SAAK,iBAAiB;AAAA,EACxB;AACF;AAeF,IAAO,2BAAQ;;;ACliBf,eAAO,4BACL,MAC6B;AAC7B,MAAI;AACF,WAAO,MAAM,2BAA2B,IAAI;AAAA,EAC9C,SAAS,OAAO;AACd,QAAI,iBAAiB,OAAO;AAC1B,YAAM,IAAI;AAAA,QACR,6CAA6C,MAAM,OAAO;AAAA,MAC5D;AAAA,IACF;AAEA,UAAM;AAAA,EACR;AACF;AAOA,eAAe,2BACb,SAC6B;AAC7B,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AAEJ,MAAI;AACF,QAAI,uBAAuB,CAAC,0BAA0B;AACpD,aAAO,MAAM,mCAAmC,OAAO;AAAA,IACzD;AAEA,QAAI,4BAA4B,CAAC,0BAA0B;AACzD,aAAO,MAAM,gCAAgC,OAAO;AAAA,IACtD;AAEA,UAAM,IAAI,MAAM,oDAAoD;AAAA,EACtE,QAAQ;AACN,WAAO,MAAM,0BAA0B,OAAO;AAAA,EAChD;AACF;AAOA,eAAe,mCACb,SAC6B;AAC7B,QAAM;AAAA,IACJ,mBAAAA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,uBAAAC;AAAA,EACF,IAAI;AAEJ,QAAM,YAAY,MAAMD;AAAA,IACtB;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,QAAM,EAAE,+BAA+B,sBAAsB,IAC3D,UAAU;AAEZ,QAAM,4BAA4BC;AAAA,IAChC;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,SAAO;AAAA,IACL,iBAAiB;AAAA,IACjB;AAAA,IACA,iBAAiB,mBAAmB;AAAA,EACtC;AACF;AAOA,eAAe,gCACb,SAC6B;AAC7B,QAAM;AAAA,IACJ,8BAAAC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AAEJ,QAAM,YAAY,MAAMA;AAAA,IACtB;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,SAAO;AAAA,IACL,iBAAiB;AAAA,IACjB,2BAA2B,CAAC;AAAA,IAC5B,iBAAiB,mBAAmB;AAAA,EACtC;AACF;AAOA,eAAe,0BACb,SAC6B;AAC7B,QAAM,EAAE,UAAU,0BAAAC,0BAAyB,IAAI;AAE/C,QAAM,YAAY,MAAMA,0BAAyB,QAAQ;AAEzD,SAAO;AAAA,IACL,iBAAiB;AAAA,IACjB,2BAA2B,CAAC;AAAA,IAC5B,iBAAiB,mBAAmB;AAAA,EACtC;AACF;","names":["fetchGasEstimates","calculateTimeEstimate","fetchLegacyGasPriceEstimates","fetchEthGasPriceEstimate"]} -\ No newline at end of file -+{"version":3,"sources":["../src/GasFeeController.ts","../src/determineGasFeeCalculations.ts"],"sourcesContent":["import type {\n ControllerGetStateAction,\n ControllerStateChangeEvent,\n RestrictedControllerMessenger,\n} from '@metamask/base-controller';\nimport {\n convertHexToDecimal,\n safelyExecute,\n toHex,\n} from '@metamask/controller-utils';\nimport EthQuery from '@metamask/eth-query';\nimport type {\n NetworkClientId,\n NetworkControllerGetEIP1559CompatibilityAction,\n NetworkControllerGetNetworkClientByIdAction,\n NetworkControllerGetStateAction,\n NetworkControllerNetworkDidChangeEvent,\n NetworkState,\n ProviderProxy,\n} from '@metamask/network-controller';\nimport { StaticIntervalPollingController } from '@metamask/polling-controller';\nimport type { Hex } from '@metamask/utils';\nimport { v1 as random } from 'uuid';\n\nimport determineGasFeeCalculations from './determineGasFeeCalculations';\nimport {\n calculateTimeEstimate,\n fetchGasEstimates,\n fetchLegacyGasPriceEstimates,\n fetchEthGasPriceEstimate,\n} from './gas-util';\n\nexport const LEGACY_GAS_PRICES_API_URL = 'https://gas.api.infura.io';\n\nexport type unknownString = 'unknown';\n\n// Fee Market describes the way gas is set after the london hardfork, and was\n// defined by EIP-1559.\nexport type FeeMarketEstimateType = 'fee-market';\n// Legacy describes gasPrice estimates from before london hardfork, when the\n// user is connected to mainnet and are presented with fast/average/slow\n// estimate levels to choose from.\nexport type LegacyEstimateType = 'legacy';\n// EthGasPrice describes a gasPrice estimate received from eth_gasPrice. Post\n// london this value should only be used for legacy type transactions when on\n// networks that support EIP-1559. This type of estimate is the most accurate\n// to display on custom networks that don't support EIP-1559.\nexport type EthGasPriceEstimateType = 'eth_gasPrice';\n// NoEstimate describes the state of the controller before receiving its first\n// estimate.\nexport type NoEstimateType = 'none';\n\n/**\n * Indicates which type of gasEstimate the controller is currently returning.\n * This is useful as a way of asserting that the shape of gasEstimates matches\n * expectations. NONE is a special case indicating that no previous gasEstimate\n * has been fetched.\n */\nexport const GAS_ESTIMATE_TYPES = {\n FEE_MARKET: 'fee-market' as FeeMarketEstimateType,\n LEGACY: 'legacy' as LegacyEstimateType,\n ETH_GASPRICE: 'eth_gasPrice' as EthGasPriceEstimateType,\n NONE: 'none' as NoEstimateType,\n};\n\nexport type GasEstimateType =\n | FeeMarketEstimateType\n | EthGasPriceEstimateType\n | LegacyEstimateType\n | NoEstimateType;\n\nexport type EstimatedGasFeeTimeBounds = {\n lowerTimeBound: number | null;\n upperTimeBound: number | unknownString;\n};\n\n/**\n * @type EthGasPriceEstimate\n *\n * A single gas price estimate for networks and accounts that don't support EIP-1559\n * This estimate comes from eth_gasPrice but is converted to dec gwei to match other\n * return values\n * @property gasPrice - A GWEI dec string\n */\n\nexport type EthGasPriceEstimate = {\n gasPrice: string;\n};\n\n/**\n * @type LegacyGasPriceEstimate\n *\n * A set of gas price estimates for networks and accounts that don't support EIP-1559\n * These estimates include low, medium and high all as strings representing gwei in\n * decimal format.\n * @property high - gasPrice, in decimal gwei string format, suggested for fast inclusion\n * @property medium - gasPrice, in decimal gwei string format, suggested for avg inclusion\n * @property low - gasPrice, in decimal gwei string format, suggested for slow inclusion\n */\nexport type LegacyGasPriceEstimate = {\n high: string;\n medium: string;\n low: string;\n};\n\n/**\n * @type Eip1559GasFee\n *\n * Data necessary to provide an estimate of a gas fee with a specific tip\n * @property minWaitTimeEstimate - The fastest the transaction will take, in milliseconds\n * @property maxWaitTimeEstimate - The slowest the transaction will take, in milliseconds\n * @property suggestedMaxPriorityFeePerGas - A suggested \"tip\", a GWEI hex number\n * @property suggestedMaxFeePerGas - A suggested max fee, the most a user will pay. a GWEI hex number\n */\nexport type Eip1559GasFee = {\n minWaitTimeEstimate: number; // a time duration in milliseconds\n maxWaitTimeEstimate: number; // a time duration in milliseconds\n suggestedMaxPriorityFeePerGas: string; // a GWEI decimal number\n suggestedMaxFeePerGas: string; // a GWEI decimal number\n};\n\n/**\n * @type GasFeeEstimates\n *\n * Data necessary to provide multiple GasFee estimates, and supporting information, to the user\n * @property low - A GasFee for a minimum necessary combination of tip and maxFee\n * @property medium - A GasFee for a recommended combination of tip and maxFee\n * @property high - A GasFee for a high combination of tip and maxFee\n * @property estimatedBaseFee - An estimate of what the base fee will be for the pending/next block. A GWEI dec number\n * @property networkCongestion - A normalized number that can be used to gauge the congestion\n * level of the network, with 0 meaning not congested and 1 meaning extremely congested\n */\nexport type GasFeeEstimates = SourcedGasFeeEstimates | FallbackGasFeeEstimates;\n\ntype SourcedGasFeeEstimates = {\n low: Eip1559GasFee;\n medium: Eip1559GasFee;\n high: Eip1559GasFee;\n estimatedBaseFee: string;\n historicalBaseFeeRange: [string, string];\n baseFeeTrend: 'up' | 'down' | 'level';\n latestPriorityFeeRange: [string, string];\n historicalPriorityFeeRange: [string, string];\n priorityFeeTrend: 'up' | 'down' | 'level';\n networkCongestion: number;\n};\n\ntype FallbackGasFeeEstimates = {\n low: Eip1559GasFee;\n medium: Eip1559GasFee;\n high: Eip1559GasFee;\n estimatedBaseFee: string;\n historicalBaseFeeRange: null;\n baseFeeTrend: null;\n latestPriorityFeeRange: null;\n historicalPriorityFeeRange: null;\n priorityFeeTrend: null;\n networkCongestion: null;\n};\n\nconst metadata = {\n gasFeeEstimatesByChainId: {\n persist: true,\n anonymous: false,\n },\n gasFeeEstimates: { persist: true, anonymous: false },\n estimatedGasFeeTimeBounds: { persist: true, anonymous: false },\n gasEstimateType: { persist: true, anonymous: false },\n nonRPCGasFeeApisDisabled: { persist: true, anonymous: false },\n};\n\nexport type GasFeeStateEthGasPrice = {\n gasFeeEstimates: EthGasPriceEstimate;\n estimatedGasFeeTimeBounds: Record;\n gasEstimateType: EthGasPriceEstimateType;\n};\n\nexport type GasFeeStateFeeMarket = {\n gasFeeEstimates: GasFeeEstimates;\n estimatedGasFeeTimeBounds: EstimatedGasFeeTimeBounds | Record;\n gasEstimateType: FeeMarketEstimateType;\n};\n\nexport type GasFeeStateLegacy = {\n gasFeeEstimates: LegacyGasPriceEstimate;\n estimatedGasFeeTimeBounds: Record;\n gasEstimateType: LegacyEstimateType;\n};\n\nexport type GasFeeStateNoEstimates = {\n gasFeeEstimates: Record;\n estimatedGasFeeTimeBounds: Record;\n gasEstimateType: NoEstimateType;\n};\n\nexport type FetchGasFeeEstimateOptions = {\n shouldUpdateState?: boolean;\n networkClientId?: NetworkClientId;\n};\n\n/**\n * @type GasFeeState\n *\n * Gas Fee controller state\n * @property gasFeeEstimates - Gas fee estimate data based on new EIP-1559 properties\n * @property estimatedGasFeeTimeBounds - Estimates representing the minimum and maximum\n */\nexport type SingleChainGasFeeState =\n | GasFeeStateEthGasPrice\n | GasFeeStateFeeMarket\n | GasFeeStateLegacy\n | GasFeeStateNoEstimates;\n\nexport type GasFeeEstimatesByChainId = {\n gasFeeEstimatesByChainId?: Record;\n};\n\nexport type GasFeeState = GasFeeEstimatesByChainId &\n SingleChainGasFeeState & {\n nonRPCGasFeeApisDisabled?: boolean;\n };\n\nconst name = 'GasFeeController';\n\nexport type GasFeeStateChange = ControllerStateChangeEvent<\n typeof name,\n GasFeeState\n>;\n\nexport type GetGasFeeState = ControllerGetStateAction;\n\nexport type GasFeeControllerActions = GetGasFeeState;\n\nexport type GasFeeControllerEvents = GasFeeStateChange;\n\ntype AllowedActions =\n | NetworkControllerGetStateAction\n | NetworkControllerGetNetworkClientByIdAction\n | NetworkControllerGetEIP1559CompatibilityAction;\n\ntype GasFeeMessenger = RestrictedControllerMessenger<\n typeof name,\n GasFeeControllerActions | AllowedActions,\n GasFeeControllerEvents | NetworkControllerNetworkDidChangeEvent,\n AllowedActions['type'],\n NetworkControllerNetworkDidChangeEvent['type']\n>;\n\nconst defaultState: GasFeeState = {\n gasFeeEstimatesByChainId: {},\n gasFeeEstimates: {},\n estimatedGasFeeTimeBounds: {},\n gasEstimateType: GAS_ESTIMATE_TYPES.NONE,\n nonRPCGasFeeApisDisabled: false,\n};\n\n/**\n * Controller that retrieves gas fee estimate data and polls for updated data on a set interval\n */\nexport class GasFeeController extends StaticIntervalPollingController<\n typeof name,\n GasFeeState,\n GasFeeMessenger\n> {\n private intervalId?: ReturnType;\n\n private readonly intervalDelay;\n\n private readonly pollTokens: Set;\n\n private readonly legacyAPIEndpoint: string;\n\n private readonly EIP1559APIEndpoint: string;\n\n private readonly getCurrentNetworkEIP1559Compatibility;\n\n private readonly getCurrentNetworkLegacyGasAPICompatibility;\n\n private readonly getCurrentAccountEIP1559Compatibility;\n\n private readonly infuraAPIKey: string;\n\n private currentChainId;\n\n private ethQuery?: EthQuery;\n\n private readonly clientId?: string;\n\n #getProvider: () => ProviderProxy;\n\n /**\n * Creates a GasFeeController instance.\n *\n * @param options - The controller options.\n * @param options.interval - The time in milliseconds to wait between polls.\n * @param options.messenger - The controller messenger.\n * @param options.state - The initial state.\n * @param options.getCurrentNetworkEIP1559Compatibility - Determines whether or not the current\n * network is EIP-1559 compatible.\n * @param options.getCurrentNetworkLegacyGasAPICompatibility - Determines whether or not the\n * current network is compatible with the legacy gas price API.\n * @param options.getCurrentAccountEIP1559Compatibility - Determines whether or not the current\n * account is EIP-1559 compatible.\n * @param options.getChainId - Returns the current chain ID.\n * @param options.getProvider - Returns a network provider for the current network.\n * @param options.onNetworkDidChange - A function for registering an event handler for the\n * network state change event.\n * @param options.clientId - The client ID used to identify to the gas estimation API who is\n * asking for estimates.\n * @param options.infuraAPIKey - The Infura API key used for infura API requests.\n */\n constructor({\n interval = 15000,\n messenger,\n state,\n getCurrentNetworkEIP1559Compatibility,\n getCurrentAccountEIP1559Compatibility,\n getChainId,\n getCurrentNetworkLegacyGasAPICompatibility,\n getProvider,\n onNetworkDidChange,\n clientId,\n infuraAPIKey,\n }: {\n interval?: number;\n messenger: GasFeeMessenger;\n state?: GasFeeState;\n getCurrentNetworkEIP1559Compatibility: () => Promise;\n getCurrentNetworkLegacyGasAPICompatibility: () => boolean;\n getCurrentAccountEIP1559Compatibility?: () => boolean;\n getChainId?: () => Hex;\n getProvider: () => ProviderProxy;\n onNetworkDidChange?: (listener: (state: NetworkState) => void) => void;\n clientId?: string;\n infuraAPIKey: string;\n }) {\n super({\n name,\n metadata,\n messenger,\n state: { ...defaultState, ...state },\n });\n this.intervalDelay = interval;\n this.setIntervalLength(interval);\n this.pollTokens = new Set();\n this.getCurrentNetworkEIP1559Compatibility =\n getCurrentNetworkEIP1559Compatibility;\n this.getCurrentNetworkLegacyGasAPICompatibility =\n getCurrentNetworkLegacyGasAPICompatibility;\n this.getCurrentAccountEIP1559Compatibility =\n getCurrentAccountEIP1559Compatibility;\n this.#getProvider = getProvider;\n this.EIP1559APIEndpoint = `${LEGACY_GAS_PRICES_API_URL}/networks//suggestedGasFees`;\n this.legacyAPIEndpoint = `${LEGACY_GAS_PRICES_API_URL}/networks//gasPrices`;\n this.clientId = clientId;\n this.infuraAPIKey = infuraAPIKey;\n\n this.ethQuery = new EthQuery(this.#getProvider());\n\n if (onNetworkDidChange && getChainId) {\n this.currentChainId = getChainId();\n onNetworkDidChange(async (networkControllerState) => {\n await this.#onNetworkControllerDidChange(networkControllerState);\n });\n } else {\n this.currentChainId = this.messagingSystem.call(\n 'NetworkController:getState',\n ).providerConfig.chainId;\n this.messagingSystem.subscribe(\n 'NetworkController:networkDidChange',\n async (networkControllerState) => {\n await this.#onNetworkControllerDidChange(networkControllerState);\n },\n );\n }\n }\n\n async resetPolling() {\n if (this.pollTokens.size !== 0) {\n const tokens = Array.from(this.pollTokens);\n this.stopPolling();\n await this.getGasFeeEstimatesAndStartPolling(tokens[0]);\n tokens.slice(1).forEach((token) => {\n this.pollTokens.add(token);\n });\n }\n }\n\n async fetchGasFeeEstimates(options?: FetchGasFeeEstimateOptions) {\n return await this._fetchGasFeeEstimateData(options);\n }\n\n async getGasFeeEstimatesAndStartPolling(\n pollToken: string | undefined,\n ): Promise {\n const _pollToken = pollToken || random();\n\n this.pollTokens.add(_pollToken);\n\n if (this.pollTokens.size === 1) {\n await this._fetchGasFeeEstimateData();\n this._poll();\n }\n\n return _pollToken;\n }\n\n /**\n * Gets and sets gasFeeEstimates in state.\n *\n * @param options - The gas fee estimate options.\n * @param options.shouldUpdateState - Determines whether the state should be updated with the\n * updated gas estimates.\n * @returns The gas fee estimates.\n */\n async _fetchGasFeeEstimateData(\n options: FetchGasFeeEstimateOptions = {},\n ): Promise {\n const { shouldUpdateState = true, networkClientId } = options;\n\n let ethQuery,\n isEIP1559Compatible,\n isLegacyGasAPICompatible,\n decimalChainId: number;\n\n if (networkClientId !== undefined) {\n const networkClient = this.messagingSystem.call(\n 'NetworkController:getNetworkClientById',\n networkClientId,\n );\n isLegacyGasAPICompatible = networkClient.configuration.chainId === '0x38';\n\n decimalChainId = convertHexToDecimal(networkClient.configuration.chainId);\n\n try {\n const result = await this.messagingSystem.call(\n 'NetworkController:getEIP1559Compatibility',\n networkClientId,\n );\n isEIP1559Compatible = result || false;\n } catch {\n isEIP1559Compatible = false;\n }\n ethQuery = new EthQuery(networkClient.provider);\n }\n\n ethQuery ??= this.ethQuery;\n\n isLegacyGasAPICompatible ??=\n this.getCurrentNetworkLegacyGasAPICompatibility();\n\n decimalChainId ??= convertHexToDecimal(this.currentChainId);\n\n try {\n isEIP1559Compatible ??= await this.getEIP1559Compatibility();\n } catch (e) {\n console.error(e);\n isEIP1559Compatible ??= false;\n }\n\n const gasFeeCalculations = await determineGasFeeCalculations({\n isEIP1559Compatible,\n isLegacyGasAPICompatible,\n fetchGasEstimates,\n fetchGasEstimatesUrl: this.EIP1559APIEndpoint.replace(\n '',\n `${decimalChainId}`,\n ),\n fetchLegacyGasPriceEstimates,\n fetchLegacyGasPriceEstimatesUrl: this.legacyAPIEndpoint.replace(\n '',\n `${decimalChainId}`,\n ),\n fetchEthGasPriceEstimate,\n calculateTimeEstimate,\n clientId: this.clientId,\n ethQuery,\n infuraAPIKey: this.infuraAPIKey,\n nonRPCGasFeeApisDisabled: this.state.nonRPCGasFeeApisDisabled,\n });\n\n if (shouldUpdateState) {\n const chainId = toHex(decimalChainId);\n this.update((state) => {\n if (this.currentChainId === chainId) {\n state.gasFeeEstimates = gasFeeCalculations.gasFeeEstimates;\n state.estimatedGasFeeTimeBounds =\n gasFeeCalculations.estimatedGasFeeTimeBounds;\n state.gasEstimateType = gasFeeCalculations.gasEstimateType;\n }\n state.gasFeeEstimatesByChainId ??= {};\n state.gasFeeEstimatesByChainId[chainId] = {\n gasFeeEstimates: gasFeeCalculations.gasFeeEstimates,\n estimatedGasFeeTimeBounds:\n gasFeeCalculations.estimatedGasFeeTimeBounds,\n gasEstimateType: gasFeeCalculations.gasEstimateType,\n } as SingleChainGasFeeState;\n });\n }\n\n return gasFeeCalculations;\n }\n\n /**\n * Remove the poll token, and stop polling if the set of poll tokens is empty.\n *\n * @param pollToken - The poll token to disconnect.\n */\n disconnectPoller(pollToken: string) {\n this.pollTokens.delete(pollToken);\n if (this.pollTokens.size === 0) {\n this.stopPolling();\n }\n }\n\n stopPolling() {\n if (this.intervalId) {\n clearInterval(this.intervalId);\n }\n this.pollTokens.clear();\n this.resetState();\n }\n\n /**\n * Prepare to discard this controller.\n *\n * This stops any active polling.\n */\n override destroy() {\n super.destroy();\n this.stopPolling();\n }\n\n private _poll() {\n if (this.intervalId) {\n clearInterval(this.intervalId);\n }\n\n this.intervalId = setInterval(async () => {\n await safelyExecute(() => this._fetchGasFeeEstimateData());\n }, this.intervalDelay);\n }\n\n /**\n * Fetching token list from the Token Service API.\n *\n * @private\n * @param networkClientId - The ID of the network client triggering the fetch.\n * @returns A promise that resolves when this operation completes.\n */\n async _executePoll(networkClientId: string): Promise {\n await this._fetchGasFeeEstimateData({ networkClientId });\n }\n\n private resetState() {\n this.update(() => {\n return defaultState;\n });\n }\n\n private async getEIP1559Compatibility() {\n const currentNetworkIsEIP1559Compatible =\n await this.getCurrentNetworkEIP1559Compatibility();\n const currentAccountIsEIP1559Compatible =\n this.getCurrentAccountEIP1559Compatibility?.() ?? true;\n\n return (\n currentNetworkIsEIP1559Compatible && currentAccountIsEIP1559Compatible\n );\n }\n\n getTimeEstimate(\n maxPriorityFeePerGas: string,\n maxFeePerGas: string,\n ): EstimatedGasFeeTimeBounds | Record {\n if (\n !this.state.gasFeeEstimates ||\n this.state.gasEstimateType !== GAS_ESTIMATE_TYPES.FEE_MARKET\n ) {\n return {};\n }\n return calculateTimeEstimate(\n maxPriorityFeePerGas,\n maxFeePerGas,\n this.state.gasFeeEstimates,\n );\n }\n\n async #onNetworkControllerDidChange(networkControllerState: NetworkState) {\n const newChainId = networkControllerState.providerConfig.chainId;\n\n if (newChainId !== this.currentChainId) {\n this.ethQuery = new EthQuery(this.#getProvider());\n await this.resetPolling();\n\n this.currentChainId = newChainId;\n }\n }\n\n enableNonRPCGasFeeApis() {\n this.update((state) => {\n state.nonRPCGasFeeApisDisabled = false;\n });\n }\n\n disableNonRPCGasFeeApis() {\n this.update((state) => {\n state.nonRPCGasFeeApisDisabled = true;\n });\n }\n}\n\nexport default GasFeeController;\n","import type {\n EstimatedGasFeeTimeBounds,\n EthGasPriceEstimate,\n GasFeeEstimates,\n GasFeeState as GasFeeCalculations,\n LegacyGasPriceEstimate,\n} from './GasFeeController';\nimport { GAS_ESTIMATE_TYPES } from './GasFeeController';\n\ntype DetermineGasFeeCalculationsRequest = {\n isEIP1559Compatible: boolean;\n isLegacyGasAPICompatible: boolean;\n fetchGasEstimates: (\n url: string,\n infuraAPIKey: string,\n clientId?: string,\n ) => Promise;\n fetchGasEstimatesUrl: string;\n fetchLegacyGasPriceEstimates: (\n url: string,\n infuraAPIKey: string,\n clientId?: string,\n ) => Promise;\n fetchLegacyGasPriceEstimatesUrl: string;\n // TODO: Replace `any` with type\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n fetchEthGasPriceEstimate: (ethQuery: any) => Promise;\n calculateTimeEstimate: (\n maxPriorityFeePerGas: string,\n maxFeePerGas: string,\n gasFeeEstimates: GasFeeEstimates,\n ) => EstimatedGasFeeTimeBounds;\n clientId: string | undefined;\n // TODO: Replace `any` with type\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n ethQuery: any;\n infuraAPIKey: string;\n nonRPCGasFeeApisDisabled?: boolean;\n};\n\n/**\n * Obtains a set of max base and priority fee estimates along with time estimates so that we\n * can present them to users when they are sending transactions or making swaps.\n *\n * @param args - The arguments.\n * @param args.isEIP1559Compatible - Governs whether or not we can use an EIP-1559-only method to\n * produce estimates.\n * @param args.isLegacyGasAPICompatible - Governs whether or not we can use a non-EIP-1559 method to\n * produce estimates (for instance, testnets do not support estimates altogether).\n * @param args.fetchGasEstimates - A function that fetches gas estimates using an EIP-1559-specific\n * API.\n * @param args.fetchGasEstimatesUrl - The URL for the API we can use to obtain EIP-1559-specific\n * estimates.\n * @param args.fetchLegacyGasPriceEstimates - A function that fetches gas estimates using an\n * non-EIP-1559-specific API.\n * @param args.fetchLegacyGasPriceEstimatesUrl - The URL for the API we can use to obtain\n * non-EIP-1559-specific estimates.\n * @param args.fetchEthGasPriceEstimate - A function that fetches gas estimates using\n * `eth_gasPrice`.\n * @param args.calculateTimeEstimate - A function that determine time estimate bounds.\n * @param args.clientId - An identifier that an API can use to know who is asking for estimates.\n * @param args.ethQuery - An EthQuery instance we can use to talk to Ethereum directly.\n * @param args.infuraAPIKey - Infura API key to use for requests to Infura.\n * @param args.nonRPCGasFeeApisDisabled - Whether to disable requests to the legacyAPIEndpoint and the EIP1559APIEndpoint\n * @returns The gas fee calculations.\n */\nexport default async function determineGasFeeCalculations(\n args: DetermineGasFeeCalculationsRequest,\n): Promise {\n try {\n return await getEstimatesUsingFallbacks(args);\n } catch (error) {\n if (error instanceof Error) {\n throw new Error(\n `Gas fee/price estimation failed. Message: ${error.message}`,\n );\n }\n\n throw error;\n }\n}\n\n/**\n * Retrieve the gas fee estimates using a series of fallback mechanisms.\n * @param request - The request object.\n * @returns The gas fee estimates.\n */\nasync function getEstimatesUsingFallbacks(\n request: DetermineGasFeeCalculationsRequest,\n): Promise {\n const {\n isEIP1559Compatible,\n isLegacyGasAPICompatible,\n nonRPCGasFeeApisDisabled,\n } = request;\n\n try {\n if (isEIP1559Compatible && !nonRPCGasFeeApisDisabled) {\n return await getEstimatesUsingFeeMarketEndpoint(request);\n }\n\n if (isLegacyGasAPICompatible && !nonRPCGasFeeApisDisabled) {\n return await getEstimatesUsingLegacyEndpoint(request);\n }\n\n throw new Error('Main gas fee/price estimation failed. Use fallback');\n } catch {\n return await getEstimatesUsingProvider(request);\n }\n}\n\n/**\n * Retrieve gas fee estimates using the EIP-1559 endpoint of the gas API.\n * @param request - The request object.\n * @returns The gas fee estimates.\n */\nasync function getEstimatesUsingFeeMarketEndpoint(\n request: DetermineGasFeeCalculationsRequest,\n): Promise {\n const {\n fetchGasEstimates,\n fetchGasEstimatesUrl,\n infuraAPIKey,\n clientId,\n calculateTimeEstimate,\n } = request;\n\n const estimates = await fetchGasEstimates(\n fetchGasEstimatesUrl,\n infuraAPIKey,\n clientId,\n );\n\n const { suggestedMaxPriorityFeePerGas, suggestedMaxFeePerGas } =\n estimates.medium;\n\n const estimatedGasFeeTimeBounds = calculateTimeEstimate(\n suggestedMaxPriorityFeePerGas,\n suggestedMaxFeePerGas,\n estimates,\n );\n\n return {\n gasFeeEstimates: estimates,\n estimatedGasFeeTimeBounds,\n gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET,\n };\n}\n\n/**\n * Retrieve gas fee estimates using the legacy endpoint of the gas API.\n * @param request - The request object.\n * @returns The gas fee estimates.\n */\nasync function getEstimatesUsingLegacyEndpoint(\n request: DetermineGasFeeCalculationsRequest,\n): Promise {\n const {\n fetchLegacyGasPriceEstimates,\n fetchLegacyGasPriceEstimatesUrl,\n infuraAPIKey,\n clientId,\n } = request;\n\n const estimates = await fetchLegacyGasPriceEstimates(\n fetchLegacyGasPriceEstimatesUrl,\n infuraAPIKey,\n clientId,\n );\n\n return {\n gasFeeEstimates: estimates,\n estimatedGasFeeTimeBounds: {},\n gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,\n };\n}\n\n/**\n * Retrieve gas fee estimates using an `eth_gasPrice` call to the RPC provider.\n * @param request - The request object.\n * @returns The gas fee estimates.\n */\nasync function getEstimatesUsingProvider(\n request: DetermineGasFeeCalculationsRequest,\n): Promise {\n const { ethQuery, fetchEthGasPriceEstimate } = request;\n\n const estimates = await fetchEthGasPriceEstimate(ethQuery);\n\n return {\n gasFeeEstimates: estimates,\n estimatedGasFeeTimeBounds: {},\n gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE,\n };\n}\n"],"mappings":";;;;;;;;;;;;AAKA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,OAAO,cAAc;AAUrB,SAAS,uCAAuC;AAEhD,SAAS,MAAM,cAAc;AAUtB,IAAM,mBAAmB;AA0BzB,IAAM,qBAAqB;AAAA,EAChC,YAAY;AAAA,EACZ,QAAQ;AAAA,EACR,cAAc;AAAA,EACd,MAAM;AACR;AAiGA,IAAM,WAAW;AAAA,EACf,0BAA0B;AAAA,IACxB,SAAS;AAAA,IACT,WAAW;AAAA,EACb;AAAA,EACA,iBAAiB,EAAE,SAAS,MAAM,WAAW,MAAM;AAAA,EACnD,2BAA2B,EAAE,SAAS,MAAM,WAAW,MAAM;AAAA,EAC7D,iBAAiB,EAAE,SAAS,MAAM,WAAW,MAAM;AAAA,EACnD,0BAA0B,EAAE,SAAS,MAAM,WAAW,MAAM;AAC9D;AAqDA,IAAM,OAAO;AA0Bb,IAAM,eAA4B;AAAA,EAChC,0BAA0B,CAAC;AAAA,EAC3B,iBAAiB,CAAC;AAAA,EAClB,2BAA2B,CAAC;AAAA,EAC5B,iBAAiB,mBAAmB;AAAA,EACpC,0BAA0B;AAC5B;AA9PA;AAmQO,IAAM,mBAAN,cAA+B,gCAIpC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgDA,YAAY;AAAA,IACV,WAAW;AAAA,IACX;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,GAYG;AACD,UAAM;AAAA,MACJ;AAAA,MACA;AAAA,MACA;AAAA,MACA,OAAO,EAAE,GAAG,cAAc,GAAG,MAAM;AAAA,IACrC,CAAC;AAuPH,uBAAM;AA5SN;AAsDE,SAAK,gBAAgB;AACrB,SAAK,kBAAkB,QAAQ;AAC/B,SAAK,aAAa,oBAAI,IAAI;AAC1B,SAAK,wCACH;AACF,SAAK,6CACH;AACF,SAAK,wCACH;AACF,uBAAK,cAAe;AACpB,SAAK,qBAAqB,GAAG,gBAAgB;AAC7C,SAAK,oBAAoB,GAAG,gBAAgB;AAC5C,SAAK,WAAW;AAChB,SAAK,eAAe;AAEpB,SAAK,WAAW,IAAI,SAAS,mBAAK,cAAL,UAAmB;AAEhD,QAAI,sBAAsB,YAAY;AACpC,WAAK,iBAAiB,WAAW;AACjC,yBAAmB,OAAO,2BAA2B;AACnD,cAAM,sBAAK,gEAAL,WAAmC;AAAA,MAC3C,CAAC;AAAA,IACH,OAAO;AACL,WAAK,iBAAiB,KAAK,gBAAgB;AAAA,QACzC;AAAA,MACF,EAAE,eAAe;AACjB,WAAK,gBAAgB;AAAA,QACnB;AAAA,QACA,OAAO,2BAA2B;AAChC,gBAAM,sBAAK,gEAAL,WAAmC;AAAA,QAC3C;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,eAAe;AACnB,QAAI,KAAK,WAAW,SAAS,GAAG;AAC9B,YAAM,SAAS,MAAM,KAAK,KAAK,UAAU;AACzC,WAAK,YAAY;AACjB,YAAM,KAAK,kCAAkC,OAAO,CAAC,CAAC;AACtD,aAAO,MAAM,CAAC,EAAE,QAAQ,CAAC,UAAU;AACjC,aAAK,WAAW,IAAI,KAAK;AAAA,MAC3B,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAEA,MAAM,qBAAqB,SAAsC;AAC/D,WAAO,MAAM,KAAK,yBAAyB,OAAO;AAAA,EACpD;AAAA,EAEA,MAAM,kCACJ,WACiB;AACjB,UAAM,aAAa,aAAa,OAAO;AAEvC,SAAK,WAAW,IAAI,UAAU;AAE9B,QAAI,KAAK,WAAW,SAAS,GAAG;AAC9B,YAAM,KAAK,yBAAyB;AACpC,WAAK,MAAM;AAAA,IACb;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,yBACJ,UAAsC,CAAC,GACjB;AACtB,UAAM,EAAE,oBAAoB,MAAM,gBAAgB,IAAI;AAEtD,QAAI,UACF,qBACA,0BACA;AAEF,QAAI,oBAAoB,QAAW;AACjC,YAAM,gBAAgB,KAAK,gBAAgB;AAAA,QACzC;AAAA,QACA;AAAA,MACF;AACA,iCAA2B,cAAc,cAAc,YAAY;AAEnE,uBAAiB,oBAAoB,cAAc,cAAc,OAAO;AAExE,UAAI;AACF,cAAM,SAAS,MAAM,KAAK,gBAAgB;AAAA,UACxC;AAAA,UACA;AAAA,QACF;AACA,8BAAsB,UAAU;AAAA,MAClC,QAAQ;AACN,8BAAsB;AAAA,MACxB;AACA,iBAAW,IAAI,SAAS,cAAc,QAAQ;AAAA,IAChD;AAEA,4BAAa,KAAK;AAElB,4DACE,KAAK,2CAA2C;AAElD,wCAAmB,oBAAoB,KAAK,cAAc;AAE1D,QAAI;AACF,oDAAwB,MAAM,KAAK,wBAAwB;AAAA,IAC7D,SAAS,GAAG;AACV,cAAQ,MAAM,CAAC;AACf,oDAAwB;AAAA,IAC1B;AAEA,UAAM,qBAAqB,MAAM,4BAA4B;AAAA,MAC3D;AAAA,MACA;AAAA,MACA;AAAA,MACA,sBAAsB,KAAK,mBAAmB;AAAA,QAC5C;AAAA,QACA,GAAG,cAAc;AAAA,MACnB;AAAA,MACA;AAAA,MACA,iCAAiC,KAAK,kBAAkB;AAAA,QACtD;AAAA,QACA,GAAG,cAAc;AAAA,MACnB;AAAA,MACA;AAAA,MACA;AAAA,MACA,UAAU,KAAK;AAAA,MACf;AAAA,MACA,cAAc,KAAK;AAAA,MACnB,0BAA0B,KAAK,MAAM;AAAA,IACvC,CAAC;AAED,QAAI,mBAAmB;AACrB,YAAM,UAAU,MAAM,cAAc;AACpC,WAAK,OAAO,CAAC,UAAU;AACrB,YAAI,KAAK,mBAAmB,SAAS;AACnC,gBAAM,kBAAkB,mBAAmB;AAC3C,gBAAM,4BACJ,mBAAmB;AACrB,gBAAM,kBAAkB,mBAAmB;AAAA,QAC7C;AACA,cAAM,6BAAN,MAAM,2BAA6B,CAAC;AACpC,cAAM,yBAAyB,OAAO,IAAI;AAAA,UACxC,iBAAiB,mBAAmB;AAAA,UACpC,2BACE,mBAAmB;AAAA,UACrB,iBAAiB,mBAAmB;AAAA,QACtC;AAAA,MACF,CAAC;AAAA,IACH;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,iBAAiB,WAAmB;AAClC,SAAK,WAAW,OAAO,SAAS;AAChC,QAAI,KAAK,WAAW,SAAS,GAAG;AAC9B,WAAK,YAAY;AAAA,IACnB;AAAA,EACF;AAAA,EAEA,cAAc;AACZ,QAAI,KAAK,YAAY;AACnB,oBAAc,KAAK,UAAU;AAAA,IAC/B;AACA,SAAK,WAAW,MAAM;AACtB,SAAK,WAAW;AAAA,EAClB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOS,UAAU;AACjB,UAAM,QAAQ;AACd,SAAK,YAAY;AAAA,EACnB;AAAA,EAEQ,QAAQ;AACd,QAAI,KAAK,YAAY;AACnB,oBAAc,KAAK,UAAU;AAAA,IAC/B;AAEA,SAAK,aAAa,YAAY,YAAY;AACxC,YAAM,cAAc,MAAM,KAAK,yBAAyB,CAAC;AAAA,IAC3D,GAAG,KAAK,aAAa;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,aAAa,iBAAwC;AACzD,UAAM,KAAK,yBAAyB,EAAE,gBAAgB,CAAC;AAAA,EACzD;AAAA,EAEQ,aAAa;AACnB,SAAK,OAAO,MAAM;AAChB,aAAO;AAAA,IACT,CAAC;AAAA,EACH;AAAA,EAEA,MAAc,0BAA0B;AACtC,UAAM,oCACJ,MAAM,KAAK,sCAAsC;AACnD,UAAM,oCACJ,KAAK,wCAAwC,KAAK;AAEpD,WACE,qCAAqC;AAAA,EAEzC;AAAA,EAEA,gBACE,sBACA,cACmD;AACnD,QACE,CAAC,KAAK,MAAM,mBACZ,KAAK,MAAM,oBAAoB,mBAAmB,YAClD;AACA,aAAO,CAAC;AAAA,IACV;AACA,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA,KAAK,MAAM;AAAA,IACb;AAAA,EACF;AAAA,EAaA,yBAAyB;AACvB,SAAK,OAAO,CAAC,UAAU;AACrB,YAAM,2BAA2B;AAAA,IACnC,CAAC;AAAA,EACH;AAAA,EAEA,0BAA0B;AACxB,SAAK,OAAO,CAAC,UAAU;AACrB,YAAM,2BAA2B;AAAA,IACnC,CAAC;AAAA,EACH;AACF;AAlUE;AA4SM;AAAA,kCAA6B,eAAC,wBAAsC;AACxE,QAAM,aAAa,uBAAuB,eAAe;AAEzD,MAAI,eAAe,KAAK,gBAAgB;AACtC,SAAK,WAAW,IAAI,SAAS,mBAAK,cAAL,UAAmB;AAChD,UAAM,KAAK,aAAa;AAExB,SAAK,iBAAiB;AAAA,EACxB;AACF;AAeF,IAAO,2BAAQ;;;ACliBf,eAAO,4BACL,MAC6B;AAC7B,MAAI;AACF,WAAO,MAAM,2BAA2B,IAAI;AAAA,EAC9C,SAAS,OAAO;AACd,QAAI,iBAAiB,OAAO;AAC1B,YAAM,IAAI;AAAA,QACR,6CAA6C,MAAM,OAAO;AAAA,MAC5D;AAAA,IACF;AAEA,UAAM;AAAA,EACR;AACF;AAOA,eAAe,2BACb,SAC6B;AAC7B,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AAEJ,MAAI;AACF,QAAI,uBAAuB,CAAC,0BAA0B;AACpD,aAAO,MAAM,mCAAmC,OAAO;AAAA,IACzD;AAEA,QAAI,4BAA4B,CAAC,0BAA0B;AACzD,aAAO,MAAM,gCAAgC,OAAO;AAAA,IACtD;AAEA,UAAM,IAAI,MAAM,oDAAoD;AAAA,EACtE,QAAQ;AACN,WAAO,MAAM,0BAA0B,OAAO;AAAA,EAChD;AACF;AAOA,eAAe,mCACb,SAC6B;AAC7B,QAAM;AAAA,IACJ,mBAAAA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,uBAAAC;AAAA,EACF,IAAI;AAEJ,QAAM,YAAY,MAAMD;AAAA,IACtB;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,QAAM,EAAE,+BAA+B,sBAAsB,IAC3D,UAAU;AAEZ,QAAM,4BAA4BC;AAAA,IAChC;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,SAAO;AAAA,IACL,iBAAiB;AAAA,IACjB;AAAA,IACA,iBAAiB,mBAAmB;AAAA,EACtC;AACF;AAOA,eAAe,gCACb,SAC6B;AAC7B,QAAM;AAAA,IACJ,8BAAAC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AAEJ,QAAM,YAAY,MAAMA;AAAA,IACtB;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,SAAO;AAAA,IACL,iBAAiB;AAAA,IACjB,2BAA2B,CAAC;AAAA,IAC5B,iBAAiB,mBAAmB;AAAA,EACtC;AACF;AAOA,eAAe,0BACb,SAC6B;AAC7B,QAAM,EAAE,UAAU,0BAAAC,0BAAyB,IAAI;AAE/C,QAAM,YAAY,MAAMA,0BAAyB,QAAQ;AAEzD,SAAO;AAAA,IACL,iBAAiB;AAAA,IACjB,2BAA2B,CAAC;AAAA,IAC5B,iBAAiB,mBAAmB;AAAA,EACtC;AACF;","names":["fetchGasEstimates","calculateTimeEstimate","fetchLegacyGasPriceEstimates","fetchEthGasPriceEstimate"]} -diff --git a/node_modules/@metamask/gas-fee-controller/dist/chunk-H5WHAYLI.js b/node_modules/@metamask/gas-fee-controller/dist/chunk-H5WHAYLI.js -index 3d6f845..1eecad6 100644 ---- a/node_modules/@metamask/gas-fee-controller/dist/chunk-H5WHAYLI.js -+++ b/node_modules/@metamask/gas-fee-controller/dist/chunk-H5WHAYLI.js -@@ -18,7 +18,7 @@ var _controllerutils = require('@metamask/controller-utils'); - var _ethquery = require('@metamask/eth-query'); var _ethquery2 = _interopRequireDefault(_ethquery); - var _pollingcontroller = require('@metamask/polling-controller'); - var _uuid = require('uuid'); --var GAS_API_BASE_URL = "https://gas.api.infura.io"; -+var LEGACY_GAS_PRICES_API_URL = `https://api.metaswap.codefi.network/gasPrices` - var GAS_ESTIMATE_TYPES = { - FEE_MARKET: "fee-market", - LEGACY: "legacy", -@@ -62,9 +62,11 @@ var GasFeeController = class extends _pollingcontroller.StaticIntervalPollingCon - * @param options.getProvider - Returns a network provider for the current network. - * @param options.onNetworkDidChange - A function for registering an event handler for the - * network state change event. -+ * @param options.legacyAPIEndpoint - The legacy gas price API URL. This option is primarily for -+ * testing purposes. -+ * @param options.EIP1559APIEndpoint - The EIP-1559 gas price API URL. - * @param options.clientId - The client ID used to identify to the gas estimation API who is - * asking for estimates. -- * @param options.infuraAPIKey - The Infura API key used for infura API requests. - */ - constructor({ - interval = 15e3, -@@ -77,7 +79,8 @@ var GasFeeController = class extends _pollingcontroller.StaticIntervalPollingCon - getProvider, - onNetworkDidChange, - clientId, -- infuraAPIKey -+ EIP1559APIEndpoint, -+ legacyAPIEndpoint - }) { - super({ - name, -@@ -94,10 +97,9 @@ var GasFeeController = class extends _pollingcontroller.StaticIntervalPollingCon - this.getCurrentNetworkLegacyGasAPICompatibility = getCurrentNetworkLegacyGasAPICompatibility; - this.getCurrentAccountEIP1559Compatibility = getCurrentAccountEIP1559Compatibility; - _chunkQ2YPK5SLjs.__privateSet.call(void 0, this, _getProvider, getProvider); -- this.EIP1559APIEndpoint = `${GAS_API_BASE_URL}/networks//suggestedGasFees`; -- this.legacyAPIEndpoint = `${GAS_API_BASE_URL}/networks//gasPrices`; -+ this.EIP1559APIEndpoint = EIP1559APIEndpoint; -+ this.legacyAPIEndpoint = legacyAPIEndpoint; - this.clientId = clientId; -- this.infuraAPIKey = infuraAPIKey; - this.ethQuery = new (0, _ethquery2.default)(_chunkQ2YPK5SLjs.__privateGet.call(void 0, this, _getProvider).call(this)); - if (onNetworkDidChange && getChainId) { - this.currentChainId = getChainId(); -@@ -193,7 +195,6 @@ var GasFeeController = class extends _pollingcontroller.StaticIntervalPollingCon - calculateTimeEstimate: _chunkQ2YPK5SLjs.calculateTimeEstimate, - clientId: this.clientId, - ethQuery, -- infuraAPIKey: this.infuraAPIKey, - nonRPCGasFeeApisDisabled: this.state.nonRPCGasFeeApisDisabled - }); - if (shouldUpdateState) { -@@ -337,15 +338,10 @@ async function getEstimatesUsingFeeMarketEndpoint(request) { - const { - fetchGasEstimates: fetchGasEstimates2, - fetchGasEstimatesUrl, -- infuraAPIKey, - clientId, - calculateTimeEstimate: calculateTimeEstimate2 - } = request; -- const estimates = await fetchGasEstimates2( -- fetchGasEstimatesUrl, -- infuraAPIKey, -- clientId -- ); -+ const estimates = await fetchGasEstimates2(fetchGasEstimatesUrl, clientId); - const { suggestedMaxPriorityFeePerGas, suggestedMaxFeePerGas } = estimates.medium; - const estimatedGasFeeTimeBounds = calculateTimeEstimate2( - suggestedMaxPriorityFeePerGas, -@@ -362,12 +358,10 @@ async function getEstimatesUsingLegacyEndpoint(request) { - const { - fetchLegacyGasPriceEstimates: fetchLegacyGasPriceEstimates2, - fetchLegacyGasPriceEstimatesUrl, -- infuraAPIKey, - clientId - } = request; - const estimates = await fetchLegacyGasPriceEstimates2( - fetchLegacyGasPriceEstimatesUrl, -- infuraAPIKey, - clientId - ); - return { -@@ -392,5 +386,5 @@ async function getEstimatesUsingProvider(request) { - - - --exports.determineGasFeeCalculations = determineGasFeeCalculations; exports.GAS_API_BASE_URL = GAS_API_BASE_URL; exports.GAS_ESTIMATE_TYPES = GAS_ESTIMATE_TYPES; exports.GasFeeController = GasFeeController; exports.GasFeeController_default = GasFeeController_default; -+exports.determineGasFeeCalculations = determineGasFeeCalculations; exports.LEGACY_GAS_PRICES_API_URL = LEGACY_GAS_PRICES_API_URL; exports.GAS_ESTIMATE_TYPES = GAS_ESTIMATE_TYPES; exports.GasFeeController = GasFeeController; exports.GasFeeController_default = GasFeeController_default; - //# sourceMappingURL=chunk-H5WHAYLI.js.map -diff --git a/node_modules/@metamask/gas-fee-controller/dist/chunk-H5WHAYLI.js.map b/node_modules/@metamask/gas-fee-controller/dist/chunk-H5WHAYLI.js.map -index ed761f5..c6236d8 100644 ---- a/node_modules/@metamask/gas-fee-controller/dist/chunk-H5WHAYLI.js.map -+++ b/node_modules/@metamask/gas-fee-controller/dist/chunk-H5WHAYLI.js.map -@@ -1 +1 @@ --{"version":3,"sources":["../src/GasFeeController.ts","../src/determineGasFeeCalculations.ts"],"names":["fetchGasEstimates","calculateTimeEstimate","fetchLegacyGasPriceEstimates","fetchEthGasPriceEstimate"],"mappings":";;;;;;;;;;;;AAKA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,OAAO,cAAc;AAUrB,SAAS,uCAAuC;AAEhD,SAAS,MAAM,cAAc;AAUtB,IAAM,mBAAmB;AA0BzB,IAAM,qBAAqB;AAAA,EAChC,YAAY;AAAA,EACZ,QAAQ;AAAA,EACR,cAAc;AAAA,EACd,MAAM;AACR;AAiGA,IAAM,WAAW;AAAA,EACf,0BAA0B;AAAA,IACxB,SAAS;AAAA,IACT,WAAW;AAAA,EACb;AAAA,EACA,iBAAiB,EAAE,SAAS,MAAM,WAAW,MAAM;AAAA,EACnD,2BAA2B,EAAE,SAAS,MAAM,WAAW,MAAM;AAAA,EAC7D,iBAAiB,EAAE,SAAS,MAAM,WAAW,MAAM;AAAA,EACnD,0BAA0B,EAAE,SAAS,MAAM,WAAW,MAAM;AAC9D;AAqDA,IAAM,OAAO;AA0Bb,IAAM,eAA4B;AAAA,EAChC,0BAA0B,CAAC;AAAA,EAC3B,iBAAiB,CAAC;AAAA,EAClB,2BAA2B,CAAC;AAAA,EAC5B,iBAAiB,mBAAmB;AAAA,EACpC,0BAA0B;AAC5B;AA9PA;AAmQO,IAAM,mBAAN,cAA+B,gCAIpC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgDA,YAAY;AAAA,IACV,WAAW;AAAA,IACX;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,GAYG;AACD,UAAM;AAAA,MACJ;AAAA,MACA;AAAA,MACA;AAAA,MACA,OAAO,EAAE,GAAG,cAAc,GAAG,MAAM;AAAA,IACrC,CAAC;AAuPH,uBAAM;AA5SN;AAsDE,SAAK,gBAAgB;AACrB,SAAK,kBAAkB,QAAQ;AAC/B,SAAK,aAAa,oBAAI,IAAI;AAC1B,SAAK,wCACH;AACF,SAAK,6CACH;AACF,SAAK,wCACH;AACF,uBAAK,cAAe;AACpB,SAAK,qBAAqB,GAAG,gBAAgB;AAC7C,SAAK,oBAAoB,GAAG,gBAAgB;AAC5C,SAAK,WAAW;AAChB,SAAK,eAAe;AAEpB,SAAK,WAAW,IAAI,SAAS,mBAAK,cAAL,UAAmB;AAEhD,QAAI,sBAAsB,YAAY;AACpC,WAAK,iBAAiB,WAAW;AACjC,yBAAmB,OAAO,2BAA2B;AACnD,cAAM,sBAAK,gEAAL,WAAmC;AAAA,MAC3C,CAAC;AAAA,IACH,OAAO;AACL,WAAK,iBAAiB,KAAK,gBAAgB;AAAA,QACzC;AAAA,MACF,EAAE,eAAe;AACjB,WAAK,gBAAgB;AAAA,QACnB;AAAA,QACA,OAAO,2BAA2B;AAChC,gBAAM,sBAAK,gEAAL,WAAmC;AAAA,QAC3C;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,eAAe;AACnB,QAAI,KAAK,WAAW,SAAS,GAAG;AAC9B,YAAM,SAAS,MAAM,KAAK,KAAK,UAAU;AACzC,WAAK,YAAY;AACjB,YAAM,KAAK,kCAAkC,OAAO,CAAC,CAAC;AACtD,aAAO,MAAM,CAAC,EAAE,QAAQ,CAAC,UAAU;AACjC,aAAK,WAAW,IAAI,KAAK;AAAA,MAC3B,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAEA,MAAM,qBAAqB,SAAsC;AAC/D,WAAO,MAAM,KAAK,yBAAyB,OAAO;AAAA,EACpD;AAAA,EAEA,MAAM,kCACJ,WACiB;AACjB,UAAM,aAAa,aAAa,OAAO;AAEvC,SAAK,WAAW,IAAI,UAAU;AAE9B,QAAI,KAAK,WAAW,SAAS,GAAG;AAC9B,YAAM,KAAK,yBAAyB;AACpC,WAAK,MAAM;AAAA,IACb;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,yBACJ,UAAsC,CAAC,GACjB;AACtB,UAAM,EAAE,oBAAoB,MAAM,gBAAgB,IAAI;AAEtD,QAAI,UACF,qBACA,0BACA;AAEF,QAAI,oBAAoB,QAAW;AACjC,YAAM,gBAAgB,KAAK,gBAAgB;AAAA,QACzC;AAAA,QACA;AAAA,MACF;AACA,iCAA2B,cAAc,cAAc,YAAY;AAEnE,uBAAiB,oBAAoB,cAAc,cAAc,OAAO;AAExE,UAAI;AACF,cAAM,SAAS,MAAM,KAAK,gBAAgB;AAAA,UACxC;AAAA,UACA;AAAA,QACF;AACA,8BAAsB,UAAU;AAAA,MAClC,QAAQ;AACN,8BAAsB;AAAA,MACxB;AACA,iBAAW,IAAI,SAAS,cAAc,QAAQ;AAAA,IAChD;AAEA,4BAAa,KAAK;AAElB,4DACE,KAAK,2CAA2C;AAElD,wCAAmB,oBAAoB,KAAK,cAAc;AAE1D,QAAI;AACF,oDAAwB,MAAM,KAAK,wBAAwB;AAAA,IAC7D,SAAS,GAAG;AACV,cAAQ,MAAM,CAAC;AACf,oDAAwB;AAAA,IAC1B;AAEA,UAAM,qBAAqB,MAAM,4BAA4B;AAAA,MAC3D;AAAA,MACA;AAAA,MACA;AAAA,MACA,sBAAsB,KAAK,mBAAmB;AAAA,QAC5C;AAAA,QACA,GAAG,cAAc;AAAA,MACnB;AAAA,MACA;AAAA,MACA,iCAAiC,KAAK,kBAAkB;AAAA,QACtD;AAAA,QACA,GAAG,cAAc;AAAA,MACnB;AAAA,MACA;AAAA,MACA;AAAA,MACA,UAAU,KAAK;AAAA,MACf;AAAA,MACA,cAAc,KAAK;AAAA,MACnB,0BAA0B,KAAK,MAAM;AAAA,IACvC,CAAC;AAED,QAAI,mBAAmB;AACrB,YAAM,UAAU,MAAM,cAAc;AACpC,WAAK,OAAO,CAAC,UAAU;AACrB,YAAI,KAAK,mBAAmB,SAAS;AACnC,gBAAM,kBAAkB,mBAAmB;AAC3C,gBAAM,4BACJ,mBAAmB;AACrB,gBAAM,kBAAkB,mBAAmB;AAAA,QAC7C;AACA,cAAM,6BAAN,MAAM,2BAA6B,CAAC;AACpC,cAAM,yBAAyB,OAAO,IAAI;AAAA,UACxC,iBAAiB,mBAAmB;AAAA,UACpC,2BACE,mBAAmB;AAAA,UACrB,iBAAiB,mBAAmB;AAAA,QACtC;AAAA,MACF,CAAC;AAAA,IACH;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,iBAAiB,WAAmB;AAClC,SAAK,WAAW,OAAO,SAAS;AAChC,QAAI,KAAK,WAAW,SAAS,GAAG;AAC9B,WAAK,YAAY;AAAA,IACnB;AAAA,EACF;AAAA,EAEA,cAAc;AACZ,QAAI,KAAK,YAAY;AACnB,oBAAc,KAAK,UAAU;AAAA,IAC/B;AACA,SAAK,WAAW,MAAM;AACtB,SAAK,WAAW;AAAA,EAClB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOS,UAAU;AACjB,UAAM,QAAQ;AACd,SAAK,YAAY;AAAA,EACnB;AAAA,EAEQ,QAAQ;AACd,QAAI,KAAK,YAAY;AACnB,oBAAc,KAAK,UAAU;AAAA,IAC/B;AAEA,SAAK,aAAa,YAAY,YAAY;AACxC,YAAM,cAAc,MAAM,KAAK,yBAAyB,CAAC;AAAA,IAC3D,GAAG,KAAK,aAAa;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,aAAa,iBAAwC;AACzD,UAAM,KAAK,yBAAyB,EAAE,gBAAgB,CAAC;AAAA,EACzD;AAAA,EAEQ,aAAa;AACnB,SAAK,OAAO,MAAM;AAChB,aAAO;AAAA,IACT,CAAC;AAAA,EACH;AAAA,EAEA,MAAc,0BAA0B;AACtC,UAAM,oCACJ,MAAM,KAAK,sCAAsC;AACnD,UAAM,oCACJ,KAAK,wCAAwC,KAAK;AAEpD,WACE,qCAAqC;AAAA,EAEzC;AAAA,EAEA,gBACE,sBACA,cACmD;AACnD,QACE,CAAC,KAAK,MAAM,mBACZ,KAAK,MAAM,oBAAoB,mBAAmB,YAClD;AACA,aAAO,CAAC;AAAA,IACV;AACA,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA,KAAK,MAAM;AAAA,IACb;AAAA,EACF;AAAA,EAaA,yBAAyB;AACvB,SAAK,OAAO,CAAC,UAAU;AACrB,YAAM,2BAA2B;AAAA,IACnC,CAAC;AAAA,EACH;AAAA,EAEA,0BAA0B;AACxB,SAAK,OAAO,CAAC,UAAU;AACrB,YAAM,2BAA2B;AAAA,IACnC,CAAC;AAAA,EACH;AACF;AAlUE;AA4SM;AAAA,kCAA6B,eAAC,wBAAsC;AACxE,QAAM,aAAa,uBAAuB,eAAe;AAEzD,MAAI,eAAe,KAAK,gBAAgB;AACtC,SAAK,WAAW,IAAI,SAAS,mBAAK,cAAL,UAAmB;AAChD,UAAM,KAAK,aAAa;AAExB,SAAK,iBAAiB;AAAA,EACxB;AACF;AAeF,IAAO,2BAAQ;;;ACliBf,eAAO,4BACL,MAC6B;AAC7B,MAAI;AACF,WAAO,MAAM,2BAA2B,IAAI;AAAA,EAC9C,SAAS,OAAO;AACd,QAAI,iBAAiB,OAAO;AAC1B,YAAM,IAAI;AAAA,QACR,6CAA6C,MAAM,OAAO;AAAA,MAC5D;AAAA,IACF;AAEA,UAAM;AAAA,EACR;AACF;AAOA,eAAe,2BACb,SAC6B;AAC7B,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AAEJ,MAAI;AACF,QAAI,uBAAuB,CAAC,0BAA0B;AACpD,aAAO,MAAM,mCAAmC,OAAO;AAAA,IACzD;AAEA,QAAI,4BAA4B,CAAC,0BAA0B;AACzD,aAAO,MAAM,gCAAgC,OAAO;AAAA,IACtD;AAEA,UAAM,IAAI,MAAM,oDAAoD;AAAA,EACtE,QAAQ;AACN,WAAO,MAAM,0BAA0B,OAAO;AAAA,EAChD;AACF;AAOA,eAAe,mCACb,SAC6B;AAC7B,QAAM;AAAA,IACJ,mBAAAA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,uBAAAC;AAAA,EACF,IAAI;AAEJ,QAAM,YAAY,MAAMD;AAAA,IACtB;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,QAAM,EAAE,+BAA+B,sBAAsB,IAC3D,UAAU;AAEZ,QAAM,4BAA4BC;AAAA,IAChC;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,SAAO;AAAA,IACL,iBAAiB;AAAA,IACjB;AAAA,IACA,iBAAiB,mBAAmB;AAAA,EACtC;AACF;AAOA,eAAe,gCACb,SAC6B;AAC7B,QAAM;AAAA,IACJ,8BAAAC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AAEJ,QAAM,YAAY,MAAMA;AAAA,IACtB;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,SAAO;AAAA,IACL,iBAAiB;AAAA,IACjB,2BAA2B,CAAC;AAAA,IAC5B,iBAAiB,mBAAmB;AAAA,EACtC;AACF;AAOA,eAAe,0BACb,SAC6B;AAC7B,QAAM,EAAE,UAAU,0BAAAC,0BAAyB,IAAI;AAE/C,QAAM,YAAY,MAAMA,0BAAyB,QAAQ;AAEzD,SAAO;AAAA,IACL,iBAAiB;AAAA,IACjB,2BAA2B,CAAC;AAAA,IAC5B,iBAAiB,mBAAmB;AAAA,EACtC;AACF","sourcesContent":["import type {\n ControllerGetStateAction,\n ControllerStateChangeEvent,\n RestrictedControllerMessenger,\n} from '@metamask/base-controller';\nimport {\n convertHexToDecimal,\n safelyExecute,\n toHex,\n} from '@metamask/controller-utils';\nimport EthQuery from '@metamask/eth-query';\nimport type {\n NetworkClientId,\n NetworkControllerGetEIP1559CompatibilityAction,\n NetworkControllerGetNetworkClientByIdAction,\n NetworkControllerGetStateAction,\n NetworkControllerNetworkDidChangeEvent,\n NetworkState,\n ProviderProxy,\n} from '@metamask/network-controller';\nimport { StaticIntervalPollingController } from '@metamask/polling-controller';\nimport type { Hex } from '@metamask/utils';\nimport { v1 as random } from 'uuid';\n\nimport determineGasFeeCalculations from './determineGasFeeCalculations';\nimport {\n calculateTimeEstimate,\n fetchGasEstimates,\n fetchLegacyGasPriceEstimates,\n fetchEthGasPriceEstimate,\n} from './gas-util';\n\nexport const GAS_API_BASE_URL = 'https://gas.api.infura.io';\n\nexport type unknownString = 'unknown';\n\n// Fee Market describes the way gas is set after the london hardfork, and was\n// defined by EIP-1559.\nexport type FeeMarketEstimateType = 'fee-market';\n// Legacy describes gasPrice estimates from before london hardfork, when the\n// user is connected to mainnet and are presented with fast/average/slow\n// estimate levels to choose from.\nexport type LegacyEstimateType = 'legacy';\n// EthGasPrice describes a gasPrice estimate received from eth_gasPrice. Post\n// london this value should only be used for legacy type transactions when on\n// networks that support EIP-1559. This type of estimate is the most accurate\n// to display on custom networks that don't support EIP-1559.\nexport type EthGasPriceEstimateType = 'eth_gasPrice';\n// NoEstimate describes the state of the controller before receiving its first\n// estimate.\nexport type NoEstimateType = 'none';\n\n/**\n * Indicates which type of gasEstimate the controller is currently returning.\n * This is useful as a way of asserting that the shape of gasEstimates matches\n * expectations. NONE is a special case indicating that no previous gasEstimate\n * has been fetched.\n */\nexport const GAS_ESTIMATE_TYPES = {\n FEE_MARKET: 'fee-market' as FeeMarketEstimateType,\n LEGACY: 'legacy' as LegacyEstimateType,\n ETH_GASPRICE: 'eth_gasPrice' as EthGasPriceEstimateType,\n NONE: 'none' as NoEstimateType,\n};\n\nexport type GasEstimateType =\n | FeeMarketEstimateType\n | EthGasPriceEstimateType\n | LegacyEstimateType\n | NoEstimateType;\n\nexport type EstimatedGasFeeTimeBounds = {\n lowerTimeBound: number | null;\n upperTimeBound: number | unknownString;\n};\n\n/**\n * @type EthGasPriceEstimate\n *\n * A single gas price estimate for networks and accounts that don't support EIP-1559\n * This estimate comes from eth_gasPrice but is converted to dec gwei to match other\n * return values\n * @property gasPrice - A GWEI dec string\n */\n\nexport type EthGasPriceEstimate = {\n gasPrice: string;\n};\n\n/**\n * @type LegacyGasPriceEstimate\n *\n * A set of gas price estimates for networks and accounts that don't support EIP-1559\n * These estimates include low, medium and high all as strings representing gwei in\n * decimal format.\n * @property high - gasPrice, in decimal gwei string format, suggested for fast inclusion\n * @property medium - gasPrice, in decimal gwei string format, suggested for avg inclusion\n * @property low - gasPrice, in decimal gwei string format, suggested for slow inclusion\n */\nexport type LegacyGasPriceEstimate = {\n high: string;\n medium: string;\n low: string;\n};\n\n/**\n * @type Eip1559GasFee\n *\n * Data necessary to provide an estimate of a gas fee with a specific tip\n * @property minWaitTimeEstimate - The fastest the transaction will take, in milliseconds\n * @property maxWaitTimeEstimate - The slowest the transaction will take, in milliseconds\n * @property suggestedMaxPriorityFeePerGas - A suggested \"tip\", a GWEI hex number\n * @property suggestedMaxFeePerGas - A suggested max fee, the most a user will pay. a GWEI hex number\n */\nexport type Eip1559GasFee = {\n minWaitTimeEstimate: number; // a time duration in milliseconds\n maxWaitTimeEstimate: number; // a time duration in milliseconds\n suggestedMaxPriorityFeePerGas: string; // a GWEI decimal number\n suggestedMaxFeePerGas: string; // a GWEI decimal number\n};\n\n/**\n * @type GasFeeEstimates\n *\n * Data necessary to provide multiple GasFee estimates, and supporting information, to the user\n * @property low - A GasFee for a minimum necessary combination of tip and maxFee\n * @property medium - A GasFee for a recommended combination of tip and maxFee\n * @property high - A GasFee for a high combination of tip and maxFee\n * @property estimatedBaseFee - An estimate of what the base fee will be for the pending/next block. A GWEI dec number\n * @property networkCongestion - A normalized number that can be used to gauge the congestion\n * level of the network, with 0 meaning not congested and 1 meaning extremely congested\n */\nexport type GasFeeEstimates = SourcedGasFeeEstimates | FallbackGasFeeEstimates;\n\ntype SourcedGasFeeEstimates = {\n low: Eip1559GasFee;\n medium: Eip1559GasFee;\n high: Eip1559GasFee;\n estimatedBaseFee: string;\n historicalBaseFeeRange: [string, string];\n baseFeeTrend: 'up' | 'down' | 'level';\n latestPriorityFeeRange: [string, string];\n historicalPriorityFeeRange: [string, string];\n priorityFeeTrend: 'up' | 'down' | 'level';\n networkCongestion: number;\n};\n\ntype FallbackGasFeeEstimates = {\n low: Eip1559GasFee;\n medium: Eip1559GasFee;\n high: Eip1559GasFee;\n estimatedBaseFee: string;\n historicalBaseFeeRange: null;\n baseFeeTrend: null;\n latestPriorityFeeRange: null;\n historicalPriorityFeeRange: null;\n priorityFeeTrend: null;\n networkCongestion: null;\n};\n\nconst metadata = {\n gasFeeEstimatesByChainId: {\n persist: true,\n anonymous: false,\n },\n gasFeeEstimates: { persist: true, anonymous: false },\n estimatedGasFeeTimeBounds: { persist: true, anonymous: false },\n gasEstimateType: { persist: true, anonymous: false },\n nonRPCGasFeeApisDisabled: { persist: true, anonymous: false },\n};\n\nexport type GasFeeStateEthGasPrice = {\n gasFeeEstimates: EthGasPriceEstimate;\n estimatedGasFeeTimeBounds: Record;\n gasEstimateType: EthGasPriceEstimateType;\n};\n\nexport type GasFeeStateFeeMarket = {\n gasFeeEstimates: GasFeeEstimates;\n estimatedGasFeeTimeBounds: EstimatedGasFeeTimeBounds | Record;\n gasEstimateType: FeeMarketEstimateType;\n};\n\nexport type GasFeeStateLegacy = {\n gasFeeEstimates: LegacyGasPriceEstimate;\n estimatedGasFeeTimeBounds: Record;\n gasEstimateType: LegacyEstimateType;\n};\n\nexport type GasFeeStateNoEstimates = {\n gasFeeEstimates: Record;\n estimatedGasFeeTimeBounds: Record;\n gasEstimateType: NoEstimateType;\n};\n\nexport type FetchGasFeeEstimateOptions = {\n shouldUpdateState?: boolean;\n networkClientId?: NetworkClientId;\n};\n\n/**\n * @type GasFeeState\n *\n * Gas Fee controller state\n * @property gasFeeEstimates - Gas fee estimate data based on new EIP-1559 properties\n * @property estimatedGasFeeTimeBounds - Estimates representing the minimum and maximum\n */\nexport type SingleChainGasFeeState =\n | GasFeeStateEthGasPrice\n | GasFeeStateFeeMarket\n | GasFeeStateLegacy\n | GasFeeStateNoEstimates;\n\nexport type GasFeeEstimatesByChainId = {\n gasFeeEstimatesByChainId?: Record;\n};\n\nexport type GasFeeState = GasFeeEstimatesByChainId &\n SingleChainGasFeeState & {\n nonRPCGasFeeApisDisabled?: boolean;\n };\n\nconst name = 'GasFeeController';\n\nexport type GasFeeStateChange = ControllerStateChangeEvent<\n typeof name,\n GasFeeState\n>;\n\nexport type GetGasFeeState = ControllerGetStateAction;\n\nexport type GasFeeControllerActions = GetGasFeeState;\n\nexport type GasFeeControllerEvents = GasFeeStateChange;\n\ntype AllowedActions =\n | NetworkControllerGetStateAction\n | NetworkControllerGetNetworkClientByIdAction\n | NetworkControllerGetEIP1559CompatibilityAction;\n\ntype GasFeeMessenger = RestrictedControllerMessenger<\n typeof name,\n GasFeeControllerActions | AllowedActions,\n GasFeeControllerEvents | NetworkControllerNetworkDidChangeEvent,\n AllowedActions['type'],\n NetworkControllerNetworkDidChangeEvent['type']\n>;\n\nconst defaultState: GasFeeState = {\n gasFeeEstimatesByChainId: {},\n gasFeeEstimates: {},\n estimatedGasFeeTimeBounds: {},\n gasEstimateType: GAS_ESTIMATE_TYPES.NONE,\n nonRPCGasFeeApisDisabled: false,\n};\n\n/**\n * Controller that retrieves gas fee estimate data and polls for updated data on a set interval\n */\nexport class GasFeeController extends StaticIntervalPollingController<\n typeof name,\n GasFeeState,\n GasFeeMessenger\n> {\n private intervalId?: ReturnType;\n\n private readonly intervalDelay;\n\n private readonly pollTokens: Set;\n\n private readonly legacyAPIEndpoint: string;\n\n private readonly EIP1559APIEndpoint: string;\n\n private readonly getCurrentNetworkEIP1559Compatibility;\n\n private readonly getCurrentNetworkLegacyGasAPICompatibility;\n\n private readonly getCurrentAccountEIP1559Compatibility;\n\n private readonly infuraAPIKey: string;\n\n private currentChainId;\n\n private ethQuery?: EthQuery;\n\n private readonly clientId?: string;\n\n #getProvider: () => ProviderProxy;\n\n /**\n * Creates a GasFeeController instance.\n *\n * @param options - The controller options.\n * @param options.interval - The time in milliseconds to wait between polls.\n * @param options.messenger - The controller messenger.\n * @param options.state - The initial state.\n * @param options.getCurrentNetworkEIP1559Compatibility - Determines whether or not the current\n * network is EIP-1559 compatible.\n * @param options.getCurrentNetworkLegacyGasAPICompatibility - Determines whether or not the\n * current network is compatible with the legacy gas price API.\n * @param options.getCurrentAccountEIP1559Compatibility - Determines whether or not the current\n * account is EIP-1559 compatible.\n * @param options.getChainId - Returns the current chain ID.\n * @param options.getProvider - Returns a network provider for the current network.\n * @param options.onNetworkDidChange - A function for registering an event handler for the\n * network state change event.\n * @param options.clientId - The client ID used to identify to the gas estimation API who is\n * asking for estimates.\n * @param options.infuraAPIKey - The Infura API key used for infura API requests.\n */\n constructor({\n interval = 15000,\n messenger,\n state,\n getCurrentNetworkEIP1559Compatibility,\n getCurrentAccountEIP1559Compatibility,\n getChainId,\n getCurrentNetworkLegacyGasAPICompatibility,\n getProvider,\n onNetworkDidChange,\n clientId,\n infuraAPIKey,\n }: {\n interval?: number;\n messenger: GasFeeMessenger;\n state?: GasFeeState;\n getCurrentNetworkEIP1559Compatibility: () => Promise;\n getCurrentNetworkLegacyGasAPICompatibility: () => boolean;\n getCurrentAccountEIP1559Compatibility?: () => boolean;\n getChainId?: () => Hex;\n getProvider: () => ProviderProxy;\n onNetworkDidChange?: (listener: (state: NetworkState) => void) => void;\n clientId?: string;\n infuraAPIKey: string;\n }) {\n super({\n name,\n metadata,\n messenger,\n state: { ...defaultState, ...state },\n });\n this.intervalDelay = interval;\n this.setIntervalLength(interval);\n this.pollTokens = new Set();\n this.getCurrentNetworkEIP1559Compatibility =\n getCurrentNetworkEIP1559Compatibility;\n this.getCurrentNetworkLegacyGasAPICompatibility =\n getCurrentNetworkLegacyGasAPICompatibility;\n this.getCurrentAccountEIP1559Compatibility =\n getCurrentAccountEIP1559Compatibility;\n this.#getProvider = getProvider;\n this.EIP1559APIEndpoint = `${GAS_API_BASE_URL}/networks//suggestedGasFees`;\n this.legacyAPIEndpoint = `${GAS_API_BASE_URL}/networks//gasPrices`;\n this.clientId = clientId;\n this.infuraAPIKey = infuraAPIKey;\n\n this.ethQuery = new EthQuery(this.#getProvider());\n\n if (onNetworkDidChange && getChainId) {\n this.currentChainId = getChainId();\n onNetworkDidChange(async (networkControllerState) => {\n await this.#onNetworkControllerDidChange(networkControllerState);\n });\n } else {\n this.currentChainId = this.messagingSystem.call(\n 'NetworkController:getState',\n ).providerConfig.chainId;\n this.messagingSystem.subscribe(\n 'NetworkController:networkDidChange',\n async (networkControllerState) => {\n await this.#onNetworkControllerDidChange(networkControllerState);\n },\n );\n }\n }\n\n async resetPolling() {\n if (this.pollTokens.size !== 0) {\n const tokens = Array.from(this.pollTokens);\n this.stopPolling();\n await this.getGasFeeEstimatesAndStartPolling(tokens[0]);\n tokens.slice(1).forEach((token) => {\n this.pollTokens.add(token);\n });\n }\n }\n\n async fetchGasFeeEstimates(options?: FetchGasFeeEstimateOptions) {\n return await this._fetchGasFeeEstimateData(options);\n }\n\n async getGasFeeEstimatesAndStartPolling(\n pollToken: string | undefined,\n ): Promise {\n const _pollToken = pollToken || random();\n\n this.pollTokens.add(_pollToken);\n\n if (this.pollTokens.size === 1) {\n await this._fetchGasFeeEstimateData();\n this._poll();\n }\n\n return _pollToken;\n }\n\n /**\n * Gets and sets gasFeeEstimates in state.\n *\n * @param options - The gas fee estimate options.\n * @param options.shouldUpdateState - Determines whether the state should be updated with the\n * updated gas estimates.\n * @returns The gas fee estimates.\n */\n async _fetchGasFeeEstimateData(\n options: FetchGasFeeEstimateOptions = {},\n ): Promise {\n const { shouldUpdateState = true, networkClientId } = options;\n\n let ethQuery,\n isEIP1559Compatible,\n isLegacyGasAPICompatible,\n decimalChainId: number;\n\n if (networkClientId !== undefined) {\n const networkClient = this.messagingSystem.call(\n 'NetworkController:getNetworkClientById',\n networkClientId,\n );\n isLegacyGasAPICompatible = networkClient.configuration.chainId === '0x38';\n\n decimalChainId = convertHexToDecimal(networkClient.configuration.chainId);\n\n try {\n const result = await this.messagingSystem.call(\n 'NetworkController:getEIP1559Compatibility',\n networkClientId,\n );\n isEIP1559Compatible = result || false;\n } catch {\n isEIP1559Compatible = false;\n }\n ethQuery = new EthQuery(networkClient.provider);\n }\n\n ethQuery ??= this.ethQuery;\n\n isLegacyGasAPICompatible ??=\n this.getCurrentNetworkLegacyGasAPICompatibility();\n\n decimalChainId ??= convertHexToDecimal(this.currentChainId);\n\n try {\n isEIP1559Compatible ??= await this.getEIP1559Compatibility();\n } catch (e) {\n console.error(e);\n isEIP1559Compatible ??= false;\n }\n\n const gasFeeCalculations = await determineGasFeeCalculations({\n isEIP1559Compatible,\n isLegacyGasAPICompatible,\n fetchGasEstimates,\n fetchGasEstimatesUrl: this.EIP1559APIEndpoint.replace(\n '',\n `${decimalChainId}`,\n ),\n fetchLegacyGasPriceEstimates,\n fetchLegacyGasPriceEstimatesUrl: this.legacyAPIEndpoint.replace(\n '',\n `${decimalChainId}`,\n ),\n fetchEthGasPriceEstimate,\n calculateTimeEstimate,\n clientId: this.clientId,\n ethQuery,\n infuraAPIKey: this.infuraAPIKey,\n nonRPCGasFeeApisDisabled: this.state.nonRPCGasFeeApisDisabled,\n });\n\n if (shouldUpdateState) {\n const chainId = toHex(decimalChainId);\n this.update((state) => {\n if (this.currentChainId === chainId) {\n state.gasFeeEstimates = gasFeeCalculations.gasFeeEstimates;\n state.estimatedGasFeeTimeBounds =\n gasFeeCalculations.estimatedGasFeeTimeBounds;\n state.gasEstimateType = gasFeeCalculations.gasEstimateType;\n }\n state.gasFeeEstimatesByChainId ??= {};\n state.gasFeeEstimatesByChainId[chainId] = {\n gasFeeEstimates: gasFeeCalculations.gasFeeEstimates,\n estimatedGasFeeTimeBounds:\n gasFeeCalculations.estimatedGasFeeTimeBounds,\n gasEstimateType: gasFeeCalculations.gasEstimateType,\n } as SingleChainGasFeeState;\n });\n }\n\n return gasFeeCalculations;\n }\n\n /**\n * Remove the poll token, and stop polling if the set of poll tokens is empty.\n *\n * @param pollToken - The poll token to disconnect.\n */\n disconnectPoller(pollToken: string) {\n this.pollTokens.delete(pollToken);\n if (this.pollTokens.size === 0) {\n this.stopPolling();\n }\n }\n\n stopPolling() {\n if (this.intervalId) {\n clearInterval(this.intervalId);\n }\n this.pollTokens.clear();\n this.resetState();\n }\n\n /**\n * Prepare to discard this controller.\n *\n * This stops any active polling.\n */\n override destroy() {\n super.destroy();\n this.stopPolling();\n }\n\n private _poll() {\n if (this.intervalId) {\n clearInterval(this.intervalId);\n }\n\n this.intervalId = setInterval(async () => {\n await safelyExecute(() => this._fetchGasFeeEstimateData());\n }, this.intervalDelay);\n }\n\n /**\n * Fetching token list from the Token Service API.\n *\n * @private\n * @param networkClientId - The ID of the network client triggering the fetch.\n * @returns A promise that resolves when this operation completes.\n */\n async _executePoll(networkClientId: string): Promise {\n await this._fetchGasFeeEstimateData({ networkClientId });\n }\n\n private resetState() {\n this.update(() => {\n return defaultState;\n });\n }\n\n private async getEIP1559Compatibility() {\n const currentNetworkIsEIP1559Compatible =\n await this.getCurrentNetworkEIP1559Compatibility();\n const currentAccountIsEIP1559Compatible =\n this.getCurrentAccountEIP1559Compatibility?.() ?? true;\n\n return (\n currentNetworkIsEIP1559Compatible && currentAccountIsEIP1559Compatible\n );\n }\n\n getTimeEstimate(\n maxPriorityFeePerGas: string,\n maxFeePerGas: string,\n ): EstimatedGasFeeTimeBounds | Record {\n if (\n !this.state.gasFeeEstimates ||\n this.state.gasEstimateType !== GAS_ESTIMATE_TYPES.FEE_MARKET\n ) {\n return {};\n }\n return calculateTimeEstimate(\n maxPriorityFeePerGas,\n maxFeePerGas,\n this.state.gasFeeEstimates,\n );\n }\n\n async #onNetworkControllerDidChange(networkControllerState: NetworkState) {\n const newChainId = networkControllerState.providerConfig.chainId;\n\n if (newChainId !== this.currentChainId) {\n this.ethQuery = new EthQuery(this.#getProvider());\n await this.resetPolling();\n\n this.currentChainId = newChainId;\n }\n }\n\n enableNonRPCGasFeeApis() {\n this.update((state) => {\n state.nonRPCGasFeeApisDisabled = false;\n });\n }\n\n disableNonRPCGasFeeApis() {\n this.update((state) => {\n state.nonRPCGasFeeApisDisabled = true;\n });\n }\n}\n\nexport default GasFeeController;\n","import type {\n EstimatedGasFeeTimeBounds,\n EthGasPriceEstimate,\n GasFeeEstimates,\n GasFeeState as GasFeeCalculations,\n LegacyGasPriceEstimate,\n} from './GasFeeController';\nimport { GAS_ESTIMATE_TYPES } from './GasFeeController';\n\ntype DetermineGasFeeCalculationsRequest = {\n isEIP1559Compatible: boolean;\n isLegacyGasAPICompatible: boolean;\n fetchGasEstimates: (\n url: string,\n infuraAPIKey: string,\n clientId?: string,\n ) => Promise;\n fetchGasEstimatesUrl: string;\n fetchLegacyGasPriceEstimates: (\n url: string,\n infuraAPIKey: string,\n clientId?: string,\n ) => Promise;\n fetchLegacyGasPriceEstimatesUrl: string;\n // TODO: Replace `any` with type\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n fetchEthGasPriceEstimate: (ethQuery: any) => Promise;\n calculateTimeEstimate: (\n maxPriorityFeePerGas: string,\n maxFeePerGas: string,\n gasFeeEstimates: GasFeeEstimates,\n ) => EstimatedGasFeeTimeBounds;\n clientId: string | undefined;\n // TODO: Replace `any` with type\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n ethQuery: any;\n infuraAPIKey: string;\n nonRPCGasFeeApisDisabled?: boolean;\n};\n\n/**\n * Obtains a set of max base and priority fee estimates along with time estimates so that we\n * can present them to users when they are sending transactions or making swaps.\n *\n * @param args - The arguments.\n * @param args.isEIP1559Compatible - Governs whether or not we can use an EIP-1559-only method to\n * produce estimates.\n * @param args.isLegacyGasAPICompatible - Governs whether or not we can use a non-EIP-1559 method to\n * produce estimates (for instance, testnets do not support estimates altogether).\n * @param args.fetchGasEstimates - A function that fetches gas estimates using an EIP-1559-specific\n * API.\n * @param args.fetchGasEstimatesUrl - The URL for the API we can use to obtain EIP-1559-specific\n * estimates.\n * @param args.fetchLegacyGasPriceEstimates - A function that fetches gas estimates using an\n * non-EIP-1559-specific API.\n * @param args.fetchLegacyGasPriceEstimatesUrl - The URL for the API we can use to obtain\n * non-EIP-1559-specific estimates.\n * @param args.fetchEthGasPriceEstimate - A function that fetches gas estimates using\n * `eth_gasPrice`.\n * @param args.calculateTimeEstimate - A function that determine time estimate bounds.\n * @param args.clientId - An identifier that an API can use to know who is asking for estimates.\n * @param args.ethQuery - An EthQuery instance we can use to talk to Ethereum directly.\n * @param args.infuraAPIKey - Infura API key to use for requests to Infura.\n * @param args.nonRPCGasFeeApisDisabled - Whether to disable requests to the legacyAPIEndpoint and the EIP1559APIEndpoint\n * @returns The gas fee calculations.\n */\nexport default async function determineGasFeeCalculations(\n args: DetermineGasFeeCalculationsRequest,\n): Promise {\n try {\n return await getEstimatesUsingFallbacks(args);\n } catch (error) {\n if (error instanceof Error) {\n throw new Error(\n `Gas fee/price estimation failed. Message: ${error.message}`,\n );\n }\n\n throw error;\n }\n}\n\n/**\n * Retrieve the gas fee estimates using a series of fallback mechanisms.\n * @param request - The request object.\n * @returns The gas fee estimates.\n */\nasync function getEstimatesUsingFallbacks(\n request: DetermineGasFeeCalculationsRequest,\n): Promise {\n const {\n isEIP1559Compatible,\n isLegacyGasAPICompatible,\n nonRPCGasFeeApisDisabled,\n } = request;\n\n try {\n if (isEIP1559Compatible && !nonRPCGasFeeApisDisabled) {\n return await getEstimatesUsingFeeMarketEndpoint(request);\n }\n\n if (isLegacyGasAPICompatible && !nonRPCGasFeeApisDisabled) {\n return await getEstimatesUsingLegacyEndpoint(request);\n }\n\n throw new Error('Main gas fee/price estimation failed. Use fallback');\n } catch {\n return await getEstimatesUsingProvider(request);\n }\n}\n\n/**\n * Retrieve gas fee estimates using the EIP-1559 endpoint of the gas API.\n * @param request - The request object.\n * @returns The gas fee estimates.\n */\nasync function getEstimatesUsingFeeMarketEndpoint(\n request: DetermineGasFeeCalculationsRequest,\n): Promise {\n const {\n fetchGasEstimates,\n fetchGasEstimatesUrl,\n infuraAPIKey,\n clientId,\n calculateTimeEstimate,\n } = request;\n\n const estimates = await fetchGasEstimates(\n fetchGasEstimatesUrl,\n infuraAPIKey,\n clientId,\n );\n\n const { suggestedMaxPriorityFeePerGas, suggestedMaxFeePerGas } =\n estimates.medium;\n\n const estimatedGasFeeTimeBounds = calculateTimeEstimate(\n suggestedMaxPriorityFeePerGas,\n suggestedMaxFeePerGas,\n estimates,\n );\n\n return {\n gasFeeEstimates: estimates,\n estimatedGasFeeTimeBounds,\n gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET,\n };\n}\n\n/**\n * Retrieve gas fee estimates using the legacy endpoint of the gas API.\n * @param request - The request object.\n * @returns The gas fee estimates.\n */\nasync function getEstimatesUsingLegacyEndpoint(\n request: DetermineGasFeeCalculationsRequest,\n): Promise {\n const {\n fetchLegacyGasPriceEstimates,\n fetchLegacyGasPriceEstimatesUrl,\n infuraAPIKey,\n clientId,\n } = request;\n\n const estimates = await fetchLegacyGasPriceEstimates(\n fetchLegacyGasPriceEstimatesUrl,\n infuraAPIKey,\n clientId,\n );\n\n return {\n gasFeeEstimates: estimates,\n estimatedGasFeeTimeBounds: {},\n gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,\n };\n}\n\n/**\n * Retrieve gas fee estimates using an `eth_gasPrice` call to the RPC provider.\n * @param request - The request object.\n * @returns The gas fee estimates.\n */\nasync function getEstimatesUsingProvider(\n request: DetermineGasFeeCalculationsRequest,\n): Promise {\n const { ethQuery, fetchEthGasPriceEstimate } = request;\n\n const estimates = await fetchEthGasPriceEstimate(ethQuery);\n\n return {\n gasFeeEstimates: estimates,\n estimatedGasFeeTimeBounds: {},\n gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE,\n };\n}\n"]} -\ No newline at end of file -+{"version":3,"sources":["../src/GasFeeController.ts","../src/determineGasFeeCalculations.ts"],"names":["fetchGasEstimates","calculateTimeEstimate","fetchLegacyGasPriceEstimates","fetchEthGasPriceEstimate"],"mappings":";;;;;;;;;;;;AAKA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,OAAO,cAAc;AAUrB,SAAS,uCAAuC;AAEhD,SAAS,MAAM,cAAc;AAUtB,IAAM,mBAAmB;AA0BzB,IAAM,qBAAqB;AAAA,EAChC,YAAY;AAAA,EACZ,QAAQ;AAAA,EACR,cAAc;AAAA,EACd,MAAM;AACR;AAiGA,IAAM,WAAW;AAAA,EACf,0BAA0B;AAAA,IACxB,SAAS;AAAA,IACT,WAAW;AAAA,EACb;AAAA,EACA,iBAAiB,EAAE,SAAS,MAAM,WAAW,MAAM;AAAA,EACnD,2BAA2B,EAAE,SAAS,MAAM,WAAW,MAAM;AAAA,EAC7D,iBAAiB,EAAE,SAAS,MAAM,WAAW,MAAM;AAAA,EACnD,0BAA0B,EAAE,SAAS,MAAM,WAAW,MAAM;AAC9D;AAqDA,IAAM,OAAO;AA0Bb,IAAM,eAA4B;AAAA,EAChC,0BAA0B,CAAC;AAAA,EAC3B,iBAAiB,CAAC;AAAA,EAClB,2BAA2B,CAAC;AAAA,EAC5B,iBAAiB,mBAAmB;AAAA,EACpC,0BAA0B;AAC5B;AA9PA;AAmQO,IAAM,mBAAN,cAA+B,gCAIpC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgDA,YAAY;AAAA,IACV,WAAW;AAAA,IACX;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,GAYG;AACD,UAAM;AAAA,MACJ;AAAA,MACA;AAAA,MACA;AAAA,MACA,OAAO,EAAE,GAAG,cAAc,GAAG,MAAM;AAAA,IACrC,CAAC;AAuPH,uBAAM;AA5SN;AAsDE,SAAK,gBAAgB;AACrB,SAAK,kBAAkB,QAAQ;AAC/B,SAAK,aAAa,oBAAI,IAAI;AAC1B,SAAK,wCACH;AACF,SAAK,6CACH;AACF,SAAK,wCACH;AACF,uBAAK,cAAe;AACpB,SAAK,qBAAqB,GAAG,gBAAgB;AAC7C,SAAK,oBAAoB,GAAG,gBAAgB;AAC5C,SAAK,WAAW;AAChB,SAAK,eAAe;AAEpB,SAAK,WAAW,IAAI,SAAS,mBAAK,cAAL,UAAmB;AAEhD,QAAI,sBAAsB,YAAY;AACpC,WAAK,iBAAiB,WAAW;AACjC,yBAAmB,OAAO,2BAA2B;AACnD,cAAM,sBAAK,gEAAL,WAAmC;AAAA,MAC3C,CAAC;AAAA,IACH,OAAO;AACL,WAAK,iBAAiB,KAAK,gBAAgB;AAAA,QACzC;AAAA,MACF,EAAE,eAAe;AACjB,WAAK,gBAAgB;AAAA,QACnB;AAAA,QACA,OAAO,2BAA2B;AAChC,gBAAM,sBAAK,gEAAL,WAAmC;AAAA,QAC3C;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,eAAe;AACnB,QAAI,KAAK,WAAW,SAAS,GAAG;AAC9B,YAAM,SAAS,MAAM,KAAK,KAAK,UAAU;AACzC,WAAK,YAAY;AACjB,YAAM,KAAK,kCAAkC,OAAO,CAAC,CAAC;AACtD,aAAO,MAAM,CAAC,EAAE,QAAQ,CAAC,UAAU;AACjC,aAAK,WAAW,IAAI,KAAK;AAAA,MAC3B,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAEA,MAAM,qBAAqB,SAAsC;AAC/D,WAAO,MAAM,KAAK,yBAAyB,OAAO;AAAA,EACpD;AAAA,EAEA,MAAM,kCACJ,WACiB;AACjB,UAAM,aAAa,aAAa,OAAO;AAEvC,SAAK,WAAW,IAAI,UAAU;AAE9B,QAAI,KAAK,WAAW,SAAS,GAAG;AAC9B,YAAM,KAAK,yBAAyB;AACpC,WAAK,MAAM;AAAA,IACb;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,yBACJ,UAAsC,CAAC,GACjB;AACtB,UAAM,EAAE,oBAAoB,MAAM,gBAAgB,IAAI;AAEtD,QAAI,UACF,qBACA,0BACA;AAEF,QAAI,oBAAoB,QAAW;AACjC,YAAM,gBAAgB,KAAK,gBAAgB;AAAA,QACzC;AAAA,QACA;AAAA,MACF;AACA,iCAA2B,cAAc,cAAc,YAAY;AAEnE,uBAAiB,oBAAoB,cAAc,cAAc,OAAO;AAExE,UAAI;AACF,cAAM,SAAS,MAAM,KAAK,gBAAgB;AAAA,UACxC;AAAA,UACA;AAAA,QACF;AACA,8BAAsB,UAAU;AAAA,MAClC,QAAQ;AACN,8BAAsB;AAAA,MACxB;AACA,iBAAW,IAAI,SAAS,cAAc,QAAQ;AAAA,IAChD;AAEA,4BAAa,KAAK;AAElB,4DACE,KAAK,2CAA2C;AAElD,wCAAmB,oBAAoB,KAAK,cAAc;AAE1D,QAAI;AACF,oDAAwB,MAAM,KAAK,wBAAwB;AAAA,IAC7D,SAAS,GAAG;AACV,cAAQ,MAAM,CAAC;AACf,oDAAwB;AAAA,IAC1B;AAEA,UAAM,qBAAqB,MAAM,4BAA4B;AAAA,MAC3D;AAAA,MACA;AAAA,MACA;AAAA,MACA,sBAAsB,KAAK,mBAAmB;AAAA,QAC5C;AAAA,QACA,GAAG,cAAc;AAAA,MACnB;AAAA,MACA;AAAA,MACA,iCAAiC,KAAK,kBAAkB;AAAA,QACtD;AAAA,QACA,GAAG,cAAc;AAAA,MACnB;AAAA,MACA;AAAA,MACA;AAAA,MACA,UAAU,KAAK;AAAA,MACf;AAAA,MACA,cAAc,KAAK;AAAA,MACnB,0BAA0B,KAAK,MAAM;AAAA,IACvC,CAAC;AAED,QAAI,mBAAmB;AACrB,YAAM,UAAU,MAAM,cAAc;AACpC,WAAK,OAAO,CAAC,UAAU;AACrB,YAAI,KAAK,mBAAmB,SAAS;AACnC,gBAAM,kBAAkB,mBAAmB;AAC3C,gBAAM,4BACJ,mBAAmB;AACrB,gBAAM,kBAAkB,mBAAmB;AAAA,QAC7C;AACA,cAAM,6BAAN,MAAM,2BAA6B,CAAC;AACpC,cAAM,yBAAyB,OAAO,IAAI;AAAA,UACxC,iBAAiB,mBAAmB;AAAA,UACpC,2BACE,mBAAmB;AAAA,UACrB,iBAAiB,mBAAmB;AAAA,QACtC;AAAA,MACF,CAAC;AAAA,IACH;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,iBAAiB,WAAmB;AAClC,SAAK,WAAW,OAAO,SAAS;AAChC,QAAI,KAAK,WAAW,SAAS,GAAG;AAC9B,WAAK,YAAY;AAAA,IACnB;AAAA,EACF;AAAA,EAEA,cAAc;AACZ,QAAI,KAAK,YAAY;AACnB,oBAAc,KAAK,UAAU;AAAA,IAC/B;AACA,SAAK,WAAW,MAAM;AACtB,SAAK,WAAW;AAAA,EAClB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOS,UAAU;AACjB,UAAM,QAAQ;AACd,SAAK,YAAY;AAAA,EACnB;AAAA,EAEQ,QAAQ;AACd,QAAI,KAAK,YAAY;AACnB,oBAAc,KAAK,UAAU;AAAA,IAC/B;AAEA,SAAK,aAAa,YAAY,YAAY;AACxC,YAAM,cAAc,MAAM,KAAK,yBAAyB,CAAC;AAAA,IAC3D,GAAG,KAAK,aAAa;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,aAAa,iBAAwC;AACzD,UAAM,KAAK,yBAAyB,EAAE,gBAAgB,CAAC;AAAA,EACzD;AAAA,EAEQ,aAAa;AACnB,SAAK,OAAO,MAAM;AAChB,aAAO;AAAA,IACT,CAAC;AAAA,EACH;AAAA,EAEA,MAAc,0BAA0B;AACtC,UAAM,oCACJ,MAAM,KAAK,sCAAsC;AACnD,UAAM,oCACJ,KAAK,wCAAwC,KAAK;AAEpD,WACE,qCAAqC;AAAA,EAEzC;AAAA,EAEA,gBACE,sBACA,cACmD;AACnD,QACE,CAAC,KAAK,MAAM,mBACZ,KAAK,MAAM,oBAAoB,mBAAmB,YAClD;AACA,aAAO,CAAC;AAAA,IACV;AACA,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA,KAAK,MAAM;AAAA,IACb;AAAA,EACF;AAAA,EAaA,yBAAyB;AACvB,SAAK,OAAO,CAAC,UAAU;AACrB,YAAM,2BAA2B;AAAA,IACnC,CAAC;AAAA,EACH;AAAA,EAEA,0BAA0B;AACxB,SAAK,OAAO,CAAC,UAAU;AACrB,YAAM,2BAA2B;AAAA,IACnC,CAAC;AAAA,EACH;AACF;AAlUE;AA4SM;AAAA,kCAA6B,eAAC,wBAAsC;AACxE,QAAM,aAAa,uBAAuB,eAAe;AAEzD,MAAI,eAAe,KAAK,gBAAgB;AACtC,SAAK,WAAW,IAAI,SAAS,mBAAK,cAAL,UAAmB;AAChD,UAAM,KAAK,aAAa;AAExB,SAAK,iBAAiB;AAAA,EACxB;AACF;AAeF,IAAO,2BAAQ;;;ACliBf,eAAO,4BACL,MAC6B;AAC7B,MAAI;AACF,WAAO,MAAM,2BAA2B,IAAI;AAAA,EAC9C,SAAS,OAAO;AACd,QAAI,iBAAiB,OAAO;AAC1B,YAAM,IAAI;AAAA,QACR,6CAA6C,MAAM,OAAO;AAAA,MAC5D;AAAA,IACF;AAEA,UAAM;AAAA,EACR;AACF;AAOA,eAAe,2BACb,SAC6B;AAC7B,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AAEJ,MAAI;AACF,QAAI,uBAAuB,CAAC,0BAA0B;AACpD,aAAO,MAAM,mCAAmC,OAAO;AAAA,IACzD;AAEA,QAAI,4BAA4B,CAAC,0BAA0B;AACzD,aAAO,MAAM,gCAAgC,OAAO;AAAA,IACtD;AAEA,UAAM,IAAI,MAAM,oDAAoD;AAAA,EACtE,QAAQ;AACN,WAAO,MAAM,0BAA0B,OAAO;AAAA,EAChD;AACF;AAOA,eAAe,mCACb,SAC6B;AAC7B,QAAM;AAAA,IACJ,mBAAAA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,uBAAAC;AAAA,EACF,IAAI;AAEJ,QAAM,YAAY,MAAMD;AAAA,IACtB;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,QAAM,EAAE,+BAA+B,sBAAsB,IAC3D,UAAU;AAEZ,QAAM,4BAA4BC;AAAA,IAChC;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,SAAO;AAAA,IACL,iBAAiB;AAAA,IACjB;AAAA,IACA,iBAAiB,mBAAmB;AAAA,EACtC;AACF;AAOA,eAAe,gCACb,SAC6B;AAC7B,QAAM;AAAA,IACJ,8BAAAC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AAEJ,QAAM,YAAY,MAAMA;AAAA,IACtB;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,SAAO;AAAA,IACL,iBAAiB;AAAA,IACjB,2BAA2B,CAAC;AAAA,IAC5B,iBAAiB,mBAAmB;AAAA,EACtC;AACF;AAOA,eAAe,0BACb,SAC6B;AAC7B,QAAM,EAAE,UAAU,0BAAAC,0BAAyB,IAAI;AAE/C,QAAM,YAAY,MAAMA,0BAAyB,QAAQ;AAEzD,SAAO;AAAA,IACL,iBAAiB;AAAA,IACjB,2BAA2B,CAAC;AAAA,IAC5B,iBAAiB,mBAAmB;AAAA,EACtC;AACF","sourcesContent":["import type {\n ControllerGetStateAction,\n ControllerStateChangeEvent,\n RestrictedControllerMessenger,\n} from '@metamask/base-controller';\nimport {\n convertHexToDecimal,\n safelyExecute,\n toHex,\n} from '@metamask/controller-utils';\nimport EthQuery from '@metamask/eth-query';\nimport type {\n NetworkClientId,\n NetworkControllerGetEIP1559CompatibilityAction,\n NetworkControllerGetNetworkClientByIdAction,\n NetworkControllerGetStateAction,\n NetworkControllerNetworkDidChangeEvent,\n NetworkState,\n ProviderProxy,\n} from '@metamask/network-controller';\nimport { StaticIntervalPollingController } from '@metamask/polling-controller';\nimport type { Hex } from '@metamask/utils';\nimport { v1 as random } from 'uuid';\n\nimport determineGasFeeCalculations from './determineGasFeeCalculations';\nimport {\n calculateTimeEstimate,\n fetchGasEstimates,\n fetchLegacyGasPriceEstimates,\n fetchEthGasPriceEstimate,\n} from './gas-util';\n\nexport const LEGACY_GAS_PRICES_API_URL = 'https://gas.api.infura.io';\n\nexport type unknownString = 'unknown';\n\n// Fee Market describes the way gas is set after the london hardfork, and was\n// defined by EIP-1559.\nexport type FeeMarketEstimateType = 'fee-market';\n// Legacy describes gasPrice estimates from before london hardfork, when the\n// user is connected to mainnet and are presented with fast/average/slow\n// estimate levels to choose from.\nexport type LegacyEstimateType = 'legacy';\n// EthGasPrice describes a gasPrice estimate received from eth_gasPrice. Post\n// london this value should only be used for legacy type transactions when on\n// networks that support EIP-1559. This type of estimate is the most accurate\n// to display on custom networks that don't support EIP-1559.\nexport type EthGasPriceEstimateType = 'eth_gasPrice';\n// NoEstimate describes the state of the controller before receiving its first\n// estimate.\nexport type NoEstimateType = 'none';\n\n/**\n * Indicates which type of gasEstimate the controller is currently returning.\n * This is useful as a way of asserting that the shape of gasEstimates matches\n * expectations. NONE is a special case indicating that no previous gasEstimate\n * has been fetched.\n */\nexport const GAS_ESTIMATE_TYPES = {\n FEE_MARKET: 'fee-market' as FeeMarketEstimateType,\n LEGACY: 'legacy' as LegacyEstimateType,\n ETH_GASPRICE: 'eth_gasPrice' as EthGasPriceEstimateType,\n NONE: 'none' as NoEstimateType,\n};\n\nexport type GasEstimateType =\n | FeeMarketEstimateType\n | EthGasPriceEstimateType\n | LegacyEstimateType\n | NoEstimateType;\n\nexport type EstimatedGasFeeTimeBounds = {\n lowerTimeBound: number | null;\n upperTimeBound: number | unknownString;\n};\n\n/**\n * @type EthGasPriceEstimate\n *\n * A single gas price estimate for networks and accounts that don't support EIP-1559\n * This estimate comes from eth_gasPrice but is converted to dec gwei to match other\n * return values\n * @property gasPrice - A GWEI dec string\n */\n\nexport type EthGasPriceEstimate = {\n gasPrice: string;\n};\n\n/**\n * @type LegacyGasPriceEstimate\n *\n * A set of gas price estimates for networks and accounts that don't support EIP-1559\n * These estimates include low, medium and high all as strings representing gwei in\n * decimal format.\n * @property high - gasPrice, in decimal gwei string format, suggested for fast inclusion\n * @property medium - gasPrice, in decimal gwei string format, suggested for avg inclusion\n * @property low - gasPrice, in decimal gwei string format, suggested for slow inclusion\n */\nexport type LegacyGasPriceEstimate = {\n high: string;\n medium: string;\n low: string;\n};\n\n/**\n * @type Eip1559GasFee\n *\n * Data necessary to provide an estimate of a gas fee with a specific tip\n * @property minWaitTimeEstimate - The fastest the transaction will take, in milliseconds\n * @property maxWaitTimeEstimate - The slowest the transaction will take, in milliseconds\n * @property suggestedMaxPriorityFeePerGas - A suggested \"tip\", a GWEI hex number\n * @property suggestedMaxFeePerGas - A suggested max fee, the most a user will pay. a GWEI hex number\n */\nexport type Eip1559GasFee = {\n minWaitTimeEstimate: number; // a time duration in milliseconds\n maxWaitTimeEstimate: number; // a time duration in milliseconds\n suggestedMaxPriorityFeePerGas: string; // a GWEI decimal number\n suggestedMaxFeePerGas: string; // a GWEI decimal number\n};\n\n/**\n * @type GasFeeEstimates\n *\n * Data necessary to provide multiple GasFee estimates, and supporting information, to the user\n * @property low - A GasFee for a minimum necessary combination of tip and maxFee\n * @property medium - A GasFee for a recommended combination of tip and maxFee\n * @property high - A GasFee for a high combination of tip and maxFee\n * @property estimatedBaseFee - An estimate of what the base fee will be for the pending/next block. A GWEI dec number\n * @property networkCongestion - A normalized number that can be used to gauge the congestion\n * level of the network, with 0 meaning not congested and 1 meaning extremely congested\n */\nexport type GasFeeEstimates = SourcedGasFeeEstimates | FallbackGasFeeEstimates;\n\ntype SourcedGasFeeEstimates = {\n low: Eip1559GasFee;\n medium: Eip1559GasFee;\n high: Eip1559GasFee;\n estimatedBaseFee: string;\n historicalBaseFeeRange: [string, string];\n baseFeeTrend: 'up' | 'down' | 'level';\n latestPriorityFeeRange: [string, string];\n historicalPriorityFeeRange: [string, string];\n priorityFeeTrend: 'up' | 'down' | 'level';\n networkCongestion: number;\n};\n\ntype FallbackGasFeeEstimates = {\n low: Eip1559GasFee;\n medium: Eip1559GasFee;\n high: Eip1559GasFee;\n estimatedBaseFee: string;\n historicalBaseFeeRange: null;\n baseFeeTrend: null;\n latestPriorityFeeRange: null;\n historicalPriorityFeeRange: null;\n priorityFeeTrend: null;\n networkCongestion: null;\n};\n\nconst metadata = {\n gasFeeEstimatesByChainId: {\n persist: true,\n anonymous: false,\n },\n gasFeeEstimates: { persist: true, anonymous: false },\n estimatedGasFeeTimeBounds: { persist: true, anonymous: false },\n gasEstimateType: { persist: true, anonymous: false },\n nonRPCGasFeeApisDisabled: { persist: true, anonymous: false },\n};\n\nexport type GasFeeStateEthGasPrice = {\n gasFeeEstimates: EthGasPriceEstimate;\n estimatedGasFeeTimeBounds: Record;\n gasEstimateType: EthGasPriceEstimateType;\n};\n\nexport type GasFeeStateFeeMarket = {\n gasFeeEstimates: GasFeeEstimates;\n estimatedGasFeeTimeBounds: EstimatedGasFeeTimeBounds | Record;\n gasEstimateType: FeeMarketEstimateType;\n};\n\nexport type GasFeeStateLegacy = {\n gasFeeEstimates: LegacyGasPriceEstimate;\n estimatedGasFeeTimeBounds: Record;\n gasEstimateType: LegacyEstimateType;\n};\n\nexport type GasFeeStateNoEstimates = {\n gasFeeEstimates: Record;\n estimatedGasFeeTimeBounds: Record;\n gasEstimateType: NoEstimateType;\n};\n\nexport type FetchGasFeeEstimateOptions = {\n shouldUpdateState?: boolean;\n networkClientId?: NetworkClientId;\n};\n\n/**\n * @type GasFeeState\n *\n * Gas Fee controller state\n * @property gasFeeEstimates - Gas fee estimate data based on new EIP-1559 properties\n * @property estimatedGasFeeTimeBounds - Estimates representing the minimum and maximum\n */\nexport type SingleChainGasFeeState =\n | GasFeeStateEthGasPrice\n | GasFeeStateFeeMarket\n | GasFeeStateLegacy\n | GasFeeStateNoEstimates;\n\nexport type GasFeeEstimatesByChainId = {\n gasFeeEstimatesByChainId?: Record;\n};\n\nexport type GasFeeState = GasFeeEstimatesByChainId &\n SingleChainGasFeeState & {\n nonRPCGasFeeApisDisabled?: boolean;\n };\n\nconst name = 'GasFeeController';\n\nexport type GasFeeStateChange = ControllerStateChangeEvent<\n typeof name,\n GasFeeState\n>;\n\nexport type GetGasFeeState = ControllerGetStateAction;\n\nexport type GasFeeControllerActions = GetGasFeeState;\n\nexport type GasFeeControllerEvents = GasFeeStateChange;\n\ntype AllowedActions =\n | NetworkControllerGetStateAction\n | NetworkControllerGetNetworkClientByIdAction\n | NetworkControllerGetEIP1559CompatibilityAction;\n\ntype GasFeeMessenger = RestrictedControllerMessenger<\n typeof name,\n GasFeeControllerActions | AllowedActions,\n GasFeeControllerEvents | NetworkControllerNetworkDidChangeEvent,\n AllowedActions['type'],\n NetworkControllerNetworkDidChangeEvent['type']\n>;\n\nconst defaultState: GasFeeState = {\n gasFeeEstimatesByChainId: {},\n gasFeeEstimates: {},\n estimatedGasFeeTimeBounds: {},\n gasEstimateType: GAS_ESTIMATE_TYPES.NONE,\n nonRPCGasFeeApisDisabled: false,\n};\n\n/**\n * Controller that retrieves gas fee estimate data and polls for updated data on a set interval\n */\nexport class GasFeeController extends StaticIntervalPollingController<\n typeof name,\n GasFeeState,\n GasFeeMessenger\n> {\n private intervalId?: ReturnType;\n\n private readonly intervalDelay;\n\n private readonly pollTokens: Set;\n\n private readonly legacyAPIEndpoint: string;\n\n private readonly EIP1559APIEndpoint: string;\n\n private readonly getCurrentNetworkEIP1559Compatibility;\n\n private readonly getCurrentNetworkLegacyGasAPICompatibility;\n\n private readonly getCurrentAccountEIP1559Compatibility;\n\n private readonly infuraAPIKey: string;\n\n private currentChainId;\n\n private ethQuery?: EthQuery;\n\n private readonly clientId?: string;\n\n #getProvider: () => ProviderProxy;\n\n /**\n * Creates a GasFeeController instance.\n *\n * @param options - The controller options.\n * @param options.interval - The time in milliseconds to wait between polls.\n * @param options.messenger - The controller messenger.\n * @param options.state - The initial state.\n * @param options.getCurrentNetworkEIP1559Compatibility - Determines whether or not the current\n * network is EIP-1559 compatible.\n * @param options.getCurrentNetworkLegacyGasAPICompatibility - Determines whether or not the\n * current network is compatible with the legacy gas price API.\n * @param options.getCurrentAccountEIP1559Compatibility - Determines whether or not the current\n * account is EIP-1559 compatible.\n * @param options.getChainId - Returns the current chain ID.\n * @param options.getProvider - Returns a network provider for the current network.\n * @param options.onNetworkDidChange - A function for registering an event handler for the\n * network state change event.\n * @param options.clientId - The client ID used to identify to the gas estimation API who is\n * asking for estimates.\n * @param options.infuraAPIKey - The Infura API key used for infura API requests.\n */\n constructor({\n interval = 15000,\n messenger,\n state,\n getCurrentNetworkEIP1559Compatibility,\n getCurrentAccountEIP1559Compatibility,\n getChainId,\n getCurrentNetworkLegacyGasAPICompatibility,\n getProvider,\n onNetworkDidChange,\n clientId,\n infuraAPIKey,\n }: {\n interval?: number;\n messenger: GasFeeMessenger;\n state?: GasFeeState;\n getCurrentNetworkEIP1559Compatibility: () => Promise;\n getCurrentNetworkLegacyGasAPICompatibility: () => boolean;\n getCurrentAccountEIP1559Compatibility?: () => boolean;\n getChainId?: () => Hex;\n getProvider: () => ProviderProxy;\n onNetworkDidChange?: (listener: (state: NetworkState) => void) => void;\n clientId?: string;\n infuraAPIKey: string;\n }) {\n super({\n name,\n metadata,\n messenger,\n state: { ...defaultState, ...state },\n });\n this.intervalDelay = interval;\n this.setIntervalLength(interval);\n this.pollTokens = new Set();\n this.getCurrentNetworkEIP1559Compatibility =\n getCurrentNetworkEIP1559Compatibility;\n this.getCurrentNetworkLegacyGasAPICompatibility =\n getCurrentNetworkLegacyGasAPICompatibility;\n this.getCurrentAccountEIP1559Compatibility =\n getCurrentAccountEIP1559Compatibility;\n this.#getProvider = getProvider;\n this.EIP1559APIEndpoint = `${LEGACY_GAS_PRICES_API_URL}/networks//suggestedGasFees`;\n this.legacyAPIEndpoint = `${LEGACY_GAS_PRICES_API_URL}/networks//gasPrices`;\n this.clientId = clientId;\n this.infuraAPIKey = infuraAPIKey;\n\n this.ethQuery = new EthQuery(this.#getProvider());\n\n if (onNetworkDidChange && getChainId) {\n this.currentChainId = getChainId();\n onNetworkDidChange(async (networkControllerState) => {\n await this.#onNetworkControllerDidChange(networkControllerState);\n });\n } else {\n this.currentChainId = this.messagingSystem.call(\n 'NetworkController:getState',\n ).providerConfig.chainId;\n this.messagingSystem.subscribe(\n 'NetworkController:networkDidChange',\n async (networkControllerState) => {\n await this.#onNetworkControllerDidChange(networkControllerState);\n },\n );\n }\n }\n\n async resetPolling() {\n if (this.pollTokens.size !== 0) {\n const tokens = Array.from(this.pollTokens);\n this.stopPolling();\n await this.getGasFeeEstimatesAndStartPolling(tokens[0]);\n tokens.slice(1).forEach((token) => {\n this.pollTokens.add(token);\n });\n }\n }\n\n async fetchGasFeeEstimates(options?: FetchGasFeeEstimateOptions) {\n return await this._fetchGasFeeEstimateData(options);\n }\n\n async getGasFeeEstimatesAndStartPolling(\n pollToken: string | undefined,\n ): Promise {\n const _pollToken = pollToken || random();\n\n this.pollTokens.add(_pollToken);\n\n if (this.pollTokens.size === 1) {\n await this._fetchGasFeeEstimateData();\n this._poll();\n }\n\n return _pollToken;\n }\n\n /**\n * Gets and sets gasFeeEstimates in state.\n *\n * @param options - The gas fee estimate options.\n * @param options.shouldUpdateState - Determines whether the state should be updated with the\n * updated gas estimates.\n * @returns The gas fee estimates.\n */\n async _fetchGasFeeEstimateData(\n options: FetchGasFeeEstimateOptions = {},\n ): Promise {\n const { shouldUpdateState = true, networkClientId } = options;\n\n let ethQuery,\n isEIP1559Compatible,\n isLegacyGasAPICompatible,\n decimalChainId: number;\n\n if (networkClientId !== undefined) {\n const networkClient = this.messagingSystem.call(\n 'NetworkController:getNetworkClientById',\n networkClientId,\n );\n isLegacyGasAPICompatible = networkClient.configuration.chainId === '0x38';\n\n decimalChainId = convertHexToDecimal(networkClient.configuration.chainId);\n\n try {\n const result = await this.messagingSystem.call(\n 'NetworkController:getEIP1559Compatibility',\n networkClientId,\n );\n isEIP1559Compatible = result || false;\n } catch {\n isEIP1559Compatible = false;\n }\n ethQuery = new EthQuery(networkClient.provider);\n }\n\n ethQuery ??= this.ethQuery;\n\n isLegacyGasAPICompatible ??=\n this.getCurrentNetworkLegacyGasAPICompatibility();\n\n decimalChainId ??= convertHexToDecimal(this.currentChainId);\n\n try {\n isEIP1559Compatible ??= await this.getEIP1559Compatibility();\n } catch (e) {\n console.error(e);\n isEIP1559Compatible ??= false;\n }\n\n const gasFeeCalculations = await determineGasFeeCalculations({\n isEIP1559Compatible,\n isLegacyGasAPICompatible,\n fetchGasEstimates,\n fetchGasEstimatesUrl: this.EIP1559APIEndpoint.replace(\n '',\n `${decimalChainId}`,\n ),\n fetchLegacyGasPriceEstimates,\n fetchLegacyGasPriceEstimatesUrl: this.legacyAPIEndpoint.replace(\n '',\n `${decimalChainId}`,\n ),\n fetchEthGasPriceEstimate,\n calculateTimeEstimate,\n clientId: this.clientId,\n ethQuery,\n infuraAPIKey: this.infuraAPIKey,\n nonRPCGasFeeApisDisabled: this.state.nonRPCGasFeeApisDisabled,\n });\n\n if (shouldUpdateState) {\n const chainId = toHex(decimalChainId);\n this.update((state) => {\n if (this.currentChainId === chainId) {\n state.gasFeeEstimates = gasFeeCalculations.gasFeeEstimates;\n state.estimatedGasFeeTimeBounds =\n gasFeeCalculations.estimatedGasFeeTimeBounds;\n state.gasEstimateType = gasFeeCalculations.gasEstimateType;\n }\n state.gasFeeEstimatesByChainId ??= {};\n state.gasFeeEstimatesByChainId[chainId] = {\n gasFeeEstimates: gasFeeCalculations.gasFeeEstimates,\n estimatedGasFeeTimeBounds:\n gasFeeCalculations.estimatedGasFeeTimeBounds,\n gasEstimateType: gasFeeCalculations.gasEstimateType,\n } as SingleChainGasFeeState;\n });\n }\n\n return gasFeeCalculations;\n }\n\n /**\n * Remove the poll token, and stop polling if the set of poll tokens is empty.\n *\n * @param pollToken - The poll token to disconnect.\n */\n disconnectPoller(pollToken: string) {\n this.pollTokens.delete(pollToken);\n if (this.pollTokens.size === 0) {\n this.stopPolling();\n }\n }\n\n stopPolling() {\n if (this.intervalId) {\n clearInterval(this.intervalId);\n }\n this.pollTokens.clear();\n this.resetState();\n }\n\n /**\n * Prepare to discard this controller.\n *\n * This stops any active polling.\n */\n override destroy() {\n super.destroy();\n this.stopPolling();\n }\n\n private _poll() {\n if (this.intervalId) {\n clearInterval(this.intervalId);\n }\n\n this.intervalId = setInterval(async () => {\n await safelyExecute(() => this._fetchGasFeeEstimateData());\n }, this.intervalDelay);\n }\n\n /**\n * Fetching token list from the Token Service API.\n *\n * @private\n * @param networkClientId - The ID of the network client triggering the fetch.\n * @returns A promise that resolves when this operation completes.\n */\n async _executePoll(networkClientId: string): Promise {\n await this._fetchGasFeeEstimateData({ networkClientId });\n }\n\n private resetState() {\n this.update(() => {\n return defaultState;\n });\n }\n\n private async getEIP1559Compatibility() {\n const currentNetworkIsEIP1559Compatible =\n await this.getCurrentNetworkEIP1559Compatibility();\n const currentAccountIsEIP1559Compatible =\n this.getCurrentAccountEIP1559Compatibility?.() ?? true;\n\n return (\n currentNetworkIsEIP1559Compatible && currentAccountIsEIP1559Compatible\n );\n }\n\n getTimeEstimate(\n maxPriorityFeePerGas: string,\n maxFeePerGas: string,\n ): EstimatedGasFeeTimeBounds | Record {\n if (\n !this.state.gasFeeEstimates ||\n this.state.gasEstimateType !== GAS_ESTIMATE_TYPES.FEE_MARKET\n ) {\n return {};\n }\n return calculateTimeEstimate(\n maxPriorityFeePerGas,\n maxFeePerGas,\n this.state.gasFeeEstimates,\n );\n }\n\n async #onNetworkControllerDidChange(networkControllerState: NetworkState) {\n const newChainId = networkControllerState.providerConfig.chainId;\n\n if (newChainId !== this.currentChainId) {\n this.ethQuery = new EthQuery(this.#getProvider());\n await this.resetPolling();\n\n this.currentChainId = newChainId;\n }\n }\n\n enableNonRPCGasFeeApis() {\n this.update((state) => {\n state.nonRPCGasFeeApisDisabled = false;\n });\n }\n\n disableNonRPCGasFeeApis() {\n this.update((state) => {\n state.nonRPCGasFeeApisDisabled = true;\n });\n }\n}\n\nexport default GasFeeController;\n","import type {\n EstimatedGasFeeTimeBounds,\n EthGasPriceEstimate,\n GasFeeEstimates,\n GasFeeState as GasFeeCalculations,\n LegacyGasPriceEstimate,\n} from './GasFeeController';\nimport { GAS_ESTIMATE_TYPES } from './GasFeeController';\n\ntype DetermineGasFeeCalculationsRequest = {\n isEIP1559Compatible: boolean;\n isLegacyGasAPICompatible: boolean;\n fetchGasEstimates: (\n url: string,\n infuraAPIKey: string,\n clientId?: string,\n ) => Promise;\n fetchGasEstimatesUrl: string;\n fetchLegacyGasPriceEstimates: (\n url: string,\n infuraAPIKey: string,\n clientId?: string,\n ) => Promise;\n fetchLegacyGasPriceEstimatesUrl: string;\n // TODO: Replace `any` with type\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n fetchEthGasPriceEstimate: (ethQuery: any) => Promise;\n calculateTimeEstimate: (\n maxPriorityFeePerGas: string,\n maxFeePerGas: string,\n gasFeeEstimates: GasFeeEstimates,\n ) => EstimatedGasFeeTimeBounds;\n clientId: string | undefined;\n // TODO: Replace `any` with type\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n ethQuery: any;\n infuraAPIKey: string;\n nonRPCGasFeeApisDisabled?: boolean;\n};\n\n/**\n * Obtains a set of max base and priority fee estimates along with time estimates so that we\n * can present them to users when they are sending transactions or making swaps.\n *\n * @param args - The arguments.\n * @param args.isEIP1559Compatible - Governs whether or not we can use an EIP-1559-only method to\n * produce estimates.\n * @param args.isLegacyGasAPICompatible - Governs whether or not we can use a non-EIP-1559 method to\n * produce estimates (for instance, testnets do not support estimates altogether).\n * @param args.fetchGasEstimates - A function that fetches gas estimates using an EIP-1559-specific\n * API.\n * @param args.fetchGasEstimatesUrl - The URL for the API we can use to obtain EIP-1559-specific\n * estimates.\n * @param args.fetchLegacyGasPriceEstimates - A function that fetches gas estimates using an\n * non-EIP-1559-specific API.\n * @param args.fetchLegacyGasPriceEstimatesUrl - The URL for the API we can use to obtain\n * non-EIP-1559-specific estimates.\n * @param args.fetchEthGasPriceEstimate - A function that fetches gas estimates using\n * `eth_gasPrice`.\n * @param args.calculateTimeEstimate - A function that determine time estimate bounds.\n * @param args.clientId - An identifier that an API can use to know who is asking for estimates.\n * @param args.ethQuery - An EthQuery instance we can use to talk to Ethereum directly.\n * @param args.infuraAPIKey - Infura API key to use for requests to Infura.\n * @param args.nonRPCGasFeeApisDisabled - Whether to disable requests to the legacyAPIEndpoint and the EIP1559APIEndpoint\n * @returns The gas fee calculations.\n */\nexport default async function determineGasFeeCalculations(\n args: DetermineGasFeeCalculationsRequest,\n): Promise {\n try {\n return await getEstimatesUsingFallbacks(args);\n } catch (error) {\n if (error instanceof Error) {\n throw new Error(\n `Gas fee/price estimation failed. Message: ${error.message}`,\n );\n }\n\n throw error;\n }\n}\n\n/**\n * Retrieve the gas fee estimates using a series of fallback mechanisms.\n * @param request - The request object.\n * @returns The gas fee estimates.\n */\nasync function getEstimatesUsingFallbacks(\n request: DetermineGasFeeCalculationsRequest,\n): Promise {\n const {\n isEIP1559Compatible,\n isLegacyGasAPICompatible,\n nonRPCGasFeeApisDisabled,\n } = request;\n\n try {\n if (isEIP1559Compatible && !nonRPCGasFeeApisDisabled) {\n return await getEstimatesUsingFeeMarketEndpoint(request);\n }\n\n if (isLegacyGasAPICompatible && !nonRPCGasFeeApisDisabled) {\n return await getEstimatesUsingLegacyEndpoint(request);\n }\n\n throw new Error('Main gas fee/price estimation failed. Use fallback');\n } catch {\n return await getEstimatesUsingProvider(request);\n }\n}\n\n/**\n * Retrieve gas fee estimates using the EIP-1559 endpoint of the gas API.\n * @param request - The request object.\n * @returns The gas fee estimates.\n */\nasync function getEstimatesUsingFeeMarketEndpoint(\n request: DetermineGasFeeCalculationsRequest,\n): Promise {\n const {\n fetchGasEstimates,\n fetchGasEstimatesUrl,\n infuraAPIKey,\n clientId,\n calculateTimeEstimate,\n } = request;\n\n const estimates = await fetchGasEstimates(\n fetchGasEstimatesUrl,\n infuraAPIKey,\n clientId,\n );\n\n const { suggestedMaxPriorityFeePerGas, suggestedMaxFeePerGas } =\n estimates.medium;\n\n const estimatedGasFeeTimeBounds = calculateTimeEstimate(\n suggestedMaxPriorityFeePerGas,\n suggestedMaxFeePerGas,\n estimates,\n );\n\n return {\n gasFeeEstimates: estimates,\n estimatedGasFeeTimeBounds,\n gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET,\n };\n}\n\n/**\n * Retrieve gas fee estimates using the legacy endpoint of the gas API.\n * @param request - The request object.\n * @returns The gas fee estimates.\n */\nasync function getEstimatesUsingLegacyEndpoint(\n request: DetermineGasFeeCalculationsRequest,\n): Promise {\n const {\n fetchLegacyGasPriceEstimates,\n fetchLegacyGasPriceEstimatesUrl,\n infuraAPIKey,\n clientId,\n } = request;\n\n const estimates = await fetchLegacyGasPriceEstimates(\n fetchLegacyGasPriceEstimatesUrl,\n infuraAPIKey,\n clientId,\n );\n\n return {\n gasFeeEstimates: estimates,\n estimatedGasFeeTimeBounds: {},\n gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,\n };\n}\n\n/**\n * Retrieve gas fee estimates using an `eth_gasPrice` call to the RPC provider.\n * @param request - The request object.\n * @returns The gas fee estimates.\n */\nasync function getEstimatesUsingProvider(\n request: DetermineGasFeeCalculationsRequest,\n): Promise {\n const { ethQuery, fetchEthGasPriceEstimate } = request;\n\n const estimates = await fetchEthGasPriceEstimate(ethQuery);\n\n return {\n gasFeeEstimates: estimates,\n estimatedGasFeeTimeBounds: {},\n gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE,\n };\n}\n"]} -diff --git a/node_modules/@metamask/gas-fee-controller/dist/chunk-Q2YPK5SL.js b/node_modules/@metamask/gas-fee-controller/dist/chunk-Q2YPK5SL.js -index 154d3ea..1ca1043 100644 ---- a/node_modules/@metamask/gas-fee-controller/dist/chunk-Q2YPK5SL.js -+++ b/node_modules/@metamask/gas-fee-controller/dist/chunk-Q2YPK5SL.js -@@ -35,11 +35,11 @@ function normalizeGWEIDecimalNumbers(n) { - const numberAsGWEI = _controllerutils.weiHexToGweiDec.call(void 0, numberAsWEIHex); - return numberAsGWEI; - } --async function fetchGasEstimates(url, infuraAPIKey, clientId) { -- const infuraAuthToken = buildInfuraAuthToken(infuraAPIKey); -- const estimates = await _controllerutils.handleFetch.call(void 0, url, { -- headers: getHeaders(infuraAuthToken, clientId) -- }); -+async function fetchGasEstimates(url, clientId) { -+ const estimates = await _controllerutils.handleFetch.call(void 0, -+ url, -+ clientId ? { headers: makeClientIdHeader(clientId) } : void 0 -+ ); - return { - low: { - ...estimates.low, -@@ -77,14 +77,16 @@ async function fetchGasEstimates(url, infuraAPIKey, clientId) { - networkCongestion: estimates.networkCongestion - }; - } --async function fetchLegacyGasPriceEstimates(url, infuraAPIKey, clientId) { -- const infuraAuthToken = buildInfuraAuthToken(infuraAPIKey); -+async function fetchLegacyGasPriceEstimates(url, clientId) { - const result = await _controllerutils.handleFetch.call(void 0, url, { - referrer: url, - referrerPolicy: "no-referrer-when-downgrade", - method: "GET", - mode: "cors", -- headers: getHeaders(infuraAuthToken, clientId) -+ headers: { -+ "Content-Type": "application/json", -+ ...clientId && makeClientIdHeader(clientId) -+ } - }); - return { - low: result.SafeGasPrice, -@@ -139,20 +141,6 @@ function calculateTimeEstimate(maxPriorityFeePerGas, maxFeePerGas, gasFeeEstimat - upperTimeBound - }; - } --function buildInfuraAuthToken(infuraAPIKey) { -- return Buffer.from(`${infuraAPIKey}:`).toString("base64"); --} --function getHeaders(infuraAuthToken, clientId) { -- return { -- "Content-Type": "application/json", -- Authorization: `Basic ${infuraAuthToken}`, -- // Only add the clientId header if clientId is a non-empty string -- ...clientId?.trim() ? makeClientIdHeader(clientId) : {} -- }; --} -- -- -- - - - -diff --git a/node_modules/@metamask/gas-fee-controller/dist/index.js b/node_modules/@metamask/gas-fee-controller/dist/index.js -index 499a7d5..53d0fc3 100644 ---- a/node_modules/@metamask/gas-fee-controller/dist/index.js -+++ b/node_modules/@metamask/gas-fee-controller/dist/index.js -@@ -8,5 +8,5 @@ require('./chunk-Q2YPK5SL.js'); - - - --exports.GAS_API_BASE_URL = _chunkH5WHAYLIjs.GAS_API_BASE_URL; exports.GAS_ESTIMATE_TYPES = _chunkH5WHAYLIjs.GAS_ESTIMATE_TYPES; exports.GasFeeController = _chunkH5WHAYLIjs.GasFeeController; -+exports.LEGACY_GAS_PRICES_API_URL = _chunkH5WHAYLIjs.LEGACY_GAS_PRICES_API_URL; exports.GAS_ESTIMATE_TYPES = _chunkH5WHAYLIjs.GAS_ESTIMATE_TYPES; exports.GasFeeController = _chunkH5WHAYLIjs.GasFeeController; - //# sourceMappingURL=index.js.map -diff --git a/node_modules/@metamask/gas-fee-controller/dist/index.mjs b/node_modules/@metamask/gas-fee-controller/dist/index.mjs -index 47fbe48..f55517b 100644 ---- a/node_modules/@metamask/gas-fee-controller/dist/index.mjs -+++ b/node_modules/@metamask/gas-fee-controller/dist/index.mjs -@@ -1,11 +1,11 @@ - import { -- GAS_API_BASE_URL, -+ LEGACY_GAS_PRICES_API_URL, - GAS_ESTIMATE_TYPES, - GasFeeController - } from "./chunk-BEVZS3YV.mjs"; - import "./chunk-KORLXV32.mjs"; - export { -- GAS_API_BASE_URL, -+ LEGACY_GAS_PRICES_API_URL, - GAS_ESTIMATE_TYPES, - GasFeeController - }; -diff --git a/node_modules/@metamask/gas-fee-controller/dist/types/GasFeeController.d.ts b/node_modules/@metamask/gas-fee-controller/dist/types/GasFeeController.d.ts -index b6ffa7c..10a5c49 100644 ---- a/node_modules/@metamask/gas-fee-controller/dist/types/GasFeeController.d.ts -+++ b/node_modules/@metamask/gas-fee-controller/dist/types/GasFeeController.d.ts -@@ -2,7 +2,7 @@ import type { ControllerGetStateAction, ControllerStateChangeEvent, RestrictedCo - import type { NetworkClientId, NetworkControllerGetEIP1559CompatibilityAction, NetworkControllerGetNetworkClientByIdAction, NetworkControllerGetStateAction, NetworkControllerNetworkDidChangeEvent, NetworkState, ProviderProxy } from '@metamask/network-controller'; - import { StaticIntervalPollingController } from '@metamask/polling-controller'; - import type { Hex } from '@metamask/utils'; --export declare const GAS_API_BASE_URL = "https://gas.api.infura.io"; -+export declare const LEGACY_GAS_PRICES_API_URL = "https://api.metaswap.codefi.network/gasPrices"; - export type unknownString = 'unknown'; - export type FeeMarketEstimateType = 'fee-market'; - export type LegacyEstimateType = 'legacy'; -@@ -160,7 +160,6 @@ export declare class GasFeeController extends StaticIntervalPollingController ProviderProxy; - onNetworkDidChange?: (listener: (state: NetworkState) => void) => void; - clientId?: string; -- infuraAPIKey: string; - }); - resetPolling(): Promise; - fetchGasFeeEstimates(options?: FetchGasFeeEstimateOptions): Promise; -diff --git a/node_modules/@metamask/gas-fee-controller/dist/types/determineGasFeeCalculations.d.ts b/node_modules/@metamask/gas-fee-controller/dist/types/determineGasFeeCalculations.d.ts -index 8091a4d..441c8eb 100644 ---- a/node_modules/@metamask/gas-fee-controller/dist/types/determineGasFeeCalculations.d.ts -+++ b/node_modules/@metamask/gas-fee-controller/dist/types/determineGasFeeCalculations.d.ts -@@ -2,15 +2,14 @@ import type { EstimatedGasFeeTimeBounds, EthGasPriceEstimate, GasFeeEstimates, G - type DetermineGasFeeCalculationsRequest = { - isEIP1559Compatible: boolean; - isLegacyGasAPICompatible: boolean; -- fetchGasEstimates: (url: string, infuraAPIKey: string, clientId?: string) => Promise; -+ fetchGasEstimates: (url: string, clientId?: string) => Promise; - fetchGasEstimatesUrl: string; -- fetchLegacyGasPriceEstimates: (url: string, infuraAPIKey: string, clientId?: string) => Promise; -+ fetchLegacyGasPriceEstimates: (url: string, clientId?: string) => Promise; - fetchLegacyGasPriceEstimatesUrl: string; - fetchEthGasPriceEstimate: (ethQuery: any) => Promise; - calculateTimeEstimate: (maxPriorityFeePerGas: string, maxFeePerGas: string, gasFeeEstimates: GasFeeEstimates) => EstimatedGasFeeTimeBounds; - clientId: string | undefined; - ethQuery: any; -- infuraAPIKey: string; - nonRPCGasFeeApisDisabled?: boolean; - }; - /** -@@ -35,7 +34,6 @@ type DetermineGasFeeCalculationsRequest = { - * @param args.calculateTimeEstimate - A function that determine time estimate bounds. - * @param args.clientId - An identifier that an API can use to know who is asking for estimates. - * @param args.ethQuery - An EthQuery instance we can use to talk to Ethereum directly. -- * @param args.infuraAPIKey - Infura API key to use for requests to Infura. - * @param args.nonRPCGasFeeApisDisabled - Whether to disable requests to the legacyAPIEndpoint and the EIP1559APIEndpoint - * @returns The gas fee calculations. - */ -diff --git a/node_modules/@metamask/gas-fee-controller/dist/types/gas-util.d.ts b/node_modules/@metamask/gas-fee-controller/dist/types/gas-util.d.ts -index 3739e0f..fb24556 100644 ---- a/node_modules/@metamask/gas-fee-controller/dist/types/gas-util.d.ts -+++ b/node_modules/@metamask/gas-fee-controller/dist/types/gas-util.d.ts -@@ -11,21 +11,19 @@ export declare function normalizeGWEIDecimalNumbers(n: string | number): any; - * Fetch gas estimates from the given URL. - * - * @param url - The gas estimate URL. -- * @param infuraAPIKey - The Infura API key used for infura API requests. - * @param clientId - The client ID used to identify to the API who is asking for estimates. - * @returns The gas estimates. - */ --export declare function fetchGasEstimates(url: string, infuraAPIKey: string, clientId?: string): Promise; -+export declare function fetchGasEstimates(url: string, clientId?: string): Promise; - /** - * Hit the legacy MetaSwaps gasPrices estimate api and return the low, medium - * high values from that API. - * - * @param url - The URL to fetch gas price estimates from. -- * @param infuraAPIKey - The Infura API key used for infura API requests. - * @param clientId - The client ID used to identify to the API who is asking for estimates. - * @returns The gas price estimates. - */ --export declare function fetchLegacyGasPriceEstimates(url: string, infuraAPIKey: string, clientId?: string): Promise; -+export declare function fetchLegacyGasPriceEstimates(url: string, clientId?: string): Promise; - /** - * Get a gas price estimate from the network using the `eth_gasPrice` method. - * diff --git a/patches/@metamask+swaps-controller+9.0.0.patch b/patches/@metamask+swaps-controller+9.0.0.patch deleted file mode 100644 index a9f9ff7230c..00000000000 --- a/patches/@metamask+swaps-controller+9.0.0.patch +++ /dev/null @@ -1,23 +0,0 @@ -diff --git a/node_modules/@metamask/swaps-controller/dist/SwapsController.js b/node_modules/@metamask/swaps-controller/dist/SwapsController.js -index fee999b..f6e2d75 100644 ---- a/node_modules/@metamask/swaps-controller/dist/SwapsController.js -+++ b/node_modules/@metamask/swaps-controller/dist/SwapsController.js -@@ -11,7 +11,8 @@ const gas_fee_controller_1 = require("@metamask/gas-fee-controller"); - const async_mutex_1 = require("async-mutex"); - const bignumber_js_1 = require("bignumber.js"); - const human_standard_token_abi_1 = __importDefault(require("human-standard-token-abi")); --const web3_1 = require("web3"); -+const web3 = require("web3"); -+const Web3 = web3.Web3 === undefined ? web3 : web3.Web3; - const swapsUtil_1 = require("./swapsUtil"); - // Functions to determine type of the return value from GasFeeController - /** -@@ -530,7 +531,7 @@ class SwapsController extends base_controller_1.BaseControllerV1 { - set provider(provider) { - if (provider) { - this.ethQuery = new eth_query_1.default(provider); -- this.web3 = new web3_1.Web3(provider); -+ this.web3 = new Web3(provider); - } - } - set chainId(chainId) { diff --git a/patches/react-native-webview+11.13.0.patch b/patches/react-native-webview+11.13.0.patch deleted file mode 100644 index d613485d404..00000000000 --- a/patches/react-native-webview+11.13.0.patch +++ /dev/null @@ -1,1401 +0,0 @@ -diff --git a/node_modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/CustomCookieJar.java b/node_modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/CustomCookieJar.java -new file mode 100644 -index 0000000..f4a6af9 ---- /dev/null -+++ b/node_modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/CustomCookieJar.java -@@ -0,0 +1,78 @@ -+package com.reactnativecommunity.webview; -+import android.util.Log; -+import android.webkit.CookieManager; -+import android.webkit.ValueCallback; -+import java.net.HttpURLConnection; -+import java.net.URL; -+import java.util.ArrayList; -+import java.util.Collection; -+import java.util.Iterator; -+import java.util.List; -+import java.util.Map; -+import okhttp3.Cookie; -+import okhttp3.CookieJar; -+import okhttp3.HttpUrl; -+ -+class CustomCookieJar implements CookieJar { -+ private Worker worker; -+ private CookieManager cookieManager; -+ -+ public CustomCookieJar() { -+ worker = new Worker(); -+ cookieManager = this.getCookieManager(); -+ } -+ -+ private CookieManager getCookieManager() { -+ CookieManager cookieManager = CookieManager.getInstance(); -+ cookieManager.setAcceptCookie(true); -+ return cookieManager; -+ } -+ -+ @Override -+ public void saveFromResponse(HttpUrl url, List cookies) { -+ worker.execute(() -> { -+ try { -+ -+ for (Cookie cookie : cookies) { -+ String _url = url.toString(); -+ String _cookie = cookie.toString(); -+ cookieManager.setCookie(_url, _cookie, new ValueCallback() { -+ @Override -+ public void onReceiveValue(Boolean value) {} -+ }); -+ cookieManager.flush(); -+ } -+ } catch (Exception e) { -+ e.printStackTrace(); -+ } -+ }); -+ } -+ -+ @Override -+ public List loadForRequest(HttpUrl httpUrl) { -+ List cookieList = new ArrayList(); -+ try { -+ -+ if (cookieManager.hasCookies()) { -+ String response = cookieManager.getCookie(httpUrl.toString()); -+ -+ if (response != null) { -+ String[] browserCookies = response.split(";"); -+ -+ for (String cookieStr : browserCookies) { -+ Cookie cookie = Cookie.parse(httpUrl, cookieStr); -+ if (cookie == null) { -+ continue; -+ } -+ cookieList.add(cookie); -+ } -+ } -+ -+ } -+ return cookieList; -+ } catch (Exception e) { -+ e.printStackTrace(); -+ return cookieList; -+ } -+ } -+} -diff --git a/node_modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java b/node_modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java -index f743bbc..b520532 100644 ---- a/node_modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java -+++ b/node_modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java -@@ -5,6 +5,7 @@ import android.annotation.TargetApi; - import android.app.Activity; - import android.app.DownloadManager; - import android.content.Context; -+import android.content.Intent; - import android.content.pm.ActivityInfo; - import android.content.pm.PackageManager; - import android.graphics.Bitmap; -@@ -14,6 +15,7 @@ import android.net.http.SslError; - import android.net.Uri; - import android.os.Build; - import android.os.Environment; -+import android.os.Handler; - import android.os.Message; - import android.os.SystemClock; - import android.text.TextUtils; -@@ -24,12 +26,17 @@ import android.view.View; - import android.view.ViewGroup; - import android.view.ViewGroup.LayoutParams; - import android.view.WindowManager; -+import android.view.inputmethod.InputMethodManager; - import android.webkit.ConsoleMessage; - import android.webkit.CookieManager; - import android.webkit.DownloadListener; - import android.webkit.GeolocationPermissions; - import android.webkit.JavascriptInterface; -+import android.webkit.JsPromptResult; -+import android.webkit.JsResult; - import android.webkit.RenderProcessGoneDetail; -+import android.webkit.ServiceWorkerClient; -+import android.webkit.ServiceWorkerController; - import android.webkit.SslErrorHandler; - import android.webkit.PermissionRequest; - import android.webkit.URLUtil; -@@ -40,6 +47,7 @@ import android.webkit.WebResourceResponse; - import android.webkit.WebSettings; - import android.webkit.WebView; - import android.webkit.WebViewClient; -+import android.widget.Button; - import android.widget.FrameLayout; - - import androidx.annotation.Nullable; -@@ -88,18 +96,54 @@ import com.reactnativecommunity.webview.events.TopRenderProcessGoneEvent; - import org.json.JSONException; - import org.json.JSONObject; - -+import java.io.ByteArrayInputStream; -+import java.io.IOException; -+import java.io.InputStream; - import java.io.UnsupportedEncodingException; -+import java.lang.reflect.Field; -+import java.net.CookieStore; -+import java.net.HttpCookie; -+import java.net.HttpURLConnection; - import java.net.MalformedURLException; -+import java.net.URI; -+import java.net.URISyntaxException; - import java.net.URL; - import java.net.URLEncoder; -+import java.nio.charset.Charset; -+import java.nio.charset.StandardCharsets; -+import java.nio.charset.UnsupportedCharsetException; -+import java.text.Bidi; - import java.util.ArrayList; -+import java.util.Arrays; -+import java.util.Collection; - import java.util.Collections; - import java.util.HashMap; -+import java.util.HashSet; - import java.util.List; - import java.util.Locale; - import java.util.Map; -+import java.util.Objects; -+import java.util.Set; - import java.util.concurrent.atomic.AtomicReference; - -+import okhttp3.MediaType; -+import okhttp3.OkHttpClient; -+import okhttp3.Request; -+import okhttp3.RequestBody; -+import okhttp3.Response; -+ -+import android.view.inputmethod.BaseInputConnection; -+import android.view.inputmethod.EditorInfo; -+import android.view.inputmethod.InputConnection; -+ -+ -+import android.content.DialogInterface; -+import android.os.Bundle; -+import android.widget.Toast; -+ -+import androidx.appcompat.app.AlertDialog; -+import androidx.appcompat.app.AppCompatActivity; -+ - /** - * Manages instances of {@link WebView} - *

-@@ -137,13 +181,19 @@ public class RNCWebViewManager extends SimpleViewManager { - public static final int COMMAND_LOAD_URL = 7; - public static final int COMMAND_FOCUS = 8; - -+ protected static final String MIME_UNKNOWN = "application/octet-stream"; -+ protected static final String HTML_ENCODING = "UTF-8"; -+ protected static final long BYTES_IN_MEGABYTE = 1000000; -+ - // android commands - public static final int COMMAND_CLEAR_FORM_DATA = 1000; - public static final int COMMAND_CLEAR_CACHE = 1001; - public static final int COMMAND_CLEAR_HISTORY = 1002; - - protected static final String REACT_CLASS = "RNCWebView"; -- protected static final String HTML_ENCODING = "UTF-8"; -+ -+ protected static final String HEADER_CONTENT_TYPE = "content-type"; -+ - protected static final String HTML_MIME_TYPE = "text/html"; - protected static final String JAVASCRIPT_INTERFACE = "ReactNativeWebView"; - protected static final String HTTP_METHOD_POST = "POST"; -@@ -155,13 +205,19 @@ public class RNCWebViewManager extends SimpleViewManager { - - protected RNCWebChromeClient mWebChromeClient = null; - protected boolean mAllowsFullscreenVideo = false; -- protected @Nullable String mUserAgent = null; -- protected @Nullable String mUserAgentWithApplicationName = null; -+ protected @Nullable String RNUserAgent = null; -+ protected @Nullable String RNUserAgentWithApplicationName = null; -+ protected static String deviceUserAgent; -+ -+ protected static OkHttpClient httpClient; - - public RNCWebViewManager() { -- mWebViewConfig = new WebViewConfig() { -- public void configWebView(WebView webView) { -- } -+ mWebViewConfig = webView -> { -+ httpClient = new OkHttpClient.Builder() -+ .cookieJar(new CustomCookieJar()) -+ .followRedirects(false) -+ .followSslRedirects(false) -+ .build(); - }; - } - -@@ -182,6 +238,7 @@ public class RNCWebViewManager extends SimpleViewManager { - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - protected WebView createViewInstance(ThemedReactContext reactContext) { - RNCWebView webView = createRNCWebViewInstance(reactContext); -+ deviceUserAgent = webView.getSettings().getUserAgentString(); - setupWebChromeClient(reactContext, webView); - reactContext.addLifecycleEventListener(webView); - mWebViewConfig.configWebView(webView); -@@ -209,47 +266,161 @@ public class RNCWebViewManager extends SimpleViewManager { - } - - webView.setDownloadListener(new DownloadListener() { -+ protected ReactContext mReactContext; -+ - public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) { - webView.setIgnoreErrFailedForThisURL(url); -+ this.mReactContext = reactContext; - - RNCWebViewModule module = getModule(reactContext); -- - DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url)); -- - String fileName = URLUtil.guessFileName(url, contentDisposition, mimetype); -- String downloadMessage = "Downloading " + fileName; -+ //Filename validation checking for files that use RTL characters and do not allow those types -+ if(fileName == null || (fileName != null && (new Bidi(fileName, Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT).isMixed()))) { -+ Toast.makeText(mReactContext, "Invalid filename or type", Toast.LENGTH_SHORT).show(); -+ } else { -+ AlertDialog.Builder builder = new AlertDialog.Builder(mReactContext); -+ builder.setMessage("Do you want to download \n" + fileName + "?"); -+ builder.setCancelable(false); -+ builder.setPositiveButton("Download", new DialogInterface.OnClickListener() { -+ public void onClick(DialogInterface dialog, int which) { -+ String downloadMessage = "Downloading " + fileName; -+ -+ //Attempt to add cookie, if it exists -+ URL urlObj = null; -+ try { -+ urlObj = new URL(url); -+ String baseUrl = urlObj.getProtocol() + "://" + urlObj.getHost(); -+ String cookie = CookieManager.getInstance().getCookie(baseUrl); -+ request.addRequestHeader("Cookie", cookie); -+ } catch (MalformedURLException e) { -+ System.out.println("Error getting cookie for DownloadManager: " + e.toString()); -+ e.printStackTrace(); -+ } - -- //Attempt to add cookie, if it exists -- URL urlObj = null; -- try { -- urlObj = new URL(url); -- String baseUrl = urlObj.getProtocol() + "://" + urlObj.getHost(); -- String cookie = CookieManager.getInstance().getCookie(baseUrl); -- request.addRequestHeader("Cookie", cookie); -- } catch (MalformedURLException e) { -- System.out.println("Error getting cookie for DownloadManager: " + e.toString()); -- e.printStackTrace(); -+ //Finish setting up request -+ request.addRequestHeader("User-Agent", userAgent); -+ request.setTitle(fileName); -+ request.setDescription(downloadMessage); -+ request.allowScanningByMediaScanner(); -+ request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); -+ request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName); -+ module.setDownloadRequest(request); -+ if (module.grantFileDownloaderPermissions()) { -+ module.downloadFile(); -+ } -+ } -+ }); -+ builder.setNegativeButton("Cancel", (DialogInterface.OnClickListener) (dialog, which) -> { -+ return; -+ }); -+ AlertDialog alertDialog = builder.create(); -+ alertDialog.show(); - } -+ } -+ }); - -- //Finish setting up request -- request.addRequestHeader("User-Agent", userAgent); -- request.setTitle(fileName); -- request.setDescription(downloadMessage); -- request.allowScanningByMediaScanner(); -- request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); -- request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName); -+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { -+ ServiceWorkerController swController = ServiceWorkerController.getInstance(); -+ swController.setServiceWorkerClient(new ServiceWorkerClient() { -+ @Override -+ public WebResourceResponse shouldInterceptRequest(WebResourceRequest request) { -+ String method = request.getMethod(); - -- module.setDownloadRequest(request); -+ if (method.equals("GET")) { -+ WebResourceResponse response = RNCWebViewManager.this.shouldInterceptRequest(request, false, webView); -+ if (response != null) { -+ return response; -+ } -+ } - -- if (module.grantFileDownloaderPermissions()) { -- module.downloadFile(); -+ return super.shouldInterceptRequest(request); - } -- } -- }); -+ }); -+ } - - return webView; - } - -+ private Boolean urlStringLooksInvalid(String urlString) { -+ return urlString == null || -+ urlString.trim().equals("") || -+ !(urlString.startsWith("http") && !urlString.startsWith("www")) || -+ urlString.contains("|"); -+ } -+ -+ public static Boolean responseRequiresJSInjection(Response response) { -+ if (response.isRedirect()) { -+ return false; -+ } -+ final String contentTypeAndCharset = response.header(HEADER_CONTENT_TYPE, MIME_UNKNOWN); -+ final int responseCode = response.code(); -+ -+ boolean contentTypeIsHtml = contentTypeAndCharset.startsWith(HTML_MIME_TYPE); -+ boolean responseCodeIsInjectible = responseCode == 200; -+ String responseBody = ""; -+ -+ if (contentTypeIsHtml && responseCodeIsInjectible) { -+ try { -+ assert response.body() != null; -+ responseBody = response.peekBody(BYTES_IN_MEGABYTE).string(); -+ } catch (IOException e) { -+ e.printStackTrace(); -+ return false; -+ } -+ -+ -+ boolean responseBodyContainsHTMLLikeString = responseBody.matches("[\\S\\s]*<[a-z]+[\\S\\s]*>[\\S\\s]*"); -+ return responseBodyContainsHTMLLikeString; -+ } else { -+ return false; -+ } -+ } -+ -+ public WebResourceResponse shouldInterceptRequest(WebResourceRequest request, Boolean onlyMainFrame, RNCWebView webView) { -+ Uri url = request.getUrl(); -+ String urlStr = url.toString(); -+ -+ if (onlyMainFrame && !request.isForMainFrame() || -+ urlStringLooksInvalid(urlStr)) { -+ return null; -+ } -+ -+ String _userAgent; -+ -+ if (RNUserAgent != null) { -+ _userAgent = RNUserAgent; -+ } else { -+ _userAgent = deviceUserAgent; -+ } -+ -+ try { -+ Request req = new Request.Builder() -+ .url(urlStr) -+ .header("User-Agent", _userAgent) -+ .build(); -+ -+ Response response = httpClient.newCall(req).execute(); -+ -+ if (!responseRequiresJSInjection(response)) { -+ return null; -+ } -+ -+ InputStream is = response.body().byteStream(); -+ MediaType contentType = response.body().contentType(); -+ Charset charset = contentType != null ? contentType.charset(StandardCharsets.UTF_8) : StandardCharsets.UTF_8; -+ -+ RNCWebView reactWebView = (RNCWebView) webView; -+ if (response.code() == HttpURLConnection.HTTP_OK) { -+ is = new InputStreamWithInjectedJS(is, reactWebView.injectedJSBeforeContentLoaded, charset); -+ } -+ -+ return new WebResourceResponse("text/html", charset.name(), is); -+ } catch (IOException e) { -+ return null; -+ } -+ } -+ - @ReactProp(name = "javaScriptEnabled") - public void setJavaScriptEnabled(WebView view, boolean enabled) { - view.getSettings().setJavaScriptEnabled(enabled); -@@ -285,13 +456,10 @@ public class RNCWebViewManager extends SimpleViewManager { - if (enabled) { - Context ctx = view.getContext(); - if (ctx != null) { -- view.getSettings().setAppCachePath(ctx.getCacheDir().getAbsolutePath()); - view.getSettings().setCacheMode(WebSettings.LOAD_DEFAULT); -- view.getSettings().setAppCacheEnabled(true); - } - } else { - view.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE); -- view.getSettings().setAppCacheEnabled(false); - } - } - -@@ -327,12 +495,12 @@ public class RNCWebViewManager extends SimpleViewManager { - public void setLayerType(WebView view, String layerTypeString) { - int layerType = View.LAYER_TYPE_NONE; - switch (layerTypeString) { -- case "hardware": -- layerType = View.LAYER_TYPE_HARDWARE; -- break; -- case "software": -- layerType = View.LAYER_TYPE_SOFTWARE; -- break; -+ case "hardware": -+ layerType = View.LAYER_TYPE_HARDWARE; -+ break; -+ case "software": -+ layerType = View.LAYER_TYPE_SOFTWARE; -+ break; - } - view.setLayerType(layerType, null); - } -@@ -387,9 +555,9 @@ public class RNCWebViewManager extends SimpleViewManager { - @ReactProp(name = "userAgent") - public void setUserAgent(WebView view, @Nullable String userAgent) { - if (userAgent != null) { -- mUserAgent = userAgent; -+ RNUserAgent = userAgent; - } else { -- mUserAgent = null; -+ RNUserAgent = null; - } - this.setUserAgentString(view); - } -@@ -399,19 +567,19 @@ public class RNCWebViewManager extends SimpleViewManager { - if(applicationName != null) { - if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - String defaultUserAgent = WebSettings.getDefaultUserAgent(view.getContext()); -- mUserAgentWithApplicationName = defaultUserAgent + " " + applicationName; -+ RNUserAgentWithApplicationName = defaultUserAgent + " " + applicationName; - } - } else { -- mUserAgentWithApplicationName = null; -+ RNUserAgentWithApplicationName = null; - } - this.setUserAgentString(view); - } - - protected void setUserAgentString(WebView view) { -- if(mUserAgent != null) { -- view.getSettings().setUserAgentString(mUserAgent); -- } else if(mUserAgentWithApplicationName != null) { -- view.getSettings().setUserAgentString(mUserAgentWithApplicationName); -+ if(RNUserAgent != null) { -+ view.getSettings().setUserAgentString(RNUserAgent); -+ } else if(RNUserAgentWithApplicationName != null) { -+ view.getSettings().setUserAgentString(RNUserAgentWithApplicationName); - } else if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - // handle unsets of `userAgent` prop as long as device is >= API 17 - view.getSettings().setUserAgentString(WebSettings.getDefaultUserAgent(view.getContext())); -@@ -490,7 +658,6 @@ public class RNCWebViewManager extends SimpleViewManager { - - // Disable caching - view.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE); -- view.getSettings().setAppCacheEnabled(false); - view.clearHistory(); - view.clearCache(true); - -@@ -842,13 +1009,116 @@ public class RNCWebViewManager extends SimpleViewManager { - } - } - -- protected static class RNCWebViewClient extends WebViewClient { -+ public static class InputStreamWithInjectedJS extends InputStream { -+ private InputStream pageIS; -+ private InputStream scriptIS; -+ private Charset charset; -+ private static final String REACT_CLASS = "InputStreamWithInjectedJS"; -+ private static Map script = new HashMap<>(); -+ -+ private boolean hasJS = false; -+ private boolean headWasFound = false; -+ private boolean scriptWasInjected = false; -+ -+ private int lowercaseD = 100; -+ private int closingTag = 62; -+ private boolean hasClosingHead = false; -+ -+ private StringBuffer contentBuffer = new StringBuffer(); -+ -+ @SuppressLint("LongLogTag") -+ private static Charset getCharset(String charsetName) { -+ Charset cs = StandardCharsets.UTF_8; -+ try { -+ if (charsetName != null) { -+ cs = Charset.forName(charsetName); -+ } -+ } catch (UnsupportedCharsetException e) { -+ Log.d(REACT_CLASS, "wrong charset: " + charsetName); -+ } -+ -+ return cs; -+ } -+ -+ private static InputStream getScript(Charset charset) { -+ String js = script.get(charset); -+ if (js == null) { -+ String defaultJs = script.get(StandardCharsets.UTF_8); -+ js = new String(defaultJs.getBytes(StandardCharsets.UTF_8), charset); -+ script.put(charset, js); -+ } -+ -+ return new ByteArrayInputStream(js.getBytes(charset)); -+ } -+ -+ InputStreamWithInjectedJS(InputStream is, String js, Charset charset) { -+ if (js == null) { -+ this.pageIS = is; -+ } else { -+ this.hasJS = true; -+ this.charset = charset; -+ Charset cs = StandardCharsets.UTF_8; -+ String jsScript = ""; -+ script.put(cs, jsScript); -+ this.pageIS = is; -+ } -+ } -+ -+ @Override -+ public int read() throws IOException { -+ if (scriptWasInjected || !hasJS) { -+ return pageIS.read(); -+ } -+ -+ if (!scriptWasInjected && headWasFound) { -+ int nextByte; -+ if (!hasClosingHead) { -+ nextByte = pageIS.read(); -+ if (nextByte != closingTag) { -+ return nextByte; -+ } -+ hasClosingHead = true; -+ return nextByte; -+ } -+ nextByte = scriptIS.read(); -+ if (nextByte == -1) { -+ scriptIS.close(); -+ scriptWasInjected = true; -+ return pageIS.read(); -+ } else { -+ return nextByte; -+ } -+ } -+ -+ if (!headWasFound) { -+ int nextByte = pageIS.read(); -+ contentBuffer.append((char) nextByte); -+ int bufferLength = contentBuffer.length(); -+ if (nextByte == lowercaseD && bufferLength >= 5) { -+ if (contentBuffer.substring(bufferLength - 5).equals(" { - @Override - public void onPageFinished(WebView webView, String url) { - super.onPageFinished(webView, url); -+ // Only return the URL that the web view is currently showing. -+ String visibleUrl = webView.getUrl(); -+ Boolean isFinishedLoading = url.equals(visibleUrl); - -- if (!mLastLoadFailed) { -+ if (!mLastLoadFailed && isFinishedLoading) { -+ if(Objects.nonNull(mWebChromeClient)) mWebChromeClient.blockJsDuringLoading = false; - RNCWebView reactWebView = (RNCWebView) webView; - - reactWebView.callInjectedJavaScript(); - -- emitFinishEvent(webView, url); -+ emitFinishEvent(webView, visibleUrl); - } - } - - @Override - public void onPageStarted(WebView webView, String url, Bitmap favicon) { - super.onPageStarted(webView, url, favicon); -+ if(Objects.nonNull(mWebChromeClient)) mWebChromeClient.blockJsDuringLoading = true; - mLastLoadFailed = false; - -- RNCWebView reactWebView = (RNCWebView) webView; -- reactWebView.callInjectedJavaScriptBeforeContentLoaded(); -- - ((RNCWebView) webView).dispatchEvent( - webView, - new TopLoadingStartEvent( -@@ -882,6 +1154,20 @@ public class RNCWebViewManager extends SimpleViewManager { - createWebViewEvent(webView, url))); - } - -+ @Override -+ public WebResourceResponse shouldInterceptRequest(WebView webView, WebResourceRequest request) { -+ String method = request.getMethod(); -+ -+ if (method.equals("GET")) { -+ WebResourceResponse response = RNCWebViewManager.this.shouldInterceptRequest(request, true, (RNCWebView)webView); -+ if (response != null) { -+ return response; -+ } -+ } -+ -+ return super.shouldInterceptRequest(webView, request); -+ } -+ - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { - final RNCWebView rncWebView = (RNCWebView) view; -@@ -891,7 +1177,6 @@ public class RNCWebViewManager extends SimpleViewManager { - final Pair> lock = RNCWebViewModule.shouldOverrideUrlLoadingLock.getNewLock(); - final int lockIdentifier = lock.first; - final AtomicReference lockObject = lock.second; -- - final WritableMap event = createWebViewEvent(view, url); - event.putInt("lockIdentifier", lockIdentifier); - rncWebView.sendDirectMessage("onShouldStartLoadWithRequest", event); -@@ -919,6 +1204,17 @@ public class RNCWebViewManager extends SimpleViewManager { - RNCWebViewModule.shouldOverrideUrlLoadingLock.removeLock(lockIdentifier); - - return shouldOverride; -+ } else if (url != null && Arrays.asList(DEEPLINK_ALLOW_LIST).contains(url)) { -+ // This case is used to support deeplinking within the webview. We are limiting this but -+ // if more links are to be supported we should consider a more scaleable solution. That is -+ // secure and scaleable. -+ Intent intent = new Intent(Intent.ACTION_VIEW); -+ intent.setData(Uri.parse(url)); -+ if(intent.resolveActivity(view.getContext().getPackageManager()) != null) { -+ view.getContext().startActivity(intent); -+ return true; -+ } else -+ return false; - } else { - FLog.w(TAG, "Couldn't use blocking synchronous call for onShouldStartLoadWithRequest due to debugging or missing Catalyst instance, falling back to old event-and-load."); - progressChangedFilter.setWaitingForCommandLoadUrl(true); -@@ -934,67 +1230,86 @@ public class RNCWebViewManager extends SimpleViewManager { - @TargetApi(Build.VERSION_CODES.N) - @Override - public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { -+ if(Objects.nonNull(mWebChromeClient)) mWebChromeClient.blockJsDuringLoading = true; -+ -+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { -+ -+ /* -+ * In order to follow redirects properly, we return null in interceptRequest(). -+ * Doing this breaks the web3 injection on the resulting page, so we have to reload to -+ * make sure web3 is available. -+ * */ -+ -+ if (request.isForMainFrame() && request.isRedirect()) { -+ view.loadUrl(request.getUrl().toString()); -+ return true; -+ } -+ } -+ - final String url = request.getUrl().toString(); -+ - return this.shouldOverrideUrlLoading(view, url); - } - -+ -+ - @Override - public void onReceivedSslError(final WebView webView, final SslErrorHandler handler, final SslError error) { -- // onReceivedSslError is called for most requests, per Android docs: https://developer.android.com/reference/android/webkit/WebViewClient#onReceivedSslError(android.webkit.WebView,%2520android.webkit.SslErrorHandler,%2520android.net.http.SslError) -- // WebView.getUrl() will return the top-level window URL. -- // If a top-level navigation triggers this error handler, the top-level URL will be the failing URL (not the URL of the currently-rendered page). -- // This is desired behavior. We later use these values to determine whether the request is a top-level navigation or a subresource request. -- String topWindowUrl = webView.getUrl(); -- String failingUrl = error.getUrl(); -- -- // Cancel request after obtaining top-level URL. -- // If request is cancelled before obtaining top-level URL, undesired behavior may occur. -- // Undesired behavior: Return value of WebView.getUrl() may be the current URL instead of the failing URL. -- handler.cancel(); -- -- if (!topWindowUrl.equalsIgnoreCase(failingUrl)) { -- // If error is not due to top-level navigation, then do not call onReceivedError() -- Log.w("RNCWebViewManager", "Resource blocked from loading due to SSL error. Blocked URL: "+failingUrl); -- return; -- } -+ // onReceivedSslError is called for most requests, per Android docs: https://developer.android.com/reference/android/webkit/WebViewClient#onReceivedSslError(android.webkit.WebView,%2520android.webkit.SslErrorHandler,%2520android.net.http.SslError) -+ // WebView.getUrl() will return the top-level window URL. -+ // If a top-level navigation triggers this error handler, the top-level URL will be the failing URL (not the URL of the currently-rendered page). -+ // This is desired behavior. We later use these values to determine whether the request is a top-level navigation or a subresource request. -+ String topWindowUrl = webView.getUrl(); -+ String failingUrl = error.getUrl(); -+ -+ // Cancel request after obtaining top-level URL. -+ // If request is cancelled before obtaining top-level URL, undesired behavior may occur. -+ // Undesired behavior: Return value of WebView.getUrl() may be the current URL instead of the failing URL. -+ handler.cancel(); -+ -+ if (!topWindowUrl.equalsIgnoreCase(failingUrl)) { -+ // If error is not due to top-level navigation, then do not call onReceivedError() -+ Log.w("RNCWebViewManager", "Resource blocked from loading due to SSL error. Blocked URL: "+failingUrl); -+ return; -+ } - -- int code = error.getPrimaryError(); -- String description = ""; -- String descriptionPrefix = "SSL error: "; -- -- // https://developer.android.com/reference/android/net/http/SslError.html -- switch (code) { -- case SslError.SSL_DATE_INVALID: -- description = "The date of the certificate is invalid"; -- break; -- case SslError.SSL_EXPIRED: -- description = "The certificate has expired"; -- break; -- case SslError.SSL_IDMISMATCH: -- description = "Hostname mismatch"; -- break; -- case SslError.SSL_INVALID: -- description = "A generic error occurred"; -- break; -- case SslError.SSL_NOTYETVALID: -- description = "The certificate is not yet valid"; -- break; -- case SslError.SSL_UNTRUSTED: -- description = "The certificate authority is not trusted"; -- break; -- default: -- description = "Unknown SSL Error"; -- break; -- } -+ int code = error.getPrimaryError(); -+ String description = ""; -+ String descriptionPrefix = "SSL error: "; - -- description = descriptionPrefix + description; -+ // https://developer.android.com/reference/android/net/http/SslError.html -+ switch (code) { -+ case SslError.SSL_DATE_INVALID: -+ description = "The date of the certificate is invalid"; -+ break; -+ case SslError.SSL_EXPIRED: -+ description = "The certificate has expired"; -+ break; -+ case SslError.SSL_IDMISMATCH: -+ description = "Hostname mismatch"; -+ break; -+ case SslError.SSL_INVALID: -+ description = "A generic error occurred"; -+ break; -+ case SslError.SSL_NOTYETVALID: -+ description = "The certificate is not yet valid"; -+ break; -+ case SslError.SSL_UNTRUSTED: -+ description = "The certificate authority is not trusted"; -+ break; -+ default: -+ description = "Unknown SSL Error"; -+ break; -+ } - -- this.onReceivedError( -- webView, -- code, -- description, -- failingUrl -- ); -+ description = descriptionPrefix + description; -+ -+ this.onReceivedError( -+ webView, -+ code, -+ description, -+ failingUrl -+ ); - } - - @Override -@@ -1005,9 +1320,9 @@ public class RNCWebViewManager extends SimpleViewManager { - String failingUrl) { - - if (ignoreErrFailedForThisURL != null -- && failingUrl.equals(ignoreErrFailedForThisURL) -- && errorCode == -1 -- && description.equals("net::ERR_FAILED")) { -+ && failingUrl.equals(ignoreErrFailedForThisURL) -+ && errorCode == -1 -+ && description.equals("net::ERR_FAILED")) { - - // This is a workaround for a bug in the WebView. - // See these chromium issues for more context: -@@ -1056,36 +1371,36 @@ public class RNCWebViewManager extends SimpleViewManager { - @TargetApi(Build.VERSION_CODES.O) - @Override - public boolean onRenderProcessGone(WebView webView, RenderProcessGoneDetail detail) { -- // WebViewClient.onRenderProcessGone was added in O. -- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { -- return false; -- } -- super.onRenderProcessGone(webView, detail); -+ // WebViewClient.onRenderProcessGone was added in O. -+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { -+ return false; -+ } -+ super.onRenderProcessGone(webView, detail); - -- if(detail.didCrash()){ -- Log.e("RNCWebViewManager", "The WebView rendering process crashed."); -- } -- else{ -- Log.w("RNCWebViewManager", "The WebView rendering process was killed by the system."); -- } -+ if(detail.didCrash()){ -+ Log.e("RNCWebViewManager", "The WebView rendering process crashed."); -+ } -+ else{ -+ Log.w("RNCWebViewManager", "The WebView rendering process was killed by the system."); -+ } - -- // if webView is null, we cannot return any event -- // since the view is already dead/disposed -- // still prevent the app crash by returning true. -- if(webView == null){ -- return true; -- } -+ // if webView is null, we cannot return any event -+ // since the view is already dead/disposed -+ // still prevent the app crash by returning true. -+ if(webView == null){ -+ return true; -+ } - -- WritableMap event = createWebViewEvent(webView, webView.getUrl()); -- event.putBoolean("didCrash", detail.didCrash()); -+ WritableMap event = createWebViewEvent(webView, webView.getUrl()); -+ event.putBoolean("didCrash", detail.didCrash()); - - ((RNCWebView) webView).dispatchEvent( -- webView, -- new TopRenderProcessGoneEvent(webView.getId(), event) -- ); -+ webView, -+ new TopRenderProcessGoneEvent(webView.getId(), event) -+ ); - -- // returning false would crash the app. -- return true; -+ // returning false would crash the app. -+ return true; - } - - protected void emitFinishEvent(WebView webView, String url) { -@@ -1138,6 +1453,7 @@ public class RNCWebViewManager extends SimpleViewManager { - - protected View mVideoView; - protected WebChromeClient.CustomViewCallback mCustomViewCallback; -+ protected boolean blockJsDuringLoading = true; //This boolean block JS prompts and alerts from displaying during loading - - /* - * - Permissions - -@@ -1211,8 +1527,8 @@ public class RNCWebViewManager extends SimpleViewManager { - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - @Override - public void onPermissionRequest(final PermissionRequest request) { -- - grantedPermissions = new ArrayList<>(); -+ ArrayList requestPermissionIdentifiers = new ArrayList<>(); - - ArrayList requestedAndroidPermissions = new ArrayList<>(); - for (String requestedResource : request.getResources()) { -@@ -1220,36 +1536,74 @@ public class RNCWebViewManager extends SimpleViewManager { - - if (requestedResource.equals(PermissionRequest.RESOURCE_AUDIO_CAPTURE)) { - androidPermission = Manifest.permission.RECORD_AUDIO; -+ requestPermissionIdentifiers.add("microphone"); - } else if (requestedResource.equals(PermissionRequest.RESOURCE_VIDEO_CAPTURE)) { - androidPermission = Manifest.permission.CAMERA; -+ requestPermissionIdentifiers.add("camera"); - } else if(requestedResource.equals(PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID)) { - androidPermission = PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID; - } - // TODO: RESOURCE_MIDI_SYSEX, RESOURCE_PROTECTED_MEDIA_ID. -- - if (androidPermission != null) { -- if (ContextCompat.checkSelfPermission(mReactContext, androidPermission) == PackageManager.PERMISSION_GRANTED) { -- grantedPermissions.add(requestedResource); -- } else { -- requestedAndroidPermissions.add(androidPermission); -- } -+ if (ContextCompat.checkSelfPermission(mReactContext, androidPermission) == PackageManager.PERMISSION_GRANTED) { -+ grantedPermissions.add(requestedResource); -+ } else { -+ requestedAndroidPermissions.add(androidPermission); -+ } - } - } - -- // If all the permissions are already granted, send the response to the WebView synchronously -- if (requestedAndroidPermissions.isEmpty()) { -- request.grant(grantedPermissions.toArray(new String[0])); -- grantedPermissions = null; -- return; -- } -- -- // Otherwise, ask to Android System for native permissions asynchronously -+ if (!requestedAndroidPermissions.isEmpty()) { -+ // Show the dialog and request the permissions -+ AlertDialog.Builder builder = new AlertDialog.Builder(mReactContext); -+ String permissionsIdentifiers = TextUtils.join(" and ", requestPermissionIdentifiers); -+ builder.setMessage("The app needs access to your " + permissionsIdentifiers + ". Allow?"); -+ builder.setCancelable(false); -+ builder.setPositiveButton("Allow", new DialogInterface.OnClickListener() { -+ public void onClick(DialogInterface dialog, int which) { -+ permissionRequest = request; -+ requestPermissions(requestedAndroidPermissions); -+ } -+ }); -+ builder.setNegativeButton("Don't allow", (DialogInterface.OnClickListener) (dialog, which) -> { -+ request.deny(); -+ }); - -- this.permissionRequest = request; -+ AlertDialog alertDialog = builder.create(); -+ alertDialog.show(); -+ // Delay making `allow` clickable for 500ms -+ Button posButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE); -+ posButton.setEnabled(false); -+ this.runDelayed(() -> posButton.setEnabled(true), 500); -+ } else if (!grantedPermissions.isEmpty()) { -+ // You need to show to the user that the website is requesting permissions -+ // If that happens and the permissions are already granted you need to ask again -+ AlertDialog.Builder builder = new AlertDialog.Builder(mReactContext); -+ String permissionsIdentifiers = TextUtils.join(" and ", requestPermissionIdentifiers); -+ builder.setMessage("The app needs access to your " + permissionsIdentifiers + ". Allow?"); -+ builder.setCancelable(false); -+ builder.setPositiveButton("Allow", new DialogInterface.OnClickListener() { -+ public void onClick(DialogInterface dialog, int which) { -+ request.grant(grantedPermissions.toArray(new String[0])); -+ } -+ }); -+ builder.setNegativeButton("Don't allow", (DialogInterface.OnClickListener) (dialog, which) -> { -+ request.deny(); -+ }); - -- requestPermissions(requestedAndroidPermissions); -+ AlertDialog alertDialog = builder.create(); -+ alertDialog.show(); -+ // Delay making `allow` clickable for 500ms -+ Button posButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE); -+ posButton.setEnabled(false); -+ this.runDelayed(() -> posButton.setEnabled(true), 500); -+ } - } - -+ private void runDelayed(Runnable function, long delayMillis) { -+ Handler handler = new Handler(); -+ handler.postDelayed(function, delayMillis); -+ } - - @Override - public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) { -@@ -1266,7 +1620,22 @@ public class RNCWebViewManager extends SimpleViewManager { - requestPermissions(Collections.singletonList(Manifest.permission.ACCESS_FINE_LOCATION)); - - } else { -- callback.invoke(origin, true, false); -+ String alertMessage = String.format("Allow this app to use your location?"); -+ AlertDialog.Builder builder = new AlertDialog.Builder(this.mWebView.getContext()); -+ builder.setMessage(alertMessage); -+ builder.setCancelable(false); -+ builder.setPositiveButton("Allow", (dialog, which) -> { -+ callback.invoke(origin, true, false); -+ }); -+ builder.setNegativeButton("Don't allow", (dialog, which) -> { -+ callback.invoke(origin, false, false); -+ }); -+ AlertDialog alertDialog = builder.create(); -+ alertDialog.show(); -+ //Delay making `allow` clickable for 500ms to avoid unwanted presses. -+ Button posButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE); -+ posButton.setEnabled(false); -+ this.runDelayed(() -> posButton.setEnabled(true), 500); - } - } - -@@ -1402,6 +1771,15 @@ public class RNCWebViewManager extends SimpleViewManager { - } - } - -+ @Override -+ public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { -+ if(blockJsDuringLoading) { -+ result.cancel(); -+ return true; -+ } else -+ return super.onJsPrompt(view, url, message, defaultValue, result); -+ } -+ - @Override - public void onHostPause() { } - -@@ -1447,6 +1825,13 @@ public class RNCWebViewManager extends SimpleViewManager { - protected boolean nestedScrollEnabled = false; - protected ProgressChangedFilter progressChangedFilter; - -+ /** -+ * Taken from EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING We can't use that -+ * value directly as it was only added on Oreo, but we can apply the value -+ * anyway. -+ */ -+ private static final int IME_FLAG_NO_PERSONALIZED_LEARNING = 0x1000000; -+ - /** - * WebView must be created with an context of the current activity - *

-@@ -1475,6 +1860,42 @@ public class RNCWebViewManager extends SimpleViewManager { - this.nestedScrollEnabled = nestedScrollEnabled; - } - -+ @Override -+ public InputConnection onCreateInputConnection(EditorInfo outAttrs) { -+ InputConnection inputConnection; -+ if (!usingGoogleKeyboard()) { -+ inputConnection = super.onCreateInputConnection(outAttrs); -+ } else { -+ inputConnection = new BaseInputConnection(this, false); -+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { -+ outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING; -+ } else { -+ // Cover OS versions below Oreo -+ outAttrs.imeOptions = IME_FLAG_NO_PERSONALIZED_LEARNING; -+ } -+ } -+ -+ return inputConnection; -+ } -+ -+ public boolean usingGoogleKeyboard() { -+ final InputMethodManager richImm = -+ (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); -+ -+ boolean isKeyboard = false; -+ -+ final Field field; -+ try { -+ field = richImm.getClass().getDeclaredField("mCurId"); -+ field.setAccessible(true); -+ Object value = field.get(richImm); -+ isKeyboard = Objects.equals(value, "com.google.android.inputmethod.latin/com.android.inputmethod.latin.LatinIME"); -+ } catch (IllegalAccessException | NoSuchFieldException e) { -+ return false; -+ } -+ return isKeyboard; -+ } -+ - @Override - public void onHostResume() { - // do nothing -@@ -1533,6 +1954,8 @@ public class RNCWebViewManager extends SimpleViewManager { - } - } - -+ -+ - public @Nullable - RNCWebViewClient getRNCWebViewClient() { - return mRNCWebViewClient; -@@ -1609,8 +2032,8 @@ public class RNCWebViewManager extends SimpleViewManager { - - public void callInjectedJavaScriptBeforeContentLoaded() { - if (getSettings().getJavaScriptEnabled() && -- injectedJSBeforeContentLoaded != null && -- !TextUtils.isEmpty(injectedJSBeforeContentLoaded)) { -+ injectedJSBeforeContentLoaded != null && -+ !TextUtils.isEmpty(injectedJSBeforeContentLoaded)) { - evaluateJavascriptWithFallback("(function() {\n" + injectedJSBeforeContentLoaded + ";\n})();"); - } - } -@@ -1672,16 +2095,16 @@ public class RNCWebViewManager extends SimpleViewManager { - - if (mOnScrollDispatchHelper.onScrollChanged(x, y)) { - ScrollEvent event = ScrollEvent.obtain( -- this.getId(), -- ScrollEventType.SCROLL, -- x, -- y, -- mOnScrollDispatchHelper.getXFlingVelocity(), -- mOnScrollDispatchHelper.getYFlingVelocity(), -- this.computeHorizontalScrollRange(), -- this.computeVerticalScrollRange(), -- this.getWidth(), -- this.getHeight()); -+ this.getId(), -+ ScrollEventType.SCROLL, -+ x, -+ y, -+ mOnScrollDispatchHelper.getXFlingVelocity(), -+ mOnScrollDispatchHelper.getYFlingVelocity(), -+ this.computeHorizontalScrollRange(), -+ this.computeVerticalScrollRange(), -+ this.getWidth(), -+ this.getHeight()); - - dispatchEvent(this, event); - } -diff --git a/node_modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/Worker.java b/node_modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/Worker.java -new file mode 100644 -index 0000000..b9581ac ---- /dev/null -+++ b/node_modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/Worker.java -@@ -0,0 +1,21 @@ -+package com.reactnativecommunity.webview; -+ -+import android.os.Handler; -+import android.os.HandlerThread; -+ -+class Worker extends HandlerThread { -+ private Handler handler; -+ -+ private static final String TAG = "WORKER"; -+ -+ public Worker() { -+ super(TAG); -+ start(); -+ handler = new Handler(getLooper()); -+ } -+ -+ public Worker execute(Runnable task) { -+ handler.post(task); -+ return this; -+ } -+} -\ No newline at end of file -diff --git a/node_modules/react-native-webview/apple/RNCWebView.m b/node_modules/react-native-webview/apple/RNCWebView.m -index 28c078a..9bb5368 100644 ---- a/node_modules/react-native-webview/apple/RNCWebView.m -+++ b/node_modules/react-native-webview/apple/RNCWebView.m -@@ -105,6 +105,7 @@ static NSDictionary* customCertificatesForHost; - UIStatusBarStyle _savedStatusBarStyle; - #endif // !TARGET_OS_OSX - BOOL _savedStatusBarHidden; -+ BOOL _disablePromptDuringLoading; //Disables the display of prompts during site navigation/loading - - #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */ - UIScrollViewContentInsetAdjustmentBehavior _savedContentInsetAdjustmentBehavior; -@@ -139,6 +140,7 @@ static NSDictionary* customCertificatesForHost; - _injectedJavaScriptForMainFrameOnly = YES; - _injectedJavaScriptBeforeContentLoaded = nil; - _injectedJavaScriptBeforeContentLoadedForMainFrameOnly = YES; -+ _disablePromptDuringLoading = YES; - - #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */ - _savedContentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; -@@ -417,6 +419,7 @@ static NSDictionary* customCertificatesForHost; - - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{ - if ([keyPath isEqual:@"estimatedProgress"] && object == self.webView) { - if(_onLoadingProgress){ -+ _disablePromptDuringLoading = YES; - NSMutableDictionary *event = [self baseEvent]; - [event addEntriesFromDictionary:@{@"progress":[NSNumber numberWithDouble:self.webView.estimatedProgress]}]; - _onLoadingProgress(event); -@@ -492,6 +495,7 @@ static NSDictionary* customCertificatesForHost; - NSMutableDictionary *event = [self baseEvent]; - [event addEntriesFromDictionary: @{@"navigationType": message.body}]; - _onLoadingFinish(event); -+ _disablePromptDuringLoading = NO; - } - } else if ([message.name isEqualToString:MessageHandlerName]) { - if (_onMessage) { -@@ -851,11 +855,13 @@ static NSDictionary* customCertificatesForHost; - - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler - { - #if !TARGET_OS_OSX -- UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert]; -- [alert addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { -- completionHandler(); -- }]]; -- [[self topViewController] presentViewController:alert animated:YES completion:NULL]; -+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.7 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ -+ UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert]; -+ [alert addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { -+ completionHandler(); -+ }]]; -+ [[self topViewController] presentViewController:alert animated:YES completion:NULL]; -+ }); - #else - NSAlert *alert = [[NSAlert alloc] init]; - [alert setMessageText:message]; -@@ -868,6 +874,51 @@ static NSDictionary* customCertificatesForHost; - /** - * confirm - */ -+// This patch made to overridde the restrictions that webView is imposing to the native Alert, by restricting its size. -+- (void)webView:(WKWebView *)webView requestMediaCapturePermissionForOrigin:(WKSecurityOrigin *)origin initiatedByFrame:(WKFrameInfo *)frame type:(WKMediaCaptureType)type decisionHandler:(void (^)(WKPermissionDecision decision))decisionHandler API_AVAILABLE(ios(15.0)){ -+ -+ NSString *deviceType; -+ -+ switch (type) { -+ case WKMediaCaptureTypeCamera: -+ deviceType = @"camera"; -+ break; -+ case WKMediaCaptureTypeMicrophone: -+ deviceType = @"microphone"; -+ break; -+ case WKMediaCaptureTypeCameraAndMicrophone: -+ deviceType = @"camera and microphone"; -+ break; -+ default: -+ deviceType = @"unknown device"; -+ } -+ -+ NSString *message = [NSString stringWithFormat:@"The webpage %@ is requesting access to your %@. Do you want to allow this?", origin.host, deviceType]; -+ -+ UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Permission Request" -+ message:message -+ preferredStyle:UIAlertControllerStyleAlert]; -+ -+ UIAlertAction *allowAction = [UIAlertAction actionWithTitle:@"Allow" -+ style:UIAlertActionStyleDefault -+ handler:^(UIAlertAction * _Nonnull action) { -+ decisionHandler(WKPermissionDecisionGrant); -+ } -+ ]; -+ -+ UIAlertAction *denyAction = [UIAlertAction actionWithTitle:@"Deny" -+ style:UIAlertActionStyleCancel -+ handler:^(UIAlertAction * _Nonnull action) { -+ decisionHandler(WKPermissionDecisionDeny); -+ } -+ ]; -+ -+ [alertController addAction:allowAction]; -+ [alertController addAction:denyAction]; -+ -+ [[self topViewController] presentViewController:alertController animated:YES completion:NULL]; -+} -+ - - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler{ - #if !TARGET_OS_OSX - UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert]; -@@ -894,44 +945,49 @@ static NSDictionary* customCertificatesForHost; - * prompt - */ - - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *))completionHandler{ --#if !TARGET_OS_OSX -- UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" message:prompt preferredStyle:UIAlertControllerStyleAlert]; -- [alert addTextFieldWithConfigurationHandler:^(UITextField *textField) { -- textField.text = defaultText; -- }]; -- UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { -- completionHandler([[alert.textFields lastObject] text]); -- }]; -- [alert addAction:okAction]; -- UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { -- completionHandler(nil); -- }]; -- [alert addAction:cancelAction]; -- alert.preferredAction = okAction; -- [[self topViewController] presentViewController:alert animated:YES completion:NULL]; --#else -- NSAlert *alert = [[NSAlert alloc] init]; -- [alert setMessageText:prompt]; -- -- const NSRect RCTSingleTextFieldFrame = NSMakeRect(0.0, 0.0, 275.0, 22.0); -- NSTextField *textField = [[NSTextField alloc] initWithFrame:RCTSingleTextFieldFrame]; -- textField.cell.scrollable = YES; -- if (@available(macOS 10.11, *)) { -- textField.maximumNumberOfLines = 1; -- } -- textField.stringValue = defaultText; -- [alert setAccessoryView:textField]; - -- [alert addButtonWithTitle:NSLocalizedString(@"OK", @"OK button")]; -- [alert addButtonWithTitle:NSLocalizedString(@"Cancel", @"Cancel button")]; -- [alert beginSheetModalForWindow:[NSApp keyWindow] completionHandler:^(NSModalResponse response) { -- if (response == NSAlertFirstButtonReturn) { -- completionHandler([textField stringValue]); -+ if(!_disablePromptDuringLoading) { -+ #if !TARGET_OS_OSX -+ UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" message:prompt preferredStyle:UIAlertControllerStyleAlert]; -+ [alert addTextFieldWithConfigurationHandler:^(UITextField *textField) { -+ textField.text = defaultText; -+ }]; -+ UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { -+ completionHandler([[alert.textFields lastObject] text]); -+ }]; -+ [alert addAction:okAction]; -+ UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { -+ completionHandler(nil); -+ }]; -+ [alert addAction:cancelAction]; -+ alert.preferredAction = okAction; -+ [[self topViewController] presentViewController:alert animated:YES completion:NULL]; -+ #else -+ NSAlert *alert = [[NSAlert alloc] init]; -+ [alert setMessageText:prompt]; -+ -+ const NSRect RCTSingleTextFieldFrame = NSMakeRect(0.0, 0.0, 275.0, 22.0); -+ NSTextField *textField = [[NSTextField alloc] initWithFrame:RCTSingleTextFieldFrame]; -+ textField.cell.scrollable = YES; -+ if (@available(macOS 10.11, *)) { -+ textField.maximumNumberOfLines = 1; -+ } -+ textField.stringValue = defaultText; -+ [alert setAccessoryView:textField]; -+ -+ [alert addButtonWithTitle:NSLocalizedString(@"OK", @"OK button")]; -+ [alert addButtonWithTitle:NSLocalizedString(@"Cancel", @"Cancel button")]; -+ [alert beginSheetModalForWindow:[NSApp keyWindow] completionHandler:^(NSModalResponse response) { -+ if (response == NSAlertFirstButtonReturn) { -+ completionHandler([textField stringValue]); -+ } else { -+ completionHandler(nil); -+ } -+ }]; -+ #endif // !TARGET_OS_OSX - } else { -- completionHandler(nil); -+ completionHandler(nil); - } -- }]; --#endif // !TARGET_OS_OSX - } - - #if !TARGET_OS_OSX -@@ -1157,6 +1213,7 @@ static NSDictionary* customCertificatesForHost; - } - - if (_onLoadingFinish) { -+ _disablePromptDuringLoading = NO; - _onLoadingFinish([self baseEvent]); - } - } -@@ -1446,3 +1503,4 @@ static NSDictionary* customCertificatesForHost; - } - - @end -+ diff --git a/ppom/package.json b/ppom/package.json index 9e735dc0ee4..0b3268cfd18 100644 --- a/ppom/package.json +++ b/ppom/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "devDependencies": { "@babel/preset-env": "^7.22.6", - "@blockaid/ppom_release": "^1.4.8", + "@blockaid/ppom_release": "^1.4.9", "babel-loader": "^9.1.2", "binary-base64-loader": "^1.0.0", "buffer": "^6.0.3", diff --git a/ppom/yarn.lock b/ppom/yarn.lock index c11d6c30153..2d93fb3c47f 100644 --- a/ppom/yarn.lock +++ b/ppom/yarn.lock @@ -882,10 +882,10 @@ "@babel/helper-validator-identifier" "^7.22.20" to-fast-properties "^2.0.0" -"@blockaid/ppom_release@^1.4.8": - version "1.4.8" - resolved "https://registry.yarnpkg.com/@blockaid/ppom_release/-/ppom_release-1.4.8.tgz#08cc25891eb6f89586834ead3c8df8985dce5193" - integrity sha512-pvcdQlGjsTn6aVc3Tmcd/PJhuAOEV91XIK9TQe8ihk4AKmpIE9jkvdaHnsi9s7PwjBoX2H7023Ycz+P2vfr5CA== +"@blockaid/ppom_release@^1.4.9": + version "1.4.9" + resolved "https://registry.yarnpkg.com/@blockaid/ppom_release/-/ppom_release-1.4.9.tgz#4c00022f7368881bf13b43c98264fa319c900f53" + integrity sha512-0xKzp0TGkdBYUAp0IS+C6EEtFBLkLlZkuIttKA3f0Sn+kplumo8qjW0SLUp4sVs4EFq5noMGzSxY7YWS3qZT6A== "@discoveryjs/json-ext@^0.5.0": version "0.5.7" diff --git a/scripts/build-inpage-bridge.sh b/scripts/build-inpage-bridge.sh index 7823e49dd80..52753a1589a 100755 --- a/scripts/build-inpage-bridge.sh +++ b/scripts/build-inpage-bridge.sh @@ -3,11 +3,7 @@ set -euo pipefail rm -f app/core/InpageBridgeWeb3.js mkdir -p scripts/inpage-bridge/dist && rm -rf scripts/inpage-bridge/dist/* -cd scripts/inpage-bridge/inpage -../../../node_modules/.bin/webpack --config webpack.config.js -cd .. -node content-script/build.js -cat dist/inpage-bundle.js content-script/index.js > dist/index-raw.js +cd scripts/inpage-bridge/ ../../node_modules/.bin/webpack --config webpack.config.js cd ../.. cp scripts/inpage-bridge/dist/index.js app/core/InpageBridgeWeb3.js diff --git a/scripts/build.sh b/scripts/build.sh index 1022d574ca6..5e8fc5c9482 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -189,6 +189,16 @@ prebuild_android(){ # Copy fonts with iconset yes | cp -rf ./app/fonts/Metamask.ttf ./android/app/src/main/assets/fonts/Metamask.ttf + #Create google-services.json file to be used by the Firebase services. + # Check if GOOGLE_SERVICES_B64 is set + if [ ! -z "$GOOGLE_SERVICES_B64" ]; then + echo -n $GOOGLE_SERVICES_B64 | base64 -d > ./android/app/google-services.json + echo "google-services.json has been created successfully." + else + echo "GOOGLE_SERVICES_B64 is not set in the .env file." + exit 1 + fi + if [ "$PRE_RELEASE" = false ] ; then if [ -e $ANDROID_ENV_FILE ] then @@ -373,7 +383,7 @@ buildIosQA(){ buildAndroidQA(){ remapEnvVariableQA - + if [ "$PRE_RELEASE" = false ] ; then adb uninstall io.metamask.qa fi diff --git a/scripts/changelog-csv.sh b/scripts/changelog-csv.sh new file mode 100755 index 00000000000..05ab977b0fc --- /dev/null +++ b/scripts/changelog-csv.sh @@ -0,0 +1,80 @@ +#!/bin/bash + +set -e +set -u +set -o pipefail + +readonly CSV_FILE='commits.csv' + +# Add release branch arg name +RELEASE_BRANCH_NAME="${1}" + +# Temporary file for new entries +NEW_ENTRIES=$(mktemp) + +# Backup file for existing CHANGELOG +CHANGELOG="CHANGELOG.md" +CHANGELOG_BACKUP="$CHANGELOG.bak" + +# Backup existing CHANGELOG.md +cp "$CHANGELOG" "$CHANGELOG_BACKUP" + +# Function to append entry to the correct category in the temp file +append_entry() { + local change_type="$1" + local entry="$2" + # Ensure the "Other" category is explicitly handled + case "$change_type" in + Added|Changed|Fixed) ;; + *) change_type="Other" ;; # Categorize as "Other" if not matching predefined categories + esac + echo "$entry" >> "$NEW_ENTRIES-$change_type" +} + +# Read the CSV file and append entries to temp files based on change type +while IFS=, read -r commit_message author pr_link team change_type +do + pr_id=$(echo "$pr_link" | grep -o '[^/]*$') + entry="- [#$pr_id]($pr_link): $commit_message" + append_entry "$change_type" "$entry" +done < <(tail -n +2 "$CSV_FILE") # Skip the header line + +# Function to insert new entries into CHANGELOG.md after a specific line +insert_new_entries() { + local marker="## Current Main Branch" + local temp_changelog=$(mktemp) + + # Find the line number of the marker + local line_num=$(grep -n "$marker" "$CHANGELOG_BACKUP" | cut -d ':' -f 1) + + # Split the existing CHANGELOG at the marker line + head -n "$line_num" "$CHANGELOG_BACKUP" > "$temp_changelog" + + # Append the release header + echo "" >> "$temp_changelog" + echo "## $RELEASE_BRANCH_NAME - " >> "$temp_changelog" + echo "" >> "$temp_changelog" + + # Append new entries for each change type if they exist + for change_type in Added Changed Fixed Other; do + if [[ -s "$NEW_ENTRIES-$change_type" ]]; then + echo "### $change_type" >> "$temp_changelog" + cat "$NEW_ENTRIES-$change_type" >> "$temp_changelog" + echo "" >> "$temp_changelog" # Add a newline for spacing + fi + done + + # Append the rest of the original CHANGELOG content + tail -n +$((line_num + 1)) "$CHANGELOG_BACKUP" >> "$temp_changelog" + + # Replace the original CHANGELOG with the updated one + mv "$temp_changelog" "$CHANGELOG" +} + +# Trap to ensure cleanup happens +trap 'rm -f "$NEW_ENTRIES-"* "$CHANGELOG_BACKUP"' EXIT + +# Insert new entries into CHANGELOG.md +insert_new_entries + +echo 'CHANGELOG updated' \ No newline at end of file diff --git a/scripts/create-release-pr.sh b/scripts/create-release-pr.sh index c0523b242b7..c0d8157525e 100755 --- a/scripts/create-release-pr.sh +++ b/scripts/create-release-pr.sh @@ -4,7 +4,8 @@ set -e set -u set -o pipefail -NEW_VERSION="${1}" +PREVIOUS_VERSION="${1}" +NEW_VERSION="${2}" RELEASE_BRANCH_PREFIX="release/" if [[ -z $NEW_VERSION ]]; then @@ -13,7 +14,7 @@ if [[ -z $NEW_VERSION ]]; then fi RELEASE_BRANCH_NAME="${RELEASE_BRANCH_PREFIX}${NEW_VERSION}" -RELEASE_BODY="This is the release candidate for version ${NEW_VERSION}." +RELEASE_BODY="This is the release candidate for version ${NEW_VERSION}. The test plan can be found at [commit.csv](https://github.com/MetaMask/metamask-mobile/blob/${RELEASE_BRANCH_NAME}/commits.csv)" git config user.name metamaskbot git config user.email metamaskbot@users.noreply.github.com @@ -30,6 +31,13 @@ git push --set-upstream origin "${RELEASE_BRANCH_NAME}" gh pr create \ --draft \ - --title "${NEW_VERSION}" \ + --title "feat: ${NEW_VERSION}" \ --body "${RELEASE_BODY}" \ --head "${RELEASE_BRANCH_NAME}"; + +#Generate changelog and test plan csv +node ./scripts/generate-rc-commits.mjs "${PREVIOUS_VERSION}" "${RELEASE_BRANCH_NAME}" +./scripts/changelog-csv.sh "${RELEASE_BRANCH_NAME}" +git add ./commits.csv +git commit -am "updated changelog and generated feature test plan" +git push \ No newline at end of file diff --git a/scripts/generate-rc-commits.mjs b/scripts/generate-rc-commits.mjs new file mode 100644 index 00000000000..eac00c504cc --- /dev/null +++ b/scripts/generate-rc-commits.mjs @@ -0,0 +1,158 @@ +// eslint-disable-next-line import/no-nodejs-modules +import fs from 'fs'; +// eslint-disable-next-line import/no-extraneous-dependencies +import simpleGit from 'simple-git'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { Octokit } from '@octokit/rest'; + +// "GITHUB_TOKEN" is an automatically generated, repository-specific access token provided by GitHub Actions. +const githubToken = process.env.GITHUB_TOKEN; +if (!githubToken) { + console.log('GITHUB_TOKEN not found'); + process.exit(1); +} + +// Initialize Octokit with your GitHub token +const octokit = new Octokit({ auth: githubToken}); + +async function getPRLabels(prNumber) { + try { + const { data } = await octokit.pulls.get({ + owner: 'MetaMask', + repo: 'metamask-mobile', + pull_number: prNumber[1], + }); + + const labels = data.labels.map(label => label.name); + + // Check if any label name contains "team" + let teamArray = labels.filter(label => label.toLowerCase().startsWith('team-')); + + if(teamArray.length > 1 && teamArray.includes('team-mobile-platform')) + teamArray = teamArray.filter(item => item !== 'team-mobile-platform'); + + return teamArray || ['Unknown']; + + } catch (error) { + console.error(`Error fetching labels for PR #${prNumber}:`, error); + return ['Unknown']; + } +} + +// Function to filter commits based on unique commit messages and group by teams +async function filterCommitsByTeam(branchA, branchB) { + try { + const git = simpleGit(); + + const logOptions = { + from: branchB, + to: branchA, + format: { + hash: '%H', + author: '%an', + message: '%s', + }, + }; + + const log = await git.log(logOptions); + const commitsByTeam = {}; + + const MAX_COMMITS = 500; // Limit the number of commits to process + + for (const commit of log.all) { + const { author, message, hash } = commit; + if (Object.keys(commitsByTeam).length >= MAX_COMMITS) { + console.error('Too many commits for script to work') + break; + } + + // Extract PR number from the commit message using regex + const prMatch = message.match(/\(#(\d{4,5})\)$/u); + if(prMatch){ + const prLink = prMatch ? `https://github.com/MetaMask/metamask-mobile/pull/${prMatch[1]}` : ''; + const teams = await getPRLabels(prMatch); + + // Initialize the team's commits array if it doesn't exist + if (!commitsByTeam[teams]) { + commitsByTeam[teams] = []; + } + + commitsByTeam[teams].push({ + message, + author, + hash: hash.substring(0, 7), + prLink, + }); + } + } + return commitsByTeam; + } catch (error) { + console.error(error); + return {}; + } +} + +function formatAsCSV(commitsByTeam) { + const csvContent = []; + for (const [team, commits] of Object.entries(commitsByTeam)) { + commits.forEach((commit) => { + const row = [ + escapeCSV(commit.message), + escapeCSV(commit.author), + commit.prLink, + escapeCSV(team), + assignChangeType(commit.message) + ]; + csvContent.push(row.join(',')); + }); + } + csvContent.unshift('Commit Message,Author,PR Link,Team,Change Type'); + + return csvContent; +} + +// Helper function to escape CSV fields +function escapeCSV(field) { + if (field.includes(',') || field.includes('"') || field.includes('\n')) { + return `"${field.replace(/"/g, '""')}"`; // Encapsulate in double quotes and escape existing quotes + } + return field; +} +// Helper function to create change type +function assignChangeType(field) { + if (field.includes('feat')) + return 'Added'; + else if (field.includes('cherry') || field.includes('bump')) + return 'Ops'; + else if (field.includes('chore') || field.includes('test') || field.includes('ci') || field.includes('docs') || field.includes('refactor')) + return 'Changed'; + else if (field.includes('fix')) + return 'Fixed'; + + return 'Unknown'; +} + +async function main() { + const args = process.argv.slice(2); + const fileTitle = 'commits.csv'; + + if (args.length !== 2) { + console.error('Usage: node generate-rc-commits.mjs branchA branchB'); + process.exit(1); + } + + const branchA = args[0]; + const branchB = args[1]; + + const commitsByTeam = await filterCommitsByTeam(branchA, branchB); + + if (Object.keys(commitsByTeam).length === 0) { + console.log('No commits found.'); + } else { + const csvContent = formatAsCSV(commitsByTeam); + fs.writeFileSync(fileTitle, csvContent.join('\n')); + console.log('CSV file ', fileTitle, ' created successfully.'); + } +} + +main(); diff --git a/scripts/inpage-bridge/content-script/build.js b/scripts/inpage-bridge/content-script/build.js deleted file mode 100644 index cd0ca95605c..00000000000 --- a/scripts/inpage-bridge/content-script/build.js +++ /dev/null @@ -1,14 +0,0 @@ -const fs = require('fs'); -const path = require('path'); - -const distPath = path.join(__dirname, '..', '/', 'dist'); - -const inpageContent = fs - .readFileSync(path.join(distPath, 'inpage-content.js')) - .toString(); - -// wrap the inpage content in a variable declaration -const code = `const inpageBundle = ${JSON.stringify(inpageContent)}`; - -fs.writeFileSync(path.join(distPath, 'inpage-bundle.js'), code, 'ascii'); -console.log('content-script.js generated succesfully'); diff --git a/scripts/inpage-bridge/inpage/webpack.config.js b/scripts/inpage-bridge/inpage/webpack.config.js deleted file mode 100644 index f1d28be96f1..00000000000 --- a/scripts/inpage-bridge/inpage/webpack.config.js +++ /dev/null @@ -1,63 +0,0 @@ -const webpack = require('webpack'); -const path = require('path'); -const { readFileSync } = require('fs'); - -const SVG_LOGO_PATH = - '../../../app/images/fox.svg'; -function getBuildIcon() { - const svg = readFileSync(SVG_LOGO_PATH, 'utf8'); - return `data:image/svg+xml,${encodeURIComponent(svg)}`; -} - -const config = { - entry: './index.js', - - output: { - path: path.resolve(__dirname, '..', 'dist'), - filename: 'inpage-content.js', - }, - - mode: 'production', - module: { - rules: [ - { - test: /\.(js|jsx|mjs)$/u, - use: { - loader: 'babel-loader', - options: { - presets: ['@babel/preset-env'], - }, - }, - }, - ], - }, - resolve: { - fallback: { - buffer: require.resolve('buffer'), - stream: require.resolve('stream-browserify'), - _stream_transform: require.resolve('readable-stream/transform'), - _stream_readable: require.resolve('readable-stream/readable'), - _stream_writable: require.resolve('readable-stream/writable'), - _stream_duplex: require.resolve('readable-stream/duplex'), - _stream_passthrough: require.resolve('readable-stream/passthrough'), - }, - }, - plugins: [ - new webpack.ProvidePlugin({ - Buffer: ['buffer', 'Buffer'], - process: 'process/browser', - }), - new webpack.DefinePlugin({ - 'process.env.METAMASK_BUILD_NAME': JSON.stringify('MetaMask'), - 'process.env.METAMASK_BUILD_ICON': JSON.stringify(getBuildIcon()), - 'process.env.METAMASK_BUILD_APP_ID': JSON.stringify('io.metamask.mobile'), - }), - ], -}; - -module.exports = (_env, argv) => { - if (argv.mode === 'development') { - config.mode = 'development'; - } - return config; -}; diff --git a/scripts/inpage-bridge/inpage/MobilePortStream.js b/scripts/inpage-bridge/src/MobilePortStream.js similarity index 100% rename from scripts/inpage-bridge/inpage/MobilePortStream.js rename to scripts/inpage-bridge/src/MobilePortStream.js diff --git a/scripts/inpage-bridge/inpage/ReactNativePostMessageStream.js b/scripts/inpage-bridge/src/ReactNativePostMessageStream.js similarity index 100% rename from scripts/inpage-bridge/inpage/ReactNativePostMessageStream.js rename to scripts/inpage-bridge/src/ReactNativePostMessageStream.js diff --git a/scripts/inpage-bridge/content-script/index.js b/scripts/inpage-bridge/src/index.js similarity index 73% rename from scripts/inpage-bridge/content-script/index.js rename to scripts/inpage-bridge/src/index.js index 16052682c1f..2a4787822fc 100644 --- a/scripts/inpage-bridge/content-script/index.js +++ b/scripts/inpage-bridge/src/index.js @@ -1,7 +1,8 @@ -/* global inpageBundle */ + +import injectInpageProvider from './provider'; if (shouldInject()) { - injectScript(inpageBundle); + injectInpageProvider(); start(); } @@ -16,28 +17,6 @@ async function start() { window._metamaskSetupProvider(); } -/** - * Injects a script tag into the current document - * - * @param {string} content - Code to be executed in the current document - */ -function injectScript(content) { - try { - const container = document.head || document.documentElement; - - // synchronously execute script in page context - const scriptTag = document.createElement('script'); - scriptTag.setAttribute('async', false); - scriptTag.textContent = content; - container.insertBefore(scriptTag, container.children[0]); - - // script executed; remove script element from DOM - container.removeChild(scriptTag); - } catch (err) { - console.error('MetaMask script injection failed', err); - } -} - /** * Determines if the provider should be injected. * @@ -47,7 +26,6 @@ function shouldInject() { return ( doctypeCheck() && suffixCheck() && - documentElementCheck() && !blockedDomainCheck() ); } @@ -86,19 +64,6 @@ function suffixCheck() { return true; } -/** - * Checks the documentElement of the current document - * - * @returns {boolean} {@code true} if the documentElement is an html node or if none exists - */ -function documentElementCheck() { - const documentElement = document.documentElement.nodeName; - if (documentElement) { - return documentElement.toLowerCase() === 'html'; - } - return true; -} - /** * Checks if the current domain is blocked * diff --git a/scripts/inpage-bridge/inpage/index.js b/scripts/inpage-bridge/src/provider.js similarity index 83% rename from scripts/inpage-bridge/inpage/index.js rename to scripts/inpage-bridge/src/provider.js index b76b2ae006a..f3cdca54008 100644 --- a/scripts/inpage-bridge/inpage/index.js +++ b/scripts/inpage-bridge/src/provider.js @@ -15,28 +15,31 @@ const metamaskStream = new ReactNativePostMessageStream({ target: CONTENT_SCRIPT, }); -// Initialize provider object (window.ethereum) -initializeProvider({ - connectionStream: metamaskStream, - shouldSendMetadata: false, - providerInfo: { - uuid: uuid(), - name: process.env.METAMASK_BUILD_NAME, - icon: process.env.METAMASK_BUILD_ICON, - rdns: process.env.METAMASK_BUILD_APP_ID, - }, -}); +const init = () => { + // Initialize provider object (window.ethereum) + initializeProvider({ + connectionStream: metamaskStream, + shouldSendMetadata: false, + providerInfo: { + uuid: uuid(), + name: process.env.METAMASK_BUILD_NAME, + icon: process.env.METAMASK_BUILD_ICON, + rdns: process.env.METAMASK_BUILD_APP_ID, + }, + }); -// Set content script post-setup function -Object.defineProperty(window, '_metamaskSetupProvider', { - value: () => { - setupProviderStreams(); - delete window._metamaskSetupProvider; - }, - configurable: true, - enumerable: false, - writable: false, -}); + // Set content script post-setup function + Object.defineProperty(window, '_metamaskSetupProvider', { + value: () => { + setupProviderStreams(); + delete window._metamaskSetupProvider; + }, + configurable: true, + enumerable: false, + writable: false, + }); + +} // Functions @@ -130,3 +133,5 @@ function notifyProviderOfStreamFailure() { window.location.origin, ); } + +export default init; diff --git a/scripts/inpage-bridge/webpack.config.js b/scripts/inpage-bridge/webpack.config.js index 8c0dff54856..b8fa14e20d1 100644 --- a/scripts/inpage-bridge/webpack.config.js +++ b/scripts/inpage-bridge/webpack.config.js @@ -1,7 +1,16 @@ +const webpack = require('webpack'); const path = require('path'); +const { readFileSync } = require('fs'); + +const SVG_LOGO_PATH = + '../../app/images/fox.svg'; +function getBuildIcon() { + const svg = readFileSync(SVG_LOGO_PATH, 'utf8'); + return `data:image/svg+xml,${encodeURIComponent(svg)}`; +} const config = { - entry: './dist/index-raw.js', + entry: './src/index.js', output: { path: path.resolve(__dirname, 'dist'), @@ -9,6 +18,41 @@ const config = { }, mode: 'production', + module: { + rules: [ + { + test: /\.(js|jsx|mjs)$/u, + use: { + loader: 'babel-loader', + options: { + presets: ['@babel/preset-env'], + }, + }, + }, + ], + }, + resolve: { + fallback: { + buffer: require.resolve('buffer'), + stream: require.resolve('stream-browserify'), + _stream_transform: require.resolve('readable-stream/transform'), + _stream_readable: require.resolve('readable-stream/readable'), + _stream_writable: require.resolve('readable-stream/writable'), + _stream_duplex: require.resolve('readable-stream/duplex'), + _stream_passthrough: require.resolve('readable-stream/passthrough'), + }, + }, + plugins: [ + new webpack.ProvidePlugin({ + Buffer: ['buffer', 'Buffer'], + process: 'process/browser', + }), + new webpack.DefinePlugin({ + 'process.env.METAMASK_BUILD_NAME': JSON.stringify('MetaMask'), + 'process.env.METAMASK_BUILD_ICON': JSON.stringify(getBuildIcon()), + 'process.env.METAMASK_BUILD_APP_ID': JSON.stringify('io.metamask.mobile'), + }), + ], }; module.exports = (_env, argv) => { diff --git a/storybook/storyLoader.js b/storybook/storyLoader.js index 03c33fb391c..46ba9e3ec70 100644 --- a/storybook/storyLoader.js +++ b/storybook/storyLoader.js @@ -79,6 +79,8 @@ function loadStories() { require('../app/component-library/components-temp/Contracts/ContractBox/ContractBox.stories'); require('../app/component-library/components-temp/CustomSpendCap/CustomSpendCap.stories'); require('../app/component-library/components-temp/TagColored/TagColored.stories'); + require('../app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.stories'); + require('../app/component-library/components-temp/Price/PercentageChange/PercentageChange.stories'); } const stories = [ @@ -157,6 +159,8 @@ const stories = [ '../app/component-library/components-temp/Contracts/ContractBox/ContractBox.stories', '../app/component-library/components-temp/CustomSpendCap/CustomSpendCap.stories', '../app/component-library/components-temp/TagColored/TagColored.stories', + '../app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.stories.tsx', + '../app/component-library/components-temp/Price/PercentageChange/PercentageChange.stories.tsx', ]; module.exports = { diff --git a/wdio/screen-objects/AddContact.js b/wdio/screen-objects/AddContact.js index 86f69542249..e6e0a4a557f 100644 --- a/wdio/screen-objects/AddContact.js +++ b/wdio/screen-objects/AddContact.js @@ -7,7 +7,7 @@ import { ADD_CONTACT_NAME_INPUT, ADD_CONTACTS_CONTAINER_ID, } from './testIDs/Screens/AddContact.testIds'; -import { EDIT_BUTTON } from './testIDs/Common.testIds'; +import { AddContactViewSelectorsIDs } from '../../e2e/selectors/Settings/Contacts/AddContactView.selectors'; class AddContacts { get container() { @@ -31,7 +31,7 @@ class AddContacts { } get editButton() { - return Selectors.getElementByPlatform(EDIT_BUTTON); + return Selectors.getElementByPlatform(AddContactViewSelectorsIDs.EDIT_BUTTON); } async waitForDisplay() { diff --git a/wdio/screen-objects/CommonScreen.js b/wdio/screen-objects/CommonScreen.js index ec55c68f7e8..b422059e565 100644 --- a/wdio/screen-objects/CommonScreen.js +++ b/wdio/screen-objects/CommonScreen.js @@ -1,15 +1,16 @@ import Selectors from '../helpers/Selectors'; import Gestures from '../helpers/Gestures'; -import { ANDROID_PROGRESS_BAR, TOAST_ID } from './testIDs/Common.testIds'; +import { ToastSelectorsIDs } from '../../e2e/selectors/Modals/ToastModal.selectors'; +import { CommonSelectorsIDs } from '../../e2e/selectors/Common.selectors'; import { NOTIFICATION_TITLE } from './testIDs/Components/Notification.testIds'; class CommonScreen { get toast() { - return Selectors.getXpathElementByResourceId(TOAST_ID); + return Selectors.getXpathElementByResourceId(ToastSelectorsIDs.CONTAINER); } get androidProgressBar() { - return Selectors.getElementByCss(ANDROID_PROGRESS_BAR); + return Selectors.getElementByCss(CommonSelectorsIDs.ANDROID_PROGRESS_BAR); } get TokenNotificationTitle() { diff --git a/wdio/screen-objects/Modals/WalletAccountModal.js b/wdio/screen-objects/Modals/WalletAccountModal.js index 8237566f91e..a01bea88abd 100644 --- a/wdio/screen-objects/Modals/WalletAccountModal.js +++ b/wdio/screen-objects/Modals/WalletAccountModal.js @@ -1,23 +1,18 @@ -import { - ACCOUNT_OVERVIEW_ID, - WALLET_ACCOUNT_NAME_LABEL_INPUT, - WALLET_ACCOUNT_NAME_LABEL_TEXT, -} from '../testIDs/Screens/WalletView.testIds'; - +import { WalletViewSelectorsIDs } from '../../../e2e/selectors/wallet/WalletView.selectors'; import Selectors from '../../helpers/Selectors'; import Gestures from '../../helpers/Gestures'; class WalletAccountModal { get accountNameLabelText() { - return Selectors.getXpathElementByResourceId(WALLET_ACCOUNT_NAME_LABEL_TEXT); + return Selectors.getXpathElementByResourceId(WalletViewSelectorsIDs.ACCOUNT_NAME_LABEL_TEXT); } get accountNameLabelInput() { - return Selectors.getElementByPlatform(WALLET_ACCOUNT_NAME_LABEL_INPUT); + return Selectors.getElementByPlatform(WalletViewSelectorsIDs.ACCOUNT_NAME_LABEL_INPUT); } get walletAccountOverview() { - return Selectors.getXpathElementByResourceId(ACCOUNT_OVERVIEW_ID); + return Selectors.getXpathElementByResourceId(WalletViewSelectorsIDs.ACCOUNT_OVERVIEW); } async longPressAccountNameLabel() { diff --git a/wdio/screen-objects/WalletMainScreen.js b/wdio/screen-objects/WalletMainScreen.js index 76fab29103d..e21fd6148a0 100644 --- a/wdio/screen-objects/WalletMainScreen.js +++ b/wdio/screen-objects/WalletMainScreen.js @@ -1,24 +1,13 @@ import Selectors from '../helpers/Selectors'; import Gestures from '../helpers/Gestures.js'; import { ONBOARDING_WIZARD_STEP_1_NO_THANKS_ID } from './testIDs/Components/OnboardingWizard.testIds'; -import { - IMPORT_NFT_BUTTON_ID, - IMPORT_TOKEN_BUTTON_ID, - MAIN_WALLET_ACCOUNT_ACTIONS, - NAVBAR_NETWORK_BUTTON, - NAVBAR_NETWORK_TEXT, - NOTIFICATION_REMIND_ME_LATER_BUTTON_ID, - SECURE_WALLET_BACKUP_ALERT_MODAL, - SHARE_ADDRESS, - SHOW_PRIVATE_KEY, - VIEW_ETHERSCAN, - WALLET_ACCOUNT_ICON, -} from './testIDs/Screens/WalletView.testIds'; +import { ProtectWalletModalSelectorsIDs } from '../../e2e/selectors/Modals/ProtectWalletModal.selectors'; +import { AccountActionsModalSelectorsIDs } from '../../e2e/selectors/Modals/AccountActionsModal.selectors'; import { NOTIFICATION_TITLE } from './testIDs/Components/Notification.testIds'; import { TabBarSelectorIDs } from '../../e2e/selectors/TabBar.selectors'; import { BACK_BUTTON_SIMPLE_WEBVIEW } from './testIDs/Components/SimpleWebView.testIds'; -import { WalletViewSelectorsIDs } from "../../e2e/selectors/wallet/WalletView.selectors.js"; +import { WalletViewSelectorsIDs } from '../../e2e/selectors/wallet/WalletView.selectors.js'; class WalletMainScreen { get noThanks() { @@ -28,19 +17,19 @@ class WalletMainScreen { } get ImportToken() { - return Selectors.getElementByPlatform(IMPORT_TOKEN_BUTTON_ID); + return Selectors.getElementByPlatform(WalletViewSelectorsIDs.IMPORT_TOKEN_BUTTON); } get ImportNFT() { - return Selectors.getElementByPlatform(IMPORT_NFT_BUTTON_ID); + return Selectors.getElementByPlatform(WalletViewSelectorsIDs.IMPORT_NFT_BUTTON); } get TokenNotificationTitle() { return Selectors.getElementByPlatform(NOTIFICATION_TITLE); } - get Identicon() { - return Selectors.getXpathElementByResourceId(WALLET_ACCOUNT_ICON); + get accountIcon() { + return Selectors.getXpathElementByResourceId(WalletViewSelectorsIDs.ACCOUNT_ICON); } get WalletScreenContainer() { @@ -48,37 +37,37 @@ class WalletMainScreen { } get networkInNavBar() { - return Selectors.getXpathElementByResourceId(NAVBAR_NETWORK_BUTTON); + return Selectors.getXpathElementByResourceId(WalletViewSelectorsIDs.NAVBAR_NETWORK_BUTTON); } get remindMeLaterNotification() { return Selectors.getElementByPlatform( - NOTIFICATION_REMIND_ME_LATER_BUTTON_ID, + ProtectWalletModalSelectorsIDs.REMIND_ME_LATER_BUTTON, ); } get backupAlertModal() { - return Selectors.getElementByPlatform(SECURE_WALLET_BACKUP_ALERT_MODAL); + return Selectors.getElementByPlatform(ProtectWalletModalSelectorsIDs.COLLAPSED_WALLET_MODAL); } get networkNavbarTitle() { - return Selectors.getXpathElementByResourceId(NAVBAR_NETWORK_TEXT); + return Selectors.getXpathElementByResourceId(WalletViewSelectorsIDs.NAVBAR_NETWORK_TEXT); } get accountActionsButton() { - return Selectors.getXpathElementByResourceId(MAIN_WALLET_ACCOUNT_ACTIONS); + return Selectors.getXpathElementByResourceId(WalletViewSelectorsIDs.ACCOUNT_ACTIONS); } get privateKeyActionButton() { - return Selectors.getElementByPlatform(SHOW_PRIVATE_KEY); + return Selectors.getElementByPlatform(AccountActionsModalSelectorsIDs.SHOW_PRIVATE_KEY); } get shareAddressActionButton() { - return Selectors.getElementByPlatform(SHARE_ADDRESS); + return Selectors.getElementByPlatform(AccountActionsModalSelectorsIDs.SHARE_ADDRESS); } get viewEtherscanActionButton() { - return Selectors.getElementByPlatform(VIEW_ETHERSCAN); + return Selectors.getElementByPlatform(AccountActionsModalSelectorsIDs.VIEW_ETHERSCAN); } get walletButton() { @@ -122,7 +111,7 @@ class WalletMainScreen { } async tapIdenticon() { - await Gestures.waitAndTap(this.Identicon); + await Gestures.waitAndTap(this.accountIcon); } async tapNetworkNavBar() { diff --git a/wdio/screen-objects/testIDs/Common.testIds.js b/wdio/screen-objects/testIDs/Common.testIds.js deleted file mode 100644 index c3fc5be7aba..00000000000 --- a/wdio/screen-objects/testIDs/Common.testIds.js +++ /dev/null @@ -1,3 +0,0 @@ -export const TOAST_ID = 'toast'; -export const ANDROID_PROGRESS_BAR = 'android.widget.ProgressBar'; -export const EDIT_BUTTON = 'edit-button'; diff --git a/wdio/screen-objects/testIDs/Screens/WalletView.testIds.js b/wdio/screen-objects/testIDs/Screens/WalletView.testIds.js index 4be4f63eb87..acc724a2389 100644 --- a/wdio/screen-objects/testIDs/Screens/WalletView.testIds.js +++ b/wdio/screen-objects/testIDs/Screens/WalletView.testIds.js @@ -1,26 +1,2 @@ export const SEND_BUTTON_ID = 'token-send-button'; -export const IMPORT_NFT_BUTTON_ID = 'import-collectible-button'; -export const IMPORT_TOKEN_BUTTON_ID = 'import-token-button'; -export const MAIN_WALLET_VIEW_VIA_TOKENS_ID = 'tokens'; - -export const WALLET_ACCOUNT_ICON = 'account-picker'; -export const WALLET_ACCOUNT_NAME_LABEL_TEXT = 'account-label'; -export const WALLET_ACCOUNT_NAME_LABEL_INPUT = 'account-label-text-input'; - -export const ACCOUNT_OVERVIEW_ID = 'account-overview'; -export const NAVBAR_NETWORK_BUTTON = 'open-networks-button'; - -export const NFT_TAB_CONTAINER_ID = 'collectible-contracts'; - -export const NOTIFICATION_REMIND_ME_LATER_BUTTON_ID = - 'notification-remind-later-button'; - -export const SECURE_WALLET_BACKUP_ALERT_MODAL = 'backup-alert'; - -export const NAVBAR_NETWORK_TEXT = 'open-networks-text'; export const getAssetTestId = (token) => `asset-${token}`; - -export const MAIN_WALLET_ACCOUNT_ACTIONS = 'main-wallet-account-actions'; -export const VIEW_ETHERSCAN = 'view-etherscan-action'; -export const SHARE_ADDRESS = 'share-address-action'; -export const SHOW_PRIVATE_KEY = 'show-private-key-action'; diff --git a/yarn.lock b/yarn.lock index 3e03fa82aef..66188da1052 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1310,21 +1310,10 @@ resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.6.0.tgz#ec6cd237440700bc23ca23087f513c75508958b0" integrity sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA== -"@consensys/ledgerhq-metamask-keyring@0.0.9": - version "0.0.9" - resolved "https://registry.yarnpkg.com/@consensys/ledgerhq-metamask-keyring/-/ledgerhq-metamask-keyring-0.0.9.tgz#f824381c9cf55c6e6aad5693263dee77a17d2ac2" - integrity sha512-o/wdUU/7s8fIZxP0CcKjaWQaoJ1bz2+3BgeQSsizR2WLqZJF8phYF6t9hwfZeFFgM1FMZDuanVapCvOi13QjiA== - dependencies: - "@ethereumjs/tx" "^4.2.0" - "@ledgerhq/hw-app-eth" "6.26.1" - "@metamask/eth-sig-util" "^7.0.0" - buffer "^6.0.3" - ethereumjs-util "^7.1.5" - -"@consensys/on-ramp-sdk@1.27.1": - version "1.27.1" - resolved "https://registry.yarnpkg.com/@consensys/on-ramp-sdk/-/on-ramp-sdk-1.27.1.tgz#a422dbfb16a23cdc53b32d9cd307d75b3a438827" - integrity sha512-C/ekiaPwFmWDCk0JhXopdvWJ5WuFvZmLPg4DrMEuqIi984+AwDkThU6fCiQ8FjmBIwdMiiYACQlXKjOAKy2/Ww== +"@consensys/on-ramp-sdk@1.28.1": + version "1.28.1" + resolved "https://registry.yarnpkg.com/@consensys/on-ramp-sdk/-/on-ramp-sdk-1.28.1.tgz#bcc5c06a20256b471d3943bfe02819edb9cd7212" + integrity sha512-C+uNWr9K/ogDw/+Px8p9Dqdk18hYYCE5uMsIIkzNAiFPJFPMCtO1iRFMpSk+6hFJEYaj3mSL9dqnM9hQKFJcVw== dependencies: async "^3.2.3" axios "^0.28.0" @@ -1334,10 +1323,26 @@ reflect-metadata "^0.1.13" uuid "^9.0.0" -"@contentful/rich-text-types@^16.0.2": - version "16.3.5" - resolved "https://registry.yarnpkg.com/@contentful/rich-text-types/-/rich-text-types-16.3.5.tgz#927433a84416101ba3dbf19bd0924e755f5e6d65" - integrity sha512-ZLq6p5uyQXg+i1XGDFu4tAc2VYS12S1KA/jIOyyZjNgC1DvDajsi1JzuiBuOuMEhi1sKEUy6Ry3Yr9jsQtOKuQ== +"@contentful/content-source-maps@^0.6.0": + version "0.6.1" + resolved "https://registry.yarnpkg.com/@contentful/content-source-maps/-/content-source-maps-0.6.1.tgz#a12828d287bdcd9f31c132f2a230bd1ac3005b77" + integrity sha512-IjsyhakG17OC5xtIa5agVRsnjjxiJf9HZjpDIV6Ix036Pd5YHJrbyyiF4KNEmLlIqe3hQ0kHGWEs9S/HfECmRQ== + dependencies: + "@vercel/stega" "^0.1.2" + json-pointer "^0.6.2" + +"@contentful/rich-text-html-renderer@^16.5.2": + version "16.6.4" + resolved "https://registry.yarnpkg.com/@contentful/rich-text-html-renderer/-/rich-text-html-renderer-16.6.4.tgz#7644c3c5e00c06aa35106b6bc222d42ea2191bd8" + integrity sha512-N6gPZgfGMElPMRwvyQtEuOV3Ti+4oMGRQoVUag8ZZqy2oPzL2d2+qh2YCeDKSSVl7lBgl+HdyyorD3WqMj2eCQ== + dependencies: + "@contentful/rich-text-types" "^16.7.0" + escape-html "^1.0.3" + +"@contentful/rich-text-types@^16.0.2", "@contentful/rich-text-types@^16.7.0": + version "16.7.0" + resolved "https://registry.yarnpkg.com/@contentful/rich-text-types/-/rich-text-types-16.7.0.tgz#e9a5d59a94867536180a4e506b1745dfe04f83f0" + integrity sha512-SMW6JjJpp05CU5xBjIJp9azxFJ/bzcRzd2o9sF49C736m3aCxSobfuUIJz0dHw92MGsiFKAfzn5yTL3JMfqBqg== "@craftzdog/react-native-buffer@^6.0.5": version "6.0.5" @@ -1883,7 +1888,7 @@ dependencies: "@ethereumjs/util" "^9.0.3" -"@ethereumjs/rlp@^4.0.1": +"@ethereumjs/rlp@^4.0.0", "@ethereumjs/rlp@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@ethereumjs/rlp/-/rlp-4.0.1.tgz#626fabfd9081baab3d0a3074b0c7ecaf674aaa41" integrity sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw== @@ -2346,6 +2351,390 @@ resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.1.tgz#b9da6a878a371829a0502c9b6c1c143ef6663f4d" integrity sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA== +"@firebase/analytics-compat@0.2.11": + version "0.2.11" + resolved "https://registry.yarnpkg.com/@firebase/analytics-compat/-/analytics-compat-0.2.11.tgz#82995b29805f306ad862773e2cd907ae8fb7b7e5" + integrity sha512-wmXxJ49pEY7H549Pa4CDPOTzkPJnfG2Yolptg72ntTgSrbKVq+Eg9cAQY6Z5Kn9ATSQRX5oGXKlNfEk5DJBvvA== + dependencies: + "@firebase/analytics" "0.10.5" + "@firebase/analytics-types" "0.8.2" + "@firebase/component" "0.6.8" + "@firebase/util" "1.9.7" + tslib "^2.1.0" + +"@firebase/analytics-types@0.8.2": + version "0.8.2" + resolved "https://registry.yarnpkg.com/@firebase/analytics-types/-/analytics-types-0.8.2.tgz#947f85346e404332aac6c996d71fd4a89cd7f87a" + integrity sha512-EnzNNLh+9/sJsimsA/FGqzakmrAUKLeJvjRHlg8df1f97NLUlFidk9600y0ZgWOp3CAxn6Hjtk+08tixlUOWyw== + +"@firebase/analytics@0.10.5": + version "0.10.5" + resolved "https://registry.yarnpkg.com/@firebase/analytics/-/analytics-0.10.5.tgz#a455028952bdc25b9da2b0070ebb09ca487ee09f" + integrity sha512-d0X2ksTOKHMf5zFAMKFZWXa8hSbgohsG507xFsGhF4Uet2b8uEUL/YLrEth67jXEbGEi1UQZX4AaGBxKNiDzjw== + dependencies: + "@firebase/component" "0.6.8" + "@firebase/installations" "0.6.8" + "@firebase/logger" "0.4.2" + "@firebase/util" "1.9.7" + tslib "^2.1.0" + +"@firebase/app-check-compat@0.3.12": + version "0.3.12" + resolved "https://registry.yarnpkg.com/@firebase/app-check-compat/-/app-check-compat-0.3.12.tgz#34d826f72e058baf1aad11713fda337046fb863c" + integrity sha512-p/5w3pMih3JVT6u7g04KXgSZr6HDsQXyeWZkIe0+r71dPOlcKyUooe9/feTc8BWpjha3rUOkqQ7+JXZObwvYoQ== + dependencies: + "@firebase/app-check" "0.8.5" + "@firebase/app-check-types" "0.5.2" + "@firebase/component" "0.6.8" + "@firebase/logger" "0.4.2" + "@firebase/util" "1.9.7" + tslib "^2.1.0" + +"@firebase/app-check-interop-types@0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.2.tgz#455b6562c7a3de3ef75ea51f72dfec5829ad6997" + integrity sha512-LMs47Vinv2HBMZi49C09dJxp0QT5LwDzFaVGf/+ITHe3BlIhUiLNttkATSXplc89A2lAaeTqjgqVkiRfUGyQiQ== + +"@firebase/app-check-types@0.5.2": + version "0.5.2" + resolved "https://registry.yarnpkg.com/@firebase/app-check-types/-/app-check-types-0.5.2.tgz#1221bd09b471e11bb149252f16640a0a51043cbc" + integrity sha512-FSOEzTzL5bLUbD2co3Zut46iyPWML6xc4x+78TeaXMSuJap5QObfb+rVvZJtla3asN4RwU7elaQaduP+HFizDA== + +"@firebase/app-check@0.8.5": + version "0.8.5" + resolved "https://registry.yarnpkg.com/@firebase/app-check/-/app-check-0.8.5.tgz#e8b0a6d603592f6a04f2d429029f5adfe1a4d2ca" + integrity sha512-WyIckkVYAfnzsPIw6EAt/qBUANkUAVl6irF0xuJ1R9ISNyUT1h7dPAwvs/g3rsx0fpBWaHRAH0IFiN6zO6yLqQ== + dependencies: + "@firebase/component" "0.6.8" + "@firebase/logger" "0.4.2" + "@firebase/util" "1.9.7" + tslib "^2.1.0" + +"@firebase/app-compat@0.2.36": + version "0.2.36" + resolved "https://registry.yarnpkg.com/@firebase/app-compat/-/app-compat-0.2.36.tgz#46926ee9ba0d54fc5ec4695e62588b63e2f7584a" + integrity sha512-qsf+pllpgy1IGe2f5vfenOHSX8Cs58sVR5L6h/zBlNy9Yo54B2jy61KxLpSOgyRZb18IlnLLGjo7VtGU1CHvHA== + dependencies: + "@firebase/app" "0.10.6" + "@firebase/component" "0.6.8" + "@firebase/logger" "0.4.2" + "@firebase/util" "1.9.7" + tslib "^2.1.0" + +"@firebase/app-types@0.9.2": + version "0.9.2" + resolved "https://registry.yarnpkg.com/@firebase/app-types/-/app-types-0.9.2.tgz#8cbcceba784753a7c0066a4809bc22f93adee080" + integrity sha512-oMEZ1TDlBz479lmABwWsWjzHwheQKiAgnuKxE0pz0IXCVx7/rtlkx1fQ6GfgK24WCrxDKMplZrT50Kh04iMbXQ== + +"@firebase/app@0.10.6": + version "0.10.6" + resolved "https://registry.yarnpkg.com/@firebase/app/-/app-0.10.6.tgz#0f96a573c18d75723ddeedb45c02c5471d9de695" + integrity sha512-/r8Ikp7TOrIIdp7v2adD2kg9SqIXMGOoJXJB1HsX7LjpjWdsoy1fMkP0HlI7GQqqRxDueHNhETx5Zn5E8HyVAQ== + dependencies: + "@firebase/component" "0.6.8" + "@firebase/logger" "0.4.2" + "@firebase/util" "1.9.7" + idb "7.1.1" + tslib "^2.1.0" + +"@firebase/auth-compat@0.5.10": + version "0.5.10" + resolved "https://registry.yarnpkg.com/@firebase/auth-compat/-/auth-compat-0.5.10.tgz#7705fc27883a8fafb2e85271e1d5cd7314609276" + integrity sha512-epDhgNIXmhl9DPuTW9Ec5NDJJKMFIdXBXiQI9O0xNHveow/ETtBCY86srzF7iCacqsd30CcpLwwXlhk8Y19Olg== + dependencies: + "@firebase/auth" "1.7.5" + "@firebase/auth-types" "0.12.2" + "@firebase/component" "0.6.8" + "@firebase/util" "1.9.7" + tslib "^2.1.0" + undici "5.28.4" + +"@firebase/auth-interop-types@0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@firebase/auth-interop-types/-/auth-interop-types-0.2.3.tgz#927f1f2139a680b55fef0bddbff2c982b08587e8" + integrity sha512-Fc9wuJGgxoxQeavybiuwgyi+0rssr76b+nHpj+eGhXFYAdudMWyfBHvFL/I5fEHniUM/UQdFzi9VXJK2iZF7FQ== + +"@firebase/auth-types@0.12.2": + version "0.12.2" + resolved "https://registry.yarnpkg.com/@firebase/auth-types/-/auth-types-0.12.2.tgz#f12d890585866e53b6ab18b16fa4d425c52eee6e" + integrity sha512-qsEBaRMoGvHO10unlDJhaKSuPn4pyoTtlQuP1ghZfzB6rNQPuhp/N/DcFZxm9i4v0SogjCbf9reWupwIvfmH6w== + +"@firebase/auth@1.7.5": + version "1.7.5" + resolved "https://registry.yarnpkg.com/@firebase/auth/-/auth-1.7.5.tgz#8135e0933e874231d7ebafc94f5796a19f5df39b" + integrity sha512-DMFR1OA/f1/voeuFbSORg9AP36pMgOoSb/DRgiDalLmIJsDTlQNMCu+givjMP4s/XL85+tBk2MerYnK/AscJjw== + dependencies: + "@firebase/component" "0.6.8" + "@firebase/logger" "0.4.2" + "@firebase/util" "1.9.7" + tslib "^2.1.0" + undici "5.28.4" + +"@firebase/component@0.6.8": + version "0.6.8" + resolved "https://registry.yarnpkg.com/@firebase/component/-/component-0.6.8.tgz#899b9318c0ce0586580e8cda7eaf61296f7fb43b" + integrity sha512-LcNvxGLLGjBwB0dJUsBGCej2fqAepWyBubs4jt1Tiuns7QLbXHuyObZ4aMeBjZjWx4m8g1LoVI9QFpSaq/k4/g== + dependencies: + "@firebase/util" "1.9.7" + tslib "^2.1.0" + +"@firebase/database-compat@1.0.6": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@firebase/database-compat/-/database-compat-1.0.6.tgz#6a4966fe4a9d8bc2cb11ee98a1bb01ab954d7d66" + integrity sha512-1OGA0sLY47mkXjhICCrUTXEYFnSSXoiXWm1SHsN62b+Lzs5aKA3aWTjTUmYIoK93kDAMPkYpulSv8jcbH4Hwew== + dependencies: + "@firebase/component" "0.6.8" + "@firebase/database" "1.0.6" + "@firebase/database-types" "1.0.4" + "@firebase/logger" "0.4.2" + "@firebase/util" "1.9.7" + tslib "^2.1.0" + +"@firebase/database-types@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@firebase/database-types/-/database-types-1.0.4.tgz#dc507f7838ed29ac3235c68ebae5fd42a562e3e8" + integrity sha512-mz9ZzbH6euFXbcBo+enuJ36I5dR5w+enJHHjy9Y5ThCdKUseqfDjW3vCp1YxE9zygFCSjJJ/z1cQ+zodvUcwPQ== + dependencies: + "@firebase/app-types" "0.9.2" + "@firebase/util" "1.9.7" + +"@firebase/database@1.0.6": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@firebase/database/-/database-1.0.6.tgz#cf0592b140e207e35c14efe6776fc92266ac408a" + integrity sha512-nrexUEG/fpVlHtWKkyfhTC3834kZ1WS7voNyqbBsBCqHXQOvznN5Z0L3nxBqdXSJyltNAf4ndFlQqm5gZiEczQ== + dependencies: + "@firebase/app-check-interop-types" "0.3.2" + "@firebase/auth-interop-types" "0.2.3" + "@firebase/component" "0.6.8" + "@firebase/logger" "0.4.2" + "@firebase/util" "1.9.7" + faye-websocket "0.11.4" + tslib "^2.1.0" + +"@firebase/firestore-compat@0.3.33": + version "0.3.33" + resolved "https://registry.yarnpkg.com/@firebase/firestore-compat/-/firestore-compat-0.3.33.tgz#8e591bfafb574c695b09101b98c1a1057f55c60e" + integrity sha512-i42a2l31N95CwYEB7zmfK0FS1mrO6pwOLwxavCrwu1BCFrVVVQhUheTPIda/iGguK/2Nog0RaIR1bo7QkZEz3g== + dependencies: + "@firebase/component" "0.6.8" + "@firebase/firestore" "4.6.4" + "@firebase/firestore-types" "3.0.2" + "@firebase/util" "1.9.7" + tslib "^2.1.0" + +"@firebase/firestore-types@3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@firebase/firestore-types/-/firestore-types-3.0.2.tgz#75c301acc5fa33943eaaa9570b963c55398cad2a" + integrity sha512-wp1A+t5rI2Qc/2q7r2ZpjUXkRVPtGMd6zCLsiWurjsQpqPgFin3AhNibKcIzoF2rnToNa/XYtyWXuifjOOwDgg== + +"@firebase/firestore@4.6.4": + version "4.6.4" + resolved "https://registry.yarnpkg.com/@firebase/firestore/-/firestore-4.6.4.tgz#f53fcfc3ecfeb844f2147a43382d013d21e64968" + integrity sha512-vk2MoH5HxYEhiNg1l+yBXq1Fkhue/11bFg4HdlTv6BJHcTnnAj2a+/afPpatcW4MOdYA3Tv+d5nGzWbbOC1SHw== + dependencies: + "@firebase/component" "0.6.8" + "@firebase/logger" "0.4.2" + "@firebase/util" "1.9.7" + "@firebase/webchannel-wrapper" "1.0.1" + "@grpc/grpc-js" "~1.9.0" + "@grpc/proto-loader" "^0.7.8" + tslib "^2.1.0" + undici "5.28.4" + +"@firebase/functions-compat@0.3.12": + version "0.3.12" + resolved "https://registry.yarnpkg.com/@firebase/functions-compat/-/functions-compat-0.3.12.tgz#aae387eb48466df1d031fc5bb755c657cfeb5994" + integrity sha512-r3XUb5VlITWpML46JymfJPkK6I9j4SNlO7qWIXUc0TUmkv0oAfVoiIt1F83/NuMZXaGr4YWA/794nVSy4GV8tw== + dependencies: + "@firebase/component" "0.6.8" + "@firebase/functions" "0.11.6" + "@firebase/functions-types" "0.6.2" + "@firebase/util" "1.9.7" + tslib "^2.1.0" + +"@firebase/functions-types@0.6.2": + version "0.6.2" + resolved "https://registry.yarnpkg.com/@firebase/functions-types/-/functions-types-0.6.2.tgz#03b4ec9259d2f57548a3909d6a35ae35ad243552" + integrity sha512-0KiJ9lZ28nS2iJJvimpY4nNccV21rkQyor5Iheu/nq8aKXJqtJdeSlZDspjPSBBiHRzo7/GMUttegnsEITqR+w== + +"@firebase/functions@0.11.6": + version "0.11.6" + resolved "https://registry.yarnpkg.com/@firebase/functions/-/functions-0.11.6.tgz#607991a3a870051e6456d7ccb0217fac6305db89" + integrity sha512-GPfIBPtpwQvsC7SQbgaUjLTdja0CsNwMoKSgrzA1FGGRk4NX6qO7VQU6XCwBiAFWbpbQex6QWkSMsCzLx1uibQ== + dependencies: + "@firebase/app-check-interop-types" "0.3.2" + "@firebase/auth-interop-types" "0.2.3" + "@firebase/component" "0.6.8" + "@firebase/messaging-interop-types" "0.2.2" + "@firebase/util" "1.9.7" + tslib "^2.1.0" + undici "5.28.4" + +"@firebase/installations-compat@0.2.8": + version "0.2.8" + resolved "https://registry.yarnpkg.com/@firebase/installations-compat/-/installations-compat-0.2.8.tgz#ebc908afe84db2754b19a62f7655608911e13819" + integrity sha512-pI2q8JFHB7yIq/szmhzGSWXtOvtzl6tCUmyykv5C8vvfOVJUH6mP4M4iwjbK8S1JotKd/K70+JWyYlxgQ0Kpyw== + dependencies: + "@firebase/component" "0.6.8" + "@firebase/installations" "0.6.8" + "@firebase/installations-types" "0.5.2" + "@firebase/util" "1.9.7" + tslib "^2.1.0" + +"@firebase/installations-types@0.5.2": + version "0.5.2" + resolved "https://registry.yarnpkg.com/@firebase/installations-types/-/installations-types-0.5.2.tgz#4d4949e0e83ced7f36cbee009355cd305a36e158" + integrity sha512-que84TqGRZJpJKHBlF2pkvc1YcXrtEDOVGiDjovP/a3s6W4nlbohGXEsBJo0JCeeg/UG9A+DEZVDUV9GpklUzA== + +"@firebase/installations@0.6.8": + version "0.6.8" + resolved "https://registry.yarnpkg.com/@firebase/installations/-/installations-0.6.8.tgz#f9c9d493bce04b04ca28814e926ef3ed71f033d6" + integrity sha512-57V374qdb2+wT5v7+ntpLXBjZkO6WRgmAUbVkRfFTM/4t980p0FesbqTAcOIiM8U866UeuuuF8lYH70D3jM/jQ== + dependencies: + "@firebase/component" "0.6.8" + "@firebase/util" "1.9.7" + idb "7.1.1" + tslib "^2.1.0" + +"@firebase/logger@0.4.2": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@firebase/logger/-/logger-0.4.2.tgz#74dfcfeedee810deb8a7080d5b7eba56aa16ffa2" + integrity sha512-Q1VuA5M1Gjqrwom6I6NUU4lQXdo9IAQieXlujeHZWvRt1b7qQ0KwBaNAjgxG27jgF9/mUwsNmO8ptBCGVYhB0A== + dependencies: + tslib "^2.1.0" + +"@firebase/messaging-compat@0.2.10": + version "0.2.10" + resolved "https://registry.yarnpkg.com/@firebase/messaging-compat/-/messaging-compat-0.2.10.tgz#08711f75e2d517fd209bfbc65b1f754b09b2121c" + integrity sha512-FXQm7rcowkDm8kFLduHV35IRYCRo+Ng0PIp/t1+EBuEbyplaKkGjZ932pE+owf/XR+G/60ku2QRBptRGLXZydg== + dependencies: + "@firebase/component" "0.6.8" + "@firebase/messaging" "0.12.10" + "@firebase/util" "1.9.7" + tslib "^2.1.0" + +"@firebase/messaging-interop-types@0.2.2": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.2.tgz#81042f7e9739733fa4571d17f6eb6869522754d0" + integrity sha512-l68HXbuD2PPzDUOFb3aG+nZj5KA3INcPwlocwLZOzPp9rFM9yeuI9YLl6DQfguTX5eAGxO0doTR+rDLDvQb5tA== + +"@firebase/messaging@0.12.10": + version "0.12.10" + resolved "https://registry.yarnpkg.com/@firebase/messaging/-/messaging-0.12.10.tgz#29909f909b9588d44864732377d88de11f3b3ed3" + integrity sha512-fGbxJPKpl2DIKNJGhbk4mYPcM+qE2gl91r6xPoiol/mN88F5Ym6UeRdMVZah+pijh9WxM55alTYwXuW40r1Y2Q== + dependencies: + "@firebase/component" "0.6.8" + "@firebase/installations" "0.6.8" + "@firebase/messaging-interop-types" "0.2.2" + "@firebase/util" "1.9.7" + idb "7.1.1" + tslib "^2.1.0" + +"@firebase/performance-compat@0.2.8": + version "0.2.8" + resolved "https://registry.yarnpkg.com/@firebase/performance-compat/-/performance-compat-0.2.8.tgz#d97bab3fd0c147c7f796e9b8f78712bc0b83699c" + integrity sha512-o7TFClRVJd3VIBoY7KZQqtCeW0PC6v9uBzM6Lfw3Nc9D7hM6OonqecYvh7NwJ6R14k+xM27frLS4BcCvFHKw2A== + dependencies: + "@firebase/component" "0.6.8" + "@firebase/logger" "0.4.2" + "@firebase/performance" "0.6.8" + "@firebase/performance-types" "0.2.2" + "@firebase/util" "1.9.7" + tslib "^2.1.0" + +"@firebase/performance-types@0.2.2": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@firebase/performance-types/-/performance-types-0.2.2.tgz#7b78cd2ab2310bac89a63348d93e67e01eb06dd7" + integrity sha512-gVq0/lAClVH5STrIdKnHnCo2UcPLjJlDUoEB/tB4KM+hAeHUxWKnpT0nemUPvxZ5nbdY/pybeyMe8Cs29gEcHA== + +"@firebase/performance@0.6.8": + version "0.6.8" + resolved "https://registry.yarnpkg.com/@firebase/performance/-/performance-0.6.8.tgz#668b0fc207389f7829fd3bfb6614fe819b7db124" + integrity sha512-F+alziiIZ6Yn8FG47mxwljq+4XkgkT2uJIFRlkyViUQRLzrogaUJW6u/+6ZrePXnouKlKIwzqos3PVJraPEcCA== + dependencies: + "@firebase/component" "0.6.8" + "@firebase/installations" "0.6.8" + "@firebase/logger" "0.4.2" + "@firebase/util" "1.9.7" + tslib "^2.1.0" + +"@firebase/remote-config-compat@0.2.8": + version "0.2.8" + resolved "https://registry.yarnpkg.com/@firebase/remote-config-compat/-/remote-config-compat-0.2.8.tgz#a6df065c1fd0a943e84ee0e76acfc6c1bede42f9" + integrity sha512-UxSFOp6dzFj2AHB8Bq/BYtbq5iFyizKx4Rd6WxAdaKYM8cnPMeK+l2v+Oogtjae+AeyHRI+MfL2acsfVe5cd2A== + dependencies: + "@firebase/component" "0.6.8" + "@firebase/logger" "0.4.2" + "@firebase/remote-config" "0.4.8" + "@firebase/remote-config-types" "0.3.2" + "@firebase/util" "1.9.7" + tslib "^2.1.0" + +"@firebase/remote-config-types@0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@firebase/remote-config-types/-/remote-config-types-0.3.2.tgz#a5d1009c6fd08036c5cd4f28764e3cd694f966d5" + integrity sha512-0BC4+Ud7y2aPTyhXJTMTFfrGGLqdYXrUB9sJVAB8NiqJswDTc4/2qrE/yfUbnQJhbSi6ZaTTBKyG3n1nplssaA== + +"@firebase/remote-config@0.4.8": + version "0.4.8" + resolved "https://registry.yarnpkg.com/@firebase/remote-config/-/remote-config-0.4.8.tgz#b6a79acdf73554e0ee31c278162b85592fc8c1f3" + integrity sha512-AMLqe6wfIRnjc6FkCWOSUjhc1fSTEf8o+cv1NolFvbiJ/tU+TqN4pI7pT+MIKQzNiq5fxLehkOx+xtAQBxPJKQ== + dependencies: + "@firebase/component" "0.6.8" + "@firebase/installations" "0.6.8" + "@firebase/logger" "0.4.2" + "@firebase/util" "1.9.7" + tslib "^2.1.0" + +"@firebase/storage-compat@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@firebase/storage-compat/-/storage-compat-0.3.9.tgz#42496a7b5f7c384f0ea590d704934465102b4527" + integrity sha512-WWgAp5bTW961oIsCc9+98m4MIVKpEqztAlIngfHfwO/x3DYoBPRl/awMRG3CAXyVxG+7B7oHC5IsnqM+vTwx2A== + dependencies: + "@firebase/component" "0.6.8" + "@firebase/storage" "0.12.6" + "@firebase/storage-types" "0.8.2" + "@firebase/util" "1.9.7" + tslib "^2.1.0" + +"@firebase/storage-types@0.8.2": + version "0.8.2" + resolved "https://registry.yarnpkg.com/@firebase/storage-types/-/storage-types-0.8.2.tgz#edb321b8a3872a9f74e1f27de046f160021c8e1f" + integrity sha512-0vWu99rdey0g53lA7IShoA2Lol1jfnPovzLDUBuon65K7uKG9G+L5uO05brD9pMw+l4HRFw23ah3GwTGpEav6g== + +"@firebase/storage@0.12.6": + version "0.12.6" + resolved "https://registry.yarnpkg.com/@firebase/storage/-/storage-0.12.6.tgz#49b2c77f10fd97da913a93e37c86cdff92a805eb" + integrity sha512-Zgb9WuehJxzhj7pGXUvkAEaH+3HvLjD9xSZ9nepuXf5f8378xME7oGJtREr/RnepdDA5YW0XIxe0QQBNHpe1nw== + dependencies: + "@firebase/component" "0.6.8" + "@firebase/util" "1.9.7" + tslib "^2.1.0" + undici "5.28.4" + +"@firebase/util@1.9.7": + version "1.9.7" + resolved "https://registry.yarnpkg.com/@firebase/util/-/util-1.9.7.tgz#c03b0ae065b3bba22800da0bd5314ef030848038" + integrity sha512-fBVNH/8bRbYjqlbIhZ+lBtdAAS4WqZumx03K06/u7fJSpz1TGjEMm1ImvKD47w+xaFKIP2ori6z8BrbakRfjJA== + dependencies: + tslib "^2.1.0" + +"@firebase/vertexai-preview@0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@firebase/vertexai-preview/-/vertexai-preview-0.0.3.tgz#73dea839439ebdbb5ccd946f297ede5b57e6e7e9" + integrity sha512-KVtUWLp+ScgiwkDKAvNkVucAyhLVQp6C6lhnVEuIg4mWhWcS3oerjAeVhZT4uNofKwWxRsOaB2Yec7DMTXlQPQ== + dependencies: + "@firebase/app-check-interop-types" "0.3.2" + "@firebase/component" "0.6.8" + "@firebase/logger" "0.4.2" + "@firebase/util" "1.9.7" + tslib "^2.1.0" + +"@firebase/webchannel-wrapper@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.1.tgz#0b62c9f47f557a5b4adc073bb0a47542ce6af4c4" + integrity sha512-jmEnr/pk0yVkA7mIlHNnxCi+wWzOFUg0WyIotgkKAb2u1J7fAeDBcVNSTjTihbAYNusCLQdW5s9IJ5qwnEufcQ== + "@flatten-js/interval-tree@^1.1.2": version "1.1.3" resolved "https://registry.yarnpkg.com/@flatten-js/interval-tree/-/interval-tree-1.1.3.tgz#7d9b4bb92042c6bbcefae5bbb822b5ec3c073e88" @@ -2387,6 +2776,24 @@ resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.1.tgz#16308cea045f0fc777b6ff20a9f25474dd8293d2" integrity sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q== +"@grpc/grpc-js@~1.9.0": + version "1.9.15" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.9.15.tgz#433d7ac19b1754af690ea650ab72190bd700739b" + integrity sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ== + dependencies: + "@grpc/proto-loader" "^0.7.8" + "@types/node" ">=12.12.47" + +"@grpc/proto-loader@^0.7.8": + version "0.7.13" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.13.tgz#f6a44b2b7c9f7b609f5748c6eac2d420e37670cf" + integrity sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw== + dependencies: + lodash.camelcase "^4.3.0" + long "^5.0.0" + protobufjs "^7.2.5" + yargs "^17.7.2" + "@hapi/hoek@^9.0.0": version "9.2.1" resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.2.1.tgz#9551142a1980503752536b5050fd99f4a7f13b17" @@ -3426,6 +3833,18 @@ rlp "^2.2.6" uuid "^8.3.2" +"@keystonehq/metamask-airgapped-keyring@^0.14.1": + version "0.14.1" + resolved "https://registry.yarnpkg.com/@keystonehq/metamask-airgapped-keyring/-/metamask-airgapped-keyring-0.14.1.tgz#1b797f7ad40fc908e411f201694fb31ebaa564d6" + integrity sha512-ffBa+LMkZUMj0KKW/YYoncxuUqsnBPn9xss1kHEgvva5GviylMcosbVyV2AAbtnRii1VK6wTSWzAzUdR8giq3A== + dependencies: + "@ethereumjs/tx" "^4.0.2" + "@keystonehq/base-eth-keyring" "^0.14.1" + "@keystonehq/bc-ur-registry-eth" "^0.19.1" + "@metamask/obs-store" "^9.0.0" + rlp "^2.2.6" + uuid "^8.3.2" + "@keystonehq/ur-decoder@^0.12.2": version "0.12.2" resolved "https://registry.yarnpkg.com/@keystonehq/ur-decoder/-/ur-decoder-0.12.2.tgz#78707d95012bc6d31c947c76c27e076d4a1c071c" @@ -3434,6 +3853,18 @@ "@keystonehq/bc-ur-registry-eth" "^0.6.12" "@ngraveio/bc-ur" "^1.1.6" +"@kwsites/file-exists@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@kwsites/file-exists/-/file-exists-1.1.1.tgz#ad1efcac13e1987d8dbaf235ef3be5b0d96faa99" + integrity sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw== + dependencies: + debug "^4.1.1" + +"@kwsites/promise-deferred@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz#8ace5259254426ccef57f3175bc64ed7095ed919" + integrity sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw== + "@lavamoat/aa@^4.2.0": version "4.2.0" resolved "https://registry.yarnpkg.com/@lavamoat/aa/-/aa-4.2.0.tgz#1262589c77386b1741fe904ebdfe97b959bc8fa4" @@ -3476,12 +3907,12 @@ rxjs "6" semver "^7.3.5" -"@ledgerhq/devices@^8.2.1", "@ledgerhq/devices@^8.2.2": - version "8.2.2" - resolved "https://registry.yarnpkg.com/@ledgerhq/devices/-/devices-8.2.2.tgz#d6d758182d690ad66e14f88426c448e8c54d259d" - integrity sha512-SKahGA4p0mZ3ovypOJ2wa5mUvUkArE3HBrwWKYf+cRs+t/Licp3OJfhj+DHIxP3AfyH2xR6CFFWECYHeKwGsDQ== +"@ledgerhq/devices@^8.2.1", "@ledgerhq/devices@^8.4.0": + version "8.4.0" + resolved "https://registry.yarnpkg.com/@ledgerhq/devices/-/devices-8.4.0.tgz#f3a03576d4a53d731bdaa212a00bd0adbfb86fb1" + integrity sha512-TUrMlWZJ+5AFp2lWMw4rGQoU+WtjIqlFX5SzQDL9phaUHrt4TFierAGHsaj5+tUHudhD4JhIaLI2cn1NOyq5NQ== dependencies: - "@ledgerhq/errors" "^6.16.3" + "@ledgerhq/errors" "^6.17.0" "@ledgerhq/logs" "^6.12.0" rxjs "^7.8.1" semver "^7.3.5" @@ -3491,10 +3922,10 @@ resolved "https://registry.yarnpkg.com/@ledgerhq/errors/-/errors-5.50.0.tgz#e3a6834cb8c19346efca214c1af84ed28e69dad9" integrity sha512-gu6aJ/BHuRlpU7kgVpy2vcYk6atjB4iauP2ymF7Gk0ez0Y/6VSMVSJvubeEQN+IV60+OBK0JgeIZG7OiHaw8ow== -"@ledgerhq/errors@^6.10.0", "@ledgerhq/errors@^6.16.2", "@ledgerhq/errors@^6.16.3": - version "6.16.3" - resolved "https://registry.yarnpkg.com/@ledgerhq/errors/-/errors-6.16.3.tgz#646f68cc7e6e8d5126bce1ca06140c5ad963bee8" - integrity sha512-3w7/SJVXOPa9mpzyll7VKoKnGwDD3BzWgN1Nom8byR40DiQvOKjHX+kKQausCedTHVNBn9euzPCNsftZ9+mxfw== +"@ledgerhq/errors@^6.10.0", "@ledgerhq/errors@^6.16.2", "@ledgerhq/errors@^6.17.0": + version "6.17.0" + resolved "https://registry.yarnpkg.com/@ledgerhq/errors/-/errors-6.17.0.tgz#0d56361fe6eb7de3b239e661710679f933f1fcca" + integrity sha512-xnOVpy/gUUkusEORdr2Qhw3Vd0MGfjyVGgkGR9Ck6FXE26OIdIQ3tNmG5BdZN+gwMMFJJVxxS4/hr0taQfZ43w== "@ledgerhq/hw-app-eth@5.27.2": version "5.27.2" @@ -3575,12 +4006,12 @@ events "^3.3.0" "@ledgerhq/hw-transport@^6.24.1", "@ledgerhq/hw-transport@^6.30.4": - version "6.30.5" - resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport/-/hw-transport-6.30.5.tgz#841c9e4bb3849536db110ca2894d693d55bf54fd" - integrity sha512-JMl//7BgPBvWxrWyMu82jj6JEYtsQyOyhYtonWNgtxn6KUZWht3gU4gxmLpeIRr+DiS7e50mW7m3GA+EudZmmA== + version "6.31.0" + resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport/-/hw-transport-6.31.0.tgz#82d8154bbcec8dc0104009a646159190fba5ae76" + integrity sha512-BY1poLk8vlJdIYngp8Zfaa/V9n14dqgt1G7iNetVRhJVFEKp9EYONeC3x6q/N7x81LUpzBk6M+T+s46Z4UiXHw== dependencies: - "@ledgerhq/devices" "^8.2.2" - "@ledgerhq/errors" "^6.16.3" + "@ledgerhq/devices" "^8.4.0" + "@ledgerhq/errors" "^6.17.0" "@ledgerhq/logs" "^6.12.0" events "^3.3.0" @@ -3760,12 +4191,12 @@ "@metamask/utils" "^8.3.0" immer "^9.0.6" -"@metamask/base-controller@^6.0.0": - version "6.0.0" - resolved "https://registry.yarnpkg.com/@metamask/base-controller/-/base-controller-6.0.0.tgz#c21563c3f887ab00da6a29c1dc258460e66698bf" - integrity sha512-N1fyGh76Kouf3jnb8oPEKTgtU/rxkbv9KTJqwp6Wr3HwZQCGvDkoz+iS3N2apYwstIGuMd7NMrcs8v6SSWunIQ== +"@metamask/base-controller@^6.0.0", "@metamask/base-controller@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@metamask/base-controller/-/base-controller-6.0.1.tgz#358c9f7f0e6a865bac242deeb4cb4e71769f962f" + integrity sha512-rU/q4D7NYlaKUks4x1K5QI4cKn9Gto/63vC47IkPiSD5ozTY/PrKU2BNpJy0ayo7b03Xgq9bbnredom/BKsytA== dependencies: - "@metamask/utils" "^8.3.0" + "@metamask/utils" "^9.0.0" immer "^9.0.6" "@metamask/browser-passworder@^4.3.0": @@ -3817,15 +4248,15 @@ eth-ens-namehash "^2.0.8" fast-deep-equal "^3.1.3" -"@metamask/controller-utils@^11.0.0": - version "11.0.0" - resolved "https://registry.yarnpkg.com/@metamask/controller-utils/-/controller-utils-11.0.0.tgz#d72e69bc48eddff1b3a6329132d170e86f787047" - integrity sha512-DQrl1VzLHaLGzhXWjS/r4+cRIxC8Ro87Wco22TKDWtZ6GfQeJOuiDAU8ZKMEBVKDJCG+YXtyECWmK7zaThjVyA== +"@metamask/controller-utils@^11.0.0", "@metamask/controller-utils@^11.0.1": + version "11.0.1" + resolved "https://registry.yarnpkg.com/@metamask/controller-utils/-/controller-utils-11.0.1.tgz#278e249721f6bccc8d4f97b1485ad25f9daa0774" + integrity sha512-slyF1MMD6K5zjD3lwLbleNlrys+h4acHmh9SNEj+YaS/034RF+PPrAKlb7s5X6xKuCpnNcJ2BkNJfScb0OKXYg== dependencies: "@ethereumjs/util" "^8.1.0" "@metamask/eth-query" "^4.0.0" "@metamask/ethjs-unit" "^0.3.0" - "@metamask/utils" "^8.3.0" + "@metamask/utils" "^9.0.0" "@spruceid/siwe-parser" "2.1.0" "@types/bn.js" "^5.1.5" bn.js "^5.2.1" @@ -3994,6 +4425,27 @@ "@metamask/safe-event-emitter" "^3.0.0" "@metamask/utils" "^8.3.0" +"@metamask/eth-json-rpc-provider@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@metamask/eth-json-rpc-provider/-/eth-json-rpc-provider-4.0.0.tgz#0ab54548fceda1829f313f2f8ec072ef91cce687" + integrity sha512-HB/I5eNsS67rE5C+px5zASyEuAoK/UFeWe4c4FIm2U4zMo7Y2EED1p10A4zHRHjpctObHdvNDcZQbfu2gHcqsQ== + dependencies: + "@metamask/json-rpc-engine" "^9.0.0" + "@metamask/safe-event-emitter" "^3.0.0" + "@metamask/utils" "^8.3.0" + +"@metamask/eth-ledger-bridge-keyring@^4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@metamask/eth-ledger-bridge-keyring/-/eth-ledger-bridge-keyring-4.1.0.tgz#90bb94b931ecba5c8ed9f0023b35f32f4ed8ac5a" + integrity sha512-ZNNV6zLwyEbzIAN8WHdTA372xst7/ajX/lvafbZDrSiiA+UuC0CfRSDOS+NOyCNnP+3NRBcJlo1ilDRYRe3ZZg== + dependencies: + "@ethereumjs/rlp" "^4.0.0" + "@ethereumjs/tx" "^4.2.0" + "@ethereumjs/util" "^8.0.0" + "@ledgerhq/hw-app-eth" "6.26.1" + "@metamask/eth-sig-util" "^7.0.1" + hdkey "^2.1.0" + "@metamask/eth-query@^3.0.1": version "3.0.1" resolved "https://registry.yarnpkg.com/@metamask/eth-query/-/eth-query-3.0.1.tgz#3439eb6c7d5ccff1d6a66df1d1802bae0c890444" @@ -4168,6 +4620,23 @@ bn.js "^5.2.1" uuid "^8.3.2" +"@metamask/gas-fee-controller@^18.0.0": + version "18.0.0" + resolved "https://registry.yarnpkg.com/@metamask/gas-fee-controller/-/gas-fee-controller-18.0.0.tgz#a6ac49fa1a637708d36bce9306407496bf61ccce" + integrity sha512-BbFLXnxx/qcbktt9EsxOQpm2Q2rWm/vgaRSMXAMv3zqNo4INhYFpLxkOFL/qifTGpOpdMvoSjjvgBaTXkDO3xQ== + dependencies: + "@metamask/base-controller" "^6.0.0" + "@metamask/controller-utils" "^11.0.0" + "@metamask/eth-query" "^4.0.0" + "@metamask/ethjs-unit" "^0.3.0" + "@metamask/network-controller" "^19.0.0" + "@metamask/polling-controller" "^8.0.0" + "@metamask/utils" "^8.3.0" + "@types/bn.js" "^5.1.5" + "@types/uuid" "^8.3.0" + bn.js "^5.2.1" + uuid "^8.3.2" + "@metamask/json-rpc-engine@^7.0.0", "@metamask/json-rpc-engine@^7.1.1", "@metamask/json-rpc-engine@^7.3.2", "@metamask/json-rpc-engine@^7.3.3": version "7.3.3" resolved "https://registry.yarnpkg.com/@metamask/json-rpc-engine/-/json-rpc-engine-7.3.3.tgz#f2b30a2164558014bfcca45db10f5af291d989af" @@ -4238,6 +4707,18 @@ superstruct "^1.0.3" uuid "^9.0.1" +"@metamask/keyring-api@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@metamask/keyring-api/-/keyring-api-8.0.0.tgz#57a4e18921f859f45e6882afe7f938955b826d91" + integrity sha512-jxow0Q9xPGTb2oY6d+t7f8lpboqTg5W24CyorZyvfwTsKQ9hNj4RhUsVszUYPySOt6O+Nu1uiYD+Se1Sk43Xwg== + dependencies: + "@metamask/snaps-sdk" "^4.2.0" + "@metamask/utils" "^8.4.0" + "@types/uuid" "^9.0.8" + bech32 "^2.0.0" + superstruct "^1.0.3" + uuid "^9.0.1" + "@metamask/keyring-controller@^16.0.0": version "16.0.0" resolved "https://registry.yarnpkg.com/@metamask/keyring-controller/-/keyring-controller-16.0.0.tgz#36530a5cbc311d496ef66701c66daf779a769fca" @@ -4257,6 +4738,25 @@ ethereumjs-wallet "^1.0.1" immer "^9.0.6" +"@metamask/keyring-controller@^17.1.0": + version "17.1.1" + resolved "https://registry.yarnpkg.com/@metamask/keyring-controller/-/keyring-controller-17.1.1.tgz#af4c9d2c3558003e16829a1ce80f4e13e7a0c7b3" + integrity sha512-NuVv9yZ+Qg2VsZtvB01MDqW5PEFYgmAKGK6Ru7mfbh/Qtov+fz9q/owEBSfXqMp4hbEs9SSzyI6E0B0/soM9iA== + dependencies: + "@ethereumjs/util" "^8.1.0" + "@keystonehq/metamask-airgapped-keyring" "^0.14.1" + "@metamask/base-controller" "^6.0.1" + "@metamask/browser-passworder" "^4.3.0" + "@metamask/eth-hd-keyring" "^7.0.1" + "@metamask/eth-sig-util" "^7.0.1" + "@metamask/eth-simple-keyring" "^6.0.1" + "@metamask/keyring-api" "^8.0.0" + "@metamask/message-manager" "^10.0.1" + "@metamask/utils" "^9.0.0" + async-mutex "^0.5.0" + ethereumjs-wallet "^1.0.1" + immer "^9.0.6" + "@metamask/logging-controller@^3.0.0", "@metamask/logging-controller@^3.0.1": version "3.0.1" resolved "https://registry.yarnpkg.com/@metamask/logging-controller/-/logging-controller-3.0.1.tgz#11c2b127bc2d7d4b753e2d757b0c09bf2742bf0a" @@ -4266,6 +4766,19 @@ "@metamask/controller-utils" "^9.0.1" uuid "^8.3.2" +"@metamask/message-manager@^10.0.1": + version "10.0.1" + resolved "https://registry.yarnpkg.com/@metamask/message-manager/-/message-manager-10.0.1.tgz#fe84e48484b6e97285b3ab3aec2789b10557218a" + integrity sha512-OxgV4v/BWNC2PYs4xL0aqOEqzX7RhJYAGxLraYvwAKEOULQ/ch5NZiplOA+i7Zz54kUxp8qq6l1mn9Xz0icORw== + dependencies: + "@metamask/base-controller" "^6.0.1" + "@metamask/controller-utils" "^11.0.1" + "@metamask/eth-sig-util" "^7.0.1" + "@metamask/utils" "^9.0.0" + "@types/uuid" "^8.3.0" + jsonschema "^1.2.4" + uuid "^8.3.2" + "@metamask/message-manager@^8.0.2": version "8.0.2" resolved "https://registry.yarnpkg.com/@metamask/message-manager/-/message-manager-8.0.2.tgz#84720f0fe9e0c02cc97b76f0bc8f3d5b0a25f731" @@ -4362,6 +4875,26 @@ immer "^9.0.6" uuid "^8.3.2" +"@metamask/network-controller@^19.0.0": + version "19.0.0" + resolved "https://registry.yarnpkg.com/@metamask/network-controller/-/network-controller-19.0.0.tgz#83cb2e6f4f8f596126b47386e45d7cd5a349803d" + integrity sha512-uUweNraW48sQ7haqJ1sU92xAtMXJXN0Q6kFycSo5HuYZ5wactmZFY9DWPmSoAN+S6xXJkLocAQgKeGVKfJntnA== + dependencies: + "@metamask/base-controller" "^6.0.0" + "@metamask/controller-utils" "^11.0.0" + "@metamask/eth-block-tracker" "^9.0.2" + "@metamask/eth-json-rpc-infura" "^9.1.0" + "@metamask/eth-json-rpc-middleware" "^12.1.1" + "@metamask/eth-json-rpc-provider" "^4.0.0" + "@metamask/eth-query" "^4.0.0" + "@metamask/json-rpc-engine" "^9.0.0" + "@metamask/rpc-errors" "^6.2.1" + "@metamask/swappable-obj-proxy" "^2.2.0" + "@metamask/utils" "^8.3.0" + async-mutex "^0.5.0" + immer "^9.0.6" + uuid "^8.3.2" + "@metamask/nonce-tracker@^5.0.0": version "5.0.0" resolved "https://registry.yarnpkg.com/@metamask/nonce-tracker/-/nonce-tracker-5.0.0.tgz#4f24e4eb7be685ddbf0757df918e807c2961ccc8" @@ -4370,6 +4903,22 @@ "@ethersproject/providers" "^5.7.2" async-mutex "^0.3.1" +"@metamask/notification-services-controller@^0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@metamask/notification-services-controller/-/notification-services-controller-0.1.1.tgz#c709baa32bf40c09b4ff8900bf5e3501f27f0923" + integrity sha512-r/6IycpMStITsGNDnQjSRm8DruwvMGuQFc8yIW31/dOkiIG4RlDnC2h/yeemKuNexxgGd2XoVrqaIXb8WMNhGw== + dependencies: + "@contentful/rich-text-html-renderer" "^16.5.2" + "@metamask/base-controller" "^6.0.0" + "@metamask/controller-utils" "^11.0.0" + "@metamask/keyring-controller" "^17.1.0" + "@metamask/profile-sync-controller" "^0.1.1" + bignumber.js "^4.1.0" + contentful "^10.3.6" + firebase "^10.11.0" + loglevel "^1.8.1" + uuid "^8.3.2" + "@metamask/number-to-bn@^1.7.1": version "1.7.1" resolved "https://registry.yarnpkg.com/@metamask/number-to-bn/-/number-to-bn-1.7.1.tgz#a449ec8b2edba211e0dc3e1e0428ff2cc2bf7ab4" @@ -4403,6 +4952,14 @@ "@metamask/safe-event-emitter" "^2.0.0" through2 "^2.0.3" +"@metamask/obs-store@^9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@metamask/obs-store/-/obs-store-9.0.0.tgz#fa50c988b4635817ff0454bc9763b1cf6b37d9e9" + integrity sha512-GDsEh2DTHgmISzJt8erf9T4Ph38iwD2yDJ6J1YFq/IcWRGnT1bkgSEVqZMv9c9JloX02T5bFIUK6+9m9EycI6A== + dependencies: + "@metamask/safe-event-emitter" "^3.0.0" + readable-stream "^3.6.2" + "@metamask/permission-controller@^10.0.0": version "10.0.0" resolved "https://registry.yarnpkg.com/@metamask/permission-controller/-/permission-controller-10.0.0.tgz#821280763cc37e9597fe7d207b5da00a881ad32a" @@ -4481,6 +5038,19 @@ fast-json-stable-stringify "^2.1.0" uuid "^8.3.2" +"@metamask/polling-controller@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@metamask/polling-controller/-/polling-controller-8.0.0.tgz#4dedac3ce3821d9435562d3a43523d3161c8e089" + integrity sha512-5dD5di0VrEB3JNcU+0AoEqdvOfBUuUNvtQ4XRyTJMhEIkq/983X42/bwESjRHx5p60vixaWcfaKZAY6frOVIEw== + dependencies: + "@metamask/base-controller" "^6.0.0" + "@metamask/controller-utils" "^11.0.0" + "@metamask/network-controller" "^19.0.0" + "@metamask/utils" "^8.3.0" + "@types/uuid" "^8.3.0" + fast-json-stable-stringify "^2.1.0" + uuid "^8.3.2" + "@metamask/post-message-stream@^8.0.0", "@metamask/post-message-stream@^8.1.0": version "8.1.0" resolved "https://registry.yarnpkg.com/@metamask/post-message-stream/-/post-message-stream-8.1.0.tgz#1139c6411b127b14a7fda43a95d1d33f4affef02" @@ -4489,10 +5059,10 @@ "@metamask/utils" "^8.1.0" readable-stream "3.6.2" -"@metamask/ppom-validator@0.31.0": - version "0.31.0" - resolved "https://registry.yarnpkg.com/@metamask/ppom-validator/-/ppom-validator-0.31.0.tgz#bff78766d777b85df7bebdb6065d6382341cc23f" - integrity sha512-+TdBp333ifttbKhMwGZwyLh5GpxWGFMyHf1z3MXu0dWaj9ntJIfSsYI4whJ123Jc0BvCdjV/W/kKaWH5r+n0RQ== +"@metamask/ppom-validator@0.32.0": + version "0.32.0" + resolved "https://registry.yarnpkg.com/@metamask/ppom-validator/-/ppom-validator-0.32.0.tgz#e732cd946605c3abb0e11f175013dab879166004" + integrity sha512-BHfKp2wZRhyamQ8YooD+r24wKrYb+ORO6zvwqOva3niWo/5iTNgFt/iQ6CS5dPeklCvyVH8RAxS6Fgk7Mu7XKA== dependencies: "@metamask/base-controller" "^3.0.0" "@metamask/controller-utils" "^8.0.1" @@ -4512,6 +5082,21 @@ "@metamask/base-controller" "^5.0.2" "@metamask/controller-utils" "^9.1.0" +"@metamask/profile-sync-controller@^0.1.1", "@metamask/profile-sync-controller@^0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@metamask/profile-sync-controller/-/profile-sync-controller-0.1.3.tgz#fbe8a0c17c4b47c75aadd3dc6fd304d960da923c" + integrity sha512-CkgVlBfqSMLmuKAdwoMsk9DZZvVxusr3Xv9pRPfOHDYn735fwrO6ra8iZdoWJndoHPBQ+iBPk/F/xecHmq/v9Q== + dependencies: + "@metamask/base-controller" "^6.0.1" + "@metamask/snaps-controllers" "^8.1.1" + "@metamask/snaps-sdk" "^4.2.0" + "@metamask/snaps-utils" "^7.4.0" + "@noble/ciphers" "^0.5.2" + "@noble/hashes" "^1.4.0" + immer "^9.0.6" + loglevel "^1.8.1" + siwe "^2.3.2" + "@metamask/providers@^13.1.0": version "13.1.0" resolved "https://registry.yarnpkg.com/@metamask/providers/-/providers-13.1.0.tgz#c5aef8e7073e097e6693cffc5f463b0632e1f1fd" @@ -4569,13 +5154,6 @@ resolved "https://registry.yarnpkg.com/@metamask/react-native-actionsheet/-/react-native-actionsheet-2.4.2.tgz#9f956fe9e784d92c8e33656877fcfaabe4a482f1" integrity sha512-oibRXUzF+7DB0Nyzp2cMGN7ztB9Sl21W1NFq1IMa00mB4/X43JY+u+LCkx625WvQUeq0GO2ZQ6hG1L5XjMumSA== -"@metamask/react-native-animated-fox@^2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@metamask/react-native-animated-fox/-/react-native-animated-fox-2.1.0.tgz#504e1f68e13ad273fb193c6f6a3832f3a5242518" - integrity sha512-Hc+DyaEIXYa7NjzqXfgh01bsoP9WbE/ENNKZ4A65YwSBmJk5ZDvhMgTMFz+qybkUllx4kn4ENkmr0SXERZ2wmg== - dependencies: - prop-types "^15.5.10" - "@metamask/react-native-button@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@metamask/react-native-button/-/react-native-button-3.0.0.tgz#4af8affd11e2b285cfc1b1752280797e1b33e62b" @@ -4602,6 +5180,14 @@ resolved "https://registry.yarnpkg.com/@metamask/react-native-splash-screen/-/react-native-splash-screen-3.2.0.tgz#06a6547c143b088e47af40eacea9ac6657ac937f" integrity sha512-V8Cn0MXe9jdaUli/DK3PoJ71tx7k3IW2v2slqflvNstvHiO3MpCtdylsYIyu+tiPwI2JiyLRzLK8s02/3jxk6g== +"@metamask/react-native-webview@^14.0.2": + version "14.0.2" + resolved "https://registry.yarnpkg.com/@metamask/react-native-webview/-/react-native-webview-14.0.2.tgz#f21938c80e5edc0ea3be4f114eb6769f5c62888c" + integrity sha512-InIp5w9622VP7jaovrSnWxxXOYuFUfASGhi/tFFkg7V3+FMs4g3V1fMpmrgZa/b5WLTELk9wgpETE9lg8Z8xfw== + dependencies: + escape-string-regexp "^4.0.0" + invariant "2.2.4" + "@metamask/rpc-errors@^6.0.0", "@metamask/rpc-errors@^6.2.1": version "6.2.1" resolved "https://registry.yarnpkg.com/@metamask/rpc-errors/-/rpc-errors-6.2.1.tgz#f5daf429ededa7cb83069dc621bd5738fe2a1d80" @@ -4739,6 +5325,23 @@ readable-web-to-node-stream "^3.0.2" tar-stream "^3.1.7" +"@metamask/snaps-execution-environments@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@metamask/snaps-execution-environments/-/snaps-execution-environments-6.5.0.tgz#e3e26ed6b94d6261f378612ab181694bb7d1d809" + integrity sha512-xYZxi4fMugOvr9lUKO1PH7jQ/Zacq5/xp1EYWRuSUjgOvhBlQJxKYoX9dfjxxWqgq/WINKkAsDV2wlzYpFVqvA== + dependencies: + "@metamask/json-rpc-engine" "^9.0.0" + "@metamask/object-multiplex" "^2.0.0" + "@metamask/post-message-stream" "^8.1.0" + "@metamask/providers" "^17.0.0" + "@metamask/rpc-errors" "^6.2.1" + "@metamask/snaps-sdk" "^6.0.0" + "@metamask/snaps-utils" "^7.7.0" + "@metamask/utils" "^8.3.0" + nanoid "^3.1.31" + readable-stream "^3.6.2" + superstruct "^1.0.3" + "@metamask/snaps-registry@^3.1.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@metamask/snaps-registry/-/snaps-registry-3.1.0.tgz#05635a09971f1e106deef73ef3181f17005b1750" @@ -4827,15 +5430,20 @@ superstruct "^1.0.3" validate-npm-package-name "^5.0.0" +"@metamask/superstruct@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@metamask/superstruct/-/superstruct-3.1.0.tgz#148f786a674fba3ac885c1093ab718515bf7f648" + integrity sha512-N08M56HdOgBfRKkrgCMZvQppkZGcArEop3kixNEtVbJKm6P9Cfg0YkI6X0s1g78sNrj2fWUwvJADdZuzJgFttA== + "@metamask/swappable-obj-proxy@^2.1.0", "@metamask/swappable-obj-proxy@^2.2.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@metamask/swappable-obj-proxy/-/swappable-obj-proxy-2.2.0.tgz#31b8e0ce57e28bf9847b3b24b214996f7748cc99" integrity sha512-0OjVwQtrrPFRGipw64yDUQA0CUXCK161LWCv2KlTTDZD8BKeWSNb0gbnpDI7HvhsJ0gki5gScZj1hF3ShDnBzA== -"@metamask/swaps-controller@^9.0.0": - version "9.0.0" - resolved "https://registry.yarnpkg.com/@metamask/swaps-controller/-/swaps-controller-9.0.0.tgz#0177a59bc3db60296304351f5241ebbb9fa2d399" - integrity sha512-1YhMBBUvR/TZDqn1zjGmjhfvZD/NqSYOrLmnPvbw2rogtVdf20XbBcHbkM8kN+8wZp3ikePoLAQO9mf9XdmsMw== +"@metamask/swaps-controller@^9.0.1": + version "9.0.1" + resolved "https://registry.yarnpkg.com/@metamask/swaps-controller/-/swaps-controller-9.0.1.tgz#41842ac6150a4deb948aa7264d866134e0cbfb34" + integrity sha512-HabEz3a2w4Zb4xmpBZ63CVn7tgBOmZOxnVAG7f3R1BH36tP/WfUBVVTnkXS8B3YMTqCt/3EzC//VsD8/tcWFOg== dependencies: "@metamask/base-controller" "^4.1.1" "@metamask/controller-utils" "^8.0.2" @@ -4964,6 +5572,21 @@ superstruct "^1.0.3" uuid "^9.0.1" +"@metamask/utils@^9.0.0": + version "9.1.0" + resolved "https://registry.yarnpkg.com/@metamask/utils/-/utils-9.1.0.tgz#54e5afcec07e6032d4dd4171e862b36daa52d77e" + integrity sha512-g2REf+xSt0OZfMoNNdC4+/Yy8eP3KUqvIArel54XRFKPoXbHI6+YjFfrLtfykWBjffOp7DTfIc3Kvk5TLfuiyg== + dependencies: + "@ethereumjs/tx" "^4.2.0" + "@metamask/superstruct" "^3.1.0" + "@noble/hashes" "^1.3.1" + "@scure/base" "^1.1.3" + "@types/debug" "^4.1.7" + debug "^4.3.4" + pony-cause "^2.1.10" + semver "^7.5.4" + uuid "^9.0.1" + "@ngraveio/bc-ur@^1.1.5", "@ngraveio/bc-ur@^1.1.6": version "1.1.6" resolved "https://registry.yarnpkg.com/@ngraveio/bc-ur/-/bc-ur-1.1.6.tgz#8f8c75fff22f6a5e4dfbc5a6b540d7fe8f42cd39" @@ -4977,10 +5600,10 @@ jsbi "^3.1.5" sha.js "^2.4.11" -"@noble/ciphers@^0.5.1": - version "0.5.2" - resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-0.5.2.tgz#879367fd51d59185259eb844d5b9a78f408b4a12" - integrity sha512-GADtQmZCdgbnNp+daPLc3OY3ibEtGGDV/+CzeM3MFnhiQ7ELQKlsHWYq0YbYUXx4jU3/Y1erAxU6r+hwpewqmQ== +"@noble/ciphers@^0.5.1", "@noble/ciphers@^0.5.2": + version "0.5.3" + resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-0.5.3.tgz#48b536311587125e0d0c1535f73ec8375cd76b23" + integrity sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w== "@noble/curves@1.3.0", "@noble/curves@~1.3.0": version "1.3.0" @@ -5105,6 +5728,11 @@ resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-4.0.0.tgz#40d203ea827b9f17f42a29c6afb93b7745ef80c7" integrity sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA== +"@octokit/auth-token@^5.0.0": + version "5.1.1" + resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-5.1.1.tgz#3bbfe905111332a17f72d80bd0b51a3e2fa2cf07" + integrity sha512-rh3G3wDO8J9wSjfI436JUKzHIxq8NaiL0tVeB2aXmG6p/9859aUOAjA9pmSPNGGZxfwmaJ9ozOJImuNVJdpvbA== + "@octokit/core@^5.0.1": version "5.2.0" resolved "https://registry.yarnpkg.com/@octokit/core/-/core-5.2.0.tgz#ddbeaefc6b44a39834e1bb2e58a49a117672a7ea" @@ -5118,6 +5746,27 @@ before-after-hook "^2.2.0" universal-user-agent "^6.0.0" +"@octokit/core@^6.1.2": + version "6.1.2" + resolved "https://registry.yarnpkg.com/@octokit/core/-/core-6.1.2.tgz#20442d0a97c411612da206411e356014d1d1bd17" + integrity sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg== + dependencies: + "@octokit/auth-token" "^5.0.0" + "@octokit/graphql" "^8.0.0" + "@octokit/request" "^9.0.0" + "@octokit/request-error" "^6.0.1" + "@octokit/types" "^13.0.0" + before-after-hook "^3.0.2" + universal-user-agent "^7.0.0" + +"@octokit/endpoint@^10.0.0": + version "10.1.1" + resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-10.1.1.tgz#1a9694e7aef6aa9d854dc78dd062945945869bcc" + integrity sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q== + dependencies: + "@octokit/types" "^13.0.0" + universal-user-agent "^7.0.2" + "@octokit/endpoint@^9.0.1": version "9.0.5" resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-9.0.5.tgz#e6c0ee684e307614c02fc6ac12274c50da465c44" @@ -5135,6 +5784,15 @@ "@octokit/types" "^13.0.0" universal-user-agent "^6.0.0" +"@octokit/graphql@^8.0.0": + version "8.1.1" + resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-8.1.1.tgz#3cacab5f2e55d91c733e3bf481d3a3f8a5f639c4" + integrity sha512-ukiRmuHTi6ebQx/HFRCXKbDlOh/7xEV6QUXaE7MJEKGNAncGI/STSbOkl12qVXZrfZdpXctx5O9X1AIaebiDBg== + dependencies: + "@octokit/request" "^9.0.0" + "@octokit/types" "^13.0.0" + universal-user-agent "^7.0.0" + "@octokit/openapi-types@^20.0.0": version "20.0.0" resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-20.0.0.tgz#9ec2daa0090eeb865ee147636e0c00f73790c6e5" @@ -5145,6 +5803,13 @@ resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-22.2.0.tgz#75aa7dcd440821d99def6a60b5f014207ae4968e" integrity sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg== +"@octokit/plugin-paginate-rest@^11.0.0": + version "11.3.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.3.0.tgz#f8511b5df06b83e662c54f249a11a0da2213c6c3" + integrity sha512-n4znWfRinnUQF6TPyxs7EctSAA3yVSP4qlJP2YgI3g9d4Ae2n5F3XDOjbUluKRxPU3rfsgpOboI4O4VtPc6Ilg== + dependencies: + "@octokit/types" "^13.5.0" + "@octokit/plugin-paginate-rest@^9.0.0": version "9.2.1" resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.1.tgz#2e2a2f0f52c9a4b1da1a3aa17dabe3c459b9e401" @@ -5152,6 +5817,11 @@ dependencies: "@octokit/types" "^12.6.0" +"@octokit/plugin-request-log@^5.1.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-5.3.0.tgz#4dea4f34316b7075d02796edcb73103266119e61" + integrity sha512-FiGcyjdtYPlr03ExBk/0ysIlEFIFGJQAVoPPMxL19B24bVSEiZQnVGBunNtaAF1YnvE/EFoDpXmITtRnyCiypQ== + "@octokit/plugin-rest-endpoint-methods@^10.0.0": version "10.4.1" resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.4.1.tgz#41ba478a558b9f554793075b2e20cd2ef973be17" @@ -5159,6 +5829,13 @@ dependencies: "@octokit/types" "^12.6.0" +"@octokit/plugin-rest-endpoint-methods@^13.0.0": + version "13.2.1" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.2.1.tgz#b5e9118b4e76180cee65e03b71bcfcf632ae12d9" + integrity sha512-YMWBw6Exh1ZBs5cCE0AnzYxSQDIJS00VlBqISTgNYmu5MBdeM07K/MAJjy/VkNaH5jpJmD/5HFUvIZ+LDB5jSQ== + dependencies: + "@octokit/types" "^13.5.0" + "@octokit/request-error@^5.1.0": version "5.1.0" resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-5.1.0.tgz#ee4138538d08c81a60be3f320cd71063064a3b30" @@ -5168,6 +5845,13 @@ deprecation "^2.0.0" once "^1.4.0" +"@octokit/request-error@^6.0.1": + version "6.1.1" + resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-6.1.1.tgz#bed1b5f52ce7fefb1077a92bf42124ff36f73f2c" + integrity sha512-1mw1gqT3fR/WFvnoVpY/zUM2o/XkMs/2AszUUG9I69xn0JFLv6PGkPhNk5lbfvROs79wiS0bqiJNxfCZcRJJdg== + dependencies: + "@octokit/types" "^13.0.0" + "@octokit/request@^8.3.0", "@octokit/request@^8.3.1": version "8.4.0" resolved "https://registry.yarnpkg.com/@octokit/request/-/request-8.4.0.tgz#7f4b7b1daa3d1f48c0977ad8fffa2c18adef8974" @@ -5178,6 +5862,26 @@ "@octokit/types" "^13.1.0" universal-user-agent "^6.0.0" +"@octokit/request@^9.0.0": + version "9.1.1" + resolved "https://registry.yarnpkg.com/@octokit/request/-/request-9.1.1.tgz#e836eb69c0fb4b59b6437af7716ca348a1232a52" + integrity sha512-pyAguc0p+f+GbQho0uNetNQMmLG1e80WjkIaqqgUkihqUp0boRU6nKItXO4VWnr+nbZiLGEyy4TeKRwqaLvYgw== + dependencies: + "@octokit/endpoint" "^10.0.0" + "@octokit/request-error" "^6.0.1" + "@octokit/types" "^13.1.0" + universal-user-agent "^7.0.2" + +"@octokit/rest@^21.0.0": + version "21.0.0" + resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-21.0.0.tgz#bde4b657193643b6b691810fe890755a3c67dd9f" + integrity sha512-XudXXOmiIjivdjNZ+fN71NLrnDM00sxSZlhqmPR3v0dVoJwyP628tSlc12xqn8nX3N0965583RBw5GPo6r8u4Q== + dependencies: + "@octokit/core" "^6.1.2" + "@octokit/plugin-paginate-rest" "^11.0.0" + "@octokit/plugin-request-log" "^5.1.0" + "@octokit/plugin-rest-endpoint-methods" "^13.0.0" + "@octokit/types@^12.6.0": version "12.6.0" resolved "https://registry.yarnpkg.com/@octokit/types/-/types-12.6.0.tgz#8100fb9eeedfe083aae66473bd97b15b62aedcb2" @@ -5185,7 +5889,7 @@ dependencies: "@octokit/openapi-types" "^20.0.0" -"@octokit/types@^13.0.0", "@octokit/types@^13.1.0": +"@octokit/types@^13.0.0", "@octokit/types@^13.1.0", "@octokit/types@^13.5.0": version "13.5.0" resolved "https://registry.yarnpkg.com/@octokit/types/-/types-13.5.0.tgz#4796e56b7b267ebc7c921dcec262b3d5bfb18883" integrity sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ== @@ -5413,6 +6117,59 @@ tiny-glob "^0.2.9" tslib "^2.4.0" +"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" + integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ== + +"@protobufjs/base64@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" + integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== + +"@protobufjs/codegen@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" + integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== + +"@protobufjs/eventemitter@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" + integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q== + +"@protobufjs/fetch@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" + integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ== + dependencies: + "@protobufjs/aspromise" "^1.1.1" + "@protobufjs/inquire" "^1.1.0" + +"@protobufjs/float@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" + integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ== + +"@protobufjs/inquire@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" + integrity sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q== + +"@protobufjs/path@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" + integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA== + +"@protobufjs/pool@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" + integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw== + +"@protobufjs/utf8@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" + integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== + "@puppeteer/browsers@1.7.1": version "1.7.1" resolved "https://registry.yarnpkg.com/@puppeteer/browsers/-/browsers-1.7.1.tgz#04f1e3aec4b87f50a7acc8f64be2149bda014f0a" @@ -5963,6 +6720,19 @@ dependencies: invariant "^2.2.4" +"@react-native-firebase/app@^20.1.0": + version "20.1.0" + resolved "https://registry.yarnpkg.com/@react-native-firebase/app/-/app-20.1.0.tgz#86b9371290f92d51821b7299eede95336949f214" + integrity sha512-FCcTtmfz/Bk2laOEKOiUrQUkAnzerkRml7d3kZzJSxaBWLFxpWJQnnXqGZmD8hNWio2QEauB8llUD71KiDk+sw== + dependencies: + opencollective-postinstall "^2.0.3" + superstruct "^0.6.2" + +"@react-native-firebase/messaging@^20.1.0": + version "20.1.0" + resolved "https://registry.yarnpkg.com/@react-native-firebase/messaging/-/messaging-20.1.0.tgz#02026259c74d1725dfc5216158b05bc6655e7951" + integrity sha512-y9FtQ9dIQSyueuLeJghvfLYnay5BqPVgl9T94p+HtUlkxinOgNDjquQFtV/QlzVOyVpLrVPmknMohvBj/fvBzg== + "@react-native-masked-view/masked-view@^0.2.6": version "0.2.6" resolved "https://registry.yarnpkg.com/@react-native-masked-view/masked-view/-/masked-view-0.2.6.tgz#b26c52d5db3ad0926b13deea79c69620966a9221" @@ -6398,9 +7168,9 @@ "@sinonjs/commons" "^3.0.0" "@socket.io/component-emitter@~3.1.0": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz#821f8442f4175d8f0467b9daf26e3a18e2d02af2" - integrity sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA== + version "3.1.0" + resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553" + integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg== "@spruceid/siwe-parser@1.1.3": version "1.1.3" @@ -6419,6 +7189,16 @@ uri-js "^4.4.1" valid-url "^1.0.9" +"@spruceid/siwe-parser@^2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@spruceid/siwe-parser/-/siwe-parser-2.1.2.tgz#3e13e7d3ac0bfdaf109a07342590eb21daee2fc3" + integrity sha512-d/r3S1LwJyMaRAKQ0awmo9whfXeE88Qt00vRj91q5uv5ATtWIQEGJ67Yr5eSZw5zp1/fZCXZYuEckt8lSkereQ== + dependencies: + "@noble/hashes" "^1.1.2" + apg-js "^4.3.0" + uri-js "^4.4.1" + valid-url "^1.0.9" + "@stablelib/aead@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@stablelib/aead/-/aead-1.0.1.tgz#c4b1106df9c23d1b867eb9b276d8f42d5fc4c0c3" @@ -8477,6 +9257,16 @@ dependencies: node-gyp-build "4.4.0" +"@trufflesuite/uws-js-unofficial@20.30.0-unofficial.0": + version "20.30.0-unofficial.0" + resolved "https://registry.yarnpkg.com/@trufflesuite/uws-js-unofficial/-/uws-js-unofficial-20.30.0-unofficial.0.tgz#2fbc2f8ef7e82fbeea6abaf7e8a9d42a02b479d3" + integrity sha512-r5X0aOQcuT6pLwTRLD+mPnAM/nlKtvIK4Z+My++A8tTOR0qTjNRx8UB8jzRj3D+p9PMAp5LnpCUUGmz7/TppwA== + dependencies: + ws "8.13.0" + optionalDependencies: + bufferutil "4.0.7" + utf-8-validate "6.0.3" + "@trysound/sax@0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" @@ -8991,10 +9781,10 @@ "@types/node" "*" form-data "^4.0.0" -"@types/node@*", "@types/node@^20.12.8": - version "20.12.8" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.8.tgz#35897bf2bfe3469847ab04634636de09552e8256" - integrity sha512-NU0rJLJnshZWdE/097cdCBbyW1h4hEg0xpovcoAQYHl8dnEyp/NAOiE45pvc+Bd1Dt+2r94v2eGFpQJ4R7g+2w== +"@types/node@*", "@types/node@>=12.12.47", "@types/node@>=13.7.0", "@types/node@^20.12.8": + version "20.14.11" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.11.tgz#09b300423343460455043ddd4d0ded6ac579b74b" + integrity sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA== dependencies: undici-types "~5.26.4" @@ -9056,9 +9846,9 @@ integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ== "@types/punycode@^2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@types/punycode/-/punycode-2.1.0.tgz#89e4f3d09b3f92e87a80505af19be7e0c31d4e83" - integrity sha512-PG5aLpW6PJOeV2fHRslP4IOMWn+G+Uq8CfnyJ+PDS8ndCbU+soO+fB3NKCKo0p/Jh2Y4aPaiQZsrOXFdzpcA6g== + version "2.1.4" + resolved "https://registry.yarnpkg.com/@types/punycode/-/punycode-2.1.4.tgz#96f8a47f1ee9fb0d0def5557fe80fac532f966fa" + integrity sha512-trzh6NzBnq8yw5e35f8xe8VTYjqM3NE7bohBtvDVf/dtUer3zYTLK1Ka3DG3p7bdtoaOHZucma6FfVKlQ134pQ== "@types/puppeteer-core@^5.4.0": version "5.4.0" @@ -9599,6 +10389,11 @@ "@typescript-eslint/types" "5.62.0" eslint-visitor-keys "^3.3.0" +"@vercel/stega@^0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@vercel/stega/-/stega-0.1.2.tgz#0c20c5c9419c4288b1de58a64b5f9f26c763b25f" + integrity sha512-P7mafQXjkrsoyTRppnt0N21udKS9wUmLXHRyP9saLXLHw32j/FgUJ3FscSWgvSqRs4cj7wKZtwqJEvWJ2jbGmA== + "@vue/compiler-core@3.3.4": version "3.3.4" resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.3.4.tgz#7fbf591c1c19e1acd28ffd284526e98b4f581128" @@ -9697,7 +10492,7 @@ "@walletconnect/types" "^1.8.0" "@walletconnect/utils" "^1.8.0" -"@walletconnect/core@2.13.0", "@walletconnect/core@^2.10.1": +"@walletconnect/core@2.13.0": version "2.13.0" resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-2.13.0.tgz#6b79b039930643e8ee85a0f512b143a35fdb8b52" integrity sha512-blDuZxQenjeXcVJvHxPznTNl6c/2DO4VNrFnus+qHmO6OtT5lZRowdMtlCaCNb1q0OxzgrmBDcTOCbFcCpio/g== @@ -9720,6 +10515,29 @@ lodash.isequal "4.5.0" uint8arrays "3.1.0" +"@walletconnect/core@2.13.3", "@walletconnect/core@^2.10.1": + version "2.13.3" + resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-2.13.3.tgz#d98fccefe36c6b365812fd0f7237a0f9634bafb6" + integrity sha512-TdF+rC6rONJGyOUtt/nLkbyQWjnkwbD3kXq3ZA0Q7+tYtmSjTDE4wbArlLbHIbtf69g+9/DpEVEQimWWcEOn2g== + dependencies: + "@walletconnect/heartbeat" "1.2.2" + "@walletconnect/jsonrpc-provider" "1.0.14" + "@walletconnect/jsonrpc-types" "1.0.4" + "@walletconnect/jsonrpc-utils" "1.0.8" + "@walletconnect/jsonrpc-ws-connection" "1.0.14" + "@walletconnect/keyvaluestorage" "1.1.1" + "@walletconnect/logger" "2.1.2" + "@walletconnect/relay-api" "1.0.10" + "@walletconnect/relay-auth" "1.0.4" + "@walletconnect/safe-json" "1.0.2" + "@walletconnect/time" "1.0.2" + "@walletconnect/types" "2.13.3" + "@walletconnect/utils" "2.13.3" + events "3.3.0" + isomorphic-unfetch "3.1.0" + lodash.isequal "4.5.0" + uint8arrays "3.1.0" + "@walletconnect/core@^1.8.0": version "1.8.0" resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-1.8.0.tgz#6b2748b90c999d9d6a70e52e26a8d5e8bfeaa81e" @@ -9886,26 +10704,26 @@ dependencies: tslib "1.14.1" -"@walletconnect/se-sdk@1.8.0": - version "1.8.0" - resolved "https://registry.yarnpkg.com/@walletconnect/se-sdk/-/se-sdk-1.8.0.tgz#109cd817d243f798f33dec089a542f8c4ef7fe49" - integrity sha512-vsSMxcO8Jp4rJ5P/T9FG1AnApZfTRSAyChyJJzDUyi+6bPv+t3Y+7rKMvntQFc9LrnzSdXpB6Fb5LwI390eksw== +"@walletconnect/se-sdk@1.8.1": + version "1.8.1" + resolved "https://registry.yarnpkg.com/@walletconnect/se-sdk/-/se-sdk-1.8.1.tgz#a4755e8a27dd9de84c0a30b3a99dae7231e6635b" + integrity sha512-bTbr3EHPY6TcGzfGWYm1lR8JXK9hfLAo8bMTeSQ5X1DSsVi8Xq7rKH04Z2vWGKbG+ma53/NSkH+BNyIUaU8/IA== dependencies: - "@walletconnect/web3wallet" "1.12.0" + "@walletconnect/web3wallet" "1.12.3" -"@walletconnect/sign-client@2.13.0": - version "2.13.0" - resolved "https://registry.yarnpkg.com/@walletconnect/sign-client/-/sign-client-2.13.0.tgz#f59993f082aec1ca5498b9519027e764c1e6d28b" - integrity sha512-En7KSvNUlQFx20IsYGsFgkNJ2lpvDvRsSFOT5PTdGskwCkUfOpB33SQJ6nCrN19gyoKPNvWg80Cy6MJI0TjNYA== +"@walletconnect/sign-client@2.13.3": + version "2.13.3" + resolved "https://registry.yarnpkg.com/@walletconnect/sign-client/-/sign-client-2.13.3.tgz#9f8c826000bf3d6ea782f7325bc87e9f260e71ce" + integrity sha512-3Pcq6trHWdBZn5X0VUFQ3zJaaqyEbMW9WNVKcZ2SakIpQAwySd08Mztvq48G98jfucdgP3tjGPbBvzHX9vJX7w== dependencies: - "@walletconnect/core" "2.13.0" + "@walletconnect/core" "2.13.3" "@walletconnect/events" "1.0.1" "@walletconnect/heartbeat" "1.2.2" "@walletconnect/jsonrpc-utils" "1.0.8" "@walletconnect/logger" "2.1.2" "@walletconnect/time" "1.0.2" - "@walletconnect/types" "2.13.0" - "@walletconnect/utils" "2.13.0" + "@walletconnect/types" "2.13.3" + "@walletconnect/utils" "2.13.3" events "3.3.0" "@walletconnect/socket-transport@^1.8.0": @@ -9924,7 +10742,7 @@ dependencies: tslib "1.14.1" -"@walletconnect/types@2.13.0", "@walletconnect/types@^2.9.0": +"@walletconnect/types@2.13.0": version "2.13.0" resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-2.13.0.tgz#cdac083651f5897084fe9ed62779f11810335ac6" integrity sha512-MWaVT0FkZwzYbD3tvk8F+2qpPlz1LUSWHuqbINUtMXnSzJtXN49Y99fR7FuBhNFtDalfuWsEK17GrNA+KnAsPQ== @@ -9936,12 +10754,24 @@ "@walletconnect/logger" "2.1.2" events "3.3.0" +"@walletconnect/types@2.13.3", "@walletconnect/types@^2.9.0": + version "2.13.3" + resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-2.13.3.tgz#0280b5c64df9a2e07752c4121eeb81dc4a59b2c2" + integrity sha512-9UdtLoQqwGFfepCPprUAXeUbKg9zyDarPRmEJVco51OWXHCOpvRgroWk54fQHDhCUIfDELjObY6XNAzNrmNYUA== + dependencies: + "@walletconnect/events" "1.0.1" + "@walletconnect/heartbeat" "1.2.2" + "@walletconnect/jsonrpc-types" "1.0.4" + "@walletconnect/keyvaluestorage" "1.1.1" + "@walletconnect/logger" "2.1.2" + events "3.3.0" + "@walletconnect/types@^1.8.0": version "1.8.0" resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-1.8.0.tgz#3f5e85b2d6b149337f727ab8a71b8471d8d9a195" integrity sha512-Cn+3I0V0vT9ghMuzh1KzZvCkiAxTq+1TR2eSqw5E5AVWfmCtECFkVZBP6uUJZ8YjwLqXheI+rnjqPy7sVM4Fyg== -"@walletconnect/utils@2.13.0", "@walletconnect/utils@^2.10.1": +"@walletconnect/utils@2.13.0": version "2.13.0" resolved "https://registry.yarnpkg.com/@walletconnect/utils/-/utils-2.13.0.tgz#1fc1fbff0d26db0830e65d1ba8cfe1a13a0616ad" integrity sha512-q1eDCsRHj5iLe7fF8RroGoPZpdo2CYMZzQSrw1iqL+2+GOeqapxxuJ1vaJkmDUkwgklfB22ufqG6KQnz78sD4w== @@ -9961,6 +10791,26 @@ query-string "7.1.3" uint8arrays "3.1.0" +"@walletconnect/utils@2.13.3", "@walletconnect/utils@^2.10.1": + version "2.13.3" + resolved "https://registry.yarnpkg.com/@walletconnect/utils/-/utils-2.13.3.tgz#500d88342c193ce92ab9d2fae3bd343be71821b2" + integrity sha512-hjyyNhnhTCezGNr6OCfKRzqRsiak+p+YP57iRo1Tsf222fsj/9JD++MP97YiDwc4e4xXaZp/boiLB+8hJHsCog== + dependencies: + "@stablelib/chacha20poly1305" "1.0.1" + "@stablelib/hkdf" "1.0.1" + "@stablelib/random" "1.0.2" + "@stablelib/sha256" "1.0.1" + "@stablelib/x25519" "1.0.3" + "@walletconnect/relay-api" "1.0.10" + "@walletconnect/safe-json" "1.0.2" + "@walletconnect/time" "1.0.2" + "@walletconnect/types" "2.13.3" + "@walletconnect/window-getters" "1.0.1" + "@walletconnect/window-metadata" "1.0.1" + detect-browser "5.3.0" + query-string "7.1.3" + uint8arrays "3.1.0" + "@walletconnect/utils@^1.8.0": version "1.8.0" resolved "https://registry.yarnpkg.com/@walletconnect/utils/-/utils-1.8.0.tgz#2591a197c1fa7429941fe428876088fda6632060" @@ -9974,19 +10824,19 @@ js-sha3 "0.8.0" query-string "6.13.5" -"@walletconnect/web3wallet@1.12.0": - version "1.12.0" - resolved "https://registry.yarnpkg.com/@walletconnect/web3wallet/-/web3wallet-1.12.0.tgz#bac10a755ddf23aacf4775c0be04b4d9df145536" - integrity sha512-u+krMeatqnlm086fG+587pHide9LOGd8gATA0EGYNnc7nVUWc3+xjhKR8C7YcWNBTGOH0cWdh/OFWMzUPnHGtw== +"@walletconnect/web3wallet@1.12.3": + version "1.12.3" + resolved "https://registry.yarnpkg.com/@walletconnect/web3wallet/-/web3wallet-1.12.3.tgz#26765f52cb653bd3696198ce4cc871df2061a01e" + integrity sha512-HBzYDjf3ZVzyTCqycyMjJnn/1sQtRe0beGc3qtq9bLX/dmezkO6Oc1lg1WlvxT7KtTqKx41usw5tjiwfNZ2+ug== dependencies: "@walletconnect/auth-client" "2.1.2" - "@walletconnect/core" "2.13.0" + "@walletconnect/core" "2.13.3" "@walletconnect/jsonrpc-provider" "1.0.14" "@walletconnect/jsonrpc-utils" "1.0.8" "@walletconnect/logger" "2.1.2" - "@walletconnect/sign-client" "2.13.0" - "@walletconnect/types" "2.13.0" - "@walletconnect/utils" "2.13.0" + "@walletconnect/sign-client" "2.13.3" + "@walletconnect/types" "2.13.3" + "@walletconnect/utils" "2.13.3" "@walletconnect/window-getters@1.0.0": version "1.0.0" @@ -10955,10 +11805,10 @@ anymatch@^3.0.3, anymatch@^3.1.3, anymatch@~3.1.2: normalize-path "^3.0.0" picomatch "^2.0.4" -apg-js@^4.1.1: - version "4.2.0" - resolved "https://registry.yarnpkg.com/apg-js/-/apg-js-4.2.0.tgz#c26e7271640e67d3d4738b93bae8ee53773b0635" - integrity sha512-4WI3AYN9DmJWK3+3r/DtVMI+RO45R0u/b7tJWb5EM2c8nIzojx8Oq5LpMalou3sQnmS9qzw7cKmHBrAjdlseWw== +apg-js@^4.1.1, apg-js@^4.3.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/apg-js/-/apg-js-4.4.0.tgz#09dcecab0731fbde233b9f2352fdd2d07e56b2cf" + integrity sha512-fefmXFknJmtgtNEXfPwZKYkMFX4Fyeyz+fNF6JWp87biGOPslJbCBVU158zvKRZfHBKnJDy8CMM40oLFGkXT8Q== app-root-dir@^1.0.2: version "1.0.2" @@ -12084,7 +12934,7 @@ axios-retry@^3.1.2: "@babel/runtime" "^7.15.4" is-retry-allowed "^2.2.0" -axios@1.4.0, axios@^0.26.0, axios@^0.28.0, axios@^0.x, axios@^1.6.7, axios@^1.6.8: +axios@1.4.0, axios@^0.26.0, axios@^0.28.0, axios@^0.x, axios@^1.6.7, axios@^1.6.8, axios@~1.6.8: version "1.6.8" resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.8.tgz#66d294951f5d988a00e87a0ffb955316a619ea66" integrity sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ== @@ -12381,11 +13231,21 @@ before-after-hook@^2.2.0: resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.3.tgz#c51e809c81a4e354084422b9b26bad88249c517c" integrity sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ== +before-after-hook@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-3.0.2.tgz#d5665a5fa8b62294a5aa0a499f933f4a1016195d" + integrity sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A== + big-integer@1.6.x: version "1.6.51" resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686" integrity sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg== +bignumber.js@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-4.1.0.tgz#db6f14067c140bd46624815a7916c92d9b6c24b1" + integrity sha512-eJzYkFYy9L4JzXsbymsFn3p54D+llV27oTQ+ziJG7WFRheJcNZilgVXMG0LoZtlQSKBsJdWtLFqOD0u+U0jZKA== + bignumber.js@^7.2.1: version "7.2.1" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-7.2.1.tgz#80c048759d826800807c4bfd521e50edbba57a5f" @@ -12543,7 +13403,7 @@ brace-expansion@^2.0.1: dependencies: balanced-match "^1.0.0" -braces@^3.0.2, braces@~3.0.2: +braces@^3.0.2, braces@^3.0.3, braces@~3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== @@ -12752,6 +13612,13 @@ bufferutil@4.0.5: dependencies: node-gyp-build "^4.3.0" +bufferutil@4.0.7: + version "4.0.7" + resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.7.tgz#60c0d19ba2c992dd8273d3f73772ffc894c153ad" + integrity sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw== + dependencies: + node-gyp-build "^4.3.0" + bufferutil@^4.0.1, bufferutil@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.8.tgz#1de6a71092d65d7766c4d8a522b261a6e787e8ea" @@ -13395,6 +14262,16 @@ cliui@^8.0.1: strip-ansi "^6.0.1" wrap-ansi "^7.0.0" +clone-deep@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-2.0.2.tgz#00db3a1e173656730d1188c3d6aced6d7ea97713" + integrity sha512-SZegPTKjCgpQH63E+eN6mVEEPdQBOUzjyJm5Pora4lrwWRFS8I0QAxV/KD6vV/i0WuijHZWQC1fMsPEdxfdVCQ== + dependencies: + for-own "^1.0.0" + is-plain-object "^2.0.4" + kind-of "^6.0.0" + shallow-clone "^1.0.0" + clone-deep@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" @@ -13779,13 +14656,14 @@ contentful-sdk-core@^8.1.0: p-throttle "^4.1.1" qs "^6.11.2" -contentful@^10.8.7: - version "10.8.7" - resolved "https://registry.yarnpkg.com/contentful/-/contentful-10.8.7.tgz#ff627069e74995367306f89359c946d3e3dda530" - integrity sha512-BkHspZUmeaqCkJXljBJBZ+WnMVU/3N1rYIxVuwH3X/0Q88zF8hb02dsZupmecm5LOHSYWg7lKKrbKit6Q0JL2A== +contentful@^10.3.6, contentful@^10.8.7: + version "10.12.10" + resolved "https://registry.yarnpkg.com/contentful/-/contentful-10.12.10.tgz#b9c47e64febcc87975f2486d77c7d014402a437c" + integrity sha512-YVc+aCvehjsJI8K/IglbLjEAl/fiyUzorCFrgR60R4v44VMa4wmFJTrY48x+YQ7DPKZw5MzFk1wgeG74fxCN2g== dependencies: + "@contentful/content-source-maps" "^0.6.0" "@contentful/rich-text-types" "^16.0.2" - axios "^1.6.7" + axios "~1.6.8" contentful-resolve-response "^1.8.1" contentful-sdk-core "^8.1.0" json-stringify-safe "^5.0.1" @@ -14361,7 +15239,7 @@ debug@3.1.0: dependencies: ms "2.0.0" -debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: +debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5, debug@~4.3.1, debug@~4.3.2: version "4.3.5" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== @@ -15260,21 +16138,21 @@ end-of-stream@^1.1.0, end-of-stream@^1.4.1, end-of-stream@^1.4.4: dependencies: once "^1.4.0" -engine.io-client@~6.5.2: - version "6.5.3" - resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.5.3.tgz#4cf6fa24845029b238f83c628916d9149c399bc5" - integrity sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q== +engine.io-client@~6.4.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.4.0.tgz#88cd3082609ca86d7d3c12f0e746d12db4f47c91" + integrity sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g== dependencies: "@socket.io/component-emitter" "~3.1.0" debug "~4.3.1" - engine.io-parser "~5.2.1" + engine.io-parser "~5.0.3" ws "~8.11.0" xmlhttprequest-ssl "~2.0.0" -engine.io-parser@~5.2.1: - version "5.2.2" - resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.2.tgz#37b48e2d23116919a3453738c5720455e64e1c49" - integrity sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw== +engine.io-parser@~5.0.3: + version "5.0.6" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.6.tgz#7811244af173e157295dec9b2718dfe42a64ef45" + integrity sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw== enhanced-resolve@^5.12.0, enhanced-resolve@^5.15.0: version "5.15.0" @@ -15620,16 +16498,16 @@ escape-html@^1.0.3, escape-html@~1.0.3: resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= -escape-string-regexp@2.0.0, escape-string-regexp@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" - integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== - escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= +escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + escape-string-regexp@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" @@ -16971,6 +17849,13 @@ fault@^1.0.2: dependencies: format "^0.2.0" +faye-websocket@0.11.4: + version "0.11.4" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" + integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== + dependencies: + websocket-driver ">=0.5.1" + fb-watchman@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.1.tgz#fc84fb39d2709cf3ff6d743706157bb5708a8a85" @@ -17206,6 +18091,39 @@ findup-sync@^5.0.0: micromatch "^4.0.4" resolve-dir "^1.0.1" +firebase@^10.11.0: + version "10.12.3" + resolved "https://registry.yarnpkg.com/firebase/-/firebase-10.12.3.tgz#b94510728f603a15367b95e12a00b366700ba7f8" + integrity sha512-dO2cQ8eP6RnM2wcGzbxnoljjjMBf1suUrHYFftjSpbPn/8bEx959cwTRDHqBx3MwSzNsg6zZV/wiWydJPhUKgw== + dependencies: + "@firebase/analytics" "0.10.5" + "@firebase/analytics-compat" "0.2.11" + "@firebase/app" "0.10.6" + "@firebase/app-check" "0.8.5" + "@firebase/app-check-compat" "0.3.12" + "@firebase/app-compat" "0.2.36" + "@firebase/app-types" "0.9.2" + "@firebase/auth" "1.7.5" + "@firebase/auth-compat" "0.5.10" + "@firebase/database" "1.0.6" + "@firebase/database-compat" "1.0.6" + "@firebase/firestore" "4.6.4" + "@firebase/firestore-compat" "0.3.33" + "@firebase/functions" "0.11.6" + "@firebase/functions-compat" "0.3.12" + "@firebase/installations" "0.6.8" + "@firebase/installations-compat" "0.2.8" + "@firebase/messaging" "0.12.10" + "@firebase/messaging-compat" "0.2.10" + "@firebase/performance" "0.6.8" + "@firebase/performance-compat" "0.2.8" + "@firebase/remote-config" "0.4.8" + "@firebase/remote-config-compat" "0.2.8" + "@firebase/storage" "0.12.6" + "@firebase/storage-compat" "0.3.9" + "@firebase/util" "1.9.7" + "@firebase/vertexai-preview" "0.0.3" + flat-cache@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" @@ -17253,10 +18171,27 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" -foreach@~2.0.1: - version "2.0.5" - resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" - integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k= +for-in@^0.1.3: + version "0.1.8" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1" + integrity sha512-F0to7vbBSHP8E3l6dCjxNOLuSFAACIxFy3UehTUlG7svlXi37HHsDkyVcHo0Pq8QwrE+pXvWSVX3ZT1T9wAZ9g== + +for-in@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + integrity sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ== + +for-own@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/for-own/-/for-own-1.0.0.tgz#c63332f415cedc4b04dbfe70cf836494c53cb44b" + integrity sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg== + dependencies: + for-in "^1.0.1" + +foreach@^2.0.4, foreach@~2.0.1: + version "2.0.6" + resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.6.tgz#87bcc8a1a0e74000ff2bf9802110708cfb02eb6e" + integrity sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg== foreground-child@^2.0.0: version "2.0.0" @@ -17506,12 +18441,13 @@ fwd-stream@^1.0.4: dependencies: readable-stream "~1.0.26-4" -ganache@^7.7.7: - version "7.7.7" - resolved "https://registry.yarnpkg.com/ganache/-/ganache-7.7.7.tgz#19939a86799f0bcb7df02e88082944466394b913" - integrity sha512-kZUuOcgDQBtbxzs4iB3chg1iAc28s2ffdOdzyTTzo4vr9sb843w4PbWd5v1hsIqtcNjurcpLaW8XRp/cw2u++g== +ganache@^7.9.2: + version "7.9.2" + resolved "https://registry.yarnpkg.com/ganache/-/ganache-7.9.2.tgz#77f506ad2735dd9109696ffa1834a9dd2f806449" + integrity sha512-7gsVVDpO9AhrFyDMWWl7SpMsPpqGcnAzjxz3k32LheIPNd64p2XsY9GYRdhWmKuryb60W1iaWPZWDkFKlbRWHA== dependencies: "@trufflesuite/bigint-buffer" "1.1.10" + "@trufflesuite/uws-js-unofficial" "20.30.0-unofficial.0" "@types/bn.js" "^5.1.0" "@types/lru-cache" "5.1.1" "@types/seedrandom" "3.0.1" @@ -18137,12 +19073,13 @@ hastscript@^5.0.0: property-information "^5.0.0" space-separated-tokens "^1.0.0" -hdkey@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/hdkey/-/hdkey-2.0.1.tgz#0a211d0c510bfc44fa3ec9d44b13b634641cad74" - integrity sha512-c+tl9PHG9/XkGgG0tD7CJpRVaE0jfZizDNmnErUAKQ4EjQSOcOUcV3EN9ZEZS8pZ4usaeiiK0H7stzuzna8feA== +hdkey@^2.0.1, hdkey@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/hdkey/-/hdkey-2.1.0.tgz#755b30b73f54e93c31919c1b2f19205a8e57cb92" + integrity sha512-i9Wzi0Dy49bNS4tXXeGeu0vIcn86xXdPQUpEYg+SO1YiO8HtomjmmRMaRyqL0r59QfcD4PfVbSF3qmsWFwAemA== dependencies: bs58check "^2.1.2" + ripemd160 "^2.0.2" safe-buffer "^5.1.1" secp256k1 "^4.0.0" @@ -18312,6 +19249,11 @@ http-errors@^1.6.3, http-errors@~1.8.0: statuses ">= 1.5.0 < 2" toidentifier "1.0.1" +http-parser-js@>=0.5.1: + version "0.5.8" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3" + integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== + http-proxy-agent@^7.0.0, http-proxy-agent@^7.0.1: version "7.0.2" resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e" @@ -18464,6 +19406,11 @@ idb-keyval@^6.2.1: resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-6.2.1.tgz#94516d625346d16f56f3b33855da11bfded2db33" integrity sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg== +idb@7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.1.tgz#d910ded866d32c7ced9befc5bfdf36f572ced72b" + integrity sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ== + idna-uts46-hx@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/idna-uts46-hx/-/idna-uts46-hx-2.3.1.tgz#a1dc5c4df37eee522bf66d969cc980e00e8711f9" @@ -18824,6 +19771,11 @@ is-docker@^2.0.0, is-docker@^2.1.1: resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== +is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + integrity sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw== + is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" @@ -20015,6 +20967,13 @@ json-parse-even-better-errors@^3.0.0: resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.0.tgz#2cb2ee33069a78870a0c7e3da560026b89669cf7" integrity sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA== +json-pointer@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/json-pointer/-/json-pointer-0.6.2.tgz#f97bd7550be5e9ea901f8c9264c9d436a22a93cd" + integrity sha512-vLWcKbOaXlO+jvRy4qNd+TI1QUPZzfJj1tpJ3vAXDych5XJf93ftpUKe5pKCrzyIIwgBJcOcCVRUfqQP25afBw== + dependencies: + foreach "^2.0.4" + json-rpc-engine@^5.1.3, json-rpc-engine@^5.3.0: version "5.4.0" resolved "https://registry.yarnpkg.com/json-rpc-engine/-/json-rpc-engine-5.4.0.tgz#75758609d849e1dba1e09021ae473f3ab63161e5" @@ -20252,7 +21211,12 @@ kind-of@^1.1.0: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-1.1.0.tgz#140a3d2d41a36d2efcfa9377b62c24f8495a5c44" integrity sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ= -kind-of@^6.0.2: +kind-of@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" + integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== + +kind-of@^6.0.0, kind-of@^6.0.1, kind-of@^6.0.2: version "6.0.3" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== @@ -20717,6 +21681,11 @@ lodash-es@^4.17.15, lodash-es@^4.17.21: resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== + lodash.clonedeep@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" @@ -20887,16 +21856,21 @@ loglevel-plugin-prefix@^0.8.4: resolved "https://registry.yarnpkg.com/loglevel-plugin-prefix/-/loglevel-plugin-prefix-0.8.4.tgz#2fe0e05f1a820317d98d8c123e634c1bd84ff644" integrity sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g== -loglevel@^1.6.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.8.0.tgz#e7ec73a57e1e7b419cb6c6ac06bf050b67356114" - integrity sha512-G6A/nJLRgWOuuwdNuA6koovfEV1YpqqAG4pRUlFaz3jj2QNZ8M4vBqnVA+HBTmU/AMNUtlOsMmSpF6NyOjztbA== +loglevel@^1.6.0, loglevel@^1.8.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.9.1.tgz#d63976ac9bcd03c7c873116d41c2a85bafff1be7" + integrity sha512-hP3I3kCrDIMuRwAwHltphhDM1r8i55H33GgqjXbrisuJhF4kRhW1dNuxsRklp4bXl8DSdLaNLuiL4A/LWRfxvg== long@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== +long@^5.0.0: + version "5.2.3" + resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1" + integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q== + longjohn@^0.2.12: version "0.2.12" resolved "https://registry.yarnpkg.com/longjohn/-/longjohn-0.2.12.tgz#7ca7446b083655c377e7512213dc754d52a64a7e" @@ -21720,6 +22694,14 @@ mitt@3.0.1, mitt@^3.0.1: resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1" integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw== +mixin-object@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mixin-object/-/mixin-object-2.0.1.tgz#4fb949441dab182540f1fe035ba60e1947a5e57e" + integrity sha512-ALGF1Jt9ouehcaXaHhn6t1yGWRqGaHkPFndtFVHfZXOvkIZ/yoGaSi0AHVTafb3ZBGg4dr/bDwnaEKqCXzchMA== + dependencies: + for-in "^0.1.3" + is-extendable "^0.1.1" + mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: version "0.5.3" resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" @@ -23802,6 +24784,24 @@ property-information@^5.0.0: dependencies: xtend "^4.0.0" +protobufjs@^7.2.5: + version "7.3.2" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.3.2.tgz#60f3b7624968868f6f739430cfbc8c9370e26df4" + integrity sha512-RXyHaACeqXeqAKGLDl68rQKbmObRsTIn4TYVUUug1KfS47YWCo5MacGITEryugIgZqORCvJWEk4l449POg5Txg== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/node" ">=13.7.0" + long "^5.0.0" + proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" @@ -23906,9 +24906,9 @@ punycode@^1.3.2, punycode@^1.4.1: integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== punycode@^2.1.0, punycode@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== puppeteer-core@13.1.3: version "13.1.3" @@ -24850,14 +25850,6 @@ react-native-webview-invoke@^0.6.2: resolved "https://registry.yarnpkg.com/react-native-webview-invoke/-/react-native-webview-invoke-0.6.2.tgz#75cc27ef98ea1cbc9386269347d3aafe90d33aa3" integrity sha512-PCzP7Zl3XwHU10JYS8nR0gwuR8XiOO0MhC8y9ZuPPI+HeISn95GvNYhOXxeLgfbdbUcpNWh1HqxPDySlfCIqxg== -react-native-webview@11.13.0: - version "11.13.0" - resolved "https://registry.yarnpkg.com/react-native-webview/-/react-native-webview-11.13.0.tgz#a2eca0f87b2ae9bba0dd8144594aeff9947cc5d6" - integrity sha512-jjQAKWv8JzRmcn76fMe4lXD84AAeR7kn43kAmUe1GX312BMLaP+RbKlgpYAlNuOBXL0YirItGKDrpaD0bNROOA== - dependencies: - escape-string-regexp "2.0.0" - invariant "2.2.4" - react-native@0.71.15: version "0.71.15" resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.71.15.tgz#7d99f478238c559b8b3fdaad2514f11d53ef135a" @@ -25714,7 +26706,7 @@ rimraf@~2.6.2: dependencies: glob "^7.1.3" -ripemd160@^2.0.0, ripemd160@^2.0.1: +ripemd160@^2.0.0, ripemd160@^2.0.1, ripemd160@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== @@ -25831,7 +26823,7 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -26153,6 +27145,15 @@ shaka-player@^2.5.9: dependencies: eme-encryption-scheme-polyfill "^2.0.1" +shallow-clone@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-1.0.0.tgz#4480cd06e882ef68b2ad88a3ea54832e2c48b571" + integrity sha512-oeXreoKR/SyNJtRJMAKPDSvd28OqEwG4eR/xc856cRGBII7gX9lvAqDxusPm0846z/w/hWYjI1NpKwJ00NHzRA== + dependencies: + is-extendable "^0.1.1" + kind-of "^5.0.0" + mixin-object "^2.0.1" + shallow-clone@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" @@ -26283,6 +27284,15 @@ simple-get@^4.0.0, simple-get@^4.0.1: once "^1.3.1" simple-concat "^1.0.0" +simple-git@^3.22.0: + version "3.25.0" + resolved "https://registry.yarnpkg.com/simple-git/-/simple-git-3.25.0.tgz#3666e76d6831f0583dc380645945b97e0ac4aab6" + integrity sha512-KIY5sBnzc4yEcJXW7Tdv4viEz8KyG+nU0hay+DWZasvdFOYKeUZ6Xc25LUHHjw0tinPT7O1eY6pzX7pRT1K8rw== + dependencies: + "@kwsites/file-exists" "^1.1.1" + "@kwsites/promise-deferred" "^1.1.1" + debug "^4.3.5" + simple-plist@^1.1.0: version "1.3.1" resolved "https://registry.yarnpkg.com/simple-plist/-/simple-plist-1.3.1.tgz#16e1d8f62c6c9b691b8383127663d834112fb017" @@ -26329,6 +27339,16 @@ sisteransi@^1.0.5: resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== +siwe@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/siwe/-/siwe-2.3.2.tgz#0794ae25f734f3068de0ab093ddd2f7867bc2d67" + integrity sha512-aSf+6+Latyttbj5nMu6GF3doMfv2UYj83hhwZgUF20ky6fTS83uVhkQABdIVnEuS8y1bBdk7p6ltb9SmlhTTlA== + dependencies: + "@spruceid/siwe-parser" "^2.1.2" + "@stablelib/random" "^1.0.1" + uri-js "^4.4.1" + valid-url "^1.0.9" + sjcl@^1.0.3: version "1.0.8" resolved "https://registry.yarnpkg.com/sjcl/-/sjcl-1.0.8.tgz#f2ec8d7dc1f0f21b069b8914a41a8f236b0e252a" @@ -26403,19 +27423,19 @@ smart-mixin@^2.0.0: integrity sha1-o0oQVeMqdbMNK048oyPcmctT9Dc= socket.io-client@^4.5.3: - version "4.7.5" - resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.7.5.tgz#919be76916989758bdc20eec63f7ee0ae45c05b7" - integrity sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ== + version "4.6.1" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.6.1.tgz#80d97d5eb0feca448a0fb6d69a7b222d3d547eab" + integrity sha512-5UswCV6hpaRsNg5kkEHVcbBIXEYoVbMQaHJBXJCyEQ+CiFPV1NIOY0XOFWG4XR4GZcB8Kn6AsRs/9cy9TbqVMQ== dependencies: "@socket.io/component-emitter" "~3.1.0" debug "~4.3.2" - engine.io-client "~6.5.2" - socket.io-parser "~4.2.4" + engine.io-client "~6.4.0" + socket.io-parser "~4.2.1" -socket.io-parser@~4.2.4: - version "4.2.4" - resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83" - integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew== +socket.io-parser@~4.2.1: + version "4.2.3" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.3.tgz#926bcc6658e2ae0883dc9dee69acbdc76e4e3667" + integrity sha512-JMafRntWVO2DCJimKsRTh/wnqVvO4hrfwOqtO7f+uzwsQMuxO6VwImtYxaQ+ieoyshWOTJyV0fA21lccEXRPpQ== dependencies: "@socket.io/component-emitter" "~3.1.0" debug "~4.3.1" @@ -26980,6 +28000,14 @@ suffix@^0.1.0: resolved "https://registry.yarnpkg.com/suffix/-/suffix-0.1.1.tgz#cc58231646a0ef1102f79478ef3a9248fd9c842f" integrity sha512-j5uf6MJtMCfC4vBe5LFktSe4bGyNTBk7I2Kdri0jeLrcv5B9pWfxVa5JQpoxgtR8vaVB7bVxsWgnfQbX5wkhAA== +superstruct@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-0.6.2.tgz#c5eb034806a17ff98d036674169ef85e4c7f6a1c" + integrity sha512-lvA97MFAJng3rfjcafT/zGTSWm6Tbpk++DP6It4Qg7oNaeM+2tdJMuVgGje21/bIpBEs6iQql1PJH6dKTjl4Ig== + dependencies: + clone-deep "^2.0.1" + kind-of "^6.0.1" + superstruct@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-1.0.3.tgz#de626a5b49c6641ff4d37da3c7598e7a87697046" @@ -27800,7 +28828,7 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== -undici@^5.25.4: +undici@5.28.4, undici@^5.25.4: version "5.28.4" resolved "https://registry.yarnpkg.com/undici/-/undici-5.28.4.tgz#6b280408edb6a1a604a9b20340f45b422e373068" integrity sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g== @@ -27870,6 +28898,11 @@ universal-user-agent@^6.0.0: resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee" integrity sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w== +universal-user-agent@^7.0.0, universal-user-agent@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-7.0.2.tgz#52e7d0e9b3dc4df06cc33cb2b9fd79041a54827e" + integrity sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q== + universalify@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" @@ -28054,6 +29087,13 @@ utf-8-validate@5.0.7: dependencies: node-gyp-build "^4.3.0" +utf-8-validate@6.0.3, utf-8-validate@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-6.0.3.tgz#7d8c936d854e86b24d1d655f138ee27d2636d777" + integrity sha512-uIuGf9TWQ/y+0Lp+KGZCMuJWc3N9BHA+l/UmHd/oUHwJJDeysyTRxNQVkbzsIWfGFbRe3OcgML/i0mvVRPOyDA== + dependencies: + node-gyp-build "^4.3.0" + utf-8-validate@^5.0.2: version "5.0.10" resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.10.tgz#d7d10ea39318171ca982718b6b96a8d2442571a2" @@ -28061,13 +29101,6 @@ utf-8-validate@^5.0.2: dependencies: node-gyp-build "^4.3.0" -utf-8-validate@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-6.0.3.tgz#7d8c936d854e86b24d1d655f138ee27d2636d777" - integrity sha512-uIuGf9TWQ/y+0Lp+KGZCMuJWc3N9BHA+l/UmHd/oUHwJJDeysyTRxNQVkbzsIWfGFbRe3OcgML/i0mvVRPOyDA== - dependencies: - node-gyp-build "^4.3.0" - utf7@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/utf7/-/utf7-1.0.2.tgz#955f490aae653ba220b9456a0a8776c199360991" @@ -28805,6 +29838,20 @@ webpack@5, webpack@^5.88.2: watchpack "^2.4.0" webpack-sources "^3.2.3" +websocket-driver@>=0.5.1: + version "0.7.4" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" + integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== + dependencies: + http-parser-js ">=0.5.1" + safe-buffer ">=5.1.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.4" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" + integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== + whatwg-fetch@^3.0.0, whatwg-fetch@^3.4.1: version "3.6.20" resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz#580ce6d791facec91d37c72890995a0b48d31c70" @@ -29074,6 +30121,11 @@ ws@7.4.6, ws@7.5.3, ws@^7, ws@^7.0.0, ws@^7.2.3, ws@^7.5.1, ws@^7.5.10: resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== +ws@8.13.0: + version "8.13.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" + integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== + ws@8.14.1, ws@8.2.3, ws@8.5.0, ws@^8.0.0, ws@^8.17.1, ws@^8.5.0, ws@^8.8.1, ws@~8.11.0: version "8.17.1" resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" @@ -29284,7 +30336,7 @@ yargs@17.7.1: y18n "^5.0.5" yargs-parser "^21.1.1" -yargs@17.7.2, yargs@^17.0.0, yargs@^17.3.1, yargs@^17.5.1: +yargs@17.7.2, yargs@^17.0.0, yargs@^17.3.1, yargs@^17.5.1, yargs@^17.7.2: version "17.7.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==