diff --git a/.github/guidelines/LABELING_GUIDELINES.md b/.github/guidelines/LABELING_GUIDELINES.md
index 84366a5de80..f3fbc606f22 100644
--- a/.github/guidelines/LABELING_GUIDELINES.md
+++ b/.github/guidelines/LABELING_GUIDELINES.md
@@ -21,6 +21,9 @@ Every PR shall include one the QA labels below:
Once PR has been tested by QA (only if the PR was labeled with `needs-qa`):
- **QA Passed**: If the PR was labeled with `needs-qa`, this label must be added once QA has signed off
+### 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 development branch (i.e. `main`), but is not yet released in production.
+
### Labels prohibited when PR needs to be merged:
Any PR that includes one of the following labels can not be merged:
diff --git a/.github/workflows/crowdin-branch-cleanup.yml b/.github/workflows/crowdin-branch-cleanup.yml
new file mode 100644
index 00000000000..7515abfa034
--- /dev/null
+++ b/.github/workflows/crowdin-branch-cleanup.yml
@@ -0,0 +1,73 @@
+name: Crowdin - Branch and Label Cleanup for merged localization PR
+# This action should delete the branch from Crowdin after the localization PR is
+# merged to the original branch. It should also remove the "ready-for-translation" label
+
+# TODO: Add trigger for merge of localization PR.
+on: workflow_dispatch
+
+jobs:
+ prestep:
+ runs-on: ubuntu-latest
+ outputs:
+ branch: ${{ steps.extract_current_branch.outputs.branch }}
+ pr: ${{ steps.get-prs.outputs.pr }}
+ steps:
+ - name: Extract current branch name
+ shell: bash
+ run: |
+ echo "running on branch ${GITHUB_REF##*/}"
+ echo "other version: ${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}"
+ echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> "$GITHUB_OUTPUT"
+ id: extract_current_branch
+
+ - name: Get PR with Label for this branch
+ id: get-prs
+ run: |
+ LABEL="ready-for-translation"
+ API_URL="https://api.github.com/repos/Metamask/crowdin-sandbox/pulls?head:${{steps.extract_current_branch.outputs.branch}}&state=open&per_page=100"
+ # Fetch the list of open pull requests with the specified label using curl
+ PRS=$(curl -sS --header "Authorization: Bearer $GITHUB_TOKEN" "$API_URL")
+ PR=$(echo "$PRS" | jq -r '.[] | select(.labels[].name == "'"$LABEL"'") | .number | @json')
+ echo "Found PR: $PR"
+ echo "pr=$PR" >> "$GITHUB_OUTPUT"
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ CURRENT_BRANCH: ${{ steps.extract_current_branch.outputs.branch }}
+
+ github_cleanup:
+ runs-on: ubuntu-latest
+ needs: prestep
+ steps:
+ - name: Remove label from PR
+ uses: actions/github-script@v3
+ if: needs.prestep.outputs.pr != null || needs.prestep.outputs.pr != ''
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const label = "ready-for-translation";
+ await github.issues.removeLabel({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: ${{ needs.prestep.outputs.pr }},
+ name: label
+ });
+
+ crowdin_cleanup:
+ runs-on: ubuntu-latest
+ needs: prestep
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
+ CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ with:
+ ref: ${{ needs.prestep.outputs.branch }}
+
+ - name: Delete branch within Crowdin
+ if: needs.prestep.outputs.branch != 'main'
+ uses: crowdin/github-action@c953b17499daa6be3e5afbf7a63616fb02d8b18d
+ with:
+ command: branch delete ${{ needs.prestep.outputs.branch }}
+ command_args: -v
diff --git a/.github/workflows/crowdin-branch-pr-ready-for-translation.yml b/.github/workflows/crowdin-branch-pr-ready-for-translation.yml
new file mode 100644
index 00000000000..36ec31ba547
--- /dev/null
+++ b/.github/workflows/crowdin-branch-pr-ready-for-translation.yml
@@ -0,0 +1,50 @@
+name: Crowdin - Ready for translations label added, push to crowdin
+
+# When an individual is working on a feature which requires translations, they can
+# add a label "ready-for-translation" which will trigger this action to push the
+# source and translation files to Crowdin. We will always push main as the base of
+# the crowdin branch creation and then push in the changes over the top. This ensures
+# that the translations which have already been done and approved previously do not
+# show as needing to be translated again in the crowdin branch.
+
+# TODO: switch to trigger on label add once testing complete
+on: workflow_dispatch
+
+jobs:
+ crowdin-upload:
+ runs-on: ubuntu-latest
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
+ CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
+
+ steps:
+ - name: Extract current branch name
+ shell: bash
+ run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> "$GITHUB_OUTPUT"
+ id: extract_current_branch
+
+ - name: Checkout
+ uses: actions/checkout@v3
+ with:
+ ref: main
+
+ - name: Crowdin push main as baseline
+ uses: crowdin/github-action@c953b17499daa6be3e5afbf7a63616fb02d8b18d
+ with:
+ crowdin_branch_name: ${{ steps.extract_current_branch.outputs.branch }}
+ upload_sources: true
+ upload_translations_args: --import-eq-suggestions --auto-approve-imported --verbose
+ upload_translations: true
+
+ - name: Checkout Branch and push to crowdin
+ uses: actions/checkout@v3
+ with:
+ ref: ${{ steps.extract_current_branch.outputs.branch }}
+ - name: Crowdin sources push
+ uses: crowdin/github-action@c953b17499daa6be3e5afbf7a63616fb02d8b18d
+ with:
+ crowdin_branch_name: ${{ steps.extract_current_branch.outputs.branch }}
+ upload_sources: true
+ upload_sources_args: --auto-update --verbose
+ upload_translations: false
diff --git a/.github/workflows/crowdin-pull-branch-pr-completed-translations.yml b/.github/workflows/crowdin-pull-branch-pr-completed-translations.yml
new file mode 100644
index 00000000000..1669e2b7b47
--- /dev/null
+++ b/.github/workflows/crowdin-pull-branch-pr-completed-translations.yml
@@ -0,0 +1,63 @@
+name: Crowdin - Find all branches with translations and trigger completion checks
+# This workflow will run on a schedule. It will pull all pull requests with a label of
+# ready-for-translation and create a matrix of the associated branches to run the
+# crowdin-reusable-translation-download.yml workflow on.
+# That workflow will check the status of the translations and if complete create a pull
+# request with the translations off of the branch.
+
+permissions:
+ contents: write
+ pull-requests: write
+
+# TODO: Add a schedule to run this workflow twice a day(?) once the testing is complete
+on: workflow_dispatch
+
+jobs:
+ run-check-and-download-for-branch:
+ needs: get-branches
+ if: ${{ needs.get-branches.outputs.matrix != '[]' && needs.get-branches.outputs.matrix != '' }}
+ strategy:
+ fail-fast: false
+ matrix:
+ branch: ${{fromJson(needs.get-branches.outputs.matrix)}}
+ uses: ./.github/workflows/crowdin-reusable-translation-download.yml
+ with:
+ branch: ${{ matrix.branch }}
+ secrets:
+ gh_token: ${{ secrets.GITHUB_TOKEN }}
+ crowdin_personal_token: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
+ crowdin_project_id: ${{ secrets.CROWDIN_PROJECT_ID }}
+
+ get-branches:
+ runs-on: ubuntu-latest
+ outputs:
+ matrix: ${{ steps.matrix-outputs.outputs.matrix }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v3
+
+ - name: Get Branches with Label
+ id: get-branches
+ run: |
+ GITHUB_TOKEN="${{ secrets.GITHUB_TOKEN }}"
+
+ LABEL="ready-for-translation"
+ API_URL="https://api.github.com/repos/Metamask/crowdin-sandbox/pulls?state=open&per_page=100"
+
+ # Fetch the list of open pull requests with the specified label using curl
+ PRS=$(curl -sS --header "Authorization: Bearer $GITHUB_TOKEN" "$API_URL")
+
+ BRANCHES=$(echo "$PRS" | jq -r '[.[] | select(.labels[].name == "'"$LABEL"'") | .head.ref] | @json')
+ echo "Found branches: $BRANCHES"
+ echo "branches=$BRANCHES" >> "$GITHUB_OUTPUT"
+
+ - name: Set up matrix
+ id: matrix-outputs
+ run: |
+ # Parse the branches output and create a matrix
+ BRANCHES="${{ toJson(steps.get-branches.outputs.branches) }}"
+ echo "Creating matrix from branches..."
+ MATRIX="${BRANCHES}"
+ echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT"
+
+
diff --git a/.github/workflows/crowdin-reusable-translation-download.yml b/.github/workflows/crowdin-reusable-translation-download.yml
new file mode 100644
index 00000000000..355beef39f7
--- /dev/null
+++ b/.github/workflows/crowdin-reusable-translation-download.yml
@@ -0,0 +1,64 @@
+name: Crowdin - Check translation progress and download if complete (unless main)
+# This is a reusable workflow that is called by crowdin-pull-branch-pr-completed-translations
+# across all branches which have a label of "ready-for-translation" aka being translated.
+# This workflow will check the translation progress and download the translations if
+# they are 100% translated. If the branch that is running this is main it will skip completion
+# check and just pull whatever translations are available.
+
+
+permissions:
+ contents: write
+ pull-requests: write
+
+on:
+ workflow_call:
+ inputs:
+ branch:
+ required: true
+ type: string
+ secrets:
+ gh_token:
+ required: true
+ crowdin_project_id:
+ required: true
+ crowdin_personal_token:
+ required: true
+
+jobs:
+ crowdin:
+ runs-on: ubuntu-latest
+ env:
+ GITHUB_TOKEN: ${{ secrets.gh_token }}
+ CROWDIN_PERSONAL_TOKEN: ${{ secrets.crowdin_personal_token }}
+ CROWDIN_PROJECT_ID: ${{ secrets.crowdin_project_id }}
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ with:
+ ref: ${{ inputs.branch }}
+
+ - name: Check translation progress
+ # when main just pull whatever you have (aka skip this) - need to test
+ if: ${{ inputs.branch != 'main' }}
+ uses: crowdin/github-action@c953b17499daa6be3e5afbf7a63616fb02d8b18d
+ with:
+ command: 'status translation'
+ command_args: '-b ${{ inputs.branch }} --fail-if-incomplete'
+
+ - name: Synchronize with Crowdin
+ uses: crowdin/github-action@c953b17499daa6be3e5afbf7a63616fb02d8b18d
+ with:
+ crowdin_branch_name: ${{ inputs.branch }}
+ upload_sources: false
+ upload_translations: false
+ download_translations: true
+ skip_untranslated_strings: true
+ export_only_approved: true
+ localization_branch_name: l10n_crowdin_translations_${{ inputs.branch }}
+
+ create_pull_request: true
+ skip_ref_checkout: true
+ pull_request_title: New Crowdin translations for ${{ inputs.branch }}
+ pull_request_body: New Crowdin pull request with translations for ${{ inputs.branch }}
+ pull_request_base_branch_name: ${{ inputs.branch }}
diff --git a/.github/workflows/crowdin-upload-both-sources-translations.yml b/.github/workflows/crowdin-upload-both-sources-translations.yml
new file mode 100644
index 00000000000..958344c7a55
--- /dev/null
+++ b/.github/workflows/crowdin-upload-both-sources-translations.yml
@@ -0,0 +1,31 @@
+name: Crowdin - Upload Both Sources and Translations Crowdin Action
+# This action is intended to ensure our main branch on crowdin is in sync with our
+# main branch on github and will run on every push to main that has changes to any
+# locales files.
+
+# TODO: Change to trigger on merge to main when locales files are changed (after testing)
+# This should replace the current crowdin_action.yml file (after testing)
+on: workflow_dispatch
+
+jobs:
+ crowdin-upload:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Extract current branch name
+ shell: bash
+ run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> "$GITHUB_OUTPUT"
+ id: extract_current_branch
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Crowdin push
+ uses: crowdin/github-action@c953b17499daa6be3e5afbf7a63616fb02d8b18d
+ with:
+ crowdin_branch_name: ${{ steps.extract_current_branch.outputs.branch }}
+ upload_sources: true
+ upload_translations: true
+ upload_translations_args: --import-eq-suggestions --auto-approve-imported --verbose
+ download_translations: false
+ env:
+ CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
+ CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b81e0948773..b95ef492a5e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,62 @@
## Current Main Branch
+## 7.20.0 - Apr 1, 2024
+### Added
+- [#8982](https://github.com/MetaMask/metamask-mobile/pull/8982): feat: Update gas fee controller to version 6.1.2
+- [#9079](https://github.com/MetaMask/metamask-mobile/pull/9079): feat: Update controller utils to 5.0.2
+- [#9036](https://github.com/MetaMask/metamask-mobile/pull/9036): feat: Adding blockaid validations for sepolia network
+- [#8668](https://github.com/MetaMask/metamask-mobile/pull/8668): feat: PPOM Version update to download files only before transaction
+- [#8720](https://github.com/MetaMask/metamask-mobile/pull/8720): feat: migrate to native primary currency
+- [#8989](https://github.com/MetaMask/metamask-mobile/pull/8989): feat: Signature Controller v5.3.1
+- [#8759](https://github.com/MetaMask/metamask-mobile/pull/8759): feat: Accounts controller integration
+- [#9007](https://github.com/MetaMask/metamask-mobile/pull/9007): feat: Assets controller updated to version 12.0.0
+- [#9005](https://github.com/MetaMask/metamask-mobile/pull/9005): feat: Update network controller to version 12.2.0
+- [#8920](https://github.com/MetaMask/metamask-mobile/pull/8920): feat: add the 'eth_phishing_detection' logic to the connect accounts flow
+- [#8952](https://github.com/MetaMask/metamask-mobile/pull/8952): feat: Network Controller update to v12.1.2
+- [#8981](https://github.com/MetaMask/metamask-mobile/pull/8981): feat: Minor update of phishing-controller
+- [#8986](https://github.com/MetaMask/metamask-mobile/pull/8986): feat: Updated Assets Controllers to v11.1.0
+
+### Changed
+- [#8967](https://github.com/MetaMask/metamask-mobile/pull/8967): chore: Handle async migrations & add migration tests
+- [#8826](https://github.com/MetaMask/metamask-mobile/pull/8826): refactor: update Ledger connection instructions
+- [#9038](https://github.com/MetaMask/metamask-mobile/pull/9038): chore: Consolidate and simplify isTest condition
+- [#9094](https://github.com/MetaMask/metamask-mobile/pull/9094): test: stabilize swaps e2e tests
+- [#9069](https://github.com/MetaMask/metamask-mobile/pull/9069): test: clean up detox build/run scripts
+- [#9078](https://github.com/MetaMask/metamask-mobile/pull/9078): chore: Sentry reduce traceSample rate to 0.04
+- [#9039](https://github.com/MetaMask/metamask-mobile/pull/9039): refactor(ramp): persist and format amount in build quote view
+- [#9033](https://github.com/MetaMask/metamask-mobile/pull/9033): Revert "feat: migrate to native primary currency (#8720)"
+- [#8918](https://github.com/MetaMask/metamask-mobile/pull/8918): ci: Simplify PR template
+- [#9008](https://github.com/MetaMask/metamask-mobile/pull/9008): chore(ramp): upgrade sdk to 1.26.8
+- [#7591](https://github.com/MetaMask/metamask-mobile/pull/7591): chore(deps-dev): bump @babel/traverse from 7.22.6 to 7.23.2 in /ppom
+- [#8842](https://github.com/MetaMask/metamask-mobile/pull/8842): ci: Create Bitrise status check for commits
+- [#8990](https://github.com/MetaMask/metamask-mobile/pull/8990): chore: Remove unnecessary react native animate fox patch
+- [#8987](https://github.com/MetaMask/metamask-mobile/pull/8987): chore: adding code owner for confirmation domain's code
+- [#8675](https://github.com/MetaMask/metamask-mobile/pull/8675): refactor: Refactor inpage blocklist to avoid usage of regex
+- [#8959](https://github.com/MetaMask/metamask-mobile/pull/8959): chore: Migrate another (3/3) batch of unit tests away from enzyme
+
+### Fixed
+- [#9000](https://github.com/MetaMask/metamask-mobile/pull/9000): fix: Revert "test: [android] run tests on the first emulator in your list …
+- [#8998](https://github.com/MetaMask/metamask-mobile/pull/8998): fix: Revert "fix: hardcode emulator name to fix failing android tests on C…
+- [#8995](https://github.com/MetaMask/metamask-mobile/pull/8995): fix: hardcode emulator name to fix failing android tests on CI
+- [#9023](https://github.com/MetaMask/metamask-mobile/pull/9023): fix: improve SVG Validation and Error Handling in AvatarFavicon Component
+- [#9001](https://github.com/MetaMask/metamask-mobile/pull/9001): fix: (#8617): Reduce enzyme usage in unit test by 25% - [2/3] (#8823)
+- [#9013](https://github.com/MetaMask/metamask-mobile/pull/9013): fix: isEIP1559 is undefined migration 29 issue
+- [#8980](https://github.com/MetaMask/metamask-mobile/pull/8980): fix: analytics on Account Right Button analytics
+- [#8991](https://github.com/MetaMask/metamask-mobile/pull/8991): fix: add Referer to Dapp Viewed events
+- [#8977](https://github.com/MetaMask/metamask-mobile/pull/8977): fix: Remove wallet connect and sdk connect prefix from ppom domain
+- [#9080](https://github.com/MetaMask/metamask-mobile/pull/9080): fix: check for preferences controller in 036 migration
+- [#8751](https://github.com/MetaMask/metamask-mobile/pull/8751): fix: removes addSubjectMetadata and improves Snaps Execution Environment Webview origin restriction.
+- [#9082](https://github.com/MetaMask/metamask-mobile/pull/9082): fix: Check for missing identities on migration 36
+- [#9031](https://github.com/MetaMask/metamask-mobile/pull/9031): fix: Remove prefix from origin before sending request to ppom
+- [#9066](https://github.com/MetaMask/metamask-mobile/pull/9066): fix: dedupe dependencies
+- [#9051](https://github.com/MetaMask/metamask-mobile/pull/9051): fix: Remove duplicate i18n, en key: `qr_hardware`
+- [#9053](https://github.com/MetaMask/metamask-mobile/pull/9053): fix: update ppom npm package to version 1.4.5
+- [#9042](https://github.com/MetaMask/metamask-mobile/pull/9042): fix: Refactor SDK initialization
+- [#8975](https://github.com/MetaMask/metamask-mobile/pull/8975): fix: remove call to private/internal methods from the `@metamask/keyring-controller`
+- [#9021](https://github.com/MetaMask/metamask-mobile/pull/9021): fix: Network not updating when changing account connected the first time on a DAPP
+- [#8932](https://github.com/MetaMask/metamask-mobile/pull/8932): fix: breaking change from `@metamask/transaction-controller` regarding Ledger transactions
+
## 7.19.1 - Apr 10, 2024
### Fixed
- [#9193](https://github.com/MetaMask/metamask-mobile/pull/9193): fix(ramp): default networks state to array
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 97bc4dfd830..4b42fcdf437 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -181,8 +181,8 @@ android {
applicationId "io.metamask"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
- versionCode 1306
- versionName "7.19.1"
+ versionCode 1308
+ versionName "7.20.0"
testBuildType System.getProperty('testBuildType', 'debug')
missingDimensionStrategy 'react-native-camera', 'general'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
diff --git a/app/component-library/components/Badges/Badge/Badge.constants.ts b/app/component-library/components/Badges/Badge/Badge.constants.ts
index 27f7f90c2d1..fb569ac371d 100644
--- a/app/component-library/components/Badges/Badge/Badge.constants.ts
+++ b/app/component-library/components/Badges/Badge/Badge.constants.ts
@@ -1,2 +1,3 @@
export const BADGE_BADGENETWORK_TEST_ID = 'badge-badgenetwork';
export const BADGE_BADGESTATUS_TEST_ID = 'badge-badgestatus';
+export const BADGE_BADGENOTIFICATIONS_TEST_ID = 'badge-badgenotifications';
diff --git a/app/component-library/components/Badges/Badge/Badge.tsx b/app/component-library/components/Badges/Badge/Badge.tsx
index 8f0bb04be9f..661385b8b97 100644
--- a/app/component-library/components/Badges/Badge/Badge.tsx
+++ b/app/component-library/components/Badges/Badge/Badge.tsx
@@ -1,15 +1,14 @@
-/* eslint-disable react/prop-types */
import React from 'react';
-// External dependencies.
import BadgeNetwork from './variants/BadgeNetwork';
import BadgeStatus from './variants/BadgeStatus';
+import BadgeNotifications from './variants/BadgeNotifications';
-// Internal dependencies.
import { BadgeProps, BadgeVariant } from './Badge.types';
import {
BADGE_BADGENETWORK_TEST_ID,
BADGE_BADGESTATUS_TEST_ID,
+ BADGE_BADGENOTIFICATIONS_TEST_ID,
} from './Badge.constants';
const Badge = ({ variant, ...props }: BadgeProps) => {
@@ -18,6 +17,13 @@ const Badge = ({ variant, ...props }: BadgeProps) => {
return ;
case BadgeVariant.Status:
return ;
+ case BadgeVariant.NotificationsKinds:
+ return (
+
+ );
default:
throw new Error('Invalid Badge Variant');
}
diff --git a/app/component-library/components/Badges/Badge/Badge.types.ts b/app/component-library/components/Badges/Badge/Badge.types.ts
index a9347f921cc..e9c19942083 100644
--- a/app/component-library/components/Badges/Badge/Badge.types.ts
+++ b/app/component-library/components/Badges/Badge/Badge.types.ts
@@ -1,19 +1,24 @@
// Internal dependencies.
import { BadgeNetworkProps } from './variants/BadgeNetwork/BadgeNetwork.types';
import { BadgeStatusProps } from './variants/BadgeStatus';
-
+import { BadgeNotificationsProps } from './variants/BadgeNotifications';
/**
* Badge variants.
*/
export enum BadgeVariant {
Network = 'network',
Status = 'status',
+ NotificationsKinds = 'notifications-kinds',
}
/**
* Badge Account component props.
*/
-export type BadgeProps = (BadgeNetworkProps | BadgeStatusProps) & {
+export type BadgeProps = (
+ | BadgeNetworkProps
+ | BadgeStatusProps
+ | BadgeNotificationsProps
+) & {
/**
* Optional prop to control the variant of Badge.
*/
diff --git a/app/component-library/components/Badges/Badge/variants/BadgeNotifications/BadgeNotifications.constants.ts b/app/component-library/components/Badges/Badge/variants/BadgeNotifications/BadgeNotifications.constants.ts
new file mode 100644
index 00000000000..20bed388e5b
--- /dev/null
+++ b/app/component-library/components/Badges/Badge/variants/BadgeNotifications/BadgeNotifications.constants.ts
@@ -0,0 +1,18 @@
+import { NotificationsKindTypes } from '../../../../../../util/notifications';
+import { IconName, IconSize } from '../../../../Icons/Icon/Icon.types';
+
+// Internal dependencies.
+import { BadgeNotificationsProps } from './BadgeNotifications.types';
+
+// Test IDs
+export const BADGE_NOTIFICATIONS_TEST_ID = 'badge-notifications';
+export const TEST_NOTIFICATIONS_ACTION = NotificationsKindTypes.transaction;
+export const TEST_RNOTIFICATIONS_ICON_NAME = IconName.Send2;
+
+// Defaults
+export const DEFAULT_BADGENOTIFICATIONS_NOTIFICATIONSICON_SIZE = IconSize.Md;
+
+export const SAMPLE_BADGENOTIFICATIONS_PROPS: BadgeNotificationsProps = {
+ name: TEST_NOTIFICATIONS_ACTION,
+ iconName: TEST_RNOTIFICATIONS_ICON_NAME,
+};
diff --git a/app/component-library/components/Badges/Badge/variants/BadgeNotifications/BadgeNotifications.stories.tsx b/app/component-library/components/Badges/Badge/variants/BadgeNotifications/BadgeNotifications.stories.tsx
new file mode 100644
index 00000000000..ddda7d95edb
--- /dev/null
+++ b/app/component-library/components/Badges/Badge/variants/BadgeNotifications/BadgeNotifications.stories.tsx
@@ -0,0 +1,37 @@
+/* eslint-disable react-native/no-inline-styles */
+/* eslint-disable react/display-name */
+import React from 'react';
+import { View } from 'react-native';
+
+// Internal dependencies.
+import { default as BadgeBadgeNotificationssComponent } from './BadgeNotifications';
+import { SAMPLE_BADGENOTIFICATIONS_PROPS } from './BadgeNotifications.constants';
+import { BadgeNotificationsProps } from './BadgeNotifications.types';
+
+const BadgeBadgeNotificationsMeta = {
+ title: 'Component Library / Badges',
+ component: BadgeBadgeNotificationssComponent,
+ argTypes: {
+ name: {
+ control: { type: 'text' },
+ defaultValue: SAMPLE_BADGENOTIFICATIONS_PROPS.name,
+ },
+ },
+};
+export default BadgeBadgeNotificationsMeta;
+
+export const BadgeNotification = {
+ render: (args: JSX.IntrinsicAttributes & BadgeNotificationsProps) => (
+
+
+
+ ),
+};
diff --git a/app/component-library/components/Badges/Badge/variants/BadgeNotifications/BadgeNotifications.styles.ts b/app/component-library/components/Badges/Badge/variants/BadgeNotifications/BadgeNotifications.styles.ts
new file mode 100644
index 00000000000..552d7d12419
--- /dev/null
+++ b/app/component-library/components/Badges/Badge/variants/BadgeNotifications/BadgeNotifications.styles.ts
@@ -0,0 +1,54 @@
+// Third party dependencies.
+import { StyleSheet, ViewStyle } from 'react-native';
+
+import { Theme } from '../../../../../../util/theme/models';
+
+import { DEFAULT_BADGENOTIFICATIONS_NOTIFICATIONSICON_SIZE } from './BadgeNotifications.constants';
+import { BadgeNotificationsStyleSheetVars } from './BadgeNotifications.types';
+
+/**
+ * Style sheet function for BadgeNotifications 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 = (params: {
+ theme: Theme;
+ vars: BadgeNotificationsStyleSheetVars;
+}) => {
+ const { vars } = params;
+ const { style, containerSize } = vars;
+ let scaleRatio = 1;
+ let opacity = 0;
+ if (containerSize) {
+ scaleRatio =
+ containerSize.height /
+ Number(DEFAULT_BADGENOTIFICATIONS_NOTIFICATIONSICON_SIZE);
+ opacity = 1;
+ }
+
+ return StyleSheet.create({
+ base: Object.assign(
+ {
+ height: '50%',
+ aspectRatio: 1,
+ minHeight: 18,
+ alignItems: 'center',
+ justifyContent: 'center',
+ backgroundColor: params.theme.colors.info.default,
+ borderColor: params.theme.colors.background.default,
+ borderWidth: 1,
+ borderRadius: 9,
+ opacity,
+ } as ViewStyle,
+ style,
+ ) as ViewStyle,
+ notificationIcon: {
+ transform: [{ scale: scaleRatio }],
+ },
+ });
+};
+
+export default styleSheet;
diff --git a/app/component-library/components/Badges/Badge/variants/BadgeNotifications/BadgeNotifications.test.tsx b/app/component-library/components/Badges/Badge/variants/BadgeNotifications/BadgeNotifications.test.tsx
new file mode 100644
index 00000000000..97c341ad8a9
--- /dev/null
+++ b/app/component-library/components/Badges/Badge/variants/BadgeNotifications/BadgeNotifications.test.tsx
@@ -0,0 +1,37 @@
+// Third party dependencies.
+import React from 'react';
+import { render } from '@testing-library/react-native';
+
+import {
+ TEST_NOTIFICATIONS_ACTION,
+ TEST_RNOTIFICATIONS_ICON_NAME,
+ BADGE_NOTIFICATIONS_TEST_ID,
+} from './BadgeNotifications.constants';
+
+// Internal dependencies.
+import BadgeNotifications from './BadgeNotifications';
+
+describe('BadgeNotifications - snapshots', () => {
+ it('should render badge notifications correctly', () => {
+ const { toJSON } = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
+
+describe('BadgeNotifications', () => {
+ it('should render badge notifications with the given content', () => {
+ const { findByTestId } = render(
+ ,
+ );
+
+ expect(findByTestId(BADGE_NOTIFICATIONS_TEST_ID)).toBeTruthy();
+ });
+});
diff --git a/app/component-library/components/Badges/Badge/variants/BadgeNotifications/BadgeNotifications.tsx b/app/component-library/components/Badges/Badge/variants/BadgeNotifications/BadgeNotifications.tsx
new file mode 100644
index 00000000000..2616c60029f
--- /dev/null
+++ b/app/component-library/components/Badges/Badge/variants/BadgeNotifications/BadgeNotifications.tsx
@@ -0,0 +1,29 @@
+// Third library dependencies.
+import React from 'react';
+
+// External dependencies.
+import BadgeBase from '../../foundation/BadgeBase';
+import { useComponentSize, useStyles } from '../../../../../hooks';
+import Icon, { IconSize, IconColor } from '../../../../Icons/Icon';
+
+// Internal dependencies
+import { BadgeNotificationsProps } from './BadgeNotifications.types';
+import styleSheet from './BadgeNotifications.styles';
+import { BADGE_NOTIFICATIONS_TEST_ID } from './BadgeNotifications.constants';
+
+const BadgeNotifications = ({ style, iconName }: BadgeNotificationsProps) => {
+ const { size: containerSize, onLayout: onLayoutContainerSize } =
+ useComponentSize();
+ const { styles } = useStyles(styleSheet, { style, containerSize });
+ return (
+
+
+
+ );
+};
+
+export default BadgeNotifications;
diff --git a/app/component-library/components/Badges/Badge/variants/BadgeNotifications/BadgeNotifications.types.ts b/app/component-library/components/Badges/Badge/variants/BadgeNotifications/BadgeNotifications.types.ts
new file mode 100644
index 00000000000..974cbc244f6
--- /dev/null
+++ b/app/component-library/components/Badges/Badge/variants/BadgeNotifications/BadgeNotifications.types.ts
@@ -0,0 +1,24 @@
+// External dependencies.
+import { BadgeBaseProps } from '../../foundation/BadgeBase/BadgeBase.types';
+import { IconName } from '../../../../../../component-library/components/Icons/Icon';
+
+/**
+ * BadgeNotifications component props.
+ */
+export interface BadgeNotificationsProps
+ extends Omit {
+ /**
+ * Required prop to provide the icon to be used by the notification badge.
+ */
+ iconName: IconName;
+}
+
+/**
+ * Style sheet BadgeNotifications parameters.
+ */
+export type BadgeNotificationsStyleSheetVars = Pick<
+ BadgeNotificationsProps,
+ 'style'
+> & {
+ containerSize: { width: number; height: number } | null;
+};
diff --git a/app/component-library/components/Badges/Badge/variants/BadgeNotifications/README.md b/app/component-library/components/Badges/Badge/variants/BadgeNotifications/README.md
new file mode 100644
index 00000000000..c146d3bb9ec
--- /dev/null
+++ b/app/component-library/components/Badges/Badge/variants/BadgeNotifications/README.md
@@ -0,0 +1,22 @@
+BadgeNotifications is used on top of an element to display notifications information. **This component is not meant to be used by itself**. It is used by [BadgeWrapper](../BadgeWrapper/BadgeWrapper.tsx), which can render this component as a badge.
+
+## Props
+
+This component extends [BadgeBaseProps](../../foundation/BadgeBase/BadgeBase.types.ts).
+
+### `iconName`
+
+Required prop for icon names used by the notifications.
+
+| TYPE | REQUIRED |
+| :--------------------------------------------------- | :------------------------------------------------------ |
+| [IconName](../../../../Icons/Icon/Icon.types.ts#L50) | Yes |
+
+## Usage
+
+```javascript
+
+```
diff --git a/app/component-library/components/Badges/Badge/variants/BadgeNotifications/__snapshots__/BadgeNotifications.test.tsx.snap b/app/component-library/components/Badges/Badge/variants/BadgeNotifications/__snapshots__/BadgeNotifications.test.tsx.snap
new file mode 100644
index 00000000000..b82847bfe99
--- /dev/null
+++ b/app/component-library/components/Badges/Badge/variants/BadgeNotifications/__snapshots__/BadgeNotifications.test.tsx.snap
@@ -0,0 +1,35 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`BadgeNotifications - snapshots should render badge notifications correctly 1`] = `
+
+
+
+`;
diff --git a/app/component-library/components/Badges/Badge/variants/BadgeNotifications/index.ts b/app/component-library/components/Badges/Badge/variants/BadgeNotifications/index.ts
new file mode 100644
index 00000000000..02e8c8522f5
--- /dev/null
+++ b/app/component-library/components/Badges/Badge/variants/BadgeNotifications/index.ts
@@ -0,0 +1,2 @@
+export { default } from './BadgeNotifications';
+export type { BadgeNotificationsProps } from './BadgeNotifications.types';
diff --git a/app/component-library/components/Badges/BadgeWrapper/BadgeWrapper.constants.tsx b/app/component-library/components/Badges/BadgeWrapper/BadgeWrapper.constants.tsx
index 081a3e5a9f4..fb68f4be8b2 100644
--- a/app/component-library/components/Badges/BadgeWrapper/BadgeWrapper.constants.tsx
+++ b/app/component-library/components/Badges/BadgeWrapper/BadgeWrapper.constants.tsx
@@ -22,6 +22,7 @@ export const BADGE_WRAPPER_BADGE_TEST_ID = 'badge-wrapper-badge';
export const DEFAULT_BADGEWRAPPER_BADGEANCHORELEMENTSHAPE =
BadgeAnchorElementShape.Circular;
export const DEFAULT_BADGEWRAPPER_BADGEPOSITION = BadgePosition.TopRight;
+export const BOTTOM_BADGEWRAPPER_BADGEPOSITION = BadgePosition.BottomRight;
// Samples
export const SAMPLE_BADGEWRAPPER_PROPS: BadgeWrapperProps = {
diff --git a/app/components/Nav/App/index.js b/app/components/Nav/App/index.js
index 33cf127bc25..52c27afd368 100644
--- a/app/components/Nav/App/index.js
+++ b/app/components/Nav/App/index.js
@@ -351,7 +351,8 @@ const App = ({ userLoggedIn }) => {
if (error) {
// Log error for analytics and continue handling deeplink
- Logger.error('Error from Branch: ' + error);
+ const branchError = new Error(error);
+ Logger.error(branchError, 'Error subscribing to branch.');
}
if (sdkInit.current) {
diff --git a/app/components/UI/Navbar/index.js b/app/components/UI/Navbar/index.js
index 06200035a85..a3bbeab17c9 100644
--- a/app/components/UI/Navbar/index.js
+++ b/app/components/UI/Navbar/index.js
@@ -51,6 +51,7 @@ import { CommonSelectorsIDs } from '../../../../e2e/selectors/Common.selectors';
import { WalletViewSelectorsIDs } from '../../../../e2e/selectors/WalletView.selectors';
import { NetworksViewSelectorsIDs } from '../../../../e2e/selectors/Settings/NetworksView.selectors';
import { SendLinkViewSelectorsIDs } from '../../../../e2e/selectors/SendLinkView.selectors';
+import { getBlockaidTransactionMetricsParams } from '../../../util/blockaid';
const trackEvent = (event, params = {}) => {
MetaMetrics.getInstance().trackEvent(event, params);
@@ -513,6 +514,7 @@ export function getSendFlowTitle(
route,
themeColors,
resetTransaction,
+ transaction,
) {
const innerStyles = StyleSheet.create({
headerButtonText: {
@@ -528,9 +530,12 @@ export function getSendFlowTitle(
});
const rightAction = () => {
const providerType = route?.params?.providerType ?? '';
+ const additionalTransactionMetricsParams =
+ getBlockaidTransactionMetricsParams(transaction);
trackEvent(MetaMetricsEvents.SEND_FLOW_CANCEL, {
view: title.split('.')[1],
network: providerType,
+ ...additionalTransactionMetricsParams,
});
resetTransaction();
navigation.dangerouslyGetParent()?.pop();
diff --git a/app/components/UI/Notification/index.js b/app/components/UI/Notification/index.js
index 4158611e9c2..77f1930faaa 100644
--- a/app/components/UI/Notification/index.js
+++ b/app/components/UI/Notification/index.js
@@ -6,7 +6,7 @@ import {
removeCurrentNotification,
hideCurrentNotification,
} from '../../../actions/notification';
-import notificationTypes from '../../../util/notifications';
+import { NotificationTypes } from '../../../util/notifications';
import TransactionNotification from './TransactionNotification';
import SimpleNotification from './SimpleNotification';
import { currentNotificationSelector } from '../../../reducers/notification';
@@ -20,7 +20,7 @@ import {
runOnJS,
} from 'react-native-reanimated';
-const { TRANSACTION, SIMPLE } = notificationTypes;
+const { TRANSACTION, SIMPLE } = NotificationTypes;
const BROWSER_ROUTE = 'BrowserView';
diff --git a/app/components/UI/Ramp/hooks/useFetchRampNetworks.ts b/app/components/UI/Ramp/hooks/useFetchRampNetworks.ts
index 77c751cc2a4..2674dd8f6b5 100644
--- a/app/components/UI/Ramp/hooks/useFetchRampNetworks.ts
+++ b/app/components/UI/Ramp/hooks/useFetchRampNetworks.ts
@@ -27,8 +27,8 @@ function useFetchRampNetworks() {
} catch (requestError) {
setError(requestError as Error);
Logger.error(
- 'useFetchOnRampNetworks::getNetworks',
requestError as Error,
+ 'useFetchOnRampNetworks::getNetworks',
);
} finally {
setIsLoading(false);
diff --git a/app/components/UI/Ramp/hooks/useRampNetworksDetail.ts b/app/components/UI/Ramp/hooks/useRampNetworksDetail.ts
index bf28c4cc64c..9aea6323451 100644
--- a/app/components/UI/Ramp/hooks/useRampNetworksDetail.ts
+++ b/app/components/UI/Ramp/hooks/useRampNetworksDetail.ts
@@ -17,8 +17,8 @@ function useRampNetworksDetail() {
} catch (requestError) {
setError(requestError as Error);
Logger.error(
- 'useRampNetworksDetail::getNetworksDetails',
requestError as Error,
+ 'useRampNetworksDetail::getNetworksDetails',
);
} finally {
setIsLoading(false);
diff --git a/app/components/UI/Ramp/orderProcessor/index.test.ts b/app/components/UI/Ramp/orderProcessor/index.test.ts
index 9e4db1c6dbd..08d04c01d01 100644
--- a/app/components/UI/Ramp/orderProcessor/index.test.ts
+++ b/app/components/UI/Ramp/orderProcessor/index.test.ts
@@ -94,7 +94,7 @@ describe('processOrder', () => {
unsupportedProviderOrder,
);
expect(Logger.error).toHaveBeenCalledWith(
- 'FiatOrders::ProcessOrder unrecognized provider',
+ new Error('FiatOrders::ProcessOrder unrecognized provider'),
unsupportedProviderOrder,
);
});
diff --git a/app/components/UI/Ramp/orderProcessor/index.ts b/app/components/UI/Ramp/orderProcessor/index.ts
index e665330650d..7739af63d2a 100644
--- a/app/components/UI/Ramp/orderProcessor/index.ts
+++ b/app/components/UI/Ramp/orderProcessor/index.ts
@@ -18,7 +18,10 @@ function processOrder(
return processAggregatorOrder(order, options);
}
default: {
- Logger.error('FiatOrders::ProcessOrder unrecognized provider', order);
+ const unrecognizedProviderError = new Error(
+ 'FiatOrders::ProcessOrder unrecognized provider',
+ );
+ Logger.error(unrecognizedProviderError, order);
return order;
}
}
diff --git a/app/components/UI/Swaps/QuotesView.js b/app/components/UI/Swaps/QuotesView.js
index d6e54ffc731..14faa6ae1ef 100644
--- a/app/components/UI/Swaps/QuotesView.js
+++ b/app/components/UI/Swaps/QuotesView.js
@@ -992,9 +992,9 @@ function SwapsQuotesView({
};
if (isHardwareAddress) {
TransactionController.hub.once(
- `${transactionMeta.id}:finished`,
+ `${transactionMeta.id}:confirmed`,
(transactionMeta) => {
- if (transactionMeta.status === TransactionStatus.submitted) {
+ if (transactionMeta.status === TransactionStatus.confirmed) {
handleSwapTransaction(
TransactionController,
newSwapsTransactions,
diff --git a/app/components/UI/Tokens/index.test.tsx b/app/components/UI/Tokens/index.test.tsx
index 4fcc4b1e5fe..c69682f3b53 100644
--- a/app/components/UI/Tokens/index.test.tsx
+++ b/app/components/UI/Tokens/index.test.tsx
@@ -189,6 +189,18 @@ describe('Tokens', () => {
expect(getByTestId(PORTFOLIO_BUTTON)).toBeDefined();
});
+ it('navigates to Portfolio url when portfolio button is pressed', () => {
+ const { getByTestId } = renderComponent(initialState);
+
+ fireEvent.press(getByTestId(PORTFOLIO_BUTTON));
+ expect(mockNavigate).toHaveBeenCalledWith(Routes.BROWSER.HOME, {
+ params: {
+ newTabUrl: `${AppConstants.PORTFOLIO.URL}/?metamaskEntry=mobile`,
+ timestamp: 123,
+ },
+ screen: Routes.BROWSER.VIEW,
+ });
+ });
it('should display unable to find conversion rate', async () => {
const state = {
engine: {
@@ -240,7 +252,7 @@ describe('Tokens', () => {
fireEvent.press(getByTestId(STAKE_BUTTON));
expect(mockNavigate).toHaveBeenCalledWith(Routes.BROWSER.HOME, {
params: {
- newTabUrl: `${AppConstants.PORTFOLIO_URL}/stake?metamaskEntry=mobile`,
+ newTabUrl: `${AppConstants.STAKE.URL}?metamaskEntry=mobile`,
timestamp: 123,
},
screen: Routes.BROWSER.VIEW,
diff --git a/app/components/UI/Tokens/index.tsx b/app/components/UI/Tokens/index.tsx
index cdb92bbccbe..9b32aaaf5ff 100644
--- a/app/components/UI/Tokens/index.tsx
+++ b/app/components/UI/Tokens/index.tsx
@@ -102,6 +102,7 @@ import ButtonIcon, {
} from '../../../../app/component-library/components/Buttons/ButtonIcon';
import Box from '../../UI/Ramp/components/Box';
import SheetHeader from '../../../../app/component-library/components/Sheet/SheetHeader';
+import { isPortfolioUrl } from '../../../../app/util/url';
const Tokens: React.FC = ({ tokens }) => {
const { colors } = useTheme();
@@ -220,16 +221,15 @@ const Tokens: React.FC = ({ tokens }) => {
const renderStakeButton = (asset: TokenI) => {
const onStakeButtonPress = () => {
- const STAKE_URL = `${AppConstants.PORTFOLIO_URL}/stake`;
const existingStakeTab = browserTabs.find((tab: BrowserTab) =>
- tab.url.includes(STAKE_URL),
+ tab.url.includes(AppConstants.STAKE.URL),
);
let existingTabId;
let newTabUrl;
if (existingStakeTab) {
existingTabId = existingStakeTab.id;
} else {
- newTabUrl = `${STAKE_URL}?metamaskEntry=mobile`;
+ newTabUrl = `${AppConstants.STAKE.URL}?metamaskEntry=mobile`;
}
const params = {
...(newTabUrl && { newTabUrl }),
@@ -245,7 +245,7 @@ const Tokens: React.FC = ({ tokens }) => {
location: 'Home Screen',
text: 'Stake',
token_symbol: asset.symbol,
- url: STAKE_URL,
+ url: AppConstants.STAKE.URL,
});
};
diff --git a/app/components/UI/Transactions/index.js b/app/components/UI/Transactions/index.js
index 19d25f14e24..258ab24e89e 100644
--- a/app/components/UI/Transactions/index.js
+++ b/app/components/UI/Transactions/index.js
@@ -52,7 +52,7 @@ import RetryModal from './RetryModal';
import PriceChartContext, {
PriceChartProvider,
} from '../AssetOverview/PriceChart/PriceChart.context';
-import { ethErrors } from 'eth-rpc-errors';
+import { providerErrors } from '@metamask/rpc-errors';
import {
selectConversionRate,
selectCurrentCurrency,
@@ -578,7 +578,7 @@ class Transactions extends PureComponent {
cancelUnsignedQRTransaction = async (tx) => {
await Engine.context.ApprovalController.reject(
tx.id,
- ethErrors.provider.userRejectedRequest(),
+ providerErrors.userRejectedRequest(),
);
};
diff --git a/app/components/Views/AccountConnect/AccountConnect.tsx b/app/components/Views/AccountConnect/AccountConnect.tsx
index fa7b47a4da2..cd4338161d2 100644
--- a/app/components/Views/AccountConnect/AccountConnect.tsx
+++ b/app/components/Views/AccountConnect/AccountConnect.tsx
@@ -67,6 +67,7 @@ import {
import AccountConnectMultiSelector from './AccountConnectMultiSelector';
import AccountConnectSingle from './AccountConnectSingle';
import AccountConnectSingleSelector from './AccountConnectSingleSelector';
+import DevLogger from '../../../core/SDKConnect/utils/DevLogger';
const createStyles = () =>
StyleSheet.create({
fullScreenModal: {
@@ -109,7 +110,9 @@ const AccountConnect = (props: AccountConnectProps) => {
: AvatarAccountType.JazzIcon,
);
- const { id: channelId, origin: metadataOrigin } = hostInfo.metadata as {
+ // on inappBrowser: hostname
+ // on sdk or walletconnect: channelId
+ const { origin: channelIdOrHostname } = hostInfo.metadata as {
id: string;
origin: string;
};
@@ -120,7 +123,9 @@ const AccountConnect = (props: AccountConnectProps) => {
const [hostname, setHostname] = useState(origin);
const urlWithProtocol = prefixUrlWithProtocol(hostname);
- const sdkConnection = SDKConnect.getInstance().getConnection({ channelId });
+ const sdkConnection = SDKConnect.getInstance().getConnection({
+ channelId: channelIdOrHostname,
+ });
// Last wallet connect session metadata
const wc2Metadata = useSelector((state: RootState) => state.sdk.wc2Metadata);
@@ -172,15 +177,27 @@ const AccountConnect = (props: AccountConnectProps) => {
);
const loadHostname = useCallback(async () => {
+ // walletconnect channelId format: 1713357238460272
+ // sdk channelId format: uuid
+ // inappbrowser channelId format: app.uniswap.io
+ DevLogger.log(
+ `AccountConnect::loadHostname hostname=${hostname} origin=${origin} metadataOrigin=${channelIdOrHostname}`,
+ sdkConnection,
+ );
+ // check if channelId contains dot, it comes from in-app browser and we can use it.
+ if (channelIdOrHostname.indexOf('.') !== -1) {
+ return origin;
+ }
+
if (sdkConnection) {
const _hostname = (
- sdkConnection?.originatorInfo?.url ?? metadataOrigin
+ sdkConnection?.originatorInfo?.url ?? channelIdOrHostname
).replace(AppConstants.MM_SDK.SDK_REMOTE_ORIGIN, '');
return _hostname;
}
- return wc2Metadata?.url ?? channelId;
- }, [channelId, metadataOrigin, sdkConnection, wc2Metadata]);
+ return wc2Metadata?.url ?? channelIdOrHostname;
+ }, [hostname, channelIdOrHostname, sdkConnection, origin, wc2Metadata]);
// Retrieve hostname info based on channelId
useEffect(() => {
@@ -203,10 +220,10 @@ const AccountConnect = (props: AccountConnectProps) => {
const cancelPermissionRequest = useCallback(
(requestId) => {
Engine.context.PermissionController.rejectPermissionsRequest(requestId);
- if (channelId && accountsLength === 0) {
+ if (channelIdOrHostname && accountsLength === 0) {
// Remove Potential SDK connection
SDKConnect.getInstance().removeChannel({
- channelId,
+ channelId: channelIdOrHostname,
sendTerminate: true,
});
}
@@ -219,7 +236,7 @@ const AccountConnect = (props: AccountConnectProps) => {
[
Engine.context.PermissionController,
accountsLength,
- channelId,
+ channelIdOrHostname,
trackEvent,
],
);
@@ -279,7 +296,7 @@ const AccountConnect = (props: AccountConnectProps) => {
...hostInfo,
metadata: {
...hostInfo.metadata,
- origin: metadataOrigin,
+ origin: channelIdOrHostname,
},
approvedAccounts: selectedAccounts,
};
@@ -342,7 +359,7 @@ const AccountConnect = (props: AccountConnectProps) => {
Engine.context.PermissionController,
toastRef,
accountsLength,
- metadataOrigin,
+ channelIdOrHostname,
triggerDappViewedEvent,
trackEvent,
]);
diff --git a/app/components/Views/LockScreen/index.js b/app/components/Views/LockScreen/index.js
index e04ccfdd933..dc0521493bc 100644
--- a/app/components/Views/LockScreen/index.js
+++ b/app/components/Views/LockScreen/index.js
@@ -140,7 +140,8 @@ class LockScreen extends PureComponent {
// Check https://github.com/MetaMask/mobile-planning/issues/1507
const { selectedAddress } = this.props;
if (typeof selectedAddress !== 'string') {
- Logger.error('unlockKeychain error', 'selectedAddress is not a string');
+ const unlockKeychainError = new Error('unlockKeychain error');
+ Logger.error(unlockKeychainError, 'selectedAddress is not a string');
}
await Authentication.appTriggeredAuth({
selectedAddress: this.props.selectedAddress,
diff --git a/app/components/Views/Login/index.js b/app/components/Views/Login/index.js
index 50282f4b630..1130ab46bd4 100644
--- a/app/components/Views/Login/index.js
+++ b/app/components/Views/Login/index.js
@@ -372,7 +372,8 @@ class Login extends PureComponent {
// Check https://github.com/MetaMask/mobile-planning/issues/1507
const { selectedAddress } = this.props;
if (typeof selectedAddress !== 'string') {
- Logger.error('Login error', 'selectedAddress is not a string');
+ const loginError = new Error('Login error');
+ Logger.error(loginError, 'selectedAddress is not a string');
}
await Authentication.userEntryAuth(
@@ -435,7 +436,7 @@ class Login extends PureComponent {
} else {
this.setState({ loading: false, error });
}
- Logger.error(error, 'Failed to unlock');
+ Logger.error(e, 'Failed to unlock');
}
};
@@ -448,7 +449,8 @@ class Login extends PureComponent {
// Check https://github.com/MetaMask/mobile-planning/issues/1507
const { selectedAddress } = this.props;
if (typeof selectedAddress !== 'string') {
- Logger.error('unlockKeychain error', 'selectedAddress is not a string');
+ const unlockKeychainError = new Error('unlockKeychain error');
+ Logger.error(unlockKeychainError, 'selectedAddress is not a string');
}
await Authentication.appTriggeredAuth({
selectedAddress: this.props.selectedAddress,
diff --git a/app/components/Views/ManualBackupStep1/index.js b/app/components/Views/ManualBackupStep1/index.js
index 378d08d6f60..61a91fe94bf 100644
--- a/app/components/Views/ManualBackupStep1/index.js
+++ b/app/components/Views/ManualBackupStep1/index.js
@@ -85,7 +85,10 @@ const ManualBackupStep1 = ({ route, navigation, appTheme }) => {
setView(CONFIRM_PASSWORD);
}
} catch (e) {
- Logger.error('Error trying to recover SRP from keyring-controller');
+ const srpRecoveryError = new Error(
+ 'Error trying to recover SRP from keyring-controller',
+ );
+ Logger.error(srpRecoveryError);
setView(CONFIRM_PASSWORD);
}
}
diff --git a/app/components/Views/RestoreWallet/WalletRestored.tsx b/app/components/Views/RestoreWallet/WalletRestored.tsx
index d481bc883e7..76191904ac8 100644
--- a/app/components/Views/RestoreWallet/WalletRestored.tsx
+++ b/app/components/Views/RestoreWallet/WalletRestored.tsx
@@ -54,7 +54,8 @@ const WalletRestored = () => {
// Log to provide insights into bug research.
// Check https://github.com/MetaMask/mobile-planning/issues/1507
if (typeof selectedAddress !== 'string') {
- Logger.error('Wallet restore error', 'selectedAddress is not a string');
+ const walletRestoreError = new Error('Wallet restore error');
+ Logger.error(walletRestoreError, 'selectedAddress is not a string');
}
await Authentication.appTriggeredAuth({ selectedAddress });
navigation.replace(Routes.ONBOARDING.HOME_NAV);
diff --git a/app/components/Views/Root/index.js b/app/components/Views/Root/index.js
index c6c45605e7c..2e4d0e2574f 100644
--- a/app/components/Views/Root/index.js
+++ b/app/components/Views/Root/index.js
@@ -42,7 +42,8 @@ export default class Root extends PureComponent {
constructor(props) {
super(props);
if (props.foxCode === '') {
- Logger.error('WARN - foxCode is an empty string');
+ const foxCodeError = new Error('WARN - foxCode is an empty string');
+ Logger.error(foxCodeError);
}
SecureKeychain.init(props.foxCode);
// Init EntryScriptWeb3 asynchronously on the background
diff --git a/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js b/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js
index ff401b97c26..c1c2bdbea82 100644
--- a/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js
+++ b/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js
@@ -473,7 +473,7 @@ class NetworkSettings extends PureComponent {
try {
endpointChainId = await jsonRpcRequest(rpcUrl, 'eth_chainId');
} catch (err) {
- Logger.error('Failed to fetch the chainId from the endpoint.', err);
+ Logger.error(err, 'Failed to fetch the chainId from the endpoint.');
providerError = err;
}
@@ -488,10 +488,10 @@ class NetworkSettings extends PureComponent {
try {
endpointChainId = new BigNumber(endpointChainId, 16).toString(10);
} catch (err) {
- Logger.error(
- 'Failed to convert endpoint chain ID to decimal',
+ Logger.error(err, {
endpointChainId,
- );
+ message: 'Failed to convert endpoint chain ID to decimal',
+ });
}
}
diff --git a/app/components/Views/Settings/SecuritySettings/__snapshots__/SecuritySettings.test.tsx.snap b/app/components/Views/Settings/SecuritySettings/__snapshots__/SecuritySettings.test.tsx.snap
index 14607b59c9b..478224ef980 100644
--- a/app/components/Views/Settings/SecuritySettings/__snapshots__/SecuritySettings.test.tsx.snap
+++ b/app/components/Views/Settings/SecuritySettings/__snapshots__/SecuritySettings.test.tsx.snap
@@ -741,7 +741,7 @@ exports[`SecuritySettings should render correctly 1`] = `
}
}
>
- Privacy preserving - no data is shared with third parties. Available on Arbitrum, Avalanche, BNB chain, Ethereum Mainnet, Optimism, Polygon and Sepolia.
+ Privacy preserving - no data is shared with third parties. Available on Arbitrum, Avalanche, BNB chain, Ethereum Mainnet, Optimism, Polygon, Sepolia and Base.
{
Engine.context.ApprovalController.reject(
id,
- ethErrors.provider.userRejectedRequest(),
+ providerErrors.userRejectedRequest(),
);
this.props.navigation.pop();
this.unmountHandled = true;
diff --git a/app/components/Views/confirmations/SendFlow/Confirm/index.js b/app/components/Views/confirmations/SendFlow/Confirm/index.js
index 5f1e0d4af8b..262d45f96ad 100644
--- a/app/components/Views/confirmations/SendFlow/Confirm/index.js
+++ b/app/components/Views/confirmations/SendFlow/Confirm/index.js
@@ -107,7 +107,7 @@ import { ConfirmViewSelectorsIDs } from '../../../../../../e2e/selectors/SendFlo
import ExtendedKeyringTypes from '../../../../../constants/keyringTypes';
import { getLedgerKeyring } from '../../../../../core/Ledger/Ledger';
import {
- getBlockaidMetricsParams,
+ getBlockaidTransactionMetricsParams,
isBlockaidFeatureEnabled,
} from '../../../../../util/blockaid';
import ppomUtil from '../../../../../lib/ppom/ppom-util';
@@ -302,23 +302,8 @@ class Confirm extends PureComponent {
}
};
- withBlockaidMetricsParams = () => {
- let blockaidParams = {};
-
- const { transaction } = this.props;
- if (
- transaction.id === transaction.currentTransactionSecurityAlertResponse?.id
- ) {
- blockaidParams = getBlockaidMetricsParams(
- transaction.currentTransactionSecurityAlertResponse?.response,
- );
- }
-
- return blockaidParams;
- };
-
updateNavBar = () => {
- const { navigation, route, resetTransaction } = this.props;
+ const { navigation, route, resetTransaction, transaction } = this.props;
const colors = this.context.colors || mockTheme.colors;
navigation.setOptions(
getSendFlowTitle(
@@ -327,6 +312,7 @@ class Confirm extends PureComponent {
route,
colors,
resetTransaction,
+ transaction,
),
);
};
@@ -858,7 +844,7 @@ class Confirm extends PureComponent {
assetType,
{
...this.getAnalyticsParams(),
- ...this.withBlockaidMetricsParams(),
+ ...getBlockaidTransactionMetricsParams(transaction),
},
),
type: 'signTransaction',
@@ -887,7 +873,7 @@ class Confirm extends PureComponent {
MetaMetricsEvents.SEND_TRANSACTION_COMPLETED,
{
...this.getAnalyticsParams(),
- ...this.withBlockaidMetricsParams(),
+ ...getBlockaidTransactionMetricsParams(transaction),
},
);
stopGasPolling();
@@ -1101,7 +1087,7 @@ class Confirm extends PureComponent {
const { transaction } = this.props;
const analyticsParams = {
...this.getAnalyticsParams(),
- ...this.withBlockaidMetricsParams(transaction),
+ ...getBlockaidTransactionMetricsParams(transaction),
external_link_clicked: 'security_alert_support_link',
};
this.props.metrics.trackEvent(
diff --git a/app/components/Views/confirmations/components/ApproveTransactionReview/index.js b/app/components/Views/confirmations/components/ApproveTransactionReview/index.js
index 0609d11c210..940b030cd76 100644
--- a/app/components/Views/confirmations/components/ApproveTransactionReview/index.js
+++ b/app/components/Views/confirmations/components/ApproveTransactionReview/index.js
@@ -51,7 +51,7 @@ import TransactionReviewDetailsCard from '../TransactionReview/TransactionReview
import AppConstants from '../../../../../core/AppConstants';
import { UINT256_HEX_MAX_VALUE } from '../../../../../constants/transaction';
import { WALLET_CONNECT_ORIGIN } from '../../../../../util/walletconnect';
-import { getBlockaidMetricsParams } from '../../../../../util/blockaid';
+import { getBlockaidTransactionMetricsParams } from '../../../../../util/blockaid';
import { withNavigation } from '@react-navigation/compat';
import {
isTestNet,
@@ -510,21 +510,6 @@ class ApproveTransactionReview extends PureComponent {
clearInterval(intervalIdForEstimatedL1Fee);
};
- withBlockaidMetricsParams = () => {
- let blockaidParams = {};
-
- const { transaction } = this.props;
- if (
- transaction.id === transaction.currentTransactionSecurityAlertResponse?.id
- ) {
- blockaidParams = getBlockaidMetricsParams(
- transaction.currentTransactionSecurityAlertResponse?.response,
- );
- }
-
- return blockaidParams;
- };
-
getAnalyticsParams = () => {
try {
const { chainId, transaction, onSetAnalyticsParams } = this.props;
@@ -697,9 +682,10 @@ class ApproveTransactionReview extends PureComponent {
};
onContactUsClicked = () => {
+ const { transaction } = this.props;
const analyticsParams = {
...this.getAnalyticsParams(),
- ...this.withBlockaidMetricsParams(),
+ ...getBlockaidTransactionMetricsParams(transaction),
external_link_clicked: 'security_alert_support_link',
};
this.props.metrics.trackEvent(
@@ -1177,13 +1163,13 @@ class ApproveTransactionReview extends PureComponent {
};
onCancelPress = () => {
- const { onCancel } = this.props;
+ const { onCancel, transaction } = this.props;
onCancel && onCancel();
this.props.metrics.trackEvent(
MetaMetricsEvents.APPROVAL_PERMISSION_UPDATED,
{
...this.getAnalyticsParams(),
- ...this.withBlockaidMetricsParams(),
+ ...getBlockaidTransactionMetricsParams(transaction),
},
);
};
@@ -1200,7 +1186,7 @@ class ApproveTransactionReview extends PureComponent {
MetaMetricsEvents.APPROVAL_PERMISSION_UPDATED,
{
...this.getAnalyticsParams(),
- ...this.withBlockaidMetricsParams(),
+ ...getBlockaidTransactionMetricsParams(this.props.transaction),
},
);
return this.setState({ isReadyToApprove: true });
diff --git a/app/components/Views/confirmations/hooks/useApprovalRequest.test.ts b/app/components/Views/confirmations/hooks/useApprovalRequest.test.ts
index fd0e559cc4c..f7e459f9530 100644
--- a/app/components/Views/confirmations/hooks/useApprovalRequest.test.ts
+++ b/app/components/Views/confirmations/hooks/useApprovalRequest.test.ts
@@ -2,7 +2,7 @@ import { ApprovalControllerState } from '@metamask/approval-controller';
import { useSelector } from 'react-redux';
import useApprovalRequest from './useApprovalRequest';
import Engine from '../../../../core/Engine';
-import { ethErrors } from 'eth-rpc-errors';
+import { providerErrors } from '@metamask/rpc-errors';
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
@@ -143,7 +143,7 @@ describe('useApprovalRequest', () => {
expect(Engine.rejectPendingApproval).toHaveBeenCalledTimes(1);
expect(Engine.rejectPendingApproval).toHaveBeenCalledWith(
APPROVAL_REQUEST.id,
- ethErrors.provider.userRejectedRequest(),
+ providerErrors.userRejectedRequest(),
);
});
diff --git a/app/components/Views/confirmations/hooks/useApprovalRequest.ts b/app/components/Views/confirmations/hooks/useApprovalRequest.ts
index 05957991b86..c65f23ee86d 100644
--- a/app/components/Views/confirmations/hooks/useApprovalRequest.ts
+++ b/app/components/Views/confirmations/hooks/useApprovalRequest.ts
@@ -1,6 +1,6 @@
import Engine from '../../../../core/Engine';
import { useCallback } from 'react';
-import { ethErrors } from 'eth-rpc-errors';
+import { providerErrors } from '@metamask/rpc-errors';
import { useSelector } from 'react-redux';
import { selectPendingApprovals } from '../../../../selectors/approvalController';
import { cloneDeep, isEqual } from 'lodash';
@@ -36,7 +36,7 @@ const useApprovalRequest = () => {
Engine.rejectPendingApproval(
approvalRequest.id,
- ethErrors.provider.userRejectedRequest(),
+ providerErrors.userRejectedRequest(),
);
}, [approvalRequest]);
diff --git a/app/core/AppConstants.ts b/app/core/AppConstants.ts
index 4ec889193f8..f78ec6fc628 100644
--- a/app/core/AppConstants.ts
+++ b/app/core/AppConstants.ts
@@ -24,6 +24,9 @@ export default {
ACTIVE: true,
URL: `${PORTFOLIO_URL}/bridge`,
},
+ STAKE: {
+ URL: `${PORTFOLIO_URL}/stake`,
+ },
CONNEXT: {
HUB_EXCHANGE_CEILING_TOKEN: 69,
MIN_DEPOSIT_ETH: 0.03,
@@ -86,7 +89,7 @@ export default {
MAX_SAFE_CHAIN_ID: 4503599627370476,
URLS: {
TERMS_AND_CONDITIONS: 'https://legal.consensys.io/metamask/terms-of-use/',
- PRIVACY_POLICY: 'https://legal.consensys.io/metamask/privacy-policy/',
+ PRIVACY_POLICY: 'https://consensys.io/privacy-policy',
DATA_RETENTION_UPDATE:
'https://consensys.net/blog/news/consensys-data-retention-update/',
CONNECTIVITY_ISSUES:
diff --git a/app/core/BackupVault/backupVault.ts b/app/core/BackupVault/backupVault.ts
index f5d837385c6..07b316443a4 100644
--- a/app/core/BackupVault/backupVault.ts
+++ b/app/core/BackupVault/backupVault.ts
@@ -45,7 +45,8 @@ export async function backupVault(
options,
);
if (backupResult === false) {
- Logger.error(VAULT_BACKUP_KEY, VAULT_BACKUP_FAILED);
+ const vaultBackupFailedError = new Error(VAULT_BACKUP_KEY);
+ Logger.error(vaultBackupFailedError, VAULT_BACKUP_FAILED);
const response: KeyringBackupResponse = {
success: false,
error: VAULT_BACKUP_FAILED,
@@ -58,7 +59,8 @@ export async function backupVault(
};
return response;
}
- Logger.error(VAULT_BACKUP_KEY, VAULT_BACKUP_FAILED_UNDEFINED);
+ const vaultBackupUndefinedError = new Error(VAULT_BACKUP_KEY);
+ Logger.error(vaultBackupUndefinedError, VAULT_BACKUP_FAILED_UNDEFINED);
const response: KeyringBackupResponse = {
success: false,
error: VAULT_BACKUP_FAILED_UNDEFINED,
@@ -80,7 +82,8 @@ export async function getVaultFromBackup(): Promise {
if (credentials) {
return { success: true, vault: credentials.password };
}
- Logger.error(VAULT_BACKUP_KEY, VAULT_FAILED_TO_GET_VAULT_FROM_BACKUP);
+ const vaultFetchError = new Error(VAULT_BACKUP_KEY);
+ Logger.error(vaultFetchError, VAULT_FAILED_TO_GET_VAULT_FROM_BACKUP);
return { success: false, error: VAULT_FAILED_TO_GET_VAULT_FROM_BACKUP };
}
diff --git a/app/core/DeeplinkManager/ParseManager/handleUniversalLink.ts b/app/core/DeeplinkManager/ParseManager/handleUniversalLink.ts
index c256e3ed300..4e51efc4d56 100644
--- a/app/core/DeeplinkManager/ParseManager/handleUniversalLink.ts
+++ b/app/core/DeeplinkManager/ParseManager/handleUniversalLink.ts
@@ -45,7 +45,7 @@ function handleUniversalLink({
SDKConnect.getInstance()
.bindAndroidSDK()
.catch((err) => {
- Logger.error(`DeepLinkManager failed to connect`, err);
+ Logger.error(err, `DeepLinkManager failed to connect`);
});
return;
}
@@ -62,7 +62,7 @@ function handleUniversalLink({
otherPublicKey: params.pubkey,
sdkConnect: SDKConnect.getInstance(),
}).catch((err: unknown) => {
- Logger.error(`DeepLinkManager failed to connect`, err);
+ Logger.error(err as Error, `DeepLinkManager failed to connect`);
});
}
return true;
diff --git a/app/core/DeeplinkManager/ParseManager/parseDeeplink.test.ts b/app/core/DeeplinkManager/ParseManager/parseDeeplink.test.ts
index 8ee6df7f6b7..9e76d72d2d5 100644
--- a/app/core/DeeplinkManager/ParseManager/parseDeeplink.test.ts
+++ b/app/core/DeeplinkManager/ParseManager/parseDeeplink.test.ts
@@ -14,6 +14,17 @@ jest.mock('./handleDappUrl');
jest.mock('./handleMetaMaskDeeplink');
jest.mock('./handleUniversalLink');
jest.mock('./connectWithWC');
+jest.mock('../../../../locales/i18n', () => ({
+ strings: jest.fn((key) => key),
+}));
+
+const invalidUrls = [
+ 'htp://incorrect-format-url',
+ 'http://',
+ ':invalid-protocol://some-url',
+ '',
+ 'https://?!&%5E#@()*]',
+];
describe('parseDeeplink', () => {
let instance: DeeplinkManager;
@@ -181,4 +192,18 @@ describe('parseDeeplink', () => {
expect(result).toBe(true);
});
+
+ invalidUrls.forEach((url) => {
+ it(`should log an error and alert the user when an invalid URL is passed => url=${url}`, () => {
+ const result = parseDeeplink({
+ deeplinkManager: instance,
+ url,
+ origin: 'testOrigin',
+ browserCallBack: mockBrowserCallBack,
+ onHandled: mockOnHandled,
+ });
+
+ expect(result).toBe(false);
+ });
+ });
});
diff --git a/app/core/DeeplinkManager/ParseManager/parseDeeplink.ts b/app/core/DeeplinkManager/ParseManager/parseDeeplink.ts
index 205e997aebe..5192dc148f6 100644
--- a/app/core/DeeplinkManager/ParseManager/parseDeeplink.ts
+++ b/app/core/DeeplinkManager/ParseManager/parseDeeplink.ts
@@ -8,6 +8,8 @@ import handleDappUrl from './handleDappUrl';
import handleMetaMaskDeeplink from './handleMetaMaskDeeplink';
import handleUniversalLink from './handleUniversalLink';
import connectWithWC from './connectWithWC';
+import { Alert } from 'react-native';
+import { strings } from '../../../../locales/i18n';
function parseDeeplink({
deeplinkManager: instance,
@@ -22,69 +24,85 @@ function parseDeeplink({
browserCallBack?: (url: string) => void;
onHandled?: () => void;
}) {
- const { urlObj, params } = extractURLParams(url);
+ try {
+ const validatedUrl = new URL(url);
+ DevLogger.log('DeepLinkManager:parse validatedUrl', validatedUrl);
- const sdkConnect = SDKConnect.getInstance();
+ const { urlObj, params } = extractURLParams(url);
- const protocol = urlObj.protocol.replace(':', '');
- DevLogger.log(
- `DeepLinkManager:parse sdkInit=${sdkConnect.hasInitialized()} origin=${origin} protocol=${protocol}`,
- url,
- );
+ const sdkConnect = SDKConnect.getInstance();
- const handled = () => (onHandled ? onHandled() : false);
+ const protocol = urlObj.protocol.replace(':', '');
+ DevLogger.log(
+ `DeepLinkManager:parse sdkInit=${sdkConnect.hasInitialized()} origin=${origin} protocol=${protocol}`,
+ url,
+ );
- const wcURL = params?.uri || urlObj.href;
+ const handled = () => (onHandled ? onHandled() : false);
- switch (urlObj.protocol.replace(':', '')) {
- case PROTOCOLS.HTTP:
- case PROTOCOLS.HTTPS:
- handleUniversalLink({
- instance,
- handled,
- urlObj,
- params,
- browserCallBack,
- origin,
- wcURL,
- url,
- });
+ const wcURL = params?.uri || urlObj.href;
- break;
- case PROTOCOLS.WC:
- connectWithWC({ handled, wcURL, origin, params });
- break;
+ switch (urlObj.protocol.replace(':', '')) {
+ case PROTOCOLS.HTTP:
+ case PROTOCOLS.HTTPS:
+ handleUniversalLink({
+ instance,
+ handled,
+ urlObj,
+ params,
+ browserCallBack,
+ origin,
+ wcURL,
+ url,
+ });
- case PROTOCOLS.ETHEREUM:
- handled();
- instance._handleEthereumUrl(url, origin).catch((err) => {
- Logger.error(err, 'Error handling ethereum url');
- });
- break;
+ break;
+ case PROTOCOLS.WC:
+ connectWithWC({ handled, wcURL, origin, params });
+ break;
- // Specific to the browser screen
- // For ex. navigate to a specific dapp
- case PROTOCOLS.DAPP:
- handleDappUrl({ instance, handled, urlObj, browserCallBack });
- break;
+ case PROTOCOLS.ETHEREUM:
+ handled();
+ instance._handleEthereumUrl(url, origin).catch((err) => {
+ Logger.error(err, 'Error handling ethereum url');
+ });
+ break;
- // Specific to the MetaMask app
- // For ex. go to settings
- case PROTOCOLS.METAMASK:
- handleMetaMaskDeeplink({
- instance,
- handled,
- wcURL,
- origin,
- params,
- url,
- });
- break;
- default:
- return false;
- }
+ // Specific to the browser screen
+ // For ex. navigate to a specific dapp
+ case PROTOCOLS.DAPP:
+ handleDappUrl({ instance, handled, urlObj, browserCallBack });
+ break;
+
+ // Specific to the MetaMask app
+ // For ex. go to settings
+ case PROTOCOLS.METAMASK:
+ handleMetaMaskDeeplink({
+ instance,
+ handled,
+ wcURL,
+ origin,
+ params,
+ url,
+ });
+ break;
+ default:
+ return false;
+ }
+
+ return true;
+ } catch (error) {
+ Logger.error(
+ error as Error,
+ 'DeepLinkManager:parse error parsing deeplink',
+ );
- return true;
+ if (error) {
+ Alert.alert(strings('deeplink.invalid'), `Invalid URL: ${url}`);
+ }
+
+ return false;
+ }
}
export default parseDeeplink;
diff --git a/app/core/Encryptor.js b/app/core/Encryptor.js
deleted file mode 100644
index 1eda492b402..00000000000
--- a/app/core/Encryptor.js
+++ /dev/null
@@ -1,77 +0,0 @@
-import { NativeModules } from 'react-native';
-const Aes = NativeModules.Aes;
-const AesForked = NativeModules.AesForked;
-
-/**
- * Class that exposes two public methods: Encrypt and Decrypt
- * This is used by the KeyringController to encrypt / decrypt the state
- * which contains sensitive seed words and addresses
- */
-export default class Encryptor {
- key = null;
-
- _generateSalt(byteCount = 32) {
- const view = new Uint8Array(byteCount);
- global.crypto.getRandomValues(view);
- // eslint-disable-next-line no-undef
- const b64encoded = btoa(String.fromCharCode.apply(null, view));
- return b64encoded;
- }
-
- _generateKey = (password, salt, lib) =>
- lib === 'original'
- ? Aes.pbkdf2(password, salt, 5000, 256)
- : AesForked.pbkdf2(password, salt);
-
- _keyFromPassword = (password, salt, lib) =>
- this._generateKey(password, salt, lib);
-
- _encryptWithKey = async (text, keyBase64) => {
- const iv = await Aes.randomKey(16);
- return Aes.encrypt(text, keyBase64, iv).then((cipher) => ({ cipher, iv }));
- };
-
- _decryptWithKey = (encryptedData, key, lib) =>
- lib === 'original'
- ? Aes.decrypt(encryptedData.cipher, key, encryptedData.iv)
- : AesForked.decrypt(encryptedData.cipher, key, encryptedData.iv);
-
- /**
- * Encrypts a JS object using a password (and AES encryption with native libraries)
- *
- * @param {string} password - Password used for encryption
- * @param {object} object - Data object to encrypt
- * @returns - Promise resolving to stringified data
- */
- encrypt = async (password, object) => {
- const salt = this._generateSalt(16);
- const key = await this._keyFromPassword(password, salt, 'original');
- const result = await this._encryptWithKey(JSON.stringify(object), key);
- result.salt = salt;
- result.lib = 'original';
- return JSON.stringify(result);
- };
-
- /**
- * Decrypts an encrypted JS object (encryptedString)
- * using a password (and AES decryption with native libraries)
- *
- * @param {string} password - Password used for decryption
- * @param {string} encryptedString - String to decrypt
- * @returns - Promise resolving to decrypted data object
- */
- decrypt = async (password, encryptedString) => {
- const encryptedData = JSON.parse(encryptedString);
- const key = await this._keyFromPassword(
- password,
- encryptedData.salt,
- encryptedData.lib,
- );
- const data = await this._decryptWithKey(
- encryptedData,
- key,
- encryptedData.lib,
- );
- return JSON.parse(data);
- };
-}
diff --git a/app/core/Encryptor/Encryptor.test.ts b/app/core/Encryptor/Encryptor.test.ts
new file mode 100644
index 00000000000..c66a2222e4a
--- /dev/null
+++ b/app/core/Encryptor/Encryptor.test.ts
@@ -0,0 +1,243 @@
+import { NativeModules } from 'react-native';
+import { Encryptor } from './Encryptor';
+import {
+ ENCRYPTION_LIBRARY,
+ DERIVATION_PARAMS,
+ KeyDerivationIteration,
+} from './constants';
+
+const Aes = NativeModules.Aes;
+const AesForked = NativeModules.AesForked;
+
+describe('Encryptor', () => {
+ let encryptor: Encryptor;
+
+ beforeEach(() => {
+ encryptor = new Encryptor({ derivationParams: DERIVATION_PARAMS });
+ });
+
+ describe('constructor', () => {
+ it('throws an error if the provided iterations do not meet the minimum required', () => {
+ expect(
+ () =>
+ new Encryptor({
+ derivationParams: {
+ algorithm: 'PBKDF2',
+ params: {
+ iterations: 100,
+ },
+ },
+ }),
+ ).toThrowError(
+ `Invalid key derivation iterations: 100. Recommended number of iterations is ${KeyDerivationIteration.Default}. Minimum required is ${KeyDerivationIteration.Minimum}.`,
+ );
+ });
+ });
+
+ describe('encrypt', () => {
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should encrypt an object correctly', async () => {
+ const password = 'testPassword';
+ const objectToEncrypt = { key: 'value' };
+
+ const encryptedString = await encryptor.encrypt(
+ password,
+ objectToEncrypt,
+ );
+ const encryptedObject = JSON.parse(encryptedString);
+
+ expect(encryptedObject).toHaveProperty('cipher');
+ expect(encryptedObject).toHaveProperty('iv');
+ expect(encryptedObject).toHaveProperty('salt');
+ expect(encryptedObject).toHaveProperty('lib', 'original');
+ });
+ });
+
+ describe('decrypt', () => {
+ let decryptAesSpy: jest.SpyInstance,
+ pbkdf2AesSpy: jest.SpyInstance,
+ decryptAesForkedSpy: jest.SpyInstance,
+ pbkdf2AesForkedSpy: jest.SpyInstance;
+
+ beforeEach(() => {
+ decryptAesSpy = jest
+ .spyOn(Aes, 'decrypt')
+ .mockResolvedValue('{"mockData": "mockedPlainText"}');
+ pbkdf2AesSpy = jest
+ .spyOn(Aes, 'pbkdf2')
+ .mockResolvedValue('mockedAesKey');
+ decryptAesForkedSpy = jest
+ .spyOn(AesForked, 'decrypt')
+ .mockResolvedValue('{"mockData": "mockedPlainText"}');
+ pbkdf2AesForkedSpy = jest
+ .spyOn(AesForked, 'pbkdf2')
+ .mockResolvedValue('mockedAesForkedKey');
+ });
+
+ afterEach(() => {
+ decryptAesSpy.mockRestore();
+ pbkdf2AesSpy.mockRestore();
+ decryptAesForkedSpy.mockRestore();
+ pbkdf2AesForkedSpy.mockRestore();
+ });
+
+ it.each([
+ {
+ lib: ENCRYPTION_LIBRARY.original,
+ expectedKey: 'mockedAesKey',
+ expectedPBKDF2Args: ['testPassword', 'mockedSalt', 600000, 256],
+ description:
+ 'with original library and default iterations number for key generation',
+ keyMetadata: DERIVATION_PARAMS,
+ },
+ {
+ lib: ENCRYPTION_LIBRARY.original,
+ expectedKey: 'mockedAesKey',
+ expectedPBKDF2Args: ['testPassword', 'mockedSalt', 5000, 256],
+ description:
+ 'with original library and old iterations number for key generation',
+ },
+ {
+ lib: 'random-lib', // Assuming not using "original" should lead to AesForked
+ expectedKey: 'mockedAesForkedKey',
+ expectedPBKDF2Args: ['testPassword', 'mockedSalt'],
+ description:
+ 'with library different to "original" and default iterations number for key generation',
+ keyMetadata: DERIVATION_PARAMS,
+ },
+ {
+ lib: 'random-lib', // Assuming not using "original" should lead to AesForked
+ expectedKey: 'mockedAesForkedKey',
+ expectedPBKDF2Args: ['testPassword', 'mockedSalt'],
+ description:
+ 'with library different to "original" and old iterations number for key generation',
+ },
+ ])(
+ 'decrypts a string correctly $description',
+ async ({ lib, expectedKey, expectedPBKDF2Args, keyMetadata }) => {
+ const password = 'testPassword';
+ const mockVault = {
+ cipher: 'mockedCipher',
+ iv: 'mockedIV',
+ salt: 'mockedSalt',
+ lib,
+ };
+
+ const decryptedObject = await encryptor.decrypt(
+ password,
+ JSON.stringify(
+ keyMetadata !== undefined
+ ? { ...mockVault, keyMetadata }
+ : mockVault,
+ ),
+ );
+
+ expect(decryptedObject).toEqual(expect.any(Object));
+ expect(
+ lib === ENCRYPTION_LIBRARY.original
+ ? decryptAesSpy
+ : decryptAesForkedSpy,
+ ).toHaveBeenCalledWith(mockVault.cipher, expectedKey, mockVault.iv);
+ expect(
+ lib === ENCRYPTION_LIBRARY.original
+ ? pbkdf2AesSpy
+ : pbkdf2AesForkedSpy,
+ ).toHaveBeenCalledWith(...expectedPBKDF2Args);
+ },
+ );
+ });
+
+ describe('isVaultUpdated', () => {
+ it('returns true if a vault has the correct format', () => {
+ expect(
+ encryptor.isVaultUpdated(
+ JSON.stringify({
+ cipher: 'mockedCipher',
+ iv: 'mockedIV',
+ salt: 'mockedSalt',
+ lib: 'original',
+ keyMetadata: DERIVATION_PARAMS,
+ }),
+ ),
+ ).toBe(true);
+ });
+
+ it('returns false if a vault has the incorrect format', () => {
+ expect(
+ encryptor.isVaultUpdated(
+ JSON.stringify({
+ cipher: 'mockedCipher',
+ iv: 'mockedIV',
+ salt: 'mockedSalt',
+ lib: 'original',
+ }),
+ ),
+ ).toBe(false);
+ });
+ });
+
+ describe('updateVault', () => {
+ let encryptSpy: jest.SpyInstance, decryptSpy: jest.SpyInstance;
+ const expectedKeyMetadata = DERIVATION_PARAMS;
+
+ beforeEach(() => {
+ encryptSpy = jest
+ .spyOn(Aes, 'encrypt')
+ .mockResolvedValue(() => Promise.resolve('mockedCipher'));
+ decryptSpy = jest
+ .spyOn(Aes, 'decrypt')
+ .mockResolvedValue('{"mockData": "mockedPlainText"}');
+ });
+
+ afterEach(() => {
+ encryptSpy.mockRestore();
+ decryptSpy.mockRestore();
+ });
+
+ it('updates a vault correctly if keyMetadata is not present', async () => {
+ const mockVault = {
+ cipher: 'mockedCipher',
+ iv: 'mockedIV',
+ salt: 'mockedSalt',
+ lib: 'original',
+ };
+
+ const updatedVault = await encryptor.updateVault(
+ JSON.stringify(mockVault),
+ 'mockPassword',
+ );
+
+ const vault = JSON.parse(updatedVault);
+
+ expect(encryptSpy).toBeCalledTimes(1);
+ expect(decryptSpy).toBeCalledTimes(1);
+ expect(vault).toHaveProperty('keyMetadata');
+ expect(vault.keyMetadata).toStrictEqual(expectedKeyMetadata);
+ });
+
+ it('does not update a vault if algorithm is PBKDF2 and the number of iterations is 900000', async () => {
+ const mockVault = {
+ cipher: 'mockedCipher',
+ iv: 'mockedIV',
+ salt: 'mockedSalt',
+ lib: 'original',
+ keyMetadata: DERIVATION_PARAMS,
+ };
+
+ const updatedVault = await encryptor.updateVault(
+ JSON.stringify(mockVault),
+ 'mockPassword',
+ );
+
+ const vault = JSON.parse(updatedVault);
+
+ expect(encryptSpy).toBeCalledTimes(0);
+ expect(decryptSpy).toBeCalledTimes(0);
+ expect(vault).toHaveProperty('keyMetadata');
+ expect(vault.keyMetadata).toStrictEqual(expectedKeyMetadata);
+ });
+ });
+});
diff --git a/app/core/Encryptor/Encryptor.ts b/app/core/Encryptor/Encryptor.ts
new file mode 100644
index 00000000000..575c424da14
--- /dev/null
+++ b/app/core/Encryptor/Encryptor.ts
@@ -0,0 +1,268 @@
+import { NativeModules } from 'react-native';
+import { hasProperty, isPlainObject, Json } from '@metamask/utils';
+import {
+ SALT_BYTES_COUNT,
+ SHA256_DIGEST_LENGTH,
+ ENCRYPTION_LIBRARY,
+ KeyDerivationIteration,
+} from './constants';
+import type {
+ EncryptionResult,
+ KeyDerivationOptions,
+ GenericEncryptor,
+} from './types';
+
+const Aes = NativeModules.Aes;
+const AesForked = NativeModules.AesForked;
+
+/**
+ * Checks if the provided object is a `KeyDerivationOptions`.
+ *
+ * @param derivationOptions - The object to check.
+ * @returns Whether or not the object is a `KeyDerivationOptions`.
+ */
+const isKeyDerivationOptions = (
+ derivationOptions: unknown,
+): derivationOptions is KeyDerivationOptions =>
+ isPlainObject(derivationOptions) &&
+ hasProperty(derivationOptions, 'algorithm') &&
+ hasProperty(derivationOptions, 'params');
+
+/**
+ * The Encryptor class provides methods for encrypting and
+ * decrypting data objects using AES encryption with native libraries.
+ * It supports generating a salt, deriving an encryption key from a
+ * password and salt, and performing the encryption and decryption processes.
+ */
+class Encryptor implements GenericEncryptor {
+ /**
+ * The key derivation parameters used for encryption and decryption operations.
+ * These parameters include the algorithm and its specific parameters, for example, number of iterations for key derivation.
+ * They are set during the construction of the Encryptor instance and used for generating encryption keys.
+ * @property derivationParams - The key derivation options.
+ */
+ private derivationParams: KeyDerivationOptions;
+
+ /**
+ * Constructs an instance of the Encryptor class.
+ * @param params - An object containing key derivation parameters.
+ * @param params.derivationParams - The key derivation options to use for encryption and decryption operations.
+ * @throws Error if the provided iterations in `derivationParams` do not meet the minimum required.
+ */
+ constructor({
+ derivationParams,
+ }: {
+ derivationParams: KeyDerivationOptions;
+ }) {
+ this.checkMinimalRequiredIterations(derivationParams.params.iterations);
+ this.derivationParams = derivationParams;
+ }
+
+ /**
+ * Throws an error if the provided number of iterations does not meet the minimum required for key derivation.
+ * This method ensures that the key derivation process is secure by enforcing a minimum number of iterations.
+ * @param iterations - The number of iterations to check.
+ * @throws Error if the number of iterations is less than the minimum required.
+ */
+ private checkMinimalRequiredIterations = (iterations: number): void => {
+ if (!this.isMinimalRequiredIterationsMet(iterations)) {
+ throw new Error(
+ `Invalid key derivation iterations: ${iterations}. Recommended number of iterations is ${KeyDerivationIteration.Default}. Minimum required is ${KeyDerivationIteration.Minimum}.`,
+ );
+ }
+ };
+
+ /**
+ * Checks if the provided number of iterations meets the minimum required for key derivation.
+ * @param iterations - The number of iterations to check.
+ * @returns A boolean indicating whether the minimum required iterations are met.
+ */
+ private isMinimalRequiredIterationsMet = (iterations: number): boolean =>
+ iterations >= KeyDerivationIteration.Minimum;
+
+ /**
+ * Generates a random base64-encoded salt string.
+ * @param byteCount - The number of bytes for the salt. Defaults to `constant.SALT_BYTES_COUNT`.
+ * @returns The base64-encoded salt string.
+ */
+ private generateSalt = (saltBytesCount = SALT_BYTES_COUNT) => {
+ const salt = new Uint8Array(saltBytesCount);
+ // @ts-expect-error - globalThis is not recognized by TypeScript
+ global.crypto.getRandomValues(salt);
+ return salt;
+ };
+
+ /**
+ * Encodes a byte array to a base64 string.
+ * @param byteArray The byte array to encode.
+ * @returns The base64-encoded string.
+ */
+ private encodeByteArrayToBase64 = (byteArray: Uint8Array): string =>
+ btoa(String.fromCharCode.apply(null, Array.from(byteArray)));
+
+ /**
+ * Wrapper method for key generation from a password.
+ * @param params.password - The password used for key derivation.
+ * @param params.salt - The salt used for key derivation.
+ * @param params.lib - The library to use ('original' or forked version).
+ * @returns A promise that resolves to the derived encryption key.
+ */
+ private generateKeyFromPassword = ({
+ password,
+ salt,
+ iterations,
+ lib,
+ }: {
+ password: string;
+ salt: string;
+ iterations: number;
+ lib: string;
+ }): Promise =>
+ lib === ENCRYPTION_LIBRARY.original
+ ? Aes.pbkdf2(password, salt, iterations, SHA256_DIGEST_LENGTH)
+ : AesForked.pbkdf2(password, salt);
+
+ /**
+ * Encrypts a text string using the provided key.
+ * @param params.text - The text to encrypt.
+ * @param params.keyBase64 - The base64-encoded encryption key.
+ * @returns A promise that resolves to an object containing the cipher text and initialization vector (IV).
+ */
+ private encryptWithKey = async ({
+ text,
+ keyBase64,
+ }: {
+ text: string;
+ keyBase64: string;
+ }): Promise => {
+ const iv = await Aes.randomKey(16);
+ return Aes.encrypt(text, keyBase64, iv).then((cipher: string) => ({
+ cipher,
+ iv,
+ }));
+ };
+
+ /**
+ * Decrypts encrypted data using the provided key.
+ * @param params.encryptedData - The encrypted data object containing the cipher text and IV.
+ * @param params.key - The decryption key.
+ * @param params.lib - The library to use ('original' or forked version) for decryption.
+ * @returns A promise that resolves to the decrypted text.
+ */
+ private decryptWithKey = ({
+ encryptedData,
+ key,
+ lib,
+ }: {
+ encryptedData: { cipher: string; iv: string };
+ key: string;
+ lib: string;
+ }): Promise =>
+ lib === ENCRYPTION_LIBRARY.original
+ ? Aes.decrypt(encryptedData.cipher, key, encryptedData.iv)
+ : AesForked.decrypt(encryptedData.cipher, key, encryptedData.iv);
+
+ /**
+ * Asynchronously encrypts a given object using AES encryption.
+ * The encryption process involves generating a salt, deriving a key from the provided password and salt,
+ * and then using the key to encrypt the object. The result includes the encrypted data, the salt used,
+ * and the library version ('original' in this case).
+ *
+ * @param params.password - The password used for generating the encryption key.
+ * @param params.object - The data object to encrypt. It can be of any type, as it will be stringified during the encryption process.
+ * @returns A promise that resolves to a string. The string is a JSON representation of an object containing the encrypted data, the salt used for encryption, and the library version.
+ */
+ encrypt = async (password: string, object: Json): Promise => {
+ const salt = this.generateSalt(16);
+ const base64salt = this.encodeByteArrayToBase64(salt);
+ const key = await this.generateKeyFromPassword({
+ password,
+ salt: base64salt,
+ iterations: this.derivationParams.params.iterations,
+ lib: ENCRYPTION_LIBRARY.original,
+ });
+ const result = await this.encryptWithKey({
+ text: JSON.stringify(object),
+ keyBase64: key,
+ });
+ result.salt = base64salt;
+ result.lib = ENCRYPTION_LIBRARY.original;
+ result.keyMetadata = this.derivationParams;
+ return JSON.stringify(result);
+ };
+
+ /**
+ * Decrypts an encrypted JS object (encryptedString)
+ * using a password (and AES decryption with native libraries)
+ *
+ * @param password - Password used for decryption
+ * @param encryptedString - String to decrypt
+ * @returns - Promise resolving to decrypted data object
+ */
+ decrypt = async (
+ password: string,
+ encryptedString: string,
+ ): Promise => {
+ const payload = JSON.parse(encryptedString);
+ const key = await this.generateKeyFromPassword({
+ password,
+ salt: payload.salt,
+ iterations:
+ payload.keyMetadata?.params.iterations || KeyDerivationIteration.Legacy,
+ lib: payload.lib,
+ });
+ const data = await this.decryptWithKey({
+ encryptedData: payload,
+ key,
+ lib: payload.lib,
+ });
+
+ return JSON.parse(data);
+ };
+
+ /**
+ * Checks if the provided vault is an updated encryption format.
+ *
+ * @param vault - The vault to check.
+ * @param targetDerivationParams - The options to use for key derivation.
+ * @returns Whether or not the vault is an updated encryption format.
+ */
+ isVaultUpdated = (
+ vault: string,
+ targetDerivationParams = this.derivationParams,
+ ): boolean => {
+ const { keyMetadata } = JSON.parse(vault);
+ return (
+ isKeyDerivationOptions(keyMetadata) &&
+ keyMetadata.algorithm === targetDerivationParams.algorithm &&
+ keyMetadata.params.iterations === targetDerivationParams.params.iterations
+ );
+ };
+
+ /**
+ * Updates the provided vault, re-encrypting
+ * data with a safer algorithm if one is available.
+ *
+ * If the provided vault is already using the latest available encryption method,
+ * it is returned as is.
+ *
+ * @param vault - The vault to update.
+ * @param password - The password to use for encryption.
+ * @param targetDerivationParams - The options to use for key derivation.
+ * @returns A promise resolving to the updated vault.
+ */
+ updateVault = async (
+ vault: string,
+ password: string,
+ targetDerivationParams = this.derivationParams,
+ ): Promise => {
+ if (this.isVaultUpdated(vault, targetDerivationParams)) {
+ return vault;
+ }
+
+ return this.encrypt(password, await this.decrypt(password, vault));
+ };
+}
+
+// eslint-disable-next-line import/prefer-default-export
+export { Encryptor };
diff --git a/app/core/Encryptor/constants.ts b/app/core/Encryptor/constants.ts
new file mode 100644
index 00000000000..edf9563e5c7
--- /dev/null
+++ b/app/core/Encryptor/constants.ts
@@ -0,0 +1,20 @@
+import { KeyDerivationOptions } from './types';
+
+export const SALT_BYTES_COUNT = 32;
+export const SHA256_DIGEST_LENGTH = 256;
+export const ENCRYPTION_LIBRARY = {
+ original: 'original',
+};
+
+export enum KeyDerivationIteration {
+ Legacy = 5_000,
+ Minimum = 600_000,
+ Default = 900_000,
+}
+
+export const DERIVATION_PARAMS: KeyDerivationOptions = {
+ algorithm: 'PBKDF2',
+ params: {
+ iterations: KeyDerivationIteration.Minimum,
+ },
+};
diff --git a/app/core/Encryptor/index.ts b/app/core/Encryptor/index.ts
new file mode 100644
index 00000000000..e9f4b61263d
--- /dev/null
+++ b/app/core/Encryptor/index.ts
@@ -0,0 +1,4 @@
+import { Encryptor } from './Encryptor';
+import { DERIVATION_PARAMS } from './constants';
+
+export { Encryptor, DERIVATION_PARAMS };
diff --git a/app/core/Encryptor/types.ts b/app/core/Encryptor/types.ts
new file mode 100644
index 00000000000..fa441927842
--- /dev/null
+++ b/app/core/Encryptor/types.ts
@@ -0,0 +1,76 @@
+import type { Json } from '@metamask/utils';
+
+/**
+ * Parameters used for key derivation.
+ * @interface KeyParams
+ * @property iterations - The number of iterations to use in the key derivation process.
+ */
+export interface KeyParams {
+ iterations: number;
+}
+
+/**
+ * Options for key derivation, specifying the algorithm and parameters to use.
+ * @interface KeyDerivationOptions
+ * @property algorithm - The name of the algorithm to use for key derivation.
+ * @property params - The parameters to use with the specified algorithm.
+ */
+export interface KeyDerivationOptions {
+ algorithm: string;
+ params: KeyParams;
+}
+
+/**
+ * The result of an encryption operation.
+ * @interface EncryptionResult
+ * @property cipher - The encrypted data.
+ * @property iv - The initialization vector used in the encryption process.
+ * @property [salt] - The salt used in the encryption process, if applicable.
+ * @property [lib] - The library or algorithm used for encryption, if applicable.
+ * @property [keyMetadata] - Metadata about the key derivation, if key derivation was used.
+ */
+export interface EncryptionResult {
+ cipher: string;
+ iv: string;
+ salt?: string;
+ lib?: string;
+ keyMetadata?: KeyDerivationOptions;
+}
+
+/**
+ * Defines the structure for a generic encryption utility.
+ * This utility provides methods for encrypting and decrypting objects
+ * using a specified password. It may also include an optional method
+ * for checking if an encrypted vault is up to date with the desired
+ * encryption algorithm and parameters.
+ */
+export interface GenericEncryptor {
+ /**
+ * Encrypts the given object with the given password.
+ *
+ * @param password - The password to encrypt with.
+ * @param object - The object to encrypt.
+ * @returns The encrypted string.
+ */
+ encrypt: (password: string, object: Json) => Promise;
+ /**
+ * Decrypts the given encrypted string with the given password.
+ *
+ * @param password - The password to decrypt with.
+ * @param encryptedString - The encrypted string to decrypt.
+ * @returns The decrypted object.
+ */
+ decrypt: (password: string, encryptedString: string) => Promise;
+ /**
+ * Optional vault migration helper. Checks if the provided vault is up to date
+ * with the desired encryption algorithm.
+ *
+ * @param vault - The encrypted string to check.
+ * @param targetDerivationParams - The desired target derivation params.
+ * @returns The updated encrypted string.
+ */
+ isVaultUpdated?: (
+ vault: string,
+ targetDerivationParams?: KeyDerivationOptions,
+ ) => boolean;
+}
diff --git a/app/core/Engine.ts b/app/core/Engine.ts
index 31ee549a2ae..0dd009d4c91 100644
--- a/app/core/Engine.ts
+++ b/app/core/Engine.ts
@@ -51,7 +51,7 @@ import {
} from '@metamask/network-controller';
import {
PhishingController,
- PhishingState,
+ PhishingControllerState,
} from '@metamask/phishing-controller';
import {
PreferencesController,
@@ -121,7 +121,7 @@ import {
LoggingControllerActions,
} from '@metamask/logging-controller';
import LedgerKeyring from '@consensys/ledgerhq-metamask-keyring';
-import Encryptor from './Encryptor';
+import { Encryptor, DERIVATION_PARAMS } from './Encryptor';
import {
isMainnetByChainId,
getDecimalChainId,
@@ -171,7 +171,7 @@ import {
import { hasProperty, Json } from '@metamask/utils';
// TODO: Export this type from the package directly
import { SwapsState } from '@metamask/swaps-controller/dist/SwapsController';
-import { ethErrors } from 'eth-rpc-errors';
+import { providerErrors } from '@metamask/rpc-errors';
import { PPOM, ppomInit } from '../lib/ppom/PPOMView';
import RNFSStorageBackend from '../lib/ppom/ppom-storage-backend';
@@ -190,7 +190,9 @@ import {
const NON_EMPTY = 'NON_EMPTY';
-const encryptor = new Encryptor();
+const encryptor = new Encryptor({
+ derivationParams: DERIVATION_PARAMS,
+});
let currentChainId: any;
///: BEGIN:ONLY_INCLUDE_IF(snaps)
@@ -260,7 +262,7 @@ export interface EngineState {
KeyringController: KeyringControllerState;
NetworkController: NetworkState;
PreferencesController: PreferencesState;
- PhishingController: PhishingState;
+ PhishingController: PhishingControllerState;
TokenBalancesController: TokenBalancesState;
TokenRatesController: TokenRatesState;
TransactionController: TransactionState;
@@ -424,6 +426,7 @@ class Engine {
listener,
),
chainId: networkController.state.providerConfig.chainId,
+ //@ts-expect-error This will be fixed when assets-controller is on v16
getNetworkClientById:
networkController.getNetworkClientById.bind(networkController),
});
@@ -514,6 +517,7 @@ class Engine {
AppConstants.TOKEN_LIST_STATE_CHANGE_EVENT,
listener,
),
+ //@ts-expect-error This will be fixed when assets-controller is on v16
getNetworkClientById:
networkController.getNetworkClientById.bind(networkController),
chainId: networkController.state.providerConfig.chainId,
@@ -570,10 +574,15 @@ class Engine {
// @ts-expect-error TODO: Resolve/patch mismatch between base-controller versions. Before: never, never. Now: string, string, which expects 3rd and 4th args to be informed for restrictedControllerMessengers
messenger: this.controllerMessenger.getRestricted<
'GasFeeController',
- never,
+ | 'NetworkController:getNetworkClientById'
+ | 'NetworkController:getEIP1559Compatibility',
'NetworkController:stateChange'
>({
name: 'GasFeeController',
+ allowedActions: [
+ 'NetworkController:getNetworkClientById',
+ 'NetworkController:getEIP1559Compatibility',
+ ],
allowedEvents: ['NetworkController:stateChange'],
}),
getProvider: () =>
@@ -582,7 +591,6 @@ class Engine {
onNetworkStateChange: (listener) =>
this.controllerMessenger.subscribe(
AppConstants.NETWORK_STATE_CHANGE_EVENT,
- //@ts-expect-error GasFeeController needs to be updated to v7 for this error disappears
listener,
),
getCurrentNetworkEIP1559Compatibility: async () =>
@@ -603,7 +611,12 @@ class Engine {
'https://gas-api.metaswap.codefi.network/networks//suggestedGasFees',
});
- const phishingController = new PhishingController();
+ const phishingController = new PhishingController({
+ messenger: this.controllerMessenger.getRestricted({
+ name: 'PhishingController',
+ allowedActions: [],
+ }),
+ });
phishingController.maybeUpdateState();
const qrKeyringBuilder = () => new QRHardwareKeyring();
@@ -1538,7 +1551,7 @@ class Engine {
rejectPendingApproval(
id: string,
- reason: Error = ethErrors.provider.userRejectedRequest(),
+ reason: Error = providerErrors.userRejectedRequest(),
opts: { ignoreMissing?: boolean; logErrors?: boolean } = {},
) {
const { ApprovalController } = this.context;
diff --git a/app/core/EngineService/EngineService.ts b/app/core/EngineService/EngineService.ts
index 2b60f9c4280..24d2e64d352 100644
--- a/app/core/EngineService/EngineService.ts
+++ b/app/core/EngineService/EngineService.ts
@@ -50,7 +50,10 @@ class EngineService {
name: 'NetworkController',
key: AppConstants.NETWORK_STATE_CHANGE_EVENT,
},
- { name: 'PhishingController' },
+ {
+ name: 'PhishingController',
+ key: `${engine.context.PhishingController.name}:maybeUpdateState`,
+ },
{ name: 'PreferencesController' },
{ name: 'TokenBalancesController' },
{ name: 'TokenRatesController' },
diff --git a/app/core/Permissions/index.ts b/app/core/Permissions/index.ts
index 14d68e87632..13ebf1f85e3 100644
--- a/app/core/Permissions/index.ts
+++ b/app/core/Permissions/index.ts
@@ -1,4 +1,4 @@
-import { errorCodes as rpcErrorCodes } from 'eth-rpc-errors';
+import { errorCodes as rpcErrorCodes } from '@metamask/rpc-errors';
import { orderBy } from 'lodash';
import { RestrictedMethods, CaveatTypes } from './constants';
import ImportedEngine from '../Engine';
diff --git a/app/core/RPCMethods/RPCMethodMiddleware.test.ts b/app/core/RPCMethods/RPCMethodMiddleware.test.ts
index c52e825a976..70480f0f7ca 100644
--- a/app/core/RPCMethods/RPCMethodMiddleware.test.ts
+++ b/app/core/RPCMethods/RPCMethodMiddleware.test.ts
@@ -8,7 +8,7 @@ import {
} from 'json-rpc-engine';
import type { Transaction } from '@metamask/transaction-controller';
import type { ProviderConfig } from '@metamask/network-controller';
-import { ethErrors } from 'eth-json-rpc-errors';
+import { providerErrors, rpcErrors } from '@metamask/rpc-errors';
import Engine from '../Engine';
import { store } from '../../store';
import { getPermittedAccounts } from '../Permissions';
@@ -572,14 +572,16 @@ describe('getRpcMethodMiddleware', () => {
method: 'eth_sendTransaction',
params: [mockTransactionParameters],
};
- const expectedError = ethErrors.provider.userRejectedRequest();
+ const expectedError = providerErrors.userRejectedRequest();
const response = await callMiddleware({ middleware, request });
- expect((response as JsonRpcFailure).error).toStrictEqual({
- code: expectedError.code,
- message: expectedError.message,
- });
+ expect((response as JsonRpcFailure).error.code).toBe(
+ expectedError.code,
+ );
+ expect((response as JsonRpcFailure).error.message).toBe(
+ expectedError.message,
+ );
});
it('returns a JSON-RPC error if the site does not have permission to use the referenced account', async () => {
@@ -609,16 +611,18 @@ describe('getRpcMethodMiddleware', () => {
method: 'eth_sendTransaction',
params: [mockTransactionParameters],
};
- const expectedError = ethErrors.rpc.invalidParams({
+ const expectedError = rpcErrors.invalidParams({
message: `Invalid parameters: must provide an Ethereum address.`,
});
const response = await callMiddleware({ middleware, request });
- expect((response as JsonRpcFailure).error).toStrictEqual({
- code: expectedError.code,
- message: expectedError.message,
- });
+ expect((response as JsonRpcFailure).error.code).toBe(
+ expectedError.code,
+ );
+ expect((response as JsonRpcFailure).error.message).toBe(
+ expectedError.message,
+ );
});
});
@@ -680,16 +684,18 @@ describe('getRpcMethodMiddleware', () => {
method: 'eth_sendTransaction',
params: [mockTransactionParameters],
};
- const expectedError = ethErrors.rpc.invalidParams({
+ const expectedError = rpcErrors.invalidParams({
message: `Invalid parameters: must provide an Ethereum address.`,
});
const response = await callMiddleware({ middleware, request });
- expect((response as JsonRpcFailure).error).toStrictEqual({
- code: expectedError.code,
- message: expectedError.message,
- });
+ expect((response as JsonRpcFailure).error.code).toBe(
+ expectedError.code,
+ );
+ expect((response as JsonRpcFailure).error.message).toBe(
+ expectedError.message,
+ );
});
});
@@ -753,16 +759,18 @@ describe('getRpcMethodMiddleware', () => {
method: 'eth_sendTransaction',
params: [mockTransactionParameters],
};
- const expectedError = ethErrors.rpc.invalidParams({
+ const expectedError = rpcErrors.invalidParams({
message: `Invalid parameters: must provide an Ethereum address.`,
});
const response = await callMiddleware({ middleware, request });
- expect((response as JsonRpcFailure).error).toStrictEqual({
- code: expectedError.code,
- message: expectedError.message,
- });
+ expect((response as JsonRpcFailure).error.code).toBe(
+ expectedError.code,
+ );
+ expect((response as JsonRpcFailure).error.message).toBe(
+ expectedError.message,
+ );
});
});
@@ -837,7 +845,7 @@ describe('getRpcMethodMiddleware', () => {
method: 'eth_sendTransaction',
params: [mockTransactionParameters],
};
- const expectedError = ethErrors.rpc.internal('Failed to add transaction');
+ const expectedError = rpcErrors.internal('Failed to add transaction');
const response = await callMiddleware({ middleware, request });
@@ -866,9 +874,7 @@ describe('getRpcMethodMiddleware', () => {
method: 'eth_sendTransaction',
params: [mockTransactionParameters],
};
- const expectedError = ethErrors.rpc.internal(
- 'Failed to process transaction',
- );
+ const expectedError = rpcErrors.internal('Failed to process transaction');
const response = await callMiddleware({ middleware, request });
@@ -941,9 +947,7 @@ describe('getRpcMethodMiddleware', () => {
method: 'personal_ecRecover',
params: [helloWorldMessage],
};
- const expectedError = ethErrors.rpc.internal(
- 'Missing signature parameter',
- );
+ const expectedError = rpcErrors.internal('Missing signature parameter');
const response = await callMiddleware({ middleware, request });
@@ -964,7 +968,7 @@ describe('getRpcMethodMiddleware', () => {
method: 'personal_ecRecover',
params: [undefined, helloWorldSignature],
};
- const expectedError = ethErrors.rpc.internal('Missing data parameter');
+ const expectedError = rpcErrors.internal('Missing data parameter');
const response = await callMiddleware({ middleware, request });
diff --git a/app/core/RPCMethods/RPCMethodMiddleware.ts b/app/core/RPCMethods/RPCMethodMiddleware.ts
index 7b7585bf86e..fa15ec599df 100644
--- a/app/core/RPCMethods/RPCMethodMiddleware.ts
+++ b/app/core/RPCMethods/RPCMethodMiddleware.ts
@@ -4,7 +4,7 @@ import {
createAsyncMiddleware,
JsonRpcEngineCallbackError,
} from 'json-rpc-engine';
-import { ethErrors } from 'eth-json-rpc-errors';
+import { providerErrors, rpcErrors } from '@metamask/rpc-errors';
import {
EndFlowOptions,
StartFlowOptions,
@@ -120,25 +120,16 @@ export const checkActiveAccountAndChainId = async ({
hostname,
formattedAddress,
});
- const validHostname = hostname?.replace(
- AppConstants.MM_SDK.SDK_REMOTE_ORIGIN,
- '',
- );
const permissionsController = (
Engine.context as { PermissionController: PermissionController }
).PermissionController;
DevLogger.log(
- `checkActiveAccountAndChainId channelId=${channelId} isWalletConnect=${isWalletConnect} validHostname=${validHostname}`,
+ `checkActiveAccountAndChainId channelId=${channelId} isWalletConnect=${isWalletConnect} hostname=${hostname}`,
permissionsController.state,
);
- let accounts: string[] = [];
- if (isWalletConnect) {
- accounts = await getPermittedAccounts(validHostname);
- } else {
- accounts = (await getPermittedAccounts(channelId ?? validHostname)) ?? [];
- }
+ const accounts = (await getPermittedAccounts(channelId ?? hostname)) ?? [];
const normalizedAccounts = accounts.map(safeToChecksumAddress);
@@ -146,14 +137,14 @@ export const checkActiveAccountAndChainId = async ({
isInvalidAccount = true;
if (accounts.length > 0) {
// Permissions issue --- requesting incorrect address
- throw ethErrors.rpc.invalidParams({
+ throw rpcErrors.invalidParams({
message: `Invalid parameters: must provide a permitted Ethereum address.`,
});
}
}
if (isInvalidAccount) {
- throw ethErrors.rpc.invalidParams({
+ throw rpcErrors.invalidParams({
message: `Invalid parameters: must provide an Ethereum address.`,
});
}
@@ -190,7 +181,7 @@ export const checkActiveAccountAndChainId = async ({
Alert.alert(
`Active chainId is ${activeChainId} but received ${chainIdRequest}`,
);
- throw ethErrors.rpc.invalidParams({
+ throw rpcErrors.invalidParams({
message: `Invalid parameters: active chainId is different than the one provided.`,
});
}
@@ -208,8 +199,23 @@ const generateRawSignature = async ({
chainId,
channelId,
getSource,
+ isWalletConnect,
checkTabActive,
-}: any) => {
+}: {
+ version: string;
+ req: any;
+ hostname: string;
+ url: { current: string };
+ title: { current: string };
+ icon: { current: string | undefined };
+ analytics: { [key: string]: string | boolean };
+ chainId: number;
+ isMMSDK: boolean;
+ channelId?: string;
+ getSource: () => string;
+ isWalletConnect: boolean;
+ checkTabActive: any;
+}) => {
const { SignatureController } = Engine.context;
const pageMeta = {
@@ -230,7 +236,7 @@ const generateRawSignature = async ({
channelId,
address: req.params[0],
chainId,
- isWalletConnect: false,
+ isWalletConnect,
});
const rawSig = await SignatureController.newUnsignedTypedMessage(
@@ -279,22 +285,18 @@ export const getRpcMethodMiddleware = ({
injectHomePageScripts,
// For analytics
analytics,
-}: RPCMethodsMiddleParameters) =>
+}: RPCMethodsMiddleParameters) => {
+ // Make sure to always have the correct origin
+ hostname = hostname.replace(AppConstants.MM_SDK.SDK_REMOTE_ORIGIN, '');
+ DevLogger.log(
+ `getRpcMethodMiddleware hostname=${hostname} channelId=${channelId}`,
+ );
// all user facing RPC calls not implemented by the provider
- createAsyncMiddleware(async (req: any, res: any, next: any) => {
+ return createAsyncMiddleware(async (req: any, res: any, next: any) => {
// Used by eth_accounts and eth_coinbase RPCs.
const getEthAccounts = async () => {
- let accounts: string[] = [];
- const validHostname = hostname.replace(
- AppConstants.MM_SDK.SDK_REMOTE_ORIGIN,
- '',
- );
- if (isMMSDK) {
- accounts =
- (await getPermittedAccounts(channelId ?? validHostname)) ?? [];
- } else {
- accounts = await getPermittedAccounts(validHostname);
- }
+ const accounts: string[] =
+ (await getPermittedAccounts(channelId ?? hostname)) ?? [];
res.result = accounts;
};
@@ -302,7 +304,7 @@ export const getRpcMethodMiddleware = ({
if (!tabId) return true;
const { browser } = store.getState();
if (tabId !== browser.activeTab)
- throw ethErrors.provider.userRejectedRequest();
+ throw providerErrors.userRejectedRequest();
};
const getSource = () => {
@@ -315,7 +317,7 @@ export const getRpcMethodMiddleware = ({
const startApprovalFlow = (opts: StartFlowOptions) => {
checkTabActive();
Engine.context.ApprovalController.clear(
- ethErrors.provider.userRejectedRequest(),
+ providerErrors.userRejectedRequest(),
);
return Engine.context.ApprovalController.startFlow(opts);
@@ -333,7 +335,7 @@ export const getRpcMethodMiddleware = ({
const requestUserApproval = async ({ type = '', requestData = {} }) => {
checkTabActive();
await Engine.context.ApprovalController.clear(
- ethErrors.provider.userRejectedRequest(),
+ providerErrors.userRejectedRequest(),
);
const responseData = await Engine.context.ApprovalController.add({
@@ -373,24 +375,16 @@ export const getRpcMethodMiddleware = ({
getPermissionsForOrigin:
Engine.context.PermissionController.getPermissions.bind(
Engine.context.PermissionController,
- hostname,
+ channelId ?? hostname,
),
},
);
handle?.catch((error) => {
- Logger.error('Failed to get permissions', error);
+ Logger.error(error as Error, 'Failed to get permissions');
});
}),
wallet_requestPermissions: async () =>
new Promise((resolve, reject) => {
- let requestId: string | undefined;
- if (isMMSDK) {
- // Extract id from hostname
- requestId = hostname.replace(
- AppConstants.MM_SDK.SDK_REMOTE_ORIGIN,
- '',
- );
- }
requestPermissionsHandler
.implementation(
req,
@@ -406,9 +400,8 @@ export const getRpcMethodMiddleware = ({
requestPermissionsForOrigin:
Engine.context.PermissionController.requestPermissions.bind(
Engine.context.PermissionController,
- { origin: requestId ?? hostname },
+ { origin: channelId ?? hostname },
req.params[0],
- { id: requestId },
),
},
)
@@ -471,12 +464,8 @@ export const getRpcMethodMiddleware = ({
},
eth_requestAccounts: async () => {
const { params } = req;
- const validHostname = hostname.replace(
- AppConstants.MM_SDK.SDK_REMOTE_ORIGIN,
- '',
- );
const permittedAccounts = await getPermittedAccounts(
- channelId ?? validHostname,
+ channelId ?? hostname,
);
if (!params?.force && permittedAccounts.length) {
@@ -484,36 +473,18 @@ export const getRpcMethodMiddleware = ({
} else {
try {
checkTabActive();
- const currentPerm =
- Engine.context.PermissionController.getPermissions(
- channelId ?? validHostname,
- );
- const accountPerm =
- Engine.context.PermissionController.getPermission(
- channelId ?? validHostname,
- 'eth_accounts',
- );
- DevLogger.log(
- `eth_requestAccounts currentPerm ${channelId ?? validHostname}`,
- currentPerm,
- accountPerm,
- );
await Engine.context.PermissionController.requestPermissions(
- { origin: channelId ?? validHostname },
+ { origin: channelId ?? hostname },
{ eth_accounts: {} },
- {
- id: channelId ?? validHostname,
- preserveExistingPermissions: true,
- },
);
DevLogger.log(`eth_requestAccounts requestPermissions`);
- const acc = await getPermittedAccounts(hostname);
+ const acc = await getPermittedAccounts(channelId ?? hostname);
DevLogger.log(`eth_requestAccounts getPermittedAccounts`, acc);
res.result = acc;
} catch (error) {
DevLogger.log(`eth_requestAccounts error`, error);
if (error) {
- throw ethErrors.provider.userRejectedRequest(
+ throw providerErrors.userRejectedRequest(
'User denied account authorization.',
);
}
@@ -526,16 +497,6 @@ export const getRpcMethodMiddleware = ({
eth_sendTransaction: async () => {
checkTabActive();
- if (isMMSDK) {
- // Append origin to the request so it can be parsed in UI TransactionHeader
- DevLogger.log(
- `SDK Transaction detected --- custom hostname -- ${hostname} --> ${
- AppConstants.MM_SDK.SDK_REMOTE_ORIGIN + url.current
- }`,
- );
- hostname = AppConstants.MM_SDK.SDK_REMOTE_ORIGIN + url.current;
- }
-
return RPCMethods.eth_sendTransaction({
hostname,
req,
@@ -561,7 +522,7 @@ export const getRpcMethodMiddleware = ({
eth_signTransaction: async () => {
// This is implemented later in our middleware stack – specifically, in
// eth-json-rpc-middleware – but our UI does not support it.
- throw ethErrors.rpc.methodNotSupported();
+ throw rpcErrors.methodNotSupported();
},
eth_sign: async () => {
const { SignatureController, PreferencesController } = Engine.context;
@@ -569,7 +530,7 @@ export const getRpcMethodMiddleware = ({
const { eth_sign } = disabledRpcMethodPreferences;
if (!eth_sign) {
- throw ethErrors.rpc.methodNotFound(
+ throw rpcErrors.methodNotFound(
'eth_sign has been disabled. You must enable it in the advanced settings',
);
}
@@ -605,7 +566,7 @@ export const getRpcMethodMiddleware = ({
res.result = rawSig;
} else {
res.result = AppConstants.ETH_SIGN_ERROR;
- throw ethErrors.rpc.invalidParams(AppConstants.ETH_SIGN_ERROR);
+ throw rpcErrors.invalidParams(AppConstants.ETH_SIGN_ERROR);
}
},
@@ -643,6 +604,7 @@ export const getRpcMethodMiddleware = ({
isWalletConnect,
});
+ DevLogger.log(`personal_sign`, params, pageMeta, hostname);
PPOMUtil.validateRequest(req);
const rawSig = await SignatureController.newUnsignedPersonalMessage({
@@ -782,7 +744,7 @@ export const getRpcMethodMiddleware = ({
resolve();
},
onScanError: (e: { toString: () => any }) => {
- throw ethErrors.rpc.internal(e.toString());
+ throw rpcErrors.internal(e.toString());
},
});
}),
@@ -793,7 +755,7 @@ export const getRpcMethodMiddleware = ({
metamask_removeFavorite: async () => {
checkTabActive();
if (!isHomepage()) {
- throw ethErrors.provider.unauthorized('Forbidden.');
+ throw providerErrors.unauthorized('Forbidden.');
}
const { bookmarks } = store.getState();
@@ -836,7 +798,7 @@ export const getRpcMethodMiddleware = ({
metamask_showTutorial: async () => {
checkTabActive();
if (!isHomepage()) {
- throw ethErrors.provider.unauthorized('Forbidden.');
+ throw providerErrors.unauthorized('Forbidden.');
}
wizardScrollAdjusted.current = false;
@@ -850,7 +812,7 @@ export const getRpcMethodMiddleware = ({
metamask_showAutocomplete: async () => {
checkTabActive();
if (!isHomepage()) {
- throw ethErrors.provider.unauthorized('Forbidden.');
+ throw providerErrors.unauthorized('Forbidden.');
}
fromHomepage.current = true;
toggleUrlModal(true);
@@ -875,16 +837,7 @@ export const getRpcMethodMiddleware = ({
*/
metamask_getProviderState: async () => {
let accounts: string[] = [];
- const validHostname = hostname.replace(
- AppConstants.MM_SDK.SDK_REMOTE_ORIGIN,
- '',
- );
- if (isMMSDK) {
- accounts =
- (await getPermittedAccounts(channelId ?? validHostname)) ?? [];
- } else {
- accounts = await getPermittedAccounts(validHostname);
- }
+ accounts = (await getPermittedAccounts(channelId ?? hostname)) ?? [];
res.result = {
...getProviderState(),
accounts,
@@ -956,4 +909,5 @@ export const getRpcMethodMiddleware = ({
throw e;
}
});
+};
export default getRpcMethodMiddleware;
diff --git a/app/core/RPCMethods/eth_sendTransaction.ts b/app/core/RPCMethods/eth_sendTransaction.ts
index 154e4c8deb6..5569709a162 100644
--- a/app/core/RPCMethods/eth_sendTransaction.ts
+++ b/app/core/RPCMethods/eth_sendTransaction.ts
@@ -3,7 +3,7 @@ import {
TransactionController,
WalletDevice,
} from '@metamask/transaction-controller';
-import { ethErrors } from 'eth-json-rpc-errors';
+import { rpcErrors } from '@metamask/rpc-errors';
import ppomUtil from '../../lib/ppom/ppom-util';
/**
@@ -78,13 +78,13 @@ async function eth_sendTransaction({
!Array.isArray(req.params) &&
!(isObject(req.params) && hasProperty(req.params, 0))
) {
- throw ethErrors.rpc.invalidParams({
+ throw rpcErrors.invalidParams({
message: `Invalid parameters: expected an array`,
});
}
const transactionParameters = req.params[0];
if (!isObject(transactionParameters)) {
- throw ethErrors.rpc.invalidParams({
+ throw rpcErrors.invalidParams({
message: `Invalid parameters: expected the first parameter to be an object`,
});
}
diff --git a/app/core/RPCMethods/wallet_addEthereumChain.js b/app/core/RPCMethods/wallet_addEthereumChain.js
index 36f7e5cc1a5..8177a94b80e 100644
--- a/app/core/RPCMethods/wallet_addEthereumChain.js
+++ b/app/core/RPCMethods/wallet_addEthereumChain.js
@@ -3,7 +3,7 @@ import validUrl from 'valid-url';
import { ChainId, isSafeChainId } from '@metamask/controller-utils';
import { jsonRpcRequest } from '../../util/jsonRpcRequest';
import Engine from '../Engine';
-import { ethErrors } from 'eth-json-rpc-errors';
+import { providerErrors, rpcErrors } from '@metamask/rpc-errors';
import {
getDecimalChainId,
isPrefixedFormattedHexString,
@@ -37,7 +37,7 @@ const wallet_addEthereumChain = async ({
Engine.context;
if (!req.params?.[0] || typeof req.params[0] !== 'object') {
- throw ethErrors.rpc.invalidParams({
+ throw rpcErrors.invalidParams({
message: `Expected single, object parameter. Received:\n${JSON.stringify(
req.params,
)}`,
@@ -65,7 +65,7 @@ const wallet_addEthereumChain = async ({
const extraKeys = Object.keys(params).filter((key) => !allowedKeys[key]);
if (extraKeys.length) {
- throw ethErrors.rpc.invalidParams(
+ throw rpcErrors.invalidParams(
`Received unexpected keys on object parameter. Unsupported keys:\n${extraKeys}`,
);
}
@@ -87,13 +87,13 @@ const wallet_addEthereumChain = async ({
: null;
if (!firstValidRPCUrl) {
- throw ethErrors.rpc.invalidParams(
+ throw rpcErrors.invalidParams(
`Expected an array with at least one valid string HTTPS url 'rpcUrls', Received:\n${rpcUrls}`,
);
}
if (blockExplorerUrls !== null && !firstValidBlockExplorerUrl) {
- throw ethErrors.rpc.invalidParams(
+ throw rpcErrors.invalidParams(
`Expected null or array with at least one valid string HTTPS URL 'blockExplorerUrl'. Received: ${blockExplorerUrls}`,
);
}
@@ -101,21 +101,19 @@ const wallet_addEthereumChain = async ({
const _chainId = typeof chainId === 'string' && chainId.toLowerCase();
if (!isPrefixedFormattedHexString(_chainId)) {
- throw ethErrors.rpc.invalidParams(
+ throw rpcErrors.invalidParams(
`Expected 0x-prefixed, unpadded, non-zero hexadecimal string 'chainId'. Received:\n${chainId}`,
);
}
if (!isSafeChainId(_chainId)) {
- throw ethErrors.rpc.invalidParams(
+ throw rpcErrors.invalidParams(
`Invalid chain ID "${_chainId}": numerical value greater than max safe value. Received:\n${chainId}`,
);
}
if (Object.values(ChainId).find((value) => value === _chainId)) {
- throw ethErrors.rpc.invalidParams(
- `May not specify default MetaMask chain.`,
- );
+ throw rpcErrors.invalidParams(`May not specify default MetaMask chain.`);
}
const networkConfigurations = selectNetworkConfigurations(store.getState());
@@ -154,7 +152,7 @@ const wallet_addEthereumChain = async ({
MetaMetricsEvents.NETWORK_REQUEST_REJECTED,
analyticsParams,
);
- throw ethErrors.provider.userRejectedRequest();
+ throw providerErrors.userRejectedRequest();
}
CurrencyRateController.setNativeCurrency(networkConfiguration.ticker);
@@ -174,21 +172,21 @@ const wallet_addEthereumChain = async ({
try {
endpointChainId = await jsonRpcRequest(firstValidRPCUrl, 'eth_chainId');
} catch (err) {
- throw ethErrors.rpc.internal({
+ throw rpcErrors.internal({
message: `Request for method 'eth_chainId on ${firstValidRPCUrl} failed`,
data: { networkErr: err },
});
}
if (_chainId !== endpointChainId) {
- throw ethErrors.rpc.invalidParams({
+ throw rpcErrors.invalidParams({
message: `Chain ID returned by RPC URL ${firstValidRPCUrl} does not match ${_chainId}`,
data: { chainId: endpointChainId },
});
}
if (typeof rawChainName !== 'string' || !rawChainName) {
- throw ethErrors.rpc.invalidParams({
+ throw rpcErrors.invalidParams({
message: `Expected non-empty string 'chainName'. Received:\n${rawChainName}`,
});
}
@@ -197,18 +195,18 @@ const wallet_addEthereumChain = async ({
if (nativeCurrency !== null) {
if (typeof nativeCurrency !== 'object' || Array.isArray(nativeCurrency)) {
- throw ethErrors.rpc.invalidParams({
+ throw rpcErrors.invalidParams({
message: `Expected null or object 'nativeCurrency'. Received:\n${nativeCurrency}`,
});
}
if (nativeCurrency.decimals !== EVM_NATIVE_TOKEN_DECIMALS) {
- throw ethErrors.rpc.invalidParams({
+ throw rpcErrors.invalidParams({
message: `Expected the number 18 for 'nativeCurrency.decimals' when 'nativeCurrency' is provided. Received: ${nativeCurrency.decimals}`,
});
}
if (!nativeCurrency.symbol || typeof nativeCurrency.symbol !== 'string') {
- throw ethErrors.rpc.invalidParams({
+ throw rpcErrors.invalidParams({
message: `Expected a string 'nativeCurrency.symbol'. Received: ${nativeCurrency.symbol}`,
});
}
@@ -216,7 +214,7 @@ const wallet_addEthereumChain = async ({
const ticker = nativeCurrency?.symbol || 'ETH';
if (typeof ticker !== 'string' || ticker.length < 2 || ticker.length > 6) {
- throw ethErrors.rpc.invalidParams({
+ throw rpcErrors.invalidParams({
message: `Expected 2-6 character string 'nativeCurrency.symbol'. Received:\n${ticker}`,
});
}
@@ -251,7 +249,7 @@ const wallet_addEthereumChain = async ({
);
// Remove all existing approvals, including other add network requests.
- ApprovalController.clear(ethErrors.provider.userRejectedRequest());
+ ApprovalController.clear(providerErrors.userRejectedRequest());
// If existing approval request was an add network request, wait for
// it to be rejected and for the corresponding approval flow to be ended.
@@ -270,7 +268,7 @@ const wallet_addEthereumChain = async ({
MetaMetricsEvents.NETWORK_REQUEST_REJECTED,
analyticsParamsAdd,
);
- throw ethErrors.provider.userRejectedRequest();
+ throw providerErrors.userRejectedRequest();
}
const networkConfigurationId =
await NetworkController.upsertNetworkConfiguration(
diff --git a/app/core/RPCMethods/wallet_addEthereumChain.test.js b/app/core/RPCMethods/wallet_addEthereumChain.test.js
index 6d2f418b716..62e0c60a76a 100644
--- a/app/core/RPCMethods/wallet_addEthereumChain.test.js
+++ b/app/core/RPCMethods/wallet_addEthereumChain.test.js
@@ -1,5 +1,5 @@
import { InteractionManager } from 'react-native';
-import { ethErrors } from 'eth-json-rpc-errors';
+import { providerErrors } from '@metamask/rpc-errors';
import wallet_addEthereumChain from './wallet_addEthereumChain';
import Engine from '../Engine';
@@ -284,7 +284,7 @@ describe('RPC Method - wallet_addEthereumChain', () => {
...otherOptions,
requestUserApproval: jest.fn(() => Promise.reject()),
}),
- ).rejects.toThrow(ethErrors.provider.userRejectedRequest());
+ ).rejects.toThrow(providerErrors.userRejectedRequest());
expect(otherOptions.startApprovalFlow).toBeCalledTimes(1);
expect(otherOptions.endApprovalFlow).toBeCalledTimes(1);
diff --git a/app/core/RPCMethods/wallet_switchEthereumChain.js b/app/core/RPCMethods/wallet_switchEthereumChain.js
index 6247d17b1ae..9b8d91f927a 100644
--- a/app/core/RPCMethods/wallet_switchEthereumChain.js
+++ b/app/core/RPCMethods/wallet_switchEthereumChain.js
@@ -1,5 +1,5 @@
import Engine from '../Engine';
-import { ethErrors } from 'eth-json-rpc-errors';
+import { providerErrors, rpcErrors } from '@metamask/rpc-errors';
import {
getDecimalChainId,
getDefaultNetworkByChainId,
@@ -23,7 +23,7 @@ const wallet_switchEthereumChain = async ({
const params = req.params?.[0];
if (!params || typeof params !== 'object') {
- throw ethErrors.rpc.invalidParams({
+ throw rpcErrors.invalidParams({
message: `Expected single, object parameter. Received:\n${JSON.stringify(
req.params,
)}`,
@@ -38,7 +38,7 @@ const wallet_switchEthereumChain = async ({
const extraKeys = Object.keys(params).filter((key) => !allowedKeys[key]);
if (extraKeys.length) {
- throw ethErrors.rpc.invalidParams(
+ throw rpcErrors.invalidParams(
`Received unexpected keys on object parameter. Unsupported keys:\n${extraKeys}`,
);
}
@@ -46,13 +46,13 @@ const wallet_switchEthereumChain = async ({
const _chainId = typeof chainId === 'string' && chainId.toLowerCase();
if (!isPrefixedFormattedHexString(_chainId)) {
- throw ethErrors.rpc.invalidParams(
+ throw rpcErrors.invalidParams(
`Expected 0x-prefixed, unpadded, non-zero hexadecimal string 'chainId'. Received:\n${chainId}`,
);
}
if (!isSafeChainId(_chainId)) {
- throw ethErrors.rpc.invalidParams(
+ throw rpcErrors.invalidParams(
`Invalid chain ID "${_chainId}": numerical value greater than max safe value. Received:\n${chainId}`,
);
}
@@ -125,7 +125,7 @@ const wallet_switchEthereumChain = async ({
return;
}
- throw ethErrors.provider.custom({
+ throw providerErrors.custom({
code: 4902, // To-be-standardized "unrecognized chain ID" error
message: `Unrecognized chain ID "${_chainId}". Try adding the chain using wallet_addEthereumChain first.`,
});
diff --git a/app/core/SDKConnect/AndroidSDK/AndroidService.ts b/app/core/SDKConnect/AndroidSDK/AndroidService.ts
index 6f267d0a5bf..61bf3983802 100644
--- a/app/core/SDKConnect/AndroidSDK/AndroidService.ts
+++ b/app/core/SDKConnect/AndroidSDK/AndroidService.ts
@@ -256,7 +256,6 @@ export default class AndroidService extends EventEmitter2 {
return permissionsController.requestPermissions(
{ origin: channelId },
{ eth_accounts: {} },
- { id: channelId },
);
}
diff --git a/app/core/SDKConnect/Connection/EventListenersHandlers/handleClientsReady.ts b/app/core/SDKConnect/Connection/EventListenersHandlers/handleClientsReady.ts
index 930a4cfe088..d16e6fd7c1e 100644
--- a/app/core/SDKConnect/Connection/EventListenersHandlers/handleClientsReady.ts
+++ b/app/core/SDKConnect/Connection/EventListenersHandlers/handleClientsReady.ts
@@ -32,7 +32,7 @@ function handleClientsReady({
updateOriginatorInfos,
approveHost,
onError: (error) => {
- Logger.error(error, '');
+ Logger.error(error as Error, '');
instance.setLoading(false);
diff --git a/app/core/SDKConnect/ConnectionManagement/reconnect.ts b/app/core/SDKConnect/ConnectionManagement/reconnect.ts
index d83a244896b..5353c20a8a3 100644
--- a/app/core/SDKConnect/ConnectionManagement/reconnect.ts
+++ b/app/core/SDKConnect/ConnectionManagement/reconnect.ts
@@ -91,11 +91,11 @@ async function reconnect({
// instance.removeChannel(channelId, true);
// }
- // instance condition should not happen keeping it for debug purpose.
- console.warn(`Priotity to deeplink - overwrite previous connection`);
- instance.removeChannel({ channelId, sendTerminate: true });
+ // issue can happen during dev because bundle takes too long to load via metro.
+ // should not happen but keeping it for reference / debug purpose.
+ console.warn(`BUNDLE WARNING: Already connecting --- Priotity to deeplink`);
+ // instance.removeChannel({ channelId, sendTerminate: true });
}
-
if (!instance.state.connections[channelId]) {
interruptReason = 'no connection';
}
@@ -130,7 +130,9 @@ async function reconnect({
}
}
- DevLogger.log(`SDKConnect::reconnect - starting reconnection`);
+ DevLogger.log(
+ `SDKConnect::reconnect - starting reconnection channel=${channelId}`,
+ );
const connection = instance.state.connections[channelId];
instance.state.connecting[channelId] = true;
diff --git a/app/core/SDKConnect/SDKDeeplinkProtocol/DeeplinkProtocolService.ts b/app/core/SDKConnect/SDKDeeplinkProtocol/DeeplinkProtocolService.ts
index 1e29ecc2810..f506581c178 100644
--- a/app/core/SDKConnect/SDKDeeplinkProtocol/DeeplinkProtocolService.ts
+++ b/app/core/SDKConnect/SDKDeeplinkProtocol/DeeplinkProtocolService.ts
@@ -258,7 +258,7 @@ export default class DeeplinkProtocolService {
await Linking.openURL(deeplink);
} catch (error) {
Logger.error(
- error,
+ error as Error,
`DeeplinkProtocolService::openDeeplink error opening deeplink`,
);
}
@@ -277,7 +277,6 @@ export default class DeeplinkProtocolService {
return permissionsController.requestPermissions(
{ origin: channelId },
{ eth_accounts: {} },
- { id: channelId },
);
}
@@ -290,10 +289,10 @@ export default class DeeplinkProtocolService {
request?: string;
}) {
if (!params.originatorInfo) {
- Logger.error(
+ const deepLinkError = new Error(
'DeeplinkProtocolService::handleConnection no originatorInfo',
- params,
);
+ Logger.error(deepLinkError, params);
return;
}
diff --git a/app/core/SDKConnect/handlers/checkPermissions.ts b/app/core/SDKConnect/handlers/checkPermissions.ts
index 05b9e4781fe..6bb083befae 100644
--- a/app/core/SDKConnect/handlers/checkPermissions.ts
+++ b/app/core/SDKConnect/handlers/checkPermissions.ts
@@ -43,20 +43,22 @@ export const checkPermissions = async ({
channelId: connection.channelId,
context: 'checkPermission',
});
- const accountPermission = permissionsController.getPermission(
- connection.channelId,
- 'eth_accounts',
- );
DevLogger.log(
`checkPermissions approved=${approved} approvalPromise=${
connection.approvalPromise !== undefined ? 'exists' : 'undefined'
}`,
- accountPermission,
);
if (approved) {
- connection.approvalPromise = undefined;
+ return true;
+ }
+
+ DevLogger.log(
+ `checkPermissions channelWasActiveRecently=${channelWasActiveRecently} OTPExpirationDuration=${OTPExpirationDuration}`,
+ );
+
+ if (channelWasActiveRecently) {
return true;
}
@@ -70,7 +72,7 @@ export const checkPermissions = async ({
DevLogger.log(`checkPermissions match`, match);
// Wait for result and clean the promise afterwards.
- // Only wait for approval is modal currently displayed
+ // Only wait for approval if modal currently displayed
if (currentRouteName === Routes.SHEET.ACCOUNT_CONNECT) {
// Make sure the root is displayed
connection.navigation?.navigate(Routes.SHEET.ACCOUNT_CONNECT);
@@ -80,55 +82,38 @@ export const checkPermissions = async ({
`checkPermissions approvalPromise exists completed -- allowed`,
allowed,
);
- connection.approvalPromise = undefined;
// Add delay for backgroundBridge to complete setup
await wait(300);
return allowed;
}
- DevLogger.log(`checkPermissions approvalPromise exists -- SKIP`);
- // Otherwise cleanup existing permissions and revalidate
- // permissionsController.revokeAllPermissions(connection.channelId);
+ console.warn(`checkPermissions approvalPromise exists -- SKIP`);
}
if (!connection.initialConnection && AppConstants.DEEPLINKS.ORIGIN_DEEPLINK) {
connection.revalidate({ channelId: connection.channelId });
}
- DevLogger.log(
- `checkPermissions channelWasActiveRecently=${channelWasActiveRecently} OTPExpirationDuration=${OTPExpirationDuration}`,
- accountPermission,
- );
- if (channelWasActiveRecently) {
- return true;
- }
-
try {
- const origin = connection.channelId;
- if (accountPermission) {
- DevLogger.log(
- `checkPermissions accountPermission exists but not active recently -- REVOKE + ASK AGAIN`,
- );
- // Revoke and ask again
- permissionsController.revokePermission(
- connection.channelId,
- 'eth_accounts',
+ const accountPermission = permissionsController.getPermission(
+ connection.channelId,
+ 'eth_accounts',
+ );
+ if (!accountPermission) {
+ connection.approvalPromise = permissionsController.requestPermissions(
+ { origin: connection.channelId },
+ { eth_accounts: {} },
+ {
+ preserveExistingPermissions: false,
+ },
);
}
- DevLogger.log(`checkPermissions Opening requestPermissions for ${origin}`);
- connection.approvalPromise = permissionsController.requestPermissions(
- { origin },
- { eth_accounts: {} },
- { id: connection.channelId, preserveExistingPermissions: true },
- );
-
await connection.approvalPromise;
// Clear previous permissions if already approved.
connection.revalidate({ channelId: connection.channelId });
- connection.approvalPromise = undefined;
return true;
} catch (err) {
- DevLogger.log(`checkPermissions error`, err);
+ console.warn(`checkPermissions error`, err);
connection.approvalPromise = undefined;
throw err;
}
diff --git a/app/core/SDKConnect/handlers/handleConnectionReady.ts b/app/core/SDKConnect/handlers/handleConnectionReady.ts
index d9bc19bac9f..a268c932235 100644
--- a/app/core/SDKConnect/handlers/handleConnectionReady.ts
+++ b/app/core/SDKConnect/handlers/handleConnectionReady.ts
@@ -7,7 +7,7 @@ import DevLogger from '../utils/DevLogger';
import checkPermissions from './checkPermissions';
import handleSendMessage from './handleSendMessage';
-import { ethErrors } from 'eth-rpc-errors';
+import { providerErrors } from '@metamask/rpc-errors';
import Engine from '../../Engine';
import { approveHostProps } from '../SDKConnect';
import generateOTP from '../utils/generateOTP.util';
@@ -48,7 +48,7 @@ export const handleConnectionReady = async ({
if (approvalController.get(connection.channelId)) {
approvalController.reject(
connection.channelId,
- ethErrors.provider.userRejectedRequest(),
+ providerErrors.userRejectedRequest(),
);
}
@@ -125,7 +125,7 @@ export const handleConnectionReady = async ({
// cleaning previous pending approval
approvalController.reject(
connection.channelId,
- ethErrors.provider.userRejectedRequest(),
+ providerErrors.userRejectedRequest(),
);
}
connection.approvalPromise = undefined;
diff --git a/app/core/SDKConnect/handlers/handleDeeplink.ts b/app/core/SDKConnect/handlers/handleDeeplink.ts
index db02fe69437..0c09211eee6 100644
--- a/app/core/SDKConnect/handlers/handleDeeplink.ts
+++ b/app/core/SDKConnect/handlers/handleDeeplink.ts
@@ -91,7 +91,7 @@ const handleDeeplink = async ({
});
}
} catch (error) {
- Logger.error(error, 'Failed to connect to channel');
+ Logger.error(error as Error, 'Failed to connect to channel');
}
};
diff --git a/app/core/SDKConnect/handlers/handleSendMessage.ts b/app/core/SDKConnect/handlers/handleSendMessage.ts
index bd1815144da..013add22370 100644
--- a/app/core/SDKConnect/handlers/handleSendMessage.ts
+++ b/app/core/SDKConnect/handlers/handleSendMessage.ts
@@ -77,16 +77,21 @@ export const handleSendMessage = async ({
connection.rpcQueueManager,
);
connection.setLoading(false);
- if (
- !method &&
- connection.navigation?.getCurrentRoute()?.name === 'AccountConnect'
- ) {
+ const currentRoute = connection.navigation?.getCurrentRoute()?.name;
+ if (!method && currentRoute === 'AccountConnect') {
DevLogger.log(`[handleSendMessage] remove modal`);
if (
Device.isIos() &&
parseInt(Platform.Version as string) >= 17 &&
connection.navigation?.canGoBack()
) {
+ const isLastPendingRequest = connection.rpcQueueManager.isEmpty();
+ if (!isLastPendingRequest) {
+ DevLogger.log(
+ `[handleSendMessage] pending request --- skip goback`,
+ );
+ return;
+ }
try {
DevLogger.log(
`[handleSendMessage] goBack()`,
@@ -94,7 +99,9 @@ export const handleSendMessage = async ({
);
connection.navigation?.goBack();
await wait(200); // delay to allow modal to close
- DevLogger.log(`[handleSendMessage] navigate to ROOT_MODAL_FLOW`);
+ DevLogger.log(
+ `[handleSendMessage] navigate to ROOT_MODAL_FLOW from ${currentRoute}`,
+ );
} catch (_e) {
// Ignore temporarily until next stage of permissions system implementation
DevLogger.log(`[handleSendMessage] error goBack()`, _e);
@@ -107,7 +114,10 @@ export const handleSendMessage = async ({
return;
}
- if (connection.trigger !== 'deeplink') {
+ if (
+ connection.trigger !== 'deeplink' &&
+ connection.origin !== AppConstants.DEEPLINKS.ORIGIN_DEEPLINK
+ ) {
DevLogger.log(`[handleSendMessage] NOT deeplink --- skip goBack()`);
return;
}
diff --git a/app/core/SecureKeychain.js b/app/core/SecureKeychain.js
index 09fb8c14382..6607c947386 100644
--- a/app/core/SecureKeychain.js
+++ b/app/core/SecureKeychain.js
@@ -1,5 +1,5 @@
import * as Keychain from 'react-native-keychain'; // eslint-disable-line import/no-namespace
-import Encryptor from './Encryptor';
+import { Encryptor, DERIVATION_PARAMS } from './Encryptor';
import { strings } from '../../locales/i18n';
import AsyncStorage from '../store/async-storage-wrapper';
import { Platform } from 'react-native';
@@ -14,7 +14,9 @@ import {
import Device from '../util/device';
const privates = new WeakMap();
-const encryptor = new Encryptor();
+const encryptor = new Encryptor({
+ derivationParams: DERIVATION_PARAMS,
+});
const defaultOptions = {
service: 'com.metamask',
authenticationPromptTitle: strings('authentication.auth_prompt_title'),
diff --git a/app/core/WalletConnect/WalletConnectV2.ts b/app/core/WalletConnect/WalletConnectV2.ts
index 0b426c61860..13afee626d1 100644
--- a/app/core/WalletConnect/WalletConnectV2.ts
+++ b/app/core/WalletConnect/WalletConnectV2.ts
@@ -169,7 +169,10 @@ class WalletConnect2Session {
);
}
} catch (error) {
- Logger.error(`WC2::constructor error while handling request`, error);
+ Logger.error(
+ error as Error,
+ `WC2::constructor error while handling request`,
+ );
}
});
}
@@ -649,7 +652,7 @@ export class WC2Manager {
await wait(1000);
this.instance = new WC2Manager(web3Wallet, deeplinkSessions, navigation);
} catch (error) {
- Logger.error(`WC2@init() failed to create instance`, error);
+ Logger.error(error as Error, `WC2@init() failed to create instance`);
}
return this.instance;
@@ -784,7 +787,7 @@ export class WC2Manager {
try {
await permissionsController.requestPermissions(
- { origin: url },
+ { origin: id + '' },
{ eth_accounts: {} },
// { id: undefined }, // Don't set id here, it will be set after session is created, identify via origin.
);
@@ -800,7 +803,7 @@ export class WC2Manager {
try {
// use Permission controller
- const approvedAccounts = await getPermittedAccounts(url);
+ const approvedAccounts = await getPermittedAccounts(id + '');
// TODO: Misleading variable name, this is not the chain ID. This should be updated to use the chain ID.
const chainId = selectChainId(store.getState());
DevLogger.log(
@@ -813,31 +816,6 @@ export class WC2Manager {
chainId: parseInt(chainId),
accounts: approvedAccounts,
});
-
- // // Create updated Permissions now that session is created
- // permissionsController.requestPermissions(
- // { origin: activeSession.topic },
- // { eth_accounts: {} },
- // { id: activeSession.topic },
- // );
- // const request: PermissionsRequest = {
- // permissions: {
- // eth_accounts: {},
- // },
- // metadata: {
- // id: activeSession.topic,
- // origin: activeSession.topic,
- // },
- // approvedAccounts,
- // };
- // await permissionsController.acceptPermissionsRequest(request);
- // Remove old permissions
- // permissionsController.revokeAllPermissions(`${id}`);
- DevLogger.log(
- `WC2::session_proposal after permissions cleanup`,
- permissionsController.state,
- );
-
const deeplink =
typeof this.deeplinkSessions[activeSession.pairingTopic] !==
'undefined';
diff --git a/app/declarations.d.ts b/app/declarations.d.ts
index d1f598112be..29c1a5991f0 100644
--- a/app/declarations.d.ts
+++ b/app/declarations.d.ts
@@ -30,8 +30,6 @@ declare module '*.png' {
export default content;
}
-// TODO: eth-json-rpc-errors does not contain types. May want to create our own types.
-declare module 'eth-json-rpc-errors';
declare module '@react-native-community/checkbox' {
import CheckBoxOriginal from '@react-native-community/checkbox';
diff --git a/app/images/palm.png b/app/images/palm.png
index 452d3c9805b..d9fa9e66aa1 100644
Binary files a/app/images/palm.png and b/app/images/palm.png differ
diff --git a/app/lib/ppom/PPOMView.tsx b/app/lib/ppom/PPOMView.tsx
index 054292f7679..956bea9b3ec 100644
--- a/app/lib/ppom/PPOMView.tsx
+++ b/app/lib/ppom/PPOMView.tsx
@@ -84,9 +84,10 @@ export class PPOMView extends Component {
this.invoke.define('console.log', (...args: any[]) =>
Logger.log('[PPOMView]', ...args),
);
- this.invoke.define('console.error', (...args: any[]) =>
- Logger.error('[PPOMView]', args),
- );
+ this.invoke.define('console.error', (...args: any[]) => {
+ const PPOMError = new Error('[PPOMView]');
+ return Logger.error(PPOMError, args);
+ });
this.invoke.define('console.warn', (...args: any[]) =>
Logger.log('[PPOMView]', ...args),
);
diff --git a/app/reducers/notification/index.js b/app/reducers/notification/index.js
index f0e9677d221..2eb6cbfcdfc 100644
--- a/app/reducers/notification/index.js
+++ b/app/reducers/notification/index.js
@@ -1,5 +1,5 @@
-import notificationTypes from '../../util/notifications';
-const { TRANSACTION, SIMPLE } = notificationTypes;
+import { NotificationTypes } from '../../util/notifications';
+const { TRANSACTION, SIMPLE } = NotificationTypes;
export const initialState = {
notifications: [],
diff --git a/app/reducers/notification/notification.test.js b/app/reducers/notification/notification.test.js
index 0f442902af7..e730c250899 100644
--- a/app/reducers/notification/notification.test.js
+++ b/app/reducers/notification/notification.test.js
@@ -1,6 +1,6 @@
import reducer, { ACTIONS, initialState } from './index';
-import notificationTypes from '../../util/notifications';
-const { TRANSACTION, SIMPLE } = notificationTypes;
+import { NotificationTypes } from '../../util/notifications';
+const { TRANSACTION, SIMPLE } = NotificationTypes;
const emptyAction = { type: null };
diff --git a/app/selectors/types.ts b/app/selectors/types.ts
index a156a39fa03..51aad3b0598 100644
--- a/app/selectors/types.ts
+++ b/app/selectors/types.ts
@@ -15,7 +15,7 @@ import { AddressBookState } from '@metamask/address-book-controller';
import { BaseState } from '@metamask/base-controller';
import { KeyringControllerMemState } from '@metamask/keyring-controller';
import { PreferencesState } from '@metamask/preferences-controller';
-import { PhishingState } from '@metamask/phishing-controller';
+import { PhishingControllerState } from '@metamask/phishing-controller';
import { TransactionState } from '@metamask/transaction-controller';
import { GasFeeController } from '@metamask/gas-fee-controller';
import { PPOMState } from '@metamask/ppom-validator';
@@ -35,7 +35,7 @@ export interface EngineState {
KeyringController: KeyringControllerMemState;
NetworkController: NetworkState;
PreferencesController: PreferencesState;
- PhishingController: PhishingState;
+ PhishingController: PhishingControllerState;
PPOMController: PPOMState;
TokenBalancesController: TokenBalancesState;
TokenRatesController: TokenRatesState;
diff --git a/app/store/migrations/020.js b/app/store/migrations/020.js
index c3c7ff85c6d..a5fa5042407 100644
--- a/app/store/migrations/020.js
+++ b/app/store/migrations/020.js
@@ -16,7 +16,7 @@ export default function migrate(state) {
state.engine.backgroundState.PreferencesController;
const networkControllerState = state.engine.backgroundState.NetworkController;
const frequentRpcList = preferencesControllerState?.frequentRpcList;
- if (networkControllerState && frequentRpcList?.length) {
+ if (networkControllerState && frequentRpcList) {
const networkConfigurations = frequentRpcList.reduce(
(networkConfigs, networkConfig) => {
const networkConfigurationId = v4();
@@ -35,7 +35,7 @@ export default function migrate(state) {
);
delete preferencesControllerState.frequentRpcList;
- networkControllerState.networkConfigurations = networkConfigurations;
+ networkControllerState.networkConfigurations = networkConfigurations ?? {};
}
return state;
}
diff --git a/app/store/migrations/020.test.js b/app/store/migrations/020.test.js
index 7aad80a1715..794cff1b031 100644
--- a/app/store/migrations/020.test.js
+++ b/app/store/migrations/020.test.js
@@ -248,4 +248,45 @@ describe('Migration #20', () => {
},
});
});
+
+ it('should convert networkConfigurations to an empty object if frequentRpcList is an empty array', () => {
+ v4.mockImplementationOnce(() => 'networkId1');
+ const oldState = {
+ foo: 'bar',
+ engine: {
+ backgroundState: {
+ NetworkController: {
+ network: 'loading',
+ },
+ OtherController: {
+ foo: 'bar',
+ },
+ PreferencesController: {
+ foo: 'bar',
+ frequentRpcList: [],
+ },
+ },
+ },
+ };
+
+ const newState = migrate(oldState);
+
+ expect(newState).toStrictEqual({
+ foo: 'bar',
+ engine: {
+ backgroundState: {
+ NetworkController: {
+ network: 'loading',
+ networkConfigurations: {},
+ },
+ OtherController: {
+ foo: 'bar',
+ },
+ PreferencesController: {
+ foo: 'bar',
+ },
+ },
+ },
+ });
+ });
});
diff --git a/app/store/persistConfig.ts b/app/store/persistConfig.ts
index ad3be84e0ef..c71c313b205 100644
--- a/app/store/persistConfig.ts
+++ b/app/store/persistConfig.ts
@@ -17,8 +17,10 @@ const MigratedStorage = {
// Using new storage system
return res;
}
- } catch {
- //Fail silently
+ } catch (error) {
+ Logger.error(error as Error, {
+ message: `Failed to get item for ${key}`,
+ });
}
// Using old storage system, should only happen once
@@ -37,14 +39,18 @@ const MigratedStorage = {
try {
return await FilesystemStorage.setItem(key, value, Device.isIos());
} catch (error) {
- Logger.error(error as Error, { message: 'Failed to set item' });
+ Logger.error(error as Error, {
+ message: `Failed to set item for ${key}`,
+ });
}
},
async removeItem(key: string) {
try {
return await FilesystemStorage.removeItem(key);
} catch (error) {
- Logger.error(error as Error, { message: 'Failed to remove item' });
+ Logger.error(error as Error, {
+ message: `Failed to remove item for ${key}`,
+ });
}
},
};
diff --git a/app/util/Logger/index.test.ts b/app/util/Logger/index.test.ts
new file mode 100644
index 00000000000..44a9214da6d
--- /dev/null
+++ b/app/util/Logger/index.test.ts
@@ -0,0 +1,99 @@
+import Logger from '.';
+import {
+ captureException,
+ withScope,
+ captureMessage,
+} from '@sentry/react-native';
+import { AGREED, METRICS_OPT_IN } from '../../constants/storage';
+import DefaultPreference from 'react-native-default-preference';
+
+jest.mock('@sentry/react-native', () => ({
+ captureException: jest.fn(),
+ captureMessage: jest.fn(),
+ withScope: jest.fn(),
+}));
+const mockedCaptureException = jest.mocked(captureException);
+const mockedCaptureMessage = jest.mocked(captureMessage);
+const mockedWithScope = jest.mocked(withScope);
+
+describe('Logger', () => {
+ beforeEach(() => {
+ DefaultPreference.get = jest.fn((key: string) => {
+ switch (key) {
+ case METRICS_OPT_IN:
+ return Promise.resolve(AGREED);
+ default:
+ return Promise.resolve('');
+ }
+ });
+ });
+
+ afterEach(() => {
+ jest.resetAllMocks();
+ });
+
+ describe('error', () => {
+ it('warns if error is not defined', async () => {
+ const warn = jest.spyOn(console, 'warn');
+ await Logger.error(undefined as any);
+ expect(warn).toBeCalledWith('No error provided');
+ });
+
+ it('skips captureException if metrics is opted out', async () => {
+ DefaultPreference.get = jest.fn((key: string) => {
+ switch (key) {
+ case METRICS_OPT_IN:
+ return Promise.resolve('');
+ default:
+ return Promise.resolve('');
+ }
+ });
+ const testError = new Error('testError');
+ await Logger.error(testError);
+ expect(mockedCaptureException).not.toBeCalled();
+ });
+
+ it('calls captureException if metrics is opted in', async () => {
+ const testError = new Error('testError');
+ await Logger.error(testError);
+ expect(mockedCaptureException).toHaveBeenCalledWith(expect.any(Error));
+ });
+
+ it('calls withScope if extra is passed in', async () => {
+ const testError = new Error('testError');
+ await Logger.error(testError, 'extraMessage');
+ expect(mockedWithScope).toHaveBeenCalledTimes(1);
+ });
+
+ it('calls withScope if extra is passed in', async () => {
+ const testError = new Error('testError');
+ await Logger.error(testError, 'extraMessage');
+ expect(mockedWithScope).toHaveBeenCalledTimes(1);
+ });
+
+ it('calls captureException when string is passed instead of Error object', async () => {
+ const testError = 'testError' as any;
+ await Logger.error(testError);
+ expect(mockedCaptureException).toHaveBeenCalledWith(expect.any(Error));
+ });
+ });
+
+ describe('message', () => {
+ it('skips captureMessage if metrics is opted out', async () => {
+ DefaultPreference.get = jest.fn((key: string) => {
+ switch (key) {
+ case METRICS_OPT_IN:
+ return Promise.resolve('');
+ default:
+ return Promise.resolve('');
+ }
+ });
+ await Logger.message('testMessage');
+ expect(mockedCaptureMessage).not.toHaveBeenCalled();
+ });
+ it('calls captureMessage if metrics is opted in', async () => {
+ await Logger.message('testMessage');
+ expect(mockedCaptureMessage).toHaveBeenCalledTimes(1);
+ });
+ });
+});
diff --git a/app/util/Logger.ts b/app/util/Logger/index.ts
similarity index 83%
rename from app/util/Logger.ts
rename to app/util/Logger/index.ts
index 851b6784d6a..8fe518cedc1 100644
--- a/app/util/Logger.ts
+++ b/app/util/Logger/index.ts
@@ -5,7 +5,7 @@ import {
withScope,
} from '@sentry/react-native';
import DefaultPreference from 'react-native-default-preference';
-import { METRICS_OPT_IN, AGREED, DEBUG } from '../constants/storage';
+import { METRICS_OPT_IN, AGREED, DEBUG } from '../../constants/storage';
interface ExtraInfo {
message?: string;
@@ -44,33 +44,30 @@ export class AsyncLogger {
/**
* console.error wrapper
*
- * @param {Error|string|unknown} error - error to be logged
+ * @param {Error} error - Error object to be logged
* @param {string|object} extra - Extra error info
* @returns - void
*/
static async error(
- error: Error | string | unknown,
- extra: ExtraInfo | string | any,
+ error: Error,
+ extra?: ExtraInfo | string | any,
): Promise {
if (__DEV__) {
console.warn(DEBUG, error); // eslint-disable-line no-console
return;
}
+ if (!error) {
+ return console.warn('No error provided');
+ }
+
// Check if user passed accepted opt-in to metrics
const metricsOptIn = await DefaultPreference.get(METRICS_OPT_IN);
if (metricsOptIn === AGREED) {
let exception = error;
- if (!error) {
- if (!extra) return console.warn('No error nor extra info provided');
-
- if (typeof extra === 'string') {
- exception = new Error(extra);
- } else {
- exception = new Error(extra.message || JSON.stringify(extra));
- }
- } else if (!(error instanceof Error)) {
+ // Continue handling non Error cases to prevent breaking changes
+ if (!(error instanceof Error)) {
if (typeof error === 'string') {
exception = new Error(error);
} else {
@@ -130,14 +127,11 @@ export default class Logger {
/**
* console.error wrapper
*
- * @param {Error|string|unknown} error - error to be logged
+ * @param {Error} error - Error to be logged
* @param {string|object} extra - Extra error info
* @returns - void
*/
- static error(
- error: Error | string | unknown,
- extra: ExtraInfo | string | any,
- ) {
+ static error(error: Error, extra?: ExtraInfo | string | any) {
AsyncLogger.error(error, extra).catch(() => {
// ignore error but avoid dangling promises
});
diff --git a/app/util/blockaid/index.test.ts b/app/util/blockaid/index.test.ts
index 5cc48d5c470..c76a243d9fd 100644
--- a/app/util/blockaid/index.test.ts
+++ b/app/util/blockaid/index.test.ts
@@ -7,9 +7,72 @@ import {
import * as NetworkControllerMock from '../../selectors/networkController';
import { NETWORKS_CHAIN_ID } from '../../constants/network';
-import { getBlockaidMetricsParams, isBlockaidSupportedOnCurrentChain } from '.';
+import {
+ getBlockaidMetricsParams,
+ isBlockaidSupportedOnCurrentChain,
+ getBlockaidTransactionMetricsParams,
+} from '.';
describe('Blockaid util', () => {
+ describe('getBlockaidTransactionMetricsParams', () => {
+ beforeEach(() => {
+ jest
+ .spyOn(NetworkControllerMock, 'selectChainId')
+ .mockReturnValue(NETWORKS_CHAIN_ID.MAINNET);
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('returns empty object when transaction id does not match security response id', () => {
+ const transaction = {
+ id: 1,
+ currentTransactionSecurityAlertResponse: {
+ id: 2,
+ response: {
+ result_type: ResultType.Malicious,
+ reason: Reason.notApplicable,
+ providerRequestsCount: {
+ eth_call: 5,
+ eth_getCode: 3,
+ },
+ features: [],
+ },
+ },
+ };
+ const result = getBlockaidTransactionMetricsParams(transaction);
+ expect(result).toStrictEqual({});
+ });
+
+ it('returns metrics params object when transaction id matches security response id', () => {
+ const transaction = {
+ id: 1,
+ currentTransactionSecurityAlertResponse: {
+ id: 1,
+ response: {
+ result_type: ResultType.Malicious,
+ reason: Reason.notApplicable,
+ providerRequestsCount: {
+ eth_call: 5,
+ eth_getCode: 3,
+ },
+ features: [],
+ },
+ },
+ };
+
+ const result = getBlockaidTransactionMetricsParams(transaction);
+ expect(result).toEqual({
+ ui_customizations: ['flagged_as_malicious'],
+ security_alert_response: ResultType.Malicious,
+ security_alert_reason: Reason.notApplicable,
+ ppom_eth_call_count: 5,
+ ppom_eth_getCode_count: 3,
+ });
+ });
+ });
+
describe('getBlockaidMetricsParams', () => {
beforeEach(() => {
jest
diff --git a/app/util/blockaid/index.ts b/app/util/blockaid/index.ts
index 250c720b302..d3e958ef936 100644
--- a/app/util/blockaid/index.ts
+++ b/app/util/blockaid/index.ts
@@ -5,6 +5,17 @@ import {
import { BLOCKAID_SUPPORTED_CHAIN_IDS, getDecimalChainId } from '../networks';
import { store } from '../../store';
import { selectChainId } from '../../selectors/networkController';
+import type { TransactionMeta } from '@metamask/transaction-controller';
+
+interface TransactionSecurityAlertResponseType {
+ currentTransactionSecurityAlertResponse: {
+ id: string;
+ response: SecurityAlertResponse;
+ };
+}
+
+export type TransactionType = TransactionMeta &
+ TransactionSecurityAlertResponseType;
export const isSupportedChainId = (chainId: string) => {
/**
@@ -67,3 +78,23 @@ export const getBlockaidMetricsParams = (
return additionalParams;
};
+
+export const getBlockaidTransactionMetricsParams = (
+ transaction: TransactionType,
+) => {
+ let blockaidParams = {};
+
+ if (!transaction) {
+ return blockaidParams;
+ }
+
+ if (
+ transaction.id === transaction?.currentTransactionSecurityAlertResponse?.id
+ ) {
+ blockaidParams = getBlockaidMetricsParams(
+ transaction.currentTransactionSecurityAlertResponse?.response,
+ );
+ }
+
+ return blockaidParams;
+};
diff --git a/app/util/middlewares.js b/app/util/middlewares.js
index d2378482551..57c2ad50711 100644
--- a/app/util/middlewares.js
+++ b/app/util/middlewares.js
@@ -92,7 +92,6 @@ export function createLoggerMiddleware(opts) {
* This will make the error log to sentry with the title "gas required exceeds allowance (59956966) or always failing transaction"
* making it easier to differentiate each error.
*/
- let errorToLog = error;
const errorParams = {
message: 'Error in RPC response',
orginalError: error,
@@ -100,19 +99,11 @@ export function createLoggerMiddleware(opts) {
req,
};
- if (error.message) {
- errorToLog = new Error(error.message);
- }
-
if (error.data) {
errorParams.data = error.data;
-
- if (error.data.message) {
- errorToLog = new Error(error.data.message);
- }
}
- Logger.error(errorToLog, errorParams);
+ Logger.error(error, errorParams);
}
}
}
diff --git a/app/util/networks/index.js b/app/util/networks/index.js
index 855a8d1e1e2..e69126857c6 100644
--- a/app/util/networks/index.js
+++ b/app/util/networks/index.js
@@ -24,7 +24,7 @@ import { isStrictHexString } from '@metamask/utils';
import Engine from '../../core/Engine';
import { toLowerCaseEquals } from '../general';
import { fastSplit } from '../number';
-import { buildUnserializedTransaction } from '../transactions/optimismTransaction';
+import buildUnserializedTransaction from '../transactions/optimismTransaction';
import handleNetworkSwitch from './handleNetworkSwitch';
import { regex } from '../../../app/util/regex';
@@ -104,6 +104,7 @@ const NetworkListKeys = Object.keys(NetworkList);
export const BLOCKAID_SUPPORTED_CHAIN_IDS = [
NETWORKS_CHAIN_ID.MAINNET,
NETWORKS_CHAIN_ID.BSC,
+ NETWORKS_CHAIN_ID.BASE,
NETWORKS_CHAIN_ID.POLYGON,
NETWORKS_CHAIN_ID.ARBITRUM,
NETWORKS_CHAIN_ID.OPTIMISM,
@@ -115,6 +116,7 @@ export const BLOCKAID_SUPPORTED_CHAIN_IDS = [
export const BLOCKAID_SUPPORTED_NETWORK_NAMES = {
[NETWORKS_CHAIN_ID.MAINNET]: 'Ethereum Mainnet',
[NETWORKS_CHAIN_ID.BSC]: 'Binance Smart Chain',
+ [NETWORKS_CHAIN_ID.BASE]: 'Base Mainnet',
[NETWORKS_CHAIN_ID.OPTIMISM]: 'Optimism',
[NETWORKS_CHAIN_ID.POLYGON]: 'Polygon',
[NETWORKS_CHAIN_ID.ARBITRUM]: 'Arbitrum',
diff --git a/app/util/notifications.js b/app/util/notifications.js
deleted file mode 100644
index e6cb4fa2a5f..00000000000
--- a/app/util/notifications.js
+++ /dev/null
@@ -1,6 +0,0 @@
-const notificationTypes = {
- TRANSACTION: 'transaction',
- SIMPLE: 'simple',
-};
-
-export default notificationTypes;
diff --git a/app/util/notifications/constants/config.ts b/app/util/notifications/constants/config.ts
new file mode 100644
index 00000000000..5c23ca3a30b
--- /dev/null
+++ b/app/util/notifications/constants/config.ts
@@ -0,0 +1,70 @@
+interface FirebaseAppOptions {
+ appId?: string;
+ apiKey?: string;
+ databaseURL?: string;
+ projectId?: string;
+ gaTrackingId?: string;
+ storageBucket?: string;
+ messagingSenderId?: string;
+ clientId?: string;
+ androidClientId?: string;
+ deepLinkURLScheme?: string;
+ [name: string]: any;
+}
+
+const getEnvStr = (test: string, prod: string, override?: string) =>
+ override ?? (process.env.NODE_ENV !== 'production' ? test : prod);
+
+const parseConfig = (configStr: string): T | null => {
+ try {
+ return JSON.parse(configStr) as T;
+ } catch {
+ return null;
+ }
+};
+
+export const NOTIFICATION_AUTH_URL = getEnvStr(
+ 'https://authentication.uat-api.cx.metamask.io',
+ 'https://authentication.api.cx.metamask.io',
+ process.env.NOTIFICATION_AUTH_URL,
+);
+export const USER_STORAGE_SERVICE_URL = getEnvStr(
+ 'https://user-storage.uat-api.cx.metamask.io',
+ 'https://user-storage.api.cx.metamask.io',
+ process.env.USER_STORAGE_SERVICE_URL,
+);
+export const TRIGGERS_SERVICE_URL = getEnvStr(
+ 'https://trigger.uat-api.cx.metamask.io',
+ 'https://trigger.api.cx.metamask.io',
+ process.env.TRIGGERS_SERVICE_URL,
+);
+export const NOTIFICATIONS_SERVICE_URL = getEnvStr(
+ 'https://notification.uat-api.cx.metamask.io',
+ 'https://notification.api.cx.metamask.io',
+ process.env.NOTIFICATIONS_SERVICE_URL,
+);
+
+export const PUSH_NOTIFICATIONS_SERVICE_URL = getEnvStr(
+ 'https://push.uat-api.cx.metamask.io',
+ 'https://push.api.cx.metamask.io',
+ process.env.PUSH_NOTIFICATIONS_SERVICE_URL,
+);
+
+export const VAPID_KEY = process.env.FCM_VAPID_KEY ?? '';
+
+function getFirebaseConfigEnv(): FirebaseAppOptions | null {
+ if (process.env.FCM_CONFIG)
+ return parseConfig(process.env.FCM_CONFIG);
+
+ return {
+ apiKey: process.env.FCM_CONFIG_API_KEY,
+ authDomain: process.env.FCM_CONFIG_AUTH_DOMAIN,
+ projectId: process.env.FCM_CONFIG_PROJECT_ID,
+ storageBucket: process.env.FCM_CONFIG_STORAGE_BUCKET,
+ messagingSenderId: process.env.FCM_CONFIG_MESSAGING_SENDER_ID,
+ appId: process.env.FCM_CONFIG_APP_ID,
+ measurementId: process.env.FCM_CONFIG_MEASUREMENT_ID,
+ };
+}
+
+export const FIREBASE_CONFIG = getFirebaseConfigEnv();
diff --git a/app/util/notifications/constants/index.ts b/app/util/notifications/constants/index.ts
new file mode 100644
index 00000000000..058f27ec79d
--- /dev/null
+++ b/app/util/notifications/constants/index.ts
@@ -0,0 +1,4 @@
+export * from './config';
+export * from './triggers';
+export * from './local-storage-keys';
+export * from './user-storage';
diff --git a/app/util/notifications/constants/local-storage-keys.ts b/app/util/notifications/constants/local-storage-keys.ts
new file mode 100644
index 00000000000..fab1c2fb791
--- /dev/null
+++ b/app/util/notifications/constants/local-storage-keys.ts
@@ -0,0 +1,8 @@
+/* eslint-disable import/prefer-default-export */
+export const enum LS_NotificationKeys {
+ READ_FEATURE_ANNOUNCEMENTS = 'notifications/featureAnnouncementsRead',
+ ENABLE_FEATURE_ANNOUNCEMENTS = 'notifications/enableFeatureAnnouncements',
+ ALL_SET_UP_DISMISS = 'notifications/lsAllSetUpDismiss',
+ STORAGE_KEY = 'user_storage/storageKey',
+ NOTIFICATION_TRIGGER_CLEANUP = 'notifications/cleanup',
+}
diff --git a/app/util/notifications/constants/triggers.ts b/app/util/notifications/constants/triggers.ts
new file mode 100644
index 00000000000..b0de5322290
--- /dev/null
+++ b/app/util/notifications/constants/triggers.ts
@@ -0,0 +1,181 @@
+export enum ChainId {
+ ETHEREUM = 1,
+ ETHEREUM_ROPSTEN = 3,
+ ETHEREUM_RINKEBY = 4,
+ ETHEREUM_GOERLI = 5,
+ ETHEREUM_HOLESKY = 17000,
+ CRONOS = 25,
+ ETHEREUM_KOVAN = 42,
+ OPTIMISM = 10,
+ OPTIMISM_KOVAN = 69,
+ GNOSIS = 100,
+ ARBITRUM = 42161,
+ ARBITRUM_TESTNET = 421611,
+ BSC = 56,
+ BSC_TESTNET = 97,
+ MOONBEAM = 1284,
+ MOONRIVER = 1285,
+ POLYGON = 137,
+ POLYGON_MUMBAI = 80001,
+ FANTOM = 250,
+ FANTOM_TESTNET = 4002,
+ CELO = 42220,
+ AVALANCHE = 43114,
+ AVALANCHE_FUJI = 43113,
+ ZKSYNC = 324,
+ ZKSYNC_TESTNET = 280,
+ AURORA = 1313161554,
+ AURORA_TESTNET = 1313161555,
+ HARMONY_ZERO = 1666600000,
+ HARMONY_ONE = 1666600001,
+ HARMONY_TWO = 1666600002,
+ HARMONY_THREE = 1666600003,
+ PALM = 11297108109,
+ PALM_TESTNET = 11297108099,
+ HECO = 128,
+ HECO_TESTNET = 256,
+ LINEA_TESTNET = 59140,
+ POLYGON_ZKEVM = 1101,
+ LINEA = 59144,
+ BASE = 8453,
+ BASE_TESTNET = 84531,
+ SCROLL = 534352,
+}
+
+export enum TRIGGER_TYPES {
+ FEATURES_ANNOUNCEMENT = 'features_announcement',
+ METAMASK_SWAP_COMPLETED = 'metamask_swap_completed',
+ ERC20_SENT = 'erc20_sent',
+ ERC20_RECEIVED = 'erc20_received',
+ ETH_SENT = 'eth_sent',
+ ETH_RECEIVED = 'eth_received',
+ ROCKETPOOL_STAKE_COMPLETED = 'rocketpool_stake_completed',
+ ROCKETPOOL_UNSTAKE_COMPLETED = 'rocketpool_unstake_completed',
+ LIDO_STAKE_COMPLETED = 'lido_stake_completed',
+ LIDO_WITHDRAWAL_REQUESTED = 'lido_withdrawal_requested',
+ LIDO_WITHDRAWAL_COMPLETED = 'lido_withdrawal_completed',
+ LIDO_STAKE_READY_TO_BE_WITHDRAWN = 'lido_stake_ready_to_be_withdrawn',
+ ERC721_SENT = 'erc721_sent',
+ ERC721_RECEIVED = 'erc721_received',
+ ERC1155_SENT = 'erc1155_sent',
+ ERC1155_RECEIVED = 'erc1155_received',
+}
+
+export const chains = {
+ ETHEREUM: `${ChainId.ETHEREUM}`,
+ OPTIMISM: `${ChainId.OPTIMISM}`,
+ BSC: `${ChainId.BSC}`,
+ POLYGON: `${ChainId.POLYGON}`,
+ ARBITRUM: `${ChainId.ARBITRUM}`,
+ AVALANCHE: `${ChainId.AVALANCHE}`,
+ LINEA: `${ChainId.LINEA}`,
+};
+
+export const SUPPORTED_CHAINS = [
+ chains.ETHEREUM,
+ chains.OPTIMISM,
+ chains.BSC,
+ chains.POLYGON,
+ chains.ARBITRUM,
+ chains.AVALANCHE,
+ chains.LINEA,
+];
+
+export interface Trigger {
+ supported_chains: (typeof SUPPORTED_CHAINS)[number][];
+}
+
+export const TRIGGERS: Partial> = {
+ [TRIGGER_TYPES.METAMASK_SWAP_COMPLETED]: {
+ supported_chains: [
+ chains.ETHEREUM,
+ chains.OPTIMISM,
+ chains.BSC,
+ chains.POLYGON,
+ chains.ARBITRUM,
+ chains.AVALANCHE,
+ ],
+ },
+ [TRIGGER_TYPES.ERC20_SENT]: {
+ supported_chains: [
+ chains.ETHEREUM,
+ chains.OPTIMISM,
+ chains.BSC,
+ chains.POLYGON,
+ chains.ARBITRUM,
+ chains.AVALANCHE,
+ chains.LINEA,
+ ],
+ },
+ [TRIGGER_TYPES.ERC20_RECEIVED]: {
+ supported_chains: [
+ chains.ETHEREUM,
+ chains.OPTIMISM,
+ chains.BSC,
+ chains.POLYGON,
+ chains.ARBITRUM,
+ chains.AVALANCHE,
+ chains.LINEA,
+ ],
+ },
+ [TRIGGER_TYPES.ERC721_SENT]: {
+ supported_chains: [chains.ETHEREUM, chains.POLYGON],
+ },
+ [TRIGGER_TYPES.ERC721_RECEIVED]: {
+ supported_chains: [chains.ETHEREUM, chains.POLYGON],
+ },
+ [TRIGGER_TYPES.ERC1155_SENT]: {
+ supported_chains: [chains.ETHEREUM, chains.POLYGON],
+ },
+ [TRIGGER_TYPES.ERC1155_RECEIVED]: {
+ supported_chains: [chains.ETHEREUM, chains.POLYGON],
+ },
+ [TRIGGER_TYPES.ETH_SENT]: {
+ supported_chains: [
+ chains.ETHEREUM,
+ chains.OPTIMISM,
+ chains.BSC,
+ chains.POLYGON,
+ chains.ARBITRUM,
+ chains.AVALANCHE,
+ chains.LINEA,
+ ],
+ },
+ [TRIGGER_TYPES.ETH_RECEIVED]: {
+ supported_chains: [
+ chains.ETHEREUM,
+ chains.OPTIMISM,
+ chains.BSC,
+ chains.POLYGON,
+ chains.ARBITRUM,
+ chains.AVALANCHE,
+ chains.LINEA,
+ ],
+ },
+ [TRIGGER_TYPES.ROCKETPOOL_STAKE_COMPLETED]: {
+ supported_chains: [chains.ETHEREUM],
+ },
+ [TRIGGER_TYPES.ROCKETPOOL_UNSTAKE_COMPLETED]: {
+ supported_chains: [chains.ETHEREUM],
+ },
+ [TRIGGER_TYPES.LIDO_STAKE_COMPLETED]: {
+ supported_chains: [chains.ETHEREUM],
+ },
+ [TRIGGER_TYPES.LIDO_WITHDRAWAL_REQUESTED]: {
+ supported_chains: [chains.ETHEREUM],
+ },
+ [TRIGGER_TYPES.LIDO_WITHDRAWAL_COMPLETED]: {
+ supported_chains: [chains.ETHEREUM],
+ },
+};
+
+export enum TRIGGER_DATA_TYPES {
+ DATA_FEATURE_ANNOUNCEMENT = 'data_feature_announcement',
+ DATA_METAMASK_SWAP_COMPLETED = 'data_metamask_swap_completed',
+ DATA_STAKE = 'data_stake',
+ DATA_LIDO_STAKE_READY_TO_BE_WITHDRAWN = 'data_lido_stake_ready_to_be_withdrawn',
+ DATA_ETH = 'data_eth',
+ DATA_ERC20 = 'data_erc20',
+ DATA_ERC721 = 'data_erc721',
+ DATA_ERC1155 = 'data_erc1155',
+}
diff --git a/app/util/notifications/constants/user-storage.ts b/app/util/notifications/constants/user-storage.ts
new file mode 100644
index 00000000000..b63502269d7
--- /dev/null
+++ b/app/util/notifications/constants/user-storage.ts
@@ -0,0 +1,3 @@
+export const USER_STORAGE_VERSION = '1';
+
+export const USER_STORAGE_VERSION_KEY: unique symbol = 'v' as any;
diff --git a/app/util/notifications/hooks/index.ts b/app/util/notifications/hooks/index.ts
index 8fe63148288..01a6a8498a7 100644
--- a/app/util/notifications/hooks/index.ts
+++ b/app/util/notifications/hooks/index.ts
@@ -3,6 +3,7 @@ import notifee, { EventType, AndroidImportance } from '@notifee/react-native';
import Device from '../../../util/device';
import { STORAGE_IDS } from '../../../util/notifications/settings/storage/constants';
import NotificationManager from '../../../core/NotificationManager';
+import Routes from '../../../constants/navigation/Routes';
const useNotificationHandler = (
bootstrapInitialNotification: () => Promise,
@@ -29,7 +30,7 @@ const useNotificationHandler = (
NotificationManager.setTransactionToView(data.id);
}
if (navigation) {
- navigation.navigate('TransactionsView');
+ navigation.navigate(Routes.TRANSACTIONS_VIEW);
}
}
}
diff --git a/app/util/notifications/index.ts b/app/util/notifications/index.ts
index e475513771e..014e4e8e4a0 100644
--- a/app/util/notifications/index.ts
+++ b/app/util/notifications/index.ts
@@ -1,4 +1,5 @@
export * from './types';
+export * from './constants';
export * from './setupAndroidChannels';
export * from './settings';
export * from './hooks';
diff --git a/app/util/notifications/types.ts b/app/util/notifications/types.ts
index 3316669beeb..d33926abed6 100644
--- a/app/util/notifications/types.ts
+++ b/app/util/notifications/types.ts
@@ -1,7 +1,8 @@
export const NotificationTypes = {
- transaction: 'transaction',
- marketing: 'marketing',
-} as const;
+ TRANSACTION: 'transaction',
+ SIMPLE: 'simple',
+ ANNOUCEMENTS: 'annoucements',
+};
export type NotificationTypesType =
(typeof NotificationTypes)[keyof typeof NotificationTypes];
@@ -18,7 +19,7 @@ export const NotificationTransactionTypes = {
cancelled: 'cancelled',
received: 'received',
received_payment: 'received_payment',
-} as const;
+};
export type NotificationTransactionTypesType =
(typeof NotificationTransactionTypes)[keyof typeof NotificationTransactionTypes];
@@ -28,3 +29,8 @@ export interface MarketingNotificationData {
route?: string;
routeProps?: string;
}
+
+export enum NotificationsKindTypes {
+ transaction = 'transaction',
+ announcements = 'announcements',
+}
diff --git a/app/util/notifications/types/featureAnnouncement/TypeAction.ts b/app/util/notifications/types/featureAnnouncement/TypeAction.ts
new file mode 100644
index 00000000000..4b98f7c6cb3
--- /dev/null
+++ b/app/util/notifications/types/featureAnnouncement/TypeAction.ts
@@ -0,0 +1,12 @@
+import type { Entry, EntryFieldTypes } from 'contentful/dist/types';
+
+export interface TypeActionFields {
+ fields: {
+ actionText: EntryFieldTypes.Text;
+ actionUrl: EntryFieldTypes.Text;
+ isExternal: EntryFieldTypes.Boolean;
+ };
+ contentTypeId: 'action';
+}
+
+export type TypeAction = Entry;
diff --git a/app/util/notifications/types/featureAnnouncement/TypeFeatureAnnouncement.ts b/app/util/notifications/types/featureAnnouncement/TypeFeatureAnnouncement.ts
new file mode 100644
index 00000000000..9b8d0d6b398
--- /dev/null
+++ b/app/util/notifications/types/featureAnnouncement/TypeFeatureAnnouncement.ts
@@ -0,0 +1,42 @@
+import type { Entry, EntryFieldTypes } from 'contentful/dist/types';
+import type { TypeActionFields } from './TypeAction';
+import type { TypeLinkFields } from './TypeLink';
+
+interface ImageFields {
+ fields: {
+ title?: EntryFieldTypes.Text;
+ description?: EntryFieldTypes.Text;
+ file?: EntryFieldTypes.Object<{
+ url: string;
+ fileName: string;
+ contentType: string;
+ details: {
+ size: number;
+ image?: {
+ width: number;
+ height: number;
+ };
+ };
+ }>;
+ };
+ contentTypeId: 'Image';
+}
+
+export interface TypeFeatureAnnouncementFields {
+ fields: {
+ title: EntryFieldTypes.Text;
+ id: EntryFieldTypes.Symbol;
+ category: EntryFieldTypes.Text; // E.g. Announcement, etc.
+ shortDescription: EntryFieldTypes.Text;
+ image: EntryFieldTypes.EntryLink;
+ longDescription: EntryFieldTypes.RichText;
+ link?: EntryFieldTypes.EntryLink;
+ action?: EntryFieldTypes.EntryLink;
+ };
+ contentTypeId: 'productAnnouncement';
+}
+
+export type TypeFeatureAnnouncement = Entry<
+ TypeFeatureAnnouncementFields,
+ 'WITHOUT_UNRESOLVABLE_LINKS'
+>;
diff --git a/app/util/notifications/types/featureAnnouncement/TypeLink.ts b/app/util/notifications/types/featureAnnouncement/TypeLink.ts
new file mode 100644
index 00000000000..583c359ca16
--- /dev/null
+++ b/app/util/notifications/types/featureAnnouncement/TypeLink.ts
@@ -0,0 +1,12 @@
+import type { Entry, EntryFieldTypes } from 'contentful/dist/types';
+
+export interface TypeLinkFields {
+ fields: {
+ linkText: EntryFieldTypes.Text;
+ linkUrl: EntryFieldTypes.Text;
+ isExternal: EntryFieldTypes.Boolean;
+ };
+ contentTypeId: 'link';
+}
+
+export type TypeLink = Entry;
diff --git a/app/util/notifications/types/featureAnnouncement/index.ts b/app/util/notifications/types/featureAnnouncement/index.ts
new file mode 100644
index 00000000000..5fa2d94f57e
--- /dev/null
+++ b/app/util/notifications/types/featureAnnouncement/index.ts
@@ -0,0 +1,11 @@
+import type { TypeFeatureAnnouncement } from './TypeFeatureAnnouncement';
+
+export type { TypeFeatureAnnouncementFields } from './TypeFeatureAnnouncement';
+
+import type { TRIGGER_TYPES } from '../../constants/triggers';
+
+export interface FeatureAnnouncementRawNotification {
+ type: TRIGGER_TYPES.FEATURES_ANNOUNCEMENT;
+ createdAt: Date;
+ data: TypeFeatureAnnouncement['fields'];
+}
diff --git a/app/util/notifications/types/halNotification/index.ts b/app/util/notifications/types/halNotification/index.ts
new file mode 100644
index 00000000000..6c24de2907c
--- /dev/null
+++ b/app/util/notifications/types/halNotification/index.ts
@@ -0,0 +1,50 @@
+import type { TRIGGER_TYPES } from '../../constants/triggers';
+import type { components } from './schema';
+import type { Compute } from '../type-utils';
+
+export type Data_MetamaskSwapCompleted =
+ components['schemas']['Data_MetamaskSwapCompleted'];
+export type Data_LidoStakeReadyToBeWithdrawn =
+ components['schemas']['Data_LidoStakeReadyToBeWithdrawn'];
+export type Data_LidoStakeCompleted =
+ components['schemas']['Data_LidoStakeCompleted'];
+export type Data_LidoWithdrawalRequested =
+ components['schemas']['Data_LidoWithdrawalRequested'];
+export type Data_LidoWithdrawalCompleted =
+ components['schemas']['Data_LidoWithdrawalCompleted'];
+export type Data_RocketPoolStakeCompleted =
+ components['schemas']['Data_RocketPoolStakeCompleted'];
+export type Data_RocketPoolUnstakeCompleted =
+ components['schemas']['Data_RocketPoolUnstakeCompleted'];
+export type Data_ETHSent = components['schemas']['Data_ETHSent'];
+export type Data_ETHReceived = components['schemas']['Data_ETHReceived'];
+export type Data_ERC20Sent = components['schemas']['Data_ERC20Sent'];
+export type Data_ERC20Received = components['schemas']['Data_ERC20Received'];
+export type Data_ERC721Sent = components['schemas']['Data_ERC721Sent'];
+export type Data_ERC721Received = components['schemas']['Data_ERC721Received'];
+
+type Notification = components['schemas']['Notification'];
+type NotificationDataKinds = NonNullable['kind'];
+type ConvertToEnum = {
+ [K in TRIGGER_TYPES]: Kind extends `${K}` ? K : never;
+}[TRIGGER_TYPES];
+
+/**
+ * Type-Foo.
+ * 1. Adds a `type` field to the notification, it converts the schema type into the ENUM we use.
+ * 2. It ensures that the `data` field is the correct Notification data for this `type`
+ * - The `Compute` utility merges the intersections (`&`) for a prettier type.
+ */
+export type HalRawNotification = {
+ [K in NotificationDataKinds]: Compute<
+ Omit & {
+ type: ConvertToEnum;
+ data: Extract;
+ }
+ >;
+}[NotificationDataKinds];
+
+export type HalRawNotificationsWithNetworkFields = Extract<
+ HalRawNotification,
+ { data: { network_fee: unknown } }
+>;
diff --git a/app/util/notifications/types/halNotification/schema.d.ts b/app/util/notifications/types/halNotification/schema.d.ts
new file mode 100644
index 00000000000..7677a01f685
--- /dev/null
+++ b/app/util/notifications/types/halNotification/schema.d.ts
@@ -0,0 +1,299 @@
+/**
+ * This file was auto-generated by openapi-typescript.
+ * Do not make direct changes to the file.
+ */
+
+export interface paths {
+ '/api/v1/notifications': {
+ /** List all notifications ordered by most recent */
+ post: {
+ parameters: {
+ query?: {
+ /** @description Page number for pagination */
+ page?: number;
+ /** @description Number of notifications per page for pagination */
+ per_page?: number;
+ };
+ };
+ requestBody?: {
+ content: {
+ 'application/json': {
+ trigger_ids: string[];
+ chain_ids?: number[];
+ kinds?: string[];
+ unread?: boolean;
+ };
+ };
+ };
+ responses: {
+ /** @description Successfully fetched a list of notifications */
+ 200: {
+ content: {
+ 'application/json': components['schemas']['Notification'][];
+ };
+ };
+ };
+ };
+ };
+ '/api/v1/notifications/mark-as-read': {
+ /** Mark notifications as read */
+ post: {
+ requestBody: {
+ content: {
+ 'application/json': {
+ ids?: string[];
+ };
+ };
+ };
+ responses: {
+ /** @description Successfully marked notifications as read */
+ 200: {
+ content: never;
+ };
+ };
+ };
+ };
+}
+
+export type webhooks = Record;
+
+export interface components {
+ schemas: {
+ Notification: {
+ /** Format: uuid */
+ id: string;
+ /** Format: uuid */
+ trigger_id: string;
+ /** @example 1 */
+ chain_id: number;
+ /** @example 17485840 */
+ block_number: number;
+ block_timestamp: string;
+ /**
+ * Format: address
+ * @example 0x881D40237659C251811CEC9c364ef91dC08D300C
+ */
+ tx_hash: string;
+ /** @example false */
+ unread: boolean;
+ /** Format: date-time */
+ created_at: string;
+ data?:
+ | components['schemas']['Data_MetamaskSwapCompleted']
+ | components['schemas']['Data_LidoStakeReadyToBeWithdrawn']
+ | components['schemas']['Data_LidoStakeCompleted']
+ | components['schemas']['Data_LidoWithdrawalRequested']
+ | components['schemas']['Data_LidoWithdrawalCompleted']
+ | components['schemas']['Data_RocketPoolStakeCompleted']
+ | components['schemas']['Data_RocketPoolUnstakeCompleted']
+ | components['schemas']['Data_ETHSent']
+ | components['schemas']['Data_ETHReceived']
+ | components['schemas']['Data_ERC20Sent']
+ | components['schemas']['Data_ERC20Received']
+ | components['schemas']['Data_ERC721Sent']
+ | components['schemas']['Data_ERC721Received']
+ | components['schemas']['Data_ERC1155Sent']
+ | components['schemas']['Data_ERC1155Received'];
+ };
+ Data_MetamaskSwapCompleted: {
+ /** @enum {string} */
+ kind: 'metamask_swap_completed';
+ network_fee: components['schemas']['NetworkFee'];
+ /** Format: decimal */
+ rate: string;
+ token_in: components['schemas']['Token'];
+ token_out: components['schemas']['Token'];
+ };
+ Data_LidoStakeCompleted: {
+ /** @enum {string} */
+ kind: 'lido_stake_completed';
+ network_fee: components['schemas']['NetworkFee'];
+ stake_in: components['schemas']['Stake'];
+ stake_out: components['schemas']['Stake'];
+ };
+ Data_LidoWithdrawalRequested: {
+ /** @enum {string} */
+ kind: 'lido_withdrawal_requested';
+ network_fee: components['schemas']['NetworkFee'];
+ stake_in: components['schemas']['Stake'];
+ stake_out: components['schemas']['Stake'];
+ };
+ Data_LidoStakeReadyToBeWithdrawn: {
+ /** @enum {string} */
+ kind: 'lido_stake_ready_to_be_withdrawn';
+ /** Format: decimal */
+ request_id: string;
+ staked_eth: components['schemas']['Stake'];
+ };
+ Data_LidoWithdrawalCompleted: {
+ /** @enum {string} */
+ kind: 'lido_withdrawal_completed';
+ network_fee: components['schemas']['NetworkFee'];
+ stake_in: components['schemas']['Stake'];
+ stake_out: components['schemas']['Stake'];
+ };
+ Data_RocketPoolStakeCompleted: {
+ /** @enum {string} */
+ kind: 'rocketpool_stake_completed';
+ network_fee: components['schemas']['NetworkFee'];
+ stake_in: components['schemas']['Stake'];
+ stake_out: components['schemas']['Stake'];
+ };
+ Data_RocketPoolUnstakeCompleted: {
+ /** @enum {string} */
+ kind: 'rocketpool_unstake_completed';
+ network_fee: components['schemas']['NetworkFee'];
+ stake_in: components['schemas']['Stake'];
+ stake_out: components['schemas']['Stake'];
+ };
+ Data_ETHSent: {
+ /** @enum {string} */
+ kind: 'eth_sent';
+ network_fee: components['schemas']['NetworkFee'];
+ /** Format: address */
+ from: string;
+ /** Format: address */
+ to: string;
+ amount: {
+ /** Format: decimal */
+ usd: string;
+ /** Format: decimal */
+ eth: string;
+ };
+ };
+ Data_ETHReceived: {
+ /** @enum {string} */
+ kind: 'eth_received';
+ network_fee: components['schemas']['NetworkFee'];
+ /** Format: address */
+ from: string;
+ /** Format: address */
+ to: string;
+ amount: {
+ /** Format: decimal */
+ usd: string;
+ /** Format: decimal */
+ eth: string;
+ };
+ };
+ Data_ERC20Sent: {
+ /** @enum {string} */
+ kind: 'erc20_sent';
+ network_fee: components['schemas']['NetworkFee'];
+ /** Format: address */
+ from: string;
+ /** Format: address */
+ to: string;
+ token: components['schemas']['Token'];
+ };
+ Data_ERC20Received: {
+ /** @enum {string} */
+ kind: 'erc20_received';
+ network_fee: components['schemas']['NetworkFee'];
+ /** Format: address */
+ from: string;
+ /** Format: address */
+ to: string;
+ token: components['schemas']['Token'];
+ };
+ Data_ERC721Sent: {
+ /** @enum {string} */
+ kind: 'erc721_sent';
+ network_fee: components['schemas']['NetworkFee'];
+ /** Format: address */
+ from: string;
+ /** Format: address */
+ to: string;
+ nft: components['schemas']['NFT'];
+ };
+ Data_ERC721Received: {
+ /** @enum {string} */
+ kind: 'erc721_received';
+ network_fee: components['schemas']['NetworkFee'];
+ /** Format: address */
+ from: string;
+ /** Format: address */
+ to: string;
+ nft: components['schemas']['NFT'];
+ };
+ Data_ERC1155Sent: {
+ /** @enum {string} */
+ kind: 'erc1155_sent';
+ network_fee: components['schemas']['NetworkFee'];
+ /** Format: address */
+ from: string;
+ /** Format: address */
+ to: string;
+ nft?: components['schemas']['NFT'];
+ };
+ Data_ERC1155Received: {
+ /** @enum {string} */
+ kind: 'erc1155_received';
+ network_fee: components['schemas']['NetworkFee'];
+ /** Format: address */
+ from: string;
+ /** Format: address */
+ to: string;
+ nft?: components['schemas']['NFT'];
+ };
+ NetworkFee: {
+ /** Format: decimal */
+ gas_price: string;
+ /** Format: decimal */
+ native_token_price_in_usd: string;
+ };
+ Token: {
+ /** Format: address */
+ address: string;
+ symbol: string;
+ name: string;
+ /** Format: decimal */
+ amount: string;
+ /** Format: int32 */
+ decimals: string;
+ /** Format: uri */
+ image: string;
+ /** Format: decimal */
+ usd: string;
+ };
+ NFT: {
+ name: string;
+ token_id: string;
+ /** Format: uri */
+ image: string;
+ collection: {
+ /** Format: address */
+ address: string;
+ name: string;
+ symbol: string;
+ /** Format: uri */
+ image: string;
+ };
+ };
+ Stake: {
+ /** Format: address */
+ address: string;
+ symbol: string;
+ name: string;
+ /** Format: decimal */
+ amount: string;
+ /** Format: int32 */
+ decimals: string;
+ /** Format: uri */
+ image: string;
+ /** Format: decimal */
+ usd: string;
+ };
+ };
+ responses: never;
+ parameters: never;
+ requestBodies: never;
+ headers: never;
+ pathItems: never;
+}
+
+export type $defs = Record;
+
+export type external = Record;
+
+export type operations = Record;
diff --git a/app/util/notifications/types/halTriggers/index.ts b/app/util/notifications/types/halTriggers/index.ts
new file mode 100644
index 00000000000..eb8fed0651a
--- /dev/null
+++ b/app/util/notifications/types/halTriggers/index.ts
@@ -0,0 +1 @@
+export * from './schema.d';
diff --git a/app/util/notifications/types/halTriggers/schema.d.ts b/app/util/notifications/types/halTriggers/schema.d.ts
new file mode 100644
index 00000000000..2caf33ffb51
--- /dev/null
+++ b/app/util/notifications/types/halTriggers/schema.d.ts
@@ -0,0 +1,213 @@
+/**
+ * This file was auto-generated by openapi-typescript.
+ * Do not make direct changes to the file.
+ */
+
+export interface paths {
+ '/triggers': {
+ /** List all triggers */
+ get: {
+ parameters: {
+ query: {
+ /** @description Filter by id */
+ id: string[];
+ /** @description Filter by kind */
+ kind?: string;
+ /** @description Filter by chain ID */
+ chain_id?: number;
+ /** @description Page number for pagination */
+ page?: number;
+ /** @description Number of triggers per page for pagination */
+ per_page?: number;
+ };
+ };
+ responses: {
+ /** @description Successfully fetched the triggers */
+ 200: {
+ content: {
+ 'application/json': components['schemas']['Trigger'][];
+ };
+ };
+ };
+ };
+ /** Create a new trigger */
+ post: {
+ parameters: {
+ header?: {
+ /** @description Trigger Token */
+ 'X-Trigger-Token'?: string;
+ };
+ };
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** Format: uuid */
+ id: string;
+ config: components['schemas']['TriggerConfigInput'];
+ };
+ };
+ };
+ responses: {
+ /** @description Successfully created the trigger */
+ 200: {
+ content: {
+ 'application/json': components['schemas']['Trigger'];
+ };
+ };
+ };
+ };
+ };
+ '/triggers/{id}': {
+ /** Get a trigger by ID */
+ get: {
+ parameters: {
+ path: {
+ /** @description ID of the trigger to get */
+ id: string;
+ };
+ };
+ responses: {
+ /** @description Successfully fetched the trigger */
+ 200: {
+ content: {
+ 'application/json': components['schemas']['Trigger'];
+ };
+ };
+ };
+ };
+ /** Delete a trigger */
+ delete: {
+ parameters: {
+ header?: {
+ /** @description Trigger Token */
+ 'X-Trigger-Token'?: string;
+ };
+ path: {
+ /** @description ID of the trigger to delete */
+ id: string;
+ };
+ };
+ responses: {
+ /** @description Successfully deleted the trigger */
+ 204: {
+ content: never;
+ };
+ };
+ };
+ };
+}
+
+export type webhooks = Record;
+
+export interface components {
+ schemas: {
+ Trigger: {
+ /** Format: uuid */
+ id: string;
+ /** Format: date-time */
+ created_at: string;
+ /** Format: date-time */
+ updated_at: string;
+ };
+ TriggerConfigInput:
+ | components['schemas']['Config_MetamaskSwapCompleted']
+ | components['schemas']['Config_UnstakedCompleted']
+ | components['schemas']['Config_ERC20Sent']
+ | components['schemas']['Config_ERC20Received']
+ | components['schemas']['Config_ETHSent']
+ | components['schemas']['Config_ETHReceived']
+ | components['schemas']['Config_RocketpoolStakeCompleted']
+ | components['schemas']['Config_RocketpoolUnstakeCompleted']
+ | components['schemas']['Config_LidoStakeCompleted']
+ | components['schemas']['Config_LidoWithdrawalRequested']
+ | components['schemas']['Config_LidoWithdrawalCompleted'];
+ Config_MetamaskSwapCompleted: {
+ /** @enum {string} */
+ kind: 'metamask_swap_completed';
+ chain_id: number;
+ /** Format: address */
+ address: string;
+ };
+ Config_UnstakedCompleted: {
+ /** @enum {string} */
+ kind: 'unstaked_completed';
+ chain_id: number;
+ /** Format: address */
+ address: string;
+ };
+ Config_ERC20Sent: {
+ /** @enum {string} */
+ kind: 'erc20_sent';
+ chain_id: number;
+ /** Format: address */
+ address: string;
+ };
+ Config_ERC20Received: {
+ /** @enum {string} */
+ kind: 'erc20_received';
+ chain_id: number;
+ /** Format: address */
+ address: string;
+ };
+ Config_ETHSent: {
+ /** @enum {string} */
+ kind: 'eth_sent';
+ chain_id: number;
+ /** Format: address */
+ address: string;
+ };
+ Config_ETHReceived: {
+ /** @enum {string} */
+ kind: 'eth_received';
+ chain_id: number;
+ /** Format: address */
+ address: string;
+ };
+ Config_RocketpoolStakeCompleted: {
+ /** @enum {string} */
+ kind: 'rocketpool_stake_completed';
+ chain_id: number;
+ /** Format: address */
+ address: string;
+ };
+ Config_RocketpoolUnstakeCompleted: {
+ /** @enum {string} */
+ kind: 'rocketpool_unstake_completed';
+ chain_id: number;
+ /** Format: address */
+ address: string;
+ };
+ Config_LidoStakeCompleted: {
+ /** @enum {string} */
+ kind: 'lido_stake_completed';
+ chain_id: number;
+ /** Format: address */
+ address: string;
+ };
+ Config_LidoWithdrawalRequested: {
+ /** @enum {string} */
+ kind: 'lido_withdrawal_requested';
+ chain_id: number;
+ /** Format: address */
+ address: string;
+ };
+ Config_LidoWithdrawalCompleted: {
+ /** @enum {string} */
+ kind: 'lido_withdrawal_completed';
+ chain_id: number;
+ /** Format: address */
+ address: string;
+ };
+ };
+ responses: never;
+ parameters: never;
+ requestBodies: never;
+ headers: never;
+ pathItems: never;
+}
+
+export type $defs = Record;
+
+export type external = Record;
+
+export type operations = Record;
diff --git a/app/util/notifications/types/index.ts b/app/util/notifications/types/index.ts
new file mode 100644
index 00000000000..e42764b3d50
--- /dev/null
+++ b/app/util/notifications/types/index.ts
@@ -0,0 +1,6 @@
+export * from './featureAnnouncement';
+export * from './halNotification';
+export * from './halTriggers';
+export * from './notification';
+export * from './userStorage';
+export * from './type-utils';
diff --git a/app/util/notifications/types/notification/index.ts b/app/util/notifications/types/notification/index.ts
new file mode 100644
index 00000000000..43d1ae3c19a
--- /dev/null
+++ b/app/util/notifications/types/notification/index.ts
@@ -0,0 +1,113 @@
+import type { FC } from 'react';
+import type { FeatureAnnouncementRawNotification } from '../featureAnnouncement';
+import type { HalRawNotification } from '../halNotification';
+import type { Compute } from '../type-utils';
+
+/**
+ * The shape of a "generic" notification.
+ * Other than the fields listed below, tt will also contain:
+ * - `type` field (declared in the Raw shapes)
+ * - `data` field (declared in the Raw shapes)
+ */
+export type Notification = Compute<
+ (FeatureAnnouncementRawNotification | HalRawNotification) & {
+ id: string;
+ createdAt: Date;
+ isRead: boolean;
+ }
+>;
+
+// NFT
+export interface NFT {
+ token_id: string;
+ image: string;
+ collection?: {
+ name: string;
+ image: string;
+ };
+}
+
+/**
+ * NotificationFC is the shared component interface for all notification components
+ */
+type NotificationFC = FC<{ notification: N }>;
+
+interface BodyHalNotification {
+ type: 'body_hal_notification';
+ Image?: NotificationFC;
+ Summary?: NotificationFC;
+ TransactionStatus: NotificationFC;
+ Data: NotificationFC;
+ NetworkFee?: NotificationFC;
+}
+
+interface BodyFeatureAnnouncement {
+ type: 'body_feature_announcement';
+ Image: NotificationFC;
+ Description: NotificationFC;
+}
+
+interface FooterHalNotification {
+ type: 'footer_hal_notification';
+ ScanLink: NotificationFC;
+}
+
+interface FooterFeatureAnnouncement {
+ type: 'footer_feature_announcement';
+ Link: NotificationFC;
+ Action: NotificationFC;
+}
+
+/**
+ * This is the object shape that contains all the components of the particular notification.
+ * the `guardFn` can be used to narrow a wide notification into the specific notification required.
+ */
+export interface NotificationComponent {
+ guardFn: (n: Notification) => n is N;
+ item: {
+ Icon: NotificationFC;
+ Title: NotificationFC;
+ Description?: NotificationFC;
+ Amount?: NotificationFC;
+ };
+ modal: {
+ header: {
+ Badge?: NotificationFC;
+ Title: NotificationFC;
+ };
+ body: BodyHalNotification | BodyFeatureAnnouncement;
+ footer: FooterHalNotification | FooterFeatureAnnouncement;
+ };
+}
+
+export const NotificationTypes = {
+ TRANSACTION: 'transaction',
+ SIMPLE: 'simple',
+ ANNOUCEMENTS: 'annoucements',
+} as const;
+
+export type NotificationTypesType =
+ (typeof NotificationTypes)[keyof typeof NotificationTypes];
+
+export const NotificationTransactionTypes = {
+ pending: 'pending',
+ pending_deposit: 'pending_deposit',
+ pending_withdrawal: 'pending_withdrawal',
+ success: 'success',
+ speedup: 'speedup',
+ success_withdrawal: 'success_withdrawal',
+ success_deposit: 'success_deposit',
+ error: 'error',
+ cancelled: 'cancelled',
+ received: 'received',
+ received_payment: 'received_payment',
+} as const;
+
+export type NotificationTransactionTypesType =
+ (typeof NotificationTransactionTypes)[keyof typeof NotificationTransactionTypes];
+
+export interface MarketingNotificationData {
+ type: 'marketing';
+ route?: string;
+ routeProps?: string;
+}
diff --git a/app/util/notifications/types/type-utils.ts b/app/util/notifications/types/type-utils.ts
new file mode 100644
index 00000000000..f05a763f5d2
--- /dev/null
+++ b/app/util/notifications/types/type-utils.ts
@@ -0,0 +1,4 @@
+/**
+ * Computes and combines intersection types for a more "prettier" type (more human readable)
+ */
+export type Compute = T extends T ? { [K in keyof T]: T[K] } : never;
diff --git a/app/util/notifications/types/userStorage/index.ts b/app/util/notifications/types/userStorage/index.ts
new file mode 100644
index 00000000000..c47836cc261
--- /dev/null
+++ b/app/util/notifications/types/userStorage/index.ts
@@ -0,0 +1,29 @@
+import type { SUPPORTED_CHAINS, TRIGGER_TYPES } from '../../constants/triggers';
+import type {
+ USER_STORAGE_VERSION_KEY,
+ USER_STORAGE_VERSION,
+} from '../../constants/user-storage';
+
+export interface UserStorage {
+ /**
+ * The Version 'v' of the User Storage.
+ * NOTE - will allow us to support upgrade/downgrades in the future
+ */
+ [USER_STORAGE_VERSION_KEY]: typeof USER_STORAGE_VERSION;
+ [address: string]: {
+ [chain in (typeof SUPPORTED_CHAINS)[number]]: {
+ [uuid: string]: {
+ /** Trigger Kind 'k' */
+ triggerTypes: TRIGGER_TYPES;
+ /**
+ * Trigger Enabled 'e'
+ * This is mostly an 'acknowledgement' to determine if a trigger has been made
+ * For example if we fail to create a trigger, we can set to false & retry (on re-log in, or elsewhere)
+ *
+ * Most of the time this is 'true', as triggers when deleted are also removed from User Storage
+ */
+ triggerEnabled: boolean;
+ };
+ };
+ };
+}
diff --git a/app/util/test/initial-background-state.json b/app/util/test/initial-background-state.json
index 6ba3a7be9f2..e08e9a366bd 100644
--- a/app/util/test/initial-background-state.json
+++ b/app/util/test/initial-background-state.json
@@ -196,7 +196,8 @@
"GasFeeController": {
"gasFeeEstimates": {},
"estimatedGasFeeTimeBounds": {},
- "gasEstimateType": "none"
+ "gasEstimateType": "none",
+ "gasFeeEstimatesByChainId": {}
},
"ApprovalController": {
"pendingApprovals": {},
diff --git a/app/util/test/testSetup.js b/app/util/test/testSetup.js
index d6e6a0a08db..3d65f9d33b3 100644
--- a/app/util/test/testSetup.js
+++ b/app/util/test/testSetup.js
@@ -194,6 +194,23 @@ NativeModules.Aes = {
const hashBase = '012345678987654';
return Promise.resolve(hashBase + uniqueAddressChar);
}),
+ pbkdf2: jest
+ .fn()
+ .mockImplementation((_password, _salt, _iterations, _keyLength) =>
+ Promise.resolve('mockedKey'),
+ ),
+ randomKey: jest.fn().mockResolvedValue('mockedIV'),
+ encrypt: jest.fn().mockResolvedValue('mockedCipher'),
+ decrypt: jest.fn().mockResolvedValue('{"mockData": "mockedPlainText"}'),
+};
+
+NativeModules.AesForked = {
+ pbkdf2: jest
+ .fn()
+ .mockImplementation((_password, _salt) =>
+ Promise.resolve('mockedKeyForked'),
+ ),
+ decrypt: jest.fn().mockResolvedValue('{"mockData": "mockedPlainTextForked"}'),
};
jest.mock(
@@ -274,6 +291,7 @@ jest.mock('react-native-default-preference', () => ({
// eslint-disable-next-line import/no-commonjs
require('react-native-reanimated/lib/module/reanimated2/jestUtils').setUpTests();
global.__reanimatedWorkletInit = jest.fn();
+global.__DEV__ = false;
jest.mock(
'../../core/Engine',
@@ -284,3 +302,13 @@ afterEach(() => {
jest.restoreAllMocks();
global.gc && global.gc(true);
});
+
+global.crypto = {
+ getRandomValues: (arr) => {
+ const uint8Max = 255;
+ for (let i = 0; i < arr.length; i++) {
+ arr[i] = Math.floor(Math.random() * (uint8Max + 1));
+ }
+ return arr;
+ },
+};
diff --git a/app/util/transactions/index.test.ts b/app/util/transactions/index.test.ts
index 6543d6e6be6..92e7f35ca48 100644
--- a/app/util/transactions/index.test.ts
+++ b/app/util/transactions/index.test.ts
@@ -21,7 +21,7 @@ import {
calculateEIP1559Times,
parseTransactionLegacy,
} from '.';
-import { buildUnserializedTransaction } from './optimismTransaction';
+import buildUnserializedTransaction from './optimismTransaction';
import Engine from '../../core/Engine';
import { strings } from '../../../locales/i18n';
diff --git a/app/util/transactions/optimismTransaction.ts b/app/util/transactions/optimismTransaction.ts
index 54c1ce4d79f..09c01665f28 100644
--- a/app/util/transactions/optimismTransaction.ts
+++ b/app/util/transactions/optimismTransaction.ts
@@ -1,10 +1,6 @@
import { omit } from 'lodash';
-import { BN } from 'ethereumjs-util';
-import Common, { Chain, Hardfork } from '@ethereumjs/common';
import { TransactionFactory, TxData } from '@ethereumjs/tx';
-import { stripHexPrefix } from '../../util/address';
-
interface TxDataWithGas extends TxData {
gas: string;
}
@@ -20,30 +16,14 @@ const buildTxParams = (txMeta: TxMeta) => ({
gasLimit: txMeta.txParams.gas,
});
-const buildTransactionCommon = (txMeta: TxMeta) =>
- // This produces a transaction whose information does not completely match an
- // Optimism transaction — for instance, DEFAULT_CHAIN is still 'mainnet' and
- // genesis points to the mainnet genesis, not the Optimism genesis — but
- // considering that all we want to do is serialize a transaction, this works
- // fine for our use case.
- Common.forCustomChain(Chain.Mainnet, {
- chainId: new BN(stripHexPrefix(txMeta.chainId), 16),
- networkId: new BN(txMeta.metamaskNetworkId, 10),
- // Optimism only supports type-0 transactions; it does not support any of
- // the newer EIPs since EIP-155. Source:
- //
- defaultHardfork: Hardfork.SpuriousDragon,
- });
-
/**
* Returns a transaction that can be serialized and fed to an Optimism smart contract.
*
- * @param {Object} txMeta
- * @returns {Object}
+ * @param txMeta - The transaction metadata.
*/
-// eslint-disable-next-line import/prefer-default-export
-export const buildUnserializedTransaction = (txMeta: TxMeta) => {
+const buildUnserializedTransaction = (txMeta: TxMeta) => {
const txParams = buildTxParams(txMeta);
- const common = buildTransactionCommon(txMeta);
- return TransactionFactory.fromTxData(txParams, { common });
+ return TransactionFactory.fromTxData(txParams);
};
+
+export default buildUnserializedTransaction;
diff --git a/app/util/validators/index.js b/app/util/validators/index.js
index dddc41cc44a..596bfd1699b 100644
--- a/app/util/validators/index.js
+++ b/app/util/validators/index.js
@@ -1,5 +1,5 @@
import { ethers } from 'ethers';
-import Encryptor from '../../core/Encryptor';
+import { Encryptor, DERIVATION_PARAMS } from '../../core/Encryptor';
import { regex } from '../regex';
export const failedSeedPhraseRequirements = (seed) => {
@@ -26,7 +26,9 @@ export const parseVaultValue = async (password, vault) => {
seedObject?.iv &&
seedObject?.lib
) {
- const encryptor = new Encryptor();
+ const encryptor = new Encryptor({
+ derivationParams: DERIVATION_PARAMS,
+ });
const result = await encryptor.decrypt(password, vault);
vaultSeed = result[0]?.data?.mnemonic;
}
diff --git a/bitrise.yml b/bitrise.yml
index 9fff778e03b..d1406d6211e 100644
--- a/bitrise.yml
+++ b/bitrise.yml
@@ -15,6 +15,7 @@ pipelines:
stages:
- create_build_release: {}
- deploy_build_release: {}
+ - create_build_qa: {} #Generate QA builds for E2E app upgrade tests
- release_notify: {}
#Releases MetaMask apps and stores ipa into App(TestFlight) Store
release_ios_to_store_pipeline:
@@ -138,6 +139,7 @@ stages:
- run_tag_smoke_swaps_android: {}
- run_tag_smoke_core_ios: {}
- run_tag_smoke_core_android: {}
+ - run_tag_upgrade_android: {}
run_e2e_ios_android_stage:
workflows:
- ios_e2e_test: {}
@@ -416,6 +418,20 @@ workflows:
- TEST_SUITE: 'Regression'
after_run:
- android_e2e_test
+ run_tag_upgrade_android:
+ meta:
+ bitrise.io:
+ stack: linux-docker-android-20.04
+ machine_type_id: elite-xl
+ envs:
+ - PRODUCTION_APP_URL: 'bs://3f81fdb66cba8140909d1ff0a05bc2ace97b307f' # Last production's QA build
+ - PRODUCTION_BUILD_VERSION_NAME: 7.20.0
+ - PRODUCTION_BUILD_VERSION_NUMBER: 1308
+ - CUCUMBER_TAG_EXPRESSION: '@upgrade and @androidApp'
+ - PRODUCTION_BUILD_STRING: 'MetaMask-QA v$PRODUCTION_BUILD_VERSION_NAME ($PRODUCTION_BUILD_VERSION_NUMBER)'
+ - NEW_BUILD_STRING: 'MetaMask-QA v$VERSION_NAME ($VERSION_NUMBER)'
+ after_run:
+ - run_android_appium_test
### Seperating workflows so they run concurrently during smoke runs
run_tag_smoke_confirmations_ios:
@@ -892,6 +908,15 @@ workflows:
bitrise.io:
stack: linux-docker-android-20.04
machine_type_id: elite-xl
+ run_android_appium_test:
+ meta:
+ bitrise.io:
+ stack: linux-docker-android-20.04
+ machine_type_id: elite-xl
+ before_run:
+ - build_android_qa
+ after_run:
+ - wdio_android_e2e_test
build_android_qa:
before_run:
- code_setup
@@ -987,20 +1012,6 @@ workflows:
inputs:
- deploy_path: browserstack_uploaded_apps.json
title: Bitrise Deploy Browserstack Uploaded Apps
- - build-router-start@0:
- inputs:
- - workflows: |-
- wdio_android_e2e_test
- - wait_for_builds: 'false'
- - abort_on_fail: 'yes'
- - access_token: $BITRISE_START_BUILD_ACCESS_TOKEN
- - environment_key_list: BROWSERSTACK_APP_URL
-
- BROWSERSTACK_DEVICE
-
- BROWSERSTACK_OS_VERSION
-
- BROWSERSTACK_TAG_EXPRESSION
wdio_android_e2e_test:
before_run:
- code_setup
@@ -1308,16 +1319,16 @@ app:
PROJECT_LOCATION_IOS: ios
- opts:
is_expand: false
- VERSION_NAME: 7.19.1
+ VERSION_NAME: 7.20.0
- opts:
is_expand: false
- VERSION_NUMBER: 1306
+ VERSION_NUMBER: 1308
- opts:
is_expand: false
- FLASK_VERSION_NAME: 7.19.1
+ FLASK_VERSION_NAME: 7.20.0
- opts:
is_expand: false
- FLASK_VERSION_NUMBER: 1306
+ FLASK_VERSION_NUMBER: 1308
- opts:
is_expand: false
ANDROID_APK_LINK: ''
diff --git a/e2e/fixtures/fixture-builder.js b/e2e/fixtures/fixture-builder.js
index c9cfdf4b869..05e4fab9666 100644
--- a/e2e/fixtures/fixture-builder.js
+++ b/e2e/fixtures/fixture-builder.js
@@ -343,6 +343,7 @@ class FixtureBuilder {
gasFeeEstimates: {},
estimatedGasFeeTimeBounds: {},
gasEstimateType: 'none',
+ gasFeeEstimatesByChainId: {},
},
TokenDetectionController: {},
NftDetectionController: {},
diff --git a/e2e/init.js b/e2e/init.js
index 63763f07630..b191bf8d222 100644
--- a/e2e/init.js
+++ b/e2e/init.js
@@ -7,5 +7,6 @@ import Utilities from './utils/Utilities';
beforeAll(async () => {
device.appLaunchArgs.modify({
detoxURLBlacklistRegex: Utilities.BlacklistURLs,
+ permissions: { notifications: 'YES' },
});
});
diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj
index 02e2ca0881f..b0a51ed9829 100644
--- a/ios/MetaMask.xcodeproj/project.pbxproj
+++ b/ios/MetaMask.xcodeproj/project.pbxproj
@@ -1207,7 +1207,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMaskDebug.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1306;
+ CURRENT_PROJECT_VERSION = 1308;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1244,7 +1244,7 @@
"${inherited}",
);
LLVM_LTO = YES;
- MARKETING_VERSION = 7.19.1;
+ MARKETING_VERSION = 7.20.0;
ONLY_ACTIVE_ARCH = YES;
OTHER_CFLAGS = "$(inherited)";
OTHER_LDFLAGS = (
@@ -1272,7 +1272,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 1306;
+ CURRENT_PROJECT_VERSION = 1308;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
@@ -1307,7 +1307,7 @@
"${inherited}",
);
LLVM_LTO = YES;
- MARKETING_VERSION = 7.19.1;
+ MARKETING_VERSION = 7.20.0;
ONLY_ACTIVE_ARCH = NO;
OTHER_CFLAGS = "$(inherited)";
OTHER_LDFLAGS = (
@@ -1335,7 +1335,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMaskDebug.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1306;
+ CURRENT_PROJECT_VERSION = 1308;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1368,7 +1368,7 @@
);
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift$(inherited)";
LLVM_LTO = YES;
- MARKETING_VERSION = 7.19.1;
+ MARKETING_VERSION = 7.20.0;
ONLY_ACTIVE_ARCH = YES;
OTHER_CFLAGS = "$(inherited)";
OTHER_LDFLAGS = (
@@ -1396,7 +1396,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 1306;
+ CURRENT_PROJECT_VERSION = 1308;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
@@ -1427,7 +1427,7 @@
);
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift$(inherited)";
LLVM_LTO = YES;
- MARKETING_VERSION = 7.19.1;
+ MARKETING_VERSION = 7.20.0;
ONLY_ACTIVE_ARCH = NO;
OTHER_CFLAGS = "$(inherited)";
OTHER_LDFLAGS = (
@@ -1554,7 +1554,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMaskDebug.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1306;
+ CURRENT_PROJECT_VERSION = 1308;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 48XVW22RCG;
@@ -1591,7 +1591,7 @@
"\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"",
);
LLVM_LTO = YES;
- MARKETING_VERSION = 7.19.1;
+ MARKETING_VERSION = 7.20.0;
ONLY_ACTIVE_ARCH = YES;
OTHER_CFLAGS = (
"$(inherited)",
@@ -1622,7 +1622,7 @@
CODE_SIGN_ENTITLEMENTS = MetaMask/MetaMask.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 1306;
+ CURRENT_PROJECT_VERSION = 1308;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 48XVW22RCG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 48XVW22RCG;
@@ -1657,7 +1657,7 @@
"\"$(SRCROOT)/MetaMask/System/Library/Frameworks\"",
);
LLVM_LTO = YES;
- MARKETING_VERSION = 7.19.1;
+ MARKETING_VERSION = 7.20.0;
ONLY_ACTIVE_ARCH = NO;
OTHER_CFLAGS = (
"$(inherited)",
diff --git a/locales/languages/de.json b/locales/languages/de.json
index de8bec0e8c4..2d2b2d944bf 100644
--- a/locales/languages/de.json
+++ b/locales/languages/de.json
@@ -900,7 +900,6 @@
"security_check_subheading": "Sicherheitsprüfungen",
"symbol_required": "Symbol ist erforderlich.",
"blockaid": "Blockaid",
- "blockaid_desc": "Wahrung der Privatsphäre – es werden keine Daten an Dritte weitergegeben. Verfügbar auf Arbitrum, Avalanche, BNB chain, Ethereum Mainnet, Optimism, Polygon und Sepolia.",
"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."
},
@@ -2420,7 +2419,6 @@
"title": "Gehen Sie mit Sicherheitsbenachrichtigungen auf Nummer sicher.",
"description_1": "Mit den Sicherheitswarnungen von Blockaid sind Sie vor bekannten Betrügereien sicher und schützen gleichzeitig Ihre Privatsphäre. Diese Funktion ist jetzt standardmäßig für alle MetaMask-Benutzer aktiviert.",
"description_2": "Gehen Sie immer mit Sorgfalt vor, bevor Sie Anfragen genehmigen.",
- "description_3": "Verfügbar auf Arbitrum, Avalanche, BNB chain, Ethereum Mainnet, Linea, Optimism, Polygon und Sepolia.",
"got_it": "Verstanden"
},
"sell": {
diff --git a/locales/languages/el.json b/locales/languages/el.json
index 019b63578d2..a3a7e6089a5 100644
--- a/locales/languages/el.json
+++ b/locales/languages/el.json
@@ -900,7 +900,6 @@
"security_check_subheading": "Έλεγχοι ασφαλείας",
"symbol_required": "Απαιτείται το σύμβολο.",
"blockaid": "Blockaid",
- "blockaid_desc": "Διαφύλαξη της ιδιωτικής ζωής - δεν κοινοποιούνται δεδομένα σε τρίτους. Διατίθεται στα Arbitrum, Avalanche, BNB chain, Ethereum Mainnet, Optimism, Polygon και Sepolia.",
"security_alerts": "Ειδοποιήσεις ασφαλείας",
"security_alerts_desc": "Αυτή η λειτουργία σας προειδοποιεί για κακόβουλη δραστηριότητα, καθώς ελέγχει τοπικά τα αιτήματα συναλλαγών και υπογραφών σας. Πάντα να κάνετε τη δική σας επιμελή έρευνα προτού εγκρίνετε οποιαδήποτε αιτήματα. Δεν υπάρχει καμία εγγύηση ότι αυτή η λειτουργία θα εντοπίσει όλες τις κακόβουλες δραστηριότητες. Ενεργοποιώντας αυτή τη λειτουργία, συμφωνείτε με τους όρους χρήσης του παρόχου."
},
@@ -2420,7 +2419,6 @@
"title": "Μείνετε ασφαλείς με ειδοποιήσεις ασφαλείας",
"description_1": "Αποφύγετε τις γνωστές απάτες, προστατεύοντας παράλληλα το απόρρητό σας με τις ειδοποιήσεις ασφαλείας που παρέχονται από το Blockaid. Αυτή η λειτουργία είναι πλέον ενεργοποιημένη από προεπιλογή για όλους τους χρήστες του MetaMask.",
"description_2": "Πάντα να προβαίνετε σε δικό σας επιμελή έλεγχο προτού εγκρίνετε τα αιτήματα.",
- "description_3": "Διατίθεται στα Arbitrum, Avalanche, BNB chain, Ethereum Mainnet, Linea, Optimism, Polygon και Sepolia.",
"got_it": "Κατανοητό"
},
"sell": {
diff --git a/locales/languages/en.json b/locales/languages/en.json
index d4a70ee9c40..d6491795fd3 100644
--- a/locales/languages/en.json
+++ b/locales/languages/en.json
@@ -542,7 +542,8 @@
"collectible_asset_contract": "Asset contract",
"share_check_out_nft": "Check out my NFT!",
"share_via": "Shared via",
- "untitled_collection": "Untitled Collection"
+ "untitled_collection": "Untitled Collection",
+ "collection": "Collection"
},
"transfer": {
"title": "Transfer",
@@ -713,6 +714,26 @@
"general_desc": "Currency conversion, primary currency, language and search engine",
"advanced_title": "Advanced",
"advanced_desc": "Access developer features, reset account, setup testnets, state logs, IPFS gateway and custom RPC",
+ "notifications_title": "Notifications",
+ "notifications_desc": "Manage your notifications",
+ "allow_notifications": "Allow notifications",
+ "allow_notifications_desc": "Stay in the loop on what’s happening in your wallet with notifications. To use notifications, we use a profile to sync some settings across your devices. Learn how we protect your privacy while using this feature.",
+ "notifications_opts" :{
+ "customize_session_title": "Customize your notifications",
+ "customize_session_desc": "Turn on the types of notifications you want to receive:",
+ "account_session_title": "Account activity",
+ "account_session_desc": "Select the accounts you want to be notified about:",
+ "assets_sent_title": "Assets Sent",
+ "assets_sent_desc": "Funds and NFT",
+ "assets_received_title": "Assets Received",
+ "assets_received_desc": "Funds and NFT",
+ "defi_title": "DeFi",
+ "defi_desc": "Staking, swapping, and bridging",
+ "snaps_title": "Snaps",
+ "snaps_desc": "New features and updates",
+ "products_announcements_title": "Product announcements",
+ "products_announcements_desc": "New products and features"
+ },
"contacts_title": "Contacts",
"contacts_desc": "Add, edit, remove, and manage your accounts",
"security_title": "Security & Privacy",
@@ -901,7 +922,7 @@
"security_check_subheading": "Security checks",
"symbol_required": "Symbol is required.",
"blockaid": "Blockaid",
- "blockaid_desc": "Privacy preserving - no data is shared with third parties. Available on Arbitrum, Avalanche, BNB chain, Ethereum Mainnet, Optimism, Polygon and Sepolia.",
+ "blockaid_desc": "Privacy preserving - no data is shared with third parties. Available on Arbitrum, Avalanche, BNB chain, Ethereum Mainnet, Optimism, Polygon, Sepolia and Base.",
"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."
},
@@ -1130,6 +1151,7 @@
"confirm": "Confirm"
},
"transaction": {
+ "transaction_id": "Transaction ID",
"alert": "ALERT",
"amount": "Amount",
"next": "Next",
@@ -1411,8 +1433,12 @@
"to": "To",
"details": "Details",
"amount": "Amount",
+ "gas_used": "Gas Used (Units)",
"gas_limit": "Gas Limit (Units)",
"gas_price": "Gas Price (GWEI)",
+ "base_fee": "Base Fee (GWEI)",
+ "priority_fee": "Priority Fee (GWEI)",
+ "max_fee": "Max Fee Per Gas",
"total": "Total",
"view_on": "VIEW ON",
"view_on_etherscan": "View on Etherscan",
@@ -1434,7 +1460,8 @@
"sign_title_device": "with your Keystone",
"sign_description_1": "After you have signed with Keystone,",
"sign_description_2": "tap on Get Signature",
- "sign_get_signature": "Get Signature"
+ "sign_get_signature": "Get Signature",
+ "network_fee": "Network Fee"
},
"address_book": {
"recents": "Recents",
@@ -1783,6 +1810,16 @@
"back_to_safety": "Back to safety"
},
"notifications": {
+ "stake_completed": "Stake completed",
+ "withdrawal_completed": "Withdrawal completed",
+ "unstake_completed": "Unstake completed",
+ "withdrawal_requested": "Withdrawal requested",
+ "stake_ready_to_be_withdrawn": "Stake ready to be withdrawn",
+ "swap_completed": "Swapped {{from}} for {{to}}",
+ "sent": "Sent to {{address}}",
+ "received": "Received from {{address}}",
+ "sent_nft": "Sent NFT to {{address}}",
+ "received_nft": "Received NFT from {{address}}",
"pending_title": "Transaction submitted",
"pending_deposit_title": "Deposit in progress!",
"pending_withdrawal_title": "Withdrawal in progress!",
@@ -1820,7 +1857,30 @@
"wc_signed_failed_title": "This sign request failed",
"wc_sent_tx_rejected_title": "You've rejected the transaction request",
"approved_tx_rejected_title": "You've rejected granting permission",
- "wc_description": "Please check the application"
+ "wc_description": "Please check the application",
+ "wallet": "Wallet",
+ "web3": "Web3",
+ "empty": {
+ "title": "Nothing to see here",
+ "message": "This is where you can find notifications once there’s activity in your wallet. "
+ },
+ "list": {
+ "0": "All",
+ "1": "Wallet",
+ "2": "Annoucements"
+ },
+ "address_copied_to_clipboard": "Address copied to clipboard",
+ "transaction_id_copied_to_clipboard": "Transaction ID copied to clipboard",
+ "activation_card": {
+ "title": "Turn on notifications",
+ "description_1": "Stay in the loop on what's happening in your wallet with notifications. ",
+ "description_2": "To use this feature, we’ll generate an anonymous ID for your account. It’s used only for syncing your data in MetaMask and doesn't link to your activities or other identifiers, ensuring your privacy.",
+ "learn_more": "Learn how we protect your privacy while using this feature. ",
+ "manage_preferences_1": "You can turn off notifications at any time in ",
+ "manage_preferences_2": "Settings > Notifications.",
+ "cancel": "Cancel",
+ "cta": "Turn on"
+ }
},
"protect_your_wallet_modal": {
"title": "Protect your wallet",
@@ -2421,7 +2481,7 @@
"title": "Stay safe with security alerts",
"description_1": "Steer safe from known scams while still protecting your privacy with security alerts powered by Blockaid. This feature is now on by default for all MetaMask users.",
"description_2": "Always do your own due diligence before approving requests.",
- "description_3": "Available on Arbitrum, Avalanche, BNB chain, Ethereum Mainnet, Linea, Optimism, Polygon and Sepolia.",
+ "description_3": "Available on Arbitrum, Avalanche, BNB chain, Ethereum Mainnet, Linea, Optimism, Polygon, Sepolia and Base.",
"got_it": "Got it"
},
"sell": {
diff --git a/locales/languages/es.json b/locales/languages/es.json
index 105581c5cae..4c073fd90ed 100644
--- a/locales/languages/es.json
+++ b/locales/languages/es.json
@@ -900,7 +900,6 @@
"security_check_subheading": "Verificaciones de seguridad",
"symbol_required": "Se requiere símbolo.",
"blockaid": "Blockaid",
- "blockaid_desc": "Protección de la privacidad: no se comparten datos con terceros. Disponible en Arbitrum, Avalanche, BNB Chain, la red principal de Ethereum, Optimism, Polygon y Sepolia.",
"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."
},
@@ -2420,7 +2419,6 @@
"title": "Manténgase seguro con alertas de seguridad",
"description_1": "Manténgase a salvo de estafas conocidas y al mismo tiempo proteja su privacidad con alertas de seguridad impulsadas por Blockaid. Esta función ahora está activada de forma predeterminada para todos los usuarios de MetaMask.",
"description_2": "Siempre asegúrese de hacer su propia diligencia debida antes de aprobar cualquier solicitud.",
- "description_3": "Disponible en Arbitrum, Avalanche, BNB Chain, la red principal de Ethereum, Linea, Optimism, Polygon y Sepolia.",
"got_it": "Entendido"
},
"sell": {
diff --git a/locales/languages/fr.json b/locales/languages/fr.json
index 3ad41d754c2..9bb3bedf5a7 100644
--- a/locales/languages/fr.json
+++ b/locales/languages/fr.json
@@ -900,7 +900,6 @@
"security_check_subheading": "Contrôles de sécurité",
"symbol_required": "Le symbole est requis.",
"blockaid": "Blockaid",
- "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 et le réseau principal Ethereum.",
"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."
},
@@ -2420,7 +2419,6 @@
"title": "Restez en sécurité grâce aux alertes de sécurité",
"description_1": "Évitez les arnaques les plus courantes et protégez vos données privées grâce aux alertes de sécurité fournies par Blockaid. Cette fonctionnalité est désormais activée par défaut pour tous les utilisateurs de MetaMask.",
"description_2": "Vous devez faire preuve de diligence raisonnable avant d’approuver toute demande.",
- "description_3": "Disponible sur Arbitrum, Avalanche, BNB chain, Linea, Optimism, Polygon, Sepolia et le réseau principal Ethereum.",
"got_it": "D’accord"
},
"sell": {
diff --git a/locales/languages/hi.json b/locales/languages/hi.json
index 512dd5dba50..0d6c1606b89 100644
--- a/locales/languages/hi.json
+++ b/locales/languages/hi.json
@@ -900,7 +900,6 @@
"security_check_subheading": "सुरक्षा जाँचें",
"symbol_required": "सिंबल की ज़रूरत है।",
"blockaid": "Blockaid",
- "blockaid_desc": "गोपनीयता को सुरक्षित रखना - कोई भी डेटा थर्ड पार्टी के साथ साझा नहीं किया जाता है। Arbitrum, Avalanche, BNB chain, Ethereum Mainnet, Optimism, Polygon और Sepolia पर उपलब्ध है।",
"security_alerts": "सुरक्षा एलर्ट",
"security_alerts_desc": "यह सुविधा स्थानीय रूप से आपके ट्रांसेक्शन और हस्ताक्षर अनुरोधों की समीक्षा करके आपको बुरी नीयत वाली गतिविधि के प्रति एलर्ट करती है। किसी भी अनुरोध को मंजूरी देने से पहले हमेशा पूरी जांच-पड़ताल ज़रूर करें। इस बात की कोई गारंटी नहीं है कि यह सुविधा सभी बुरी नीयत वाली गतिविधि का पता लगा लेगी। इस सुविधा को सक्षम करके आप प्रदाता की उपयोग की शर्तों से सहमत होते हैं।"
},
@@ -2420,7 +2419,6 @@
"title": "सुरक्षा चेतावनियों के माध्यम से सुरक्षित रहें",
"description_1": "ब्लॉकएड द्वारा संचालित सिक्योरिटी अलर्ट के साथ अपनी गोपनीयता की रक्षा करते हुए पहचाने गए स्कैम से सुरक्षित रहें। यह सुविधा अब सभी MetaMask उपयोगकर्ताओं के लिए डिफ़ॉल्ट रूप से चालू है।",
"description_2": "किसी भी रिक्वेस्ट को मंजूरी देने से पहले हमेशा पूरी जांच-पड़ताल ज़रूर करें।",
- "description_3": "Arbitrum, Avalanche, BNB chain, Ethereum Mainnet, Linea, Optimism, Polygon और Sepolia पर उपलब्ध है।",
"got_it": "समझ गए"
},
"sell": {
diff --git a/locales/languages/id.json b/locales/languages/id.json
index 08ed94813e6..1da1eb83972 100644
--- a/locales/languages/id.json
+++ b/locales/languages/id.json
@@ -900,7 +900,6 @@
"security_check_subheading": "Pemeriksaan keamanan",
"symbol_required": "Simbol wajib diisi.",
"blockaid": "Blockaid",
- "blockaid_desc": "Menjaga privasi - tidak ada data yang dibagikan kepada pihak ketiga. Tersedia di Arbitrum, Avalanche, BNB chain, Mainnet Ethereum, Optimism, Polygon, dan Sepolia.",
"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."
},
@@ -2420,7 +2419,6 @@
"title": "Tetap aman bersama peringatan keamanan",
"description_1": "Hindari penipuan yang diketahui sambil tetap melindungi privasi Anda bersama peringatan keamanan yang didukung oleh Blockaid. Saat ini fitur ini diaktifkan secara default untuk semua pengguna MetaMask.",
"description_2": "Selalu lakukan uji tuntas sendiri sebelum menyetujui permintaan.",
- "description_3": "Tersedia di Arbitrum, Avalanche, BNB chain, Mainnet Ethereum, Linea, Optimism, Polygon, dan Sepolia.",
"got_it": "Mengerti"
},
"sell": {
diff --git a/locales/languages/ja.json b/locales/languages/ja.json
index fb54d874ae9..e6795227f27 100644
--- a/locales/languages/ja.json
+++ b/locales/languages/ja.json
@@ -900,7 +900,6 @@
"security_check_subheading": "セキュリティチェック",
"symbol_required": "シンボルが必要です",
"blockaid": "Blockaid",
- "blockaid_desc": "プライバシーを保護 - サードパーティとデータが一切共有されません。Arbitrum、Avalanche、BNB Chain、イーサリアムメインネット、Optimism、Polygon、Sepoliaで利用可能。",
"security_alerts": "セキュリティアラート",
"security_alerts_desc": "この機能は、トランザクションと署名要求をローカルで確認することで、悪質な行為に関するアラートを発します。要求を承認する前に、必ず独自のデューデリジェンスを行ってください。この機能がすべての悪質な行為を検出するという保証はありません。この機能を有効にすることで、プロバイダーの利用規約に同意したものとみなされます。"
},
@@ -2420,7 +2419,6 @@
"title": "セキュリティアラートで安全を確保",
"description_1": "Blockaidのセキュリティアラートで、プライバシーを確保しつつ既知の詐欺から身を守ります。この機能は、MetaMaskユーザー全員を対象にデフォルトでオンになります。",
"description_2": "要求を承認する前に、必ず独自のデューデリジェンスを行ってください。",
- "description_3": "Arbitrum、Avalanche、BNB Chain、イーサリアムメインネット、Linea、Optimism、Polygon、Sepoliaで利用できます。",
"got_it": "了解"
},
"sell": {
diff --git a/locales/languages/ko.json b/locales/languages/ko.json
index a736b14a913..b5dc2c2708b 100644
--- a/locales/languages/ko.json
+++ b/locales/languages/ko.json
@@ -900,7 +900,6 @@
"security_check_subheading": "보안 점검",
"symbol_required": "심볼은 필수입니다.",
"blockaid": "Blockaid",
- "blockaid_desc": "개인정보 보호 - 제3자와 데이터를 공유하지 않습니다. Arbitrum, Avalanche, BNB chain, 이더리움 메인넷, Optimism, Polygon, Sepolia에서 사용할 수 있습니다.",
"security_alerts": "보안 경고",
"security_alerts_desc": "이 기능은 트랜잭션 및 서명 요청을 로컬에서 검토하여 악의적인 활동이 있는 경우 경고합니다. 요청을 승인하기 전에 항상 직접 검토하세요. 이 기능이 모든 악성 활동을 탐지하는 것은 아닙니다. 이 기능을 활성화하면 제공 업체의 이용 약관에 동의하는 것이 됩니다."
},
@@ -2420,7 +2419,6 @@
"title": "보안 경고로 안전성을 유지하세요",
"description_1": "Blockaid가 제공하는 보안 경고를 통해 개인정보를 보호하고 사기도 방지하세요. 이 기능은 이제 모든 MetaMask 사용자에게 기본적으로 적용됩니다.",
"description_2": "요청을 승인하기 전에 항상 직접 확인하세요.",
- "description_3": "Arbitrum, Avalanche, BNB chain, 이더리움 메인넷, Linea, Optimism, Polygon, Sepolia에서 사용할 수 있습니다.",
"got_it": "확인"
},
"sell": {
diff --git a/locales/languages/pt.json b/locales/languages/pt.json
index 9689d7b463c..ac6d9cefa4c 100644
--- a/locales/languages/pt.json
+++ b/locales/languages/pt.json
@@ -900,7 +900,6 @@
"security_check_subheading": "Verificações de segurança",
"symbol_required": "O símbolo é obrigatório.",
"blockaid": "Blockaid",
- "blockaid_desc": "Proteção de privacidade: nenhum dado é compartilhado com terceiros. Disponível em Arbitrum, Avalanche, BNB Chain, Mainnet da Ethereum, Optimism, Polygon e Sepolia.",
"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."
},
@@ -2420,7 +2419,6 @@
"title": "Fique protegido com os alertas de segurança",
"description_1": "Fique protegido contra golpes conhecidos enquanto resguarda sua privacidade com os alertas de segurança da Blockaid. Esse recurso agora é ativado por padrão para todos os usuários da MetaMask.",
"description_2": "Sempre faça sua própria devida diligência antes de aprovar solicitações.",
- "description_3": "Disponível em Arbitrum, Avalanche, BNB Chain, Mainnet da Ethereum, Linea, Optimism, Polygon e Sepolia.",
"got_it": "Entendi"
},
"sell": {
diff --git a/locales/languages/ru.json b/locales/languages/ru.json
index 5124895a5a6..e7f071dc300 100644
--- a/locales/languages/ru.json
+++ b/locales/languages/ru.json
@@ -900,7 +900,6 @@
"security_check_subheading": "Проверки безопасности",
"symbol_required": "Требуется символ.",
"blockaid": "Blockaid",
- "blockaid_desc": "Сохранение конфиденциальности – никакие данные не передаются третьим сторонам. Доступно в Arbitrum, Avalanche, BNB Chain, Мейн-нете Ethereum, Optimism, Polygon и Sepolia.",
"security_alerts": "Оповещения безопасности",
"security_alerts_desc": "Эта функция предупреждает вас о вредоносной активности, проверяя запросы транзакций и подписей локально. Всегда проводите комплексную проверку перед утверждением каких-либо запросов. Нет никакой гарантии, что эта функция обнаружит всю вредоносную активность. Включая эту функцию, вы соглашаетесь с условиями использования поставщика."
},
@@ -2420,7 +2419,6 @@
"title": "Избегайте угроз с помощью оповещений безопасности",
"description_1": "Защитите себя от известных способов мошенничества, сохраняя при этом свою конфиденциальность с помощью предупреждений безопасности от Blockaid. Эта функция теперь включена по умолчанию для всех пользователей MetaMask.",
"description_2": "Всегда проводите собственную комплексную проверку перед утверждением запросов.",
- "description_3": "Доступно в Arbitrum, Avalanche, BNB Chain, Мейн-нете Ethereum, Linea, Optimism, Polygon и Sepolia.",
"got_it": "Понятно"
},
"sell": {
diff --git a/locales/languages/tl.json b/locales/languages/tl.json
index b60b745dd35..952a15b32c6 100644
--- a/locales/languages/tl.json
+++ b/locales/languages/tl.json
@@ -900,7 +900,6 @@
"security_check_subheading": "Pagsusuring panseguridad",
"symbol_required": "Kailangan ang simbolo.",
"blockaid": "Blockaid",
- "blockaid_desc": "Pagpapanatili ng pagkapribado - walang data na ibinahagi sa mga third party. Available sa Arbitrum, Avalanche, BNB chain, Ethereum Mainnet, Optimism Polygon, at Sepolia.",
"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."
},
@@ -2420,7 +2419,6 @@
"title": "Manatiling ligtas gamit ang mga alerto sa seguridad",
"description_1": "Manatiling ligtas sa mga scam habang patuloy na pinoprotektahan ang iyong pagkapribado sa pamamagitan ng mga alerto sa seguridad na pinatatakbo ng Blockaid. Ang tampok na ito ay naka-on nang default para sa lahat ng user ng MetaMask.",
"description_2": "Palaging gumawa ng sarili mong pagsusuri bago aprubahan ang mga kahilingan.",
- "description_3": "Available sa Arbitrum, Avalanche, BNB chain, Ethereum Mainnet, Linea, Optimism, Polygon at Sepolia.",
"got_it": "Nakuha ko"
},
"sell": {
diff --git a/locales/languages/tr.json b/locales/languages/tr.json
index 4f245e84218..aaa5d342426 100644
--- a/locales/languages/tr.json
+++ b/locales/languages/tr.json
@@ -900,7 +900,6 @@
"security_check_subheading": "Güvenlik kontrolleri",
"symbol_required": "Sembol gerekli.",
"blockaid": "Blockaid",
- "blockaid_desc": "Gizlilik koruması - Hiçbir veri üçüncü taraflarla paylaşılmaz. Arbitrum, Avalanche, BNB chain, Ethereum Ana Ağı, Optimism, Polygon ve Sepolia için sunulur.",
"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."
},
@@ -2420,7 +2419,6 @@
"title": "Güvenlik uyarıları ile güvende kalın",
"description_1": "Blockaid tarafından desteklenen güvenlik uyarıları ile gizliliğinizi korumaya devam ederken bilinen dolandırıcılıklardan güvenli bir şekilde uzakta kalın. Bu özellik şu anda tüm MetaMask kullanıcıları için varsayılan olarak açıktır.",
"description_2": "Talepleri onaylamadan önce kendiniz her zaman gerekli özeni gösterin.",
- "description_3": "Arbitrum, Avalanche, BNB chain, Ethereum Mainnet, Linea, Optimism, Polygon ve Sepolia için sunulur.",
"got_it": "Anladım"
},
"sell": {
diff --git a/locales/languages/vi.json b/locales/languages/vi.json
index db67ede16e8..5948ec9f929 100644
--- a/locales/languages/vi.json
+++ b/locales/languages/vi.json
@@ -900,7 +900,6 @@
"security_check_subheading": "Kiểm tra bảo mật",
"symbol_required": "Ký hiệu là bắt buộc.",
"blockaid": "Blockaid",
- "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 và Sepolia.",
"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."
},
@@ -2420,7 +2419,6 @@
"title": "Đảm bảo an toàn với cảnh báo bảo mật",
"description_1": "Đảm bảo an toàn trước các hành vi lừa đảo đã biết, đồng thời vẫn bảo vệ quyền riêng tư của bạn với các cảnh báo bảo mật do Blockaid cung cấp. Tính năng này hiện được bật theo mặc định dành cho tất cả người dùng MetaMask.",
"description_2": "Luôn tự thẩm định trước khi chấp thuận các yêu cầu.",
- "description_3": "Có sẵn trên Arbitrum, Avalanche, BNB chain, Ethereum Mainnet, Linea, Optimism, Polygon và Sepolia.",
"got_it": "Đã hiểu"
},
"sell": {
diff --git a/locales/languages/zh.json b/locales/languages/zh.json
index ba7b15914ab..bbe6d2dc0dd 100644
--- a/locales/languages/zh.json
+++ b/locales/languages/zh.json
@@ -900,7 +900,6 @@
"security_check_subheading": "安全检查",
"symbol_required": "需要符号。",
"blockaid": "Blockaid",
- "blockaid_desc": "隐私保护 - 不会与第三方共享任何数据。适用于 Arbitrum、Avalanche、BNB Chain、以太坊主网、Optimism、Polygon 和 Sepolia。",
"security_alerts": "安全提醒",
"security_alerts_desc": "此功能通过本地审查您的交易和签名请求来提醒您注意恶意活动。在批准任何请求之前,请务必自行进行审慎调查。无法保证此功能能够检测到所有恶意活动。启用此功能即表示您同意提供商的使用条款。"
},
@@ -2420,7 +2419,6 @@
"title": "安全警报助您确保安全",
"description_1": "通过由 Blockaid 提供的安全提醒避免受到已知欺诈,同时保护您的隐私。目前所有 MetaMask 用户默认启用此功能。",
"description_2": "在批准请求之前,必须自行作出审慎调查。",
- "description_3": "适用于 Arbitrum、Avalanche、BNB Chain、以太坊主网、Linea、Optimism、Polygon 和 Sepolia。",
"got_it": "知道了"
},
"sell": {
diff --git a/package.json b/package.json
index edfa342a2e0..b403554b537 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "metamask",
- "version": "7.19.1",
+ "version": "7.20.0",
"private": true,
"scripts": {
"audit:ci": "./scripts/yarn-audit.sh",
@@ -121,14 +121,14 @@
"axios": "1.6.0",
"react-native-ble-plx": "3.1.2",
"**/babel-runtime/regenerator-runtime": "^0.13.8",
- "@metamask/transaction-controller/@metamask/controller-utils": "^5.0.0"
-
+ "@metamask/transaction-controller/@metamask/controller-utils": "^5.0.0",
+ "@spruceid/siwe-parser": "2.1.0",
+ "apg-js": "4.2.0"
},
"dependencies": {
"@consensys/ledgerhq-metamask-keyring": "0.0.9",
"@consensys/on-ramp-sdk": "1.26.8",
"@eth-optimism/contracts": "0.0.0-2021919175625",
- "@ethereumjs/common": "^2.3.1",
"@ethereumjs/tx": "^3.2.1",
"@ethersproject/abi": "^5.7.0",
"@exodus/react-native-payments": "git+https://github.com/MetaMask/react-native-payments.git#dbc8cbbed570892d2fea5e3d183bf243e062c1e5",
@@ -147,18 +147,19 @@
"@metamask/design-tokens": "^2.0.0",
"@metamask/eth-sig-util": "^4.0.1",
"@metamask/etherscan-link": "^2.0.0",
- "@metamask/gas-fee-controller": "6.1.2",
+ "@metamask/gas-fee-controller": "^10.0.0",
"@metamask/key-tree": "^9.0.0",
"@metamask/keyring-api": "^4.0.0",
- "@metamask/keyring-controller": "^8.1.0",
+ "@metamask/keyring-controller": "^9.0.0",
"@metamask/logging-controller": "^1.0.1",
- "@metamask/network-controller": "13.0.1",
+ "@metamask/network-controller": "^15.0.0",
"@metamask/permission-controller": "7.1.0",
- "@metamask/phishing-controller": "6.0.1",
+ "@metamask/phishing-controller": "^8.0.0",
"@metamask/post-message-stream": "8.0.0",
"@metamask/ppom-validator": "0.29.0",
"@metamask/preferences-controller": "^4.0.0",
"@metamask/react-native-button": "^3.0.0",
+ "@metamask/rpc-errors": "^6.2.1",
"@metamask/scure-bip39": "^2.1.0",
"@metamask/sdk-communication-layer": "^0.18.1",
"@metamask/signature-controller": "6.0.0",
@@ -208,17 +209,16 @@
"cockatiel": "^3.1.2",
"compare-versions": "^3.6.0",
"content-hash": "2.5.2",
+ "contentful": "^10.8.7",
"crypto-js": "^4.2.0",
"d3-shape": "^3.2.0",
"dnode": "1.2.2",
"eciesjs": "^0.3.15",
"eth-block-tracker": "^7.0.1",
"eth-ens-namehash": "2.0.8",
- "eth-json-rpc-errors": "^2.0.2",
"eth-json-rpc-filters": "4.2.2",
"eth-json-rpc-infura": "5.1.0",
"eth-json-rpc-middleware": "4.3.0",
- "eth-rpc-errors": "^4.0.3",
"eth-url-parser": "1.0.4",
"ethereumjs-abi": "0.6.6",
"ethereumjs-util": "6.1.0",
diff --git a/patches/@metamask+eth-keyring-controller+13.0.1.patch b/patches/@metamask+eth-keyring-controller+15.1.0.patch
similarity index 77%
rename from patches/@metamask+eth-keyring-controller+13.0.1.patch
rename to patches/@metamask+eth-keyring-controller+15.1.0.patch
index 1aed5a46e8f..8bc3bb7b32a 100644
--- a/patches/@metamask+eth-keyring-controller+13.0.1.patch
+++ b/patches/@metamask+eth-keyring-controller+15.1.0.patch
@@ -1,12 +1,12 @@
diff --git a/node_modules/@metamask/eth-keyring-controller/dist/KeyringController.js b/node_modules/@metamask/eth-keyring-controller/dist/KeyringController.js
-index 3644209..027666c 100644
+index 3f70b51..f2d99d0 100644
--- a/node_modules/@metamask/eth-keyring-controller/dist/KeyringController.js
+++ b/node_modules/@metamask/eth-keyring-controller/dist/KeyringController.js
-@@ -637,6 +637,19 @@ class KeyringController extends events_1.EventEmitter {
+@@ -651,6 +651,19 @@ class KeyringController extends events_1.EventEmitter {
serializedKeyrings.push(...this.unsupportedKeyrings);
let vault;
let newEncryptionKey;
-+
++
+ /**
+ * ============================== PATCH INFORMATION ==============================
+ * The HD keyring is the default keyring for all wallets if this keyring is missing
@@ -19,6 +19,6 @@ index 3644209..027666c 100644
+ throw new Error(`HD keyring missing while saving keyrings - the available types are [${types}]`);
+ }
+
- if (this.cacheEncryptionKey) {
- if (this.password) {
- const { vault: newVault, exportedKeyString } = await this.encryptor.encryptWithDetail(this.password, serializedKeyrings);
+ if (__classPrivateFieldGet(this, _KeyringController_cacheEncryptionKey, "f")) {
+ assertIsExportableKeyEncryptor(__classPrivateFieldGet(this, _KeyringController_encryptor, "f"));
+ if (encryptionKey) {
diff --git a/patches/@metamask+keyring-controller+8.1.0.patch b/patches/@metamask+keyring-controller+9.0.0.patch
similarity index 95%
rename from patches/@metamask+keyring-controller+8.1.0.patch
rename to patches/@metamask+keyring-controller+9.0.0.patch
index 2373eba1e7e..fb5bc2538ee 100644
--- a/patches/@metamask+keyring-controller+8.1.0.patch
+++ b/patches/@metamask+keyring-controller+9.0.0.patch
@@ -1,8 +1,8 @@
diff --git a/node_modules/@metamask/keyring-controller/dist/KeyringController.js b/node_modules/@metamask/keyring-controller/dist/KeyringController.js
-index 08a8714..a096388 100644
+index da9523a..2cb136d 100644
--- a/node_modules/@metamask/keyring-controller/dist/KeyringController.js
+++ b/node_modules/@metamask/keyring-controller/dist/KeyringController.js
-@@ -773,15 +773,25 @@ class KeyringController extends base_controller_1.BaseControllerV2 {
+@@ -785,15 +785,25 @@ class KeyringController extends base_controller_1.BaseControllerV2 {
return (yield __classPrivateFieldGet(this, _KeyringController_keyring, "f").getKeyringForAccount(account)).type;
});
}
diff --git a/patches/@metamask+network-controller+13.0.1.patch b/patches/@metamask+network-controller+15.2.0.patch
similarity index 84%
rename from patches/@metamask+network-controller+13.0.1.patch
rename to patches/@metamask+network-controller+15.2.0.patch
index e2afbc7e9f4..78aa30bff20 100644
--- a/patches/@metamask+network-controller+13.0.1.patch
+++ b/patches/@metamask+network-controller+15.2.0.patch
@@ -1,16 +1,5 @@
-diff --git a/node_modules/@metamask/network-controller/dist/.patch.txt b/node_modules/@metamask/network-controller/dist/.patch.txt
-new file mode 100644
-index 0000000..9322b99
---- /dev/null
-+++ b/node_modules/@metamask/network-controller/dist/.patch.txt
-@@ -0,0 +1,5 @@
-+PATCH GENERATED on NetwokController.js is needed because
-+controller in general are not prepared to be destroyed.
-+We are still considering the approach to take.
-+If it make sense the controllers be able to be destroyed or
-+refactor the vault corruption flow, without destroy all Engine instance
diff --git a/node_modules/@metamask/network-controller/dist/NetworkController.d.ts b/node_modules/@metamask/network-controller/dist/NetworkController.d.ts
-index 577683a..e928606 100644
+index 4f7faf6..78c3a46 100644
--- a/node_modules/@metamask/network-controller/dist/NetworkController.d.ts
+++ b/node_modules/@metamask/network-controller/dist/NetworkController.d.ts
@@ -69,7 +69,7 @@ export declare type NetworkConfiguration = {
@@ -23,7 +12,7 @@ index 577683a..e928606 100644
}>;
/**
diff --git a/node_modules/@metamask/network-controller/dist/NetworkController.js b/node_modules/@metamask/network-controller/dist/NetworkController.js
-index e740dc7..20caba0 100644
+index 4bce7f5..405b954 100644
--- a/node_modules/@metamask/network-controller/dist/NetworkController.js
+++ b/node_modules/@metamask/network-controller/dist/NetworkController.js
@@ -22,7 +22,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
@@ -43,15 +32,15 @@ index e740dc7..20caba0 100644
_NetworkController_autoManagedNetworkClientRegistry.set(this, void 0);
if (!infuraProjectId || typeof infuraProjectId !== 'string') {
throw new Error('Invalid Infura project ID');
-@@ -248,6 +249,7 @@ class NetworkController extends base_controller_1.BaseControllerV2 {
- });
- this.messagingSystem.registerActionHandler(`${this.name}:getNetworkClientById`, this.getNetworkClientById.bind(this));
+@@ -252,6 +253,7 @@ class NetworkController extends base_controller_1.BaseControllerV2 {
+ this.messagingSystem.registerActionHandler(`${this.name}:setProviderType`, this.setProviderType.bind(this));
+ this.messagingSystem.registerActionHandler(`${this.name}:findNetworkClientIdByChainId`, this.findNetworkClientIdByChainId.bind(this));
__classPrivateFieldSet(this, _NetworkController_previousProviderConfig, this.state.providerConfig, "f");
+ __classPrivateFieldSet(this, _NetworkController_destroyed, false, "f");
}
/**
* Accesses the provider and block tracker for the currently selected network.
-@@ -434,7 +436,7 @@ class NetworkController extends base_controller_1.BaseControllerV2 {
+@@ -438,7 +440,7 @@ class NetworkController extends base_controller_1.BaseControllerV2 {
updatedNetworkStatus = constants_1.NetworkStatus.Unknown;
}
}
@@ -60,7 +49,7 @@ index e740dc7..20caba0 100644
// If the network has changed, then `lookupNetwork` either has been or is
// in the process of being called, so we don't need to go further.
return;
-@@ -710,6 +712,7 @@ class NetworkController extends base_controller_1.BaseControllerV2 {
+@@ -715,6 +717,7 @@ class NetworkController extends base_controller_1.BaseControllerV2 {
destroy() {
var _a;
return __awaiter(this, void 0, void 0, function* () {
@@ -68,12 +57,12 @@ index e740dc7..20caba0 100644
yield ((_a = __classPrivateFieldGet(this, _NetworkController_blockTrackerProxy, "f")) === null || _a === void 0 ? void 0 : _a.destroy());
});
}
-@@ -740,7 +743,7 @@ class NetworkController extends base_controller_1.BaseControllerV2 {
+@@ -745,7 +748,7 @@ class NetworkController extends base_controller_1.BaseControllerV2 {
}
}
exports.NetworkController = NetworkController;
-_NetworkController_ethQuery = new WeakMap(), _NetworkController_infuraProjectId = new WeakMap(), _NetworkController_trackMetaMetricsEvent = new WeakMap(), _NetworkController_previousProviderConfig = new WeakMap(), _NetworkController_providerProxy = new WeakMap(), _NetworkController_provider = new WeakMap(), _NetworkController_blockTrackerProxy = new WeakMap(), _NetworkController_autoManagedNetworkClientRegistry = new WeakMap(), _NetworkController_instances = new WeakSet(), _NetworkController_refreshNetwork = function _NetworkController_refreshNetwork() {
-+_NetworkController_ethQuery = new WeakMap(), _NetworkController_infuraProjectId = new WeakMap(), _NetworkController_trackMetaMetricsEvent = new WeakMap(), _NetworkController_previousProviderConfig = new WeakMap(), _NetworkController_providerProxy = new WeakMap(), _NetworkController_provider = new WeakMap(), _NetworkController_blockTrackerProxy = new WeakMap(), _NetworkController_destroyed = new WeakMap(), _NetworkController_autoManagedNetworkClientRegistry = new WeakMap(), _NetworkController_instances = new WeakSet(), _NetworkController_refreshNetwork = function _NetworkController_refreshNetwork() {
++_NetworkController_ethQuery = new WeakMap(), _NetworkController_infuraProjectId = new WeakMap(), _NetworkController_trackMetaMetricsEvent = new WeakMap(), _NetworkController_previousProviderConfig = new WeakMap(), _NetworkController_providerProxy = new WeakMap(), _NetworkController_destroyed = new WeakMap(), _NetworkController_provider = new WeakMap(), _NetworkController_blockTrackerProxy = new WeakMap(), _NetworkController_autoManagedNetworkClientRegistry = new WeakMap(), _NetworkController_instances = new WeakSet(), _NetworkController_refreshNetwork = function _NetworkController_refreshNetwork() {
return __awaiter(this, void 0, void 0, function* () {
this.messagingSystem.publish('NetworkController:networkWillChange');
__classPrivateFieldGet(this, _NetworkController_instances, "m", _NetworkController_applyNetworkSelection).call(this);
diff --git a/storybook/storyLoader.js b/storybook/storyLoader.js
index e8e6d81cdc7..d9331573e9a 100644
--- a/storybook/storyLoader.js
+++ b/storybook/storyLoader.js
@@ -16,6 +16,7 @@ function loadStories() {
require('../app/component-library/components/Badges/Badge/Badge.stories');
require('../app/component-library/components/Badges/Badge/variants/BadgeNetwork/BadgeNetwork.stories');
require('../app/component-library/components/Badges/Badge/variants/BadgeStatus/BadgeStatus.stories');
+ require('../app/component-library/components/Badges/Badge/variants/BadgeNotifications/BadgeNotifications.stories');
require('../app/component-library/components/Badges/BadgeWrapper/BadgeWrapper.stories');
require('../app/component-library/components/Banners/Banner/Banner.stories');
require('../app/component-library/components/Banners/Banner/variants/BannerAlert/BannerAlert.stories');
diff --git a/wdio.conf.js b/wdio.conf.js
index 14f218656f6..f8039c174f7 100644
--- a/wdio.conf.js
+++ b/wdio.conf.js
@@ -160,7 +160,18 @@ export const config = {
// your test setup with almost no effort. Unlike plugins, they don't add new
// commands. Instead, they hook themselves up into the test process.
/** services: ['chromedriver','appium'], ***/
- services: ['appium'],
+ services: [
+ [
+ 'appium',
+ {
+ args: {
+ address: 'localhost',
+ port: 4723
+ },
+ logPath: './'
+ }
+ ]
+ ],
// Appium service with custom chrome driver path
/*services: [
diff --git a/wdio/config/android.config.browserstack.js b/wdio/config/android.config.browserstack.js
index 92f8a591fc4..66209e38ccd 100644
--- a/wdio/config/android.config.browserstack.js
+++ b/wdio/config/android.config.browserstack.js
@@ -19,9 +19,10 @@ config.capabilities = [
build: 'Android QA E2E Smoke Tests',
device: process.env.BROWSERSTACK_DEVICE || 'Google Pixel 6',
os_version: process.env.BROWSERSTACK_OS_VERSION || '12.0',
- app: process.env.BROWSERSTACK_APP_URL,
+ app: process.env.PRODUCTION_APP_URL || process.env.BROWSERSTACK_APP_URL,
'browserstack.debug': true,
'browserstack.local': true,
+ 'browserstack.midSessionInstallApps' : [process.env.BROWSERSTACK_APP_URL],
},
];
@@ -29,7 +30,7 @@ config.waitforTimeout = 10000;
config.connectionRetryTimeout = 90000;
config.connectionRetryCount = 3;
config.cucumberOpts.tagExpression =
- process.env.BROWSERSTACK_TAG_EXPRESSION || '@performance and @androidApp'; // pass tag to run tests specific to android
+ process.env.CUCUMBER_TAG_EXPRESSION || '@performance and @androidApp'; // pass tag to run tests specific to android
config.onPrepare = function (config, capabilities) {
removeSync('./wdio/reports');
diff --git a/wdio/config/android.config.browserstack.local.js b/wdio/config/android.config.browserstack.local.js
index 5c39d43df44..0240bb84409 100644
--- a/wdio/config/android.config.browserstack.local.js
+++ b/wdio/config/android.config.browserstack.local.js
@@ -18,9 +18,10 @@ config.capabilities = [
build: 'Android QA E2E Tests',
device: 'Google Pixel 3a',
os_version: '9.0',
- app: process.env.BROWSERSTACK_APP_URL, // TODO: Add package ID when upload to BrowserStack
+ app: process.env.PRODUCTION_APP_URL || process.env.BROWSERSTACK_APP_URL,
'browserstack.debug': true,
'browserstack.local': true,
+ 'browserstack.midSessionInstallApps' : [process.env.BROWSERSTACK_APP_URL],
},
];
diff --git a/wdio/features/Performance/UpgradeApp.feature b/wdio/features/Performance/UpgradeApp.feature
new file mode 100644
index 00000000000..28c7529ccae
--- /dev/null
+++ b/wdio/features/Performance/UpgradeApp.feature
@@ -0,0 +1,25 @@
+@androidApp
+@upgrade
+@fixturesSkipOnboarding
+Feature: Measure the app launch times for warm starts
+
+ Scenario: Measure warm start launch time after importing a wallet
+ Given the app displayed the splash animation
+ And I have imported my wallet
+ And I tap No Thanks on the Enable security check screen
+ And I tap No thanks on the onboarding welcome tutorial
+ And I close the Whats New modal
+ And I am on the wallet view
+ When I tap on the Settings tab option
+ And I scroll up
+ And In settings I tap on "About MetaMask"
+ Then version "PRODUCTION_BUILD_STRING" is displayed for app upgrade step
+ When I install upgrade the app
+ And I relaunch the app
+ And the splash animation completes
+ And I fill my password in the Login screen
+ And I log into my wallet
+ And I tap on the Settings tab option
+ And In settings I tap on "About MetaMask"
+ Then version "NEW_BUILD_STRING" is displayed for app upgrade step
+ And removed test app
diff --git a/wdio/helpers/Gestures.js b/wdio/helpers/Gestures.js
index 2ba2f1e79bf..39ce51feb84 100644
--- a/wdio/helpers/Gestures.js
+++ b/wdio/helpers/Gestures.js
@@ -67,7 +67,7 @@ const Actions = {
class Gestures {
static async waitAndTap(element) {
const elem = await element;
- await elem.waitForDisplayed();
+ await elem.waitForDisplayed({timeout: 25000});
(await elem).touchAction(Actions.TAP);
}
diff --git a/wdio/screen-objects/Modals/TabBarModal.js b/wdio/screen-objects/Modals/TabBarModal.js
index 9608fd74a25..b9ca62a0776 100644
--- a/wdio/screen-objects/Modals/TabBarModal.js
+++ b/wdio/screen-objects/Modals/TabBarModal.js
@@ -52,6 +52,7 @@ class TabBarModal {
}
async tapSettingButton() {
+ await driver.pause(10000);
await Gestures.waitAndTap(this.settingsButton);
}
diff --git a/wdio/screen-objects/Onboarding/OnboardingCarousel.js b/wdio/screen-objects/Onboarding/OnboardingCarousel.js
index afb31a9f989..dff49a47a0b 100644
--- a/wdio/screen-objects/Onboarding/OnboardingCarousel.js
+++ b/wdio/screen-objects/Onboarding/OnboardingCarousel.js
@@ -42,6 +42,12 @@ class WelcomeScreen {
}
}
+ async waitForSplashAnimationToComplete() {
+ const elem = await this.splashScreenMetamaskAnimationId;
+ await elem.waitForExist();
+ await elem.waitForExist({ reverse: true });
+ }
+
async isScreenDisplayed() {
expect(this.screen).toBeDisplayed();
}
diff --git a/wdio/screen-objects/testIDs/Screens/Settings.testIds.js b/wdio/screen-objects/testIDs/Screens/Settings.testIds.js
index 517b8490831..2af2c3732fc 100644
--- a/wdio/screen-objects/testIDs/Screens/Settings.testIds.js
+++ b/wdio/screen-objects/testIDs/Screens/Settings.testIds.js
@@ -1,11 +1 @@
-export const GENERAL_SETTINGS = 'general-settings';
-export const SECURITY_SETTINGS = 'security-settings';
-export const ADVANCED_SETTINGS = 'advanced-settings';
-export const CONTACTS_SETTINGS = 'contacts-settings';
-export const NETWORKS_SETTINGS = 'networks-settings';
-export const ON_RAMP_SETTINGS = 'on-ramp-settings';
-export const EXPERIMENTAL_SETTINGS = 'experimental-settings';
-export const ABOUT_METAMASK_SETTINGS = 'about-metamask-settings';
-export const REQUEST_SETTINGS = 'request-settings';
-export const CONTACT_SETTINGS = 'contact-settings';
export const LOCK_SETTINGS = 'lock-settings';
diff --git a/wdio/step-definitions/common-steps.js b/wdio/step-definitions/common-steps.js
index 9d77cbba24a..6077e1ddb89 100644
--- a/wdio/step-definitions/common-steps.js
+++ b/wdio/step-definitions/common-steps.js
@@ -14,6 +14,7 @@ import OnboardingWizardModal from '../screen-objects/Modals/OnboardingWizardModa
import LoginScreen from '../screen-objects/LoginScreen';
import TermOfUseScreen from '../screen-objects/Modals/TermOfUseScreen';
import WhatsNewModal from '../screen-objects/Modals/WhatsNewModal';
+import Gestures from "../helpers/Gestures";
Then(/^the Welcome screen is displayed$/, async () => {
await WelcomeScreen.isScreenDisplayed();
@@ -139,6 +140,13 @@ Then(/^"([^"]*)?" is displayed/, async (text) => {
await CommonScreen.isTextDisplayed(text);
});
+Then(/^version "([^"]*)?" is displayed for app upgrade step/, async (text) => {
+ const appUpgradeText = process.env[text];
+ const timeout = 1000;
+ await driver.pause(timeout);
+ await CommonScreen.isTextDisplayed(appUpgradeText);
+});
+
Then(/^"([^"]*)?" is not displayed/, async (text) => {
const timeout = 1000;
await driver.pause(timeout);
@@ -168,7 +176,8 @@ Then(
When(/^I log into my wallet$/, async () => {
await LoginScreen.tapUnlockButton();
- await WalletMainScreen.isVisible();
+ await driver.pause(10000);
+ await WalletMainScreen.isMainWalletViewVisible();
});
When(/^I kill the app$/, async () => {3
@@ -264,3 +273,27 @@ When(/^I tap on the Settings tab option$/, async () => {
When(/^I tap on the Activity tab option$/, async () => {
await TabBarModal.tapActivityButton();
});
+
+When(/^I install upgrade the app$/, async () => {
+ await driver.installApp(process.env.BROWSERSTACK_APP_URL)
+});
+
+When(/^I scroll up$/, async () => {
+ await Gestures.swipeUp(0.5);
+});
+
+Then(/^removed test app$/, async () => {
+ const platform = await driver.getPlatform();
+ // TODO: Use environment variables for bundle IDs
+ if (platform === 'iOS') {
+ await driver.removeApp('io.metamask.MetaMask-QA');
+ }
+
+ if (platform === 'Android') {
+ await driver.removeApp('io.metamask.qa');
+ }
+});
+
+Given(/^the splash animation completes$/, async () => {
+ await WelcomeScreen.waitForSplashAnimationToComplete();
+});
diff --git a/yarn.lock b/yarn.lock
index c82eecc115e..4a9f26e872f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1311,6 +1311,11 @@
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==
+
"@craftzdog/react-native-buffer@^6.0.5":
version "6.0.5"
resolved "https://registry.yarnpkg.com/@craftzdog/react-native-buffer/-/react-native-buffer-6.0.5.tgz#0d4fbe0dd104186d2806655e3c0d25cebdae91d3"
@@ -1879,7 +1884,7 @@
"@ethereumjs/util" "^8.1.0"
ethereum-cryptography "^2.0.0"
-"@ethereumjs/util@^8.0.0", "@ethereumjs/util@^8.0.2", "@ethereumjs/util@^8.0.6", "@ethereumjs/util@^8.1.0":
+"@ethereumjs/util@^8.0.0", "@ethereumjs/util@^8.1.0":
version "8.1.0"
resolved "https://registry.yarnpkg.com/@ethereumjs/util/-/util-8.1.0.tgz#299df97fb6b034e0577ce9f94c7d9d1004409ed4"
integrity sha512-zQ0IqbdX8FZ9aw11vP+dZkKDkS+kgIvQPHnSAXzP9pLu+Rfu3D3XEeLbicvoXJTYnhZiPmsZUxgdzXwNKxRPbA==
@@ -3654,7 +3659,7 @@
"@metamask/utils" "^8.3.0"
immer "^9.0.6"
-"@metamask/browser-passworder@^4.1.0":
+"@metamask/browser-passworder@^4.3.0":
version "4.3.0"
resolved "https://registry.yarnpkg.com/@metamask/browser-passworder/-/browser-passworder-4.3.0.tgz#62c200750efcea864bd31d685120331859e1ab1e"
integrity sha512-RU1TVVV5DkbZRr6zPYg0NkexZ0/T2LCKNvF3A50jvUweyxDFuoNbSTN6z8K3Fy8O6/X2JQ1yyAbVzxZLq0qrGg==
@@ -3789,15 +3794,16 @@
resolved "https://registry.yarnpkg.com/@metamask/eslint-config/-/eslint-config-9.0.0.tgz#22d4911b705f7e4e566efbdda0e37912da33e30f"
integrity sha512-mWlLGQKjXXFOj9EtDClKSoTLeQuPW2kM1w3EpUMf4goYAQ+kLXCCa8pEff6h8ApWAnjhYmXydA1znQ2J4XvD+A==
-"@metamask/eth-hd-keyring@^6.0.0":
- version "6.0.0"
- resolved "https://registry.yarnpkg.com/@metamask/eth-hd-keyring/-/eth-hd-keyring-6.0.0.tgz#a46788d4bbc7aa5d8263ed6e4348101a80519a69"
- integrity sha512-dEj/I6Ag9FyCmjPcRXeXCkRXkVJE/uElhDVRcLBU6mT/GsXKgzVWXC/k0dhE8rEDrQbidhl+8wEElSJ2LI1InA==
+"@metamask/eth-hd-keyring@^7.0.1":
+ version "7.0.1"
+ resolved "https://registry.yarnpkg.com/@metamask/eth-hd-keyring/-/eth-hd-keyring-7.0.1.tgz#799006d8fd57c5580dc5843f74a7343eeb2985f3"
+ integrity sha512-EVdaZxsBgDBIcUAHIDYLI10TepQwUuwzBMJ2GQjNwiCwkARYHeckKnvlCkPcYWhwlnjhkfFg+iJn24cA5poulw==
dependencies:
- "@ethereumjs/util" "^8.0.2"
- "@metamask/eth-sig-util" "^5.0.2"
- "@metamask/scure-bip39" "^2.0.3"
- ethereum-cryptography "^1.1.2"
+ "@ethereumjs/util" "^8.1.0"
+ "@metamask/eth-sig-util" "^7.0.0"
+ "@metamask/scure-bip39" "^2.1.0"
+ "@metamask/utils" "^8.1.0"
+ ethereum-cryptography "^2.1.2"
"@metamask/eth-json-rpc-infura@^8.1.1":
version "8.1.1"
@@ -3860,27 +3866,27 @@
"@metamask/safe-event-emitter" "^3.0.0"
"@metamask/utils" "^5.0.1"
-"@metamask/eth-json-rpc-provider@^2.1.0", "@metamask/eth-json-rpc-provider@^2.3.0":
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/@metamask/eth-json-rpc-provider/-/eth-json-rpc-provider-2.3.0.tgz#09bc28077292e7e6d56e7c9095e70eb314d778aa"
- integrity sha512-PZpeOBksEuyFb30ObH9JTqdjVuaskFvRamDwpKNcgL+UexPNdB0jdjgVRWwquuBHn6thKNxsBIO1z8belpBVUQ==
+"@metamask/eth-json-rpc-provider@^2.1.0", "@metamask/eth-json-rpc-provider@^2.2.0", "@metamask/eth-json-rpc-provider@^2.3.0":
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/@metamask/eth-json-rpc-provider/-/eth-json-rpc-provider-2.3.2.tgz#39a3ec6cdf82b6f2ce764ebfd9ff78997a2aa608"
+ integrity sha512-VaZx++3gfi85+j9zB5TvqSWLeZ6QpsNjIk56Nq6OMDp2U8iUEPgjdA8CybOtkyDb88EbpuOSzHZcdHEeIX3zPw==
dependencies:
- "@metamask/json-rpc-engine" "^7.3.0"
+ "@metamask/json-rpc-engine" "^7.3.2"
"@metamask/safe-event-emitter" "^3.0.0"
- "@metamask/utils" "^8.2.0"
+ "@metamask/utils" "^8.3.0"
-"@metamask/eth-keyring-controller@^13.0.1":
- version "13.0.1"
- resolved "https://registry.yarnpkg.com/@metamask/eth-keyring-controller/-/eth-keyring-controller-13.0.1.tgz#9756a70ed2ea4f4dc6a8c335ac55b6322990e435"
- integrity sha512-zxULUAAR4CUiIw5lYeELfkyKqfxoepbRjxpEJ9OaSPMZz66oVQGCxIyzKLgXe5i782WfGEihTHLHj338A0RLZw==
+"@metamask/eth-keyring-controller@^15.0.0":
+ version "15.1.0"
+ resolved "https://registry.yarnpkg.com/@metamask/eth-keyring-controller/-/eth-keyring-controller-15.1.0.tgz#dc7c5ec3a075a2eb3f90b1f452cd67232714451d"
+ integrity sha512-FL6bVet2Rp3n6z+tKM9Lp0dBhTNj7wPKFLBvTTqJq7wBEbXir/AdN7JtnSXWC4BzbfsonXtnGuX353z0x3+8lw==
dependencies:
"@ethereumjs/tx" "^4.2.0"
- "@metamask/browser-passworder" "^4.1.0"
- "@metamask/eth-hd-keyring" "^6.0.0"
- "@metamask/eth-sig-util" "^6.0.0"
- "@metamask/eth-simple-keyring" "^5.0.0"
+ "@metamask/browser-passworder" "^4.3.0"
+ "@metamask/eth-hd-keyring" "^7.0.1"
+ "@metamask/eth-sig-util" "^7.0.0"
+ "@metamask/eth-simple-keyring" "^6.0.1"
"@metamask/obs-store" "^8.1.0"
- "@metamask/utils" "^8.1.0"
+ "@metamask/utils" "^8.2.0"
"@metamask/eth-query@^3.0.1":
version "3.0.1"
@@ -3909,18 +3915,6 @@
tweetnacl "^1.0.3"
tweetnacl-util "^0.15.1"
-"@metamask/eth-sig-util@^5.0.1", "@metamask/eth-sig-util@^5.0.2":
- version "5.1.0"
- resolved "https://registry.yarnpkg.com/@metamask/eth-sig-util/-/eth-sig-util-5.1.0.tgz#a47f62800ee1917fef976ba67544a0ccd7d1bd6b"
- integrity sha512-mlgziIHYlA9pi/XZerChqg4NocdOgBPB9NmxgXWQO2U2hH8RGOJQrz6j/AIKkYxgCMIE2PY000+joOwXfzeTDQ==
- dependencies:
- "@ethereumjs/util" "^8.0.6"
- bn.js "^4.12.0"
- ethereum-cryptography "^2.0.0"
- ethjs-util "^0.1.6"
- tweetnacl "^1.0.3"
- tweetnacl-util "^0.15.1"
-
"@metamask/eth-sig-util@^6.0.0":
version "6.0.2"
resolved "https://registry.yarnpkg.com/@metamask/eth-sig-util/-/eth-sig-util-6.0.2.tgz#d81dc87e0cd5a6580010911501976b48821746ad"
@@ -3946,14 +3940,15 @@
tweetnacl "^1.0.3"
tweetnacl-util "^0.15.1"
-"@metamask/eth-simple-keyring@^5.0.0":
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/@metamask/eth-simple-keyring/-/eth-simple-keyring-5.0.0.tgz#307772d1aa3298e41a2444b428cd8f4522b7bf5c"
- integrity sha512-UJfP36Z9g1eeD8mSHWaVqUvkgbgYm3S7YuzlMzQi+WgPnWu81CdbldMMtvreTlu4I1mTyljXLDMjIp65P0bygQ==
+"@metamask/eth-simple-keyring@^6.0.1":
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/@metamask/eth-simple-keyring/-/eth-simple-keyring-6.0.1.tgz#2c914c51746aba3588530a256842ce0436481ab5"
+ integrity sha512-UEMOojkcyPAJF30lRUesZvqESeUZIOHQgWyHQv3FzwE4ekXgJMfvvW+TuGydcGY2aeIdJy9l9wJQF0/+V9wNhg==
dependencies:
- "@ethereumjs/util" "^8.0.0"
- "@metamask/eth-sig-util" "^5.0.1"
- ethereum-cryptography "^1.1.2"
+ "@ethereumjs/util" "^8.1.0"
+ "@metamask/eth-sig-util" "^7.0.0"
+ "@metamask/utils" "^8.1.0"
+ ethereum-cryptography "^2.1.2"
randombytes "^2.1.0"
"@metamask/eth-snap-keyring@^2.1.1":
@@ -3993,28 +3988,29 @@
"@metamask/number-to-bn" "^1.7.1"
bn.js "^5.2.1"
-"@metamask/gas-fee-controller@6.1.2":
- version "6.1.2"
- resolved "https://registry.yarnpkg.com/@metamask/gas-fee-controller/-/gas-fee-controller-6.1.2.tgz#b7a9a611e15a8914636a05411d18686bd554ddf3"
- integrity sha512-NN1Hlmq5u/q2jIJxuGpPwDQaCIZCnKv+v3SghXajmFSAMYC1REgfay/VITI1A0Drfv3dQ8gHlYll2hAK+lu5oQ==
+"@metamask/gas-fee-controller@^10.0.0":
+ version "10.0.1"
+ resolved "https://registry.yarnpkg.com/@metamask/gas-fee-controller/-/gas-fee-controller-10.0.1.tgz#2804513add865810e1467e6578d7cf287a94d8f3"
+ integrity sha512-5GtQtq1g//uMjrSf/aAdGVL0agCiry5kKxjh/dxhKpBKrnCQIKg3Ruw2rc+vXW1JtYZKMXTeiIfb00syrNLpSw==
dependencies:
- "@metamask/base-controller" "^3.2.1"
- "@metamask/controller-utils" "^4.3.2"
- "@metamask/eth-query" "^3.0.1"
- "@metamask/network-controller" "^12.1.2"
- "@metamask/utils" "^6.2.0"
+ "@metamask/base-controller" "^3.2.3"
+ "@metamask/controller-utils" "^5.0.2"
+ "@metamask/eth-query" "^4.0.0"
+ "@metamask/network-controller" "^16.0.0"
+ "@metamask/polling-controller" "^1.0.2"
+ "@metamask/utils" "^8.2.0"
"@types/uuid" "^8.3.0"
ethereumjs-util "^7.0.10"
ethjs-unit "^0.1.6"
immer "^9.0.6"
uuid "^8.3.2"
-"@metamask/json-rpc-engine@^7.0.0", "@metamask/json-rpc-engine@^7.1.1", "@metamask/json-rpc-engine@^7.3.0", "@metamask/json-rpc-engine@^7.3.1", "@metamask/json-rpc-engine@^7.3.2":
- version "7.3.2"
- resolved "https://registry.yarnpkg.com/@metamask/json-rpc-engine/-/json-rpc-engine-7.3.2.tgz#e8f0695811619eef7b7c894ba5cf782db9f1c2cb"
- integrity sha512-dVjBPlni4CoiBpESVqrxh6k4OR14w6GRXKSSXHFuITjuhALE42gNCkXTpL4cjNeOBUgTba3eGe5EI8cyc2QLRg==
+"@metamask/json-rpc-engine@^7.0.0", "@metamask/json-rpc-engine@^7.1.1", "@metamask/json-rpc-engine@^7.2.0", "@metamask/json-rpc-engine@^7.3.0", "@metamask/json-rpc-engine@^7.3.1", "@metamask/json-rpc-engine@^7.3.2":
+ version "7.3.3"
+ resolved "https://registry.yarnpkg.com/@metamask/json-rpc-engine/-/json-rpc-engine-7.3.3.tgz#f2b30a2164558014bfcca45db10f5af291d989af"
+ integrity sha512-dwZPq8wx9yV3IX2caLi9q9xZBw2XeIoYqdyihDDDpuHVCEiqadJLwqM3zy+uwf6F1QYQ65A8aOMQg1Uw7LMLNg==
dependencies:
- "@metamask/rpc-errors" "^6.1.0"
+ "@metamask/rpc-errors" "^6.2.1"
"@metamask/safe-event-emitter" "^3.0.0"
"@metamask/utils" "^8.3.0"
@@ -4054,17 +4050,17 @@
superstruct "^1.0.3"
uuid "^9.0.0"
-"@metamask/keyring-controller@^8.1.0":
- version "8.1.0"
- resolved "https://registry.yarnpkg.com/@metamask/keyring-controller/-/keyring-controller-8.1.0.tgz#e8c9a9a4e4689492b1a4e440ece321a9eea3d279"
- integrity sha512-b5T2tULBoYjuc/xeP7JxsVPo2zABJsTR3JkboKUkcptL72qGHgMix1eQgPeU4msMHrDC4hc5Bgc8ZD2PxtjzEw==
+"@metamask/keyring-controller@^9.0.0":
+ version "9.0.0"
+ resolved "https://registry.yarnpkg.com/@metamask/keyring-controller/-/keyring-controller-9.0.0.tgz#d0b3790ed43b8626edd6c4e092c14a9e3c8a3de4"
+ integrity sha512-KSD59AiYvChFt2NyIuHslU6IW5F5pMxyEWKQ3FqjmNEtbtk36DCvXay//8IK+ZURWh6TdWanu2O/+9bn/UcUVQ==
dependencies:
"@keystonehq/metamask-airgapped-keyring" "^0.13.1"
"@metamask/base-controller" "^3.2.3"
- "@metamask/eth-keyring-controller" "^13.0.1"
+ "@metamask/eth-keyring-controller" "^15.0.0"
"@metamask/message-manager" "^7.3.5"
"@metamask/preferences-controller" "^4.4.3"
- "@metamask/utils" "^8.1.0"
+ "@metamask/utils" "^8.2.0"
async-mutex "^0.2.6"
ethereumjs-util "^7.0.10"
ethereumjs-wallet "^1.0.1"
@@ -4102,13 +4098,13 @@
resolved "https://registry.yarnpkg.com/@metamask/mobile-provider/-/mobile-provider-3.0.0.tgz#8a6a5a0874c8cbe4b468f63dfc57117d207f9595"
integrity sha512-XwFJk0rd9lAZR5xS3VC7ypEhD7DvZR2gi2Ch6PHnODIqeS9Te3OdVKK5+jHI4his8v/zs6LWdFdlRtx5/jL96w==
-"@metamask/network-controller@13.0.1", "@metamask/network-controller@^13.0.1":
- version "13.0.1"
- resolved "https://registry.yarnpkg.com/@metamask/network-controller/-/network-controller-13.0.1.tgz#b8d3cd4abe8e03a8d030409588da6f05461955c5"
- integrity sha512-stoS3Dj4b3B8M6dCFs0uE7Zz1Y535yeSUJxJQ12v2t1OlB4JfV5mNL67SJR8h+efR+t336H3EujxevEiW32CVA==
+"@metamask/network-controller@^12.0.0":
+ version "12.2.0"
+ resolved "https://registry.yarnpkg.com/@metamask/network-controller/-/network-controller-12.2.0.tgz#679d81e009cfd2b9fdaf884267d7a11b535f1922"
+ integrity sha512-MBsEOy8htQtN46s/pbmEPsfe+UyOe3aUGTeswG0Iy0qGKyqu+uEx/nhydaLpMHFlOLbboPWtUmnPRLFE9pQDEA==
dependencies:
- "@metamask/base-controller" "^3.2.2"
- "@metamask/controller-utils" "^5.0.1"
+ "@metamask/base-controller" "^3.2.1"
+ "@metamask/controller-utils" "^4.3.2"
"@metamask/eth-json-rpc-infura" "^8.1.1"
"@metamask/eth-json-rpc-middleware" "^11.0.2"
"@metamask/eth-json-rpc-provider" "^1.0.0"
@@ -4122,13 +4118,13 @@
json-rpc-engine "^6.1.0"
uuid "^8.3.2"
-"@metamask/network-controller@^12.0.0", "@metamask/network-controller@^12.1.2":
- version "12.2.0"
- resolved "https://registry.yarnpkg.com/@metamask/network-controller/-/network-controller-12.2.0.tgz#679d81e009cfd2b9fdaf884267d7a11b535f1922"
- integrity sha512-MBsEOy8htQtN46s/pbmEPsfe+UyOe3aUGTeswG0Iy0qGKyqu+uEx/nhydaLpMHFlOLbboPWtUmnPRLFE9pQDEA==
+"@metamask/network-controller@^13.0.1":
+ version "13.0.1"
+ resolved "https://registry.yarnpkg.com/@metamask/network-controller/-/network-controller-13.0.1.tgz#b8d3cd4abe8e03a8d030409588da6f05461955c5"
+ integrity sha512-stoS3Dj4b3B8M6dCFs0uE7Zz1Y535yeSUJxJQ12v2t1OlB4JfV5mNL67SJR8h+efR+t336H3EujxevEiW32CVA==
dependencies:
- "@metamask/base-controller" "^3.2.1"
- "@metamask/controller-utils" "^4.3.2"
+ "@metamask/base-controller" "^3.2.2"
+ "@metamask/controller-utils" "^5.0.1"
"@metamask/eth-json-rpc-infura" "^8.1.1"
"@metamask/eth-json-rpc-middleware" "^11.0.2"
"@metamask/eth-json-rpc-provider" "^1.0.0"
@@ -4142,6 +4138,46 @@
json-rpc-engine "^6.1.0"
uuid "^8.3.2"
+"@metamask/network-controller@^15.0.0":
+ version "15.2.0"
+ resolved "https://registry.yarnpkg.com/@metamask/network-controller/-/network-controller-15.2.0.tgz#1e31aa7799a386ea70aa9a7e158fd419aa7f0f99"
+ integrity sha512-9rzKRQCBEsU/djyrCUALYfT2OQFW1jSXq2b3/8zG6DFumwVAZb7RTokW3V0LIKAMIe6od6HwTR9lnVWvk5tbpQ==
+ dependencies:
+ "@metamask/base-controller" "^3.2.3"
+ "@metamask/controller-utils" "^5.0.2"
+ "@metamask/eth-json-rpc-infura" "^9.0.0"
+ "@metamask/eth-json-rpc-middleware" "^12.0.1"
+ "@metamask/eth-json-rpc-provider" "^2.2.0"
+ "@metamask/eth-query" "^3.0.1"
+ "@metamask/json-rpc-engine" "^7.2.0"
+ "@metamask/rpc-errors" "^6.1.0"
+ "@metamask/swappable-obj-proxy" "^2.1.0"
+ "@metamask/utils" "^8.2.0"
+ async-mutex "^0.2.6"
+ eth-block-tracker "^8.0.0"
+ immer "^9.0.6"
+ uuid "^8.3.2"
+
+"@metamask/network-controller@^16.0.0":
+ version "16.0.0"
+ resolved "https://registry.yarnpkg.com/@metamask/network-controller/-/network-controller-16.0.0.tgz#06ca8c92910660149ce49b54cffc89e918bb13b3"
+ integrity sha512-TSI3DlkHgdo9Kpad+8bPR8SQGgo4V/PVjqPajxwngZXsiRU1Tyfu3yfvbGHmGqKXD8YjCxFqUKtLr2jsQQWo6g==
+ dependencies:
+ "@metamask/base-controller" "^3.2.3"
+ "@metamask/controller-utils" "^5.0.2"
+ "@metamask/eth-json-rpc-infura" "^9.0.0"
+ "@metamask/eth-json-rpc-middleware" "^12.0.1"
+ "@metamask/eth-json-rpc-provider" "^2.3.0"
+ "@metamask/eth-query" "^4.0.0"
+ "@metamask/json-rpc-engine" "^7.3.0"
+ "@metamask/rpc-errors" "^6.1.0"
+ "@metamask/swappable-obj-proxy" "^2.1.0"
+ "@metamask/utils" "^8.2.0"
+ async-mutex "^0.2.6"
+ eth-block-tracker "^8.0.0"
+ immer "^9.0.6"
+ uuid "^8.3.2"
+
"@metamask/network-controller@^17.0.0":
version "17.0.0"
resolved "https://registry.yarnpkg.com/@metamask/network-controller/-/network-controller-17.0.0.tgz#ba43edbc9e98bde59c43a467ec3dbaf8f8cab6cd"
@@ -4243,18 +4279,7 @@
immer "^9.0.6"
nanoid "^3.1.31"
-"@metamask/phishing-controller@6.0.1":
- version "6.0.1"
- resolved "https://registry.yarnpkg.com/@metamask/phishing-controller/-/phishing-controller-6.0.1.tgz#40f71bac980c84d31033ddc8e61e99f02dd1899c"
- integrity sha512-wDJ0tbaRbvoCgwBgdxJPvF2XZLynq/wrgrfsKMG3+nZ6pNPLXwLq43vyaglaXWvlXgbMTgw7alftUfSRQGR/SA==
- dependencies:
- "@metamask/base-controller" "^3.2.1"
- "@metamask/controller-utils" "^4.3.2"
- "@types/punycode" "^2.1.0"
- eth-phishing-detect "^1.2.0"
- punycode "^2.1.1"
-
-"@metamask/phishing-controller@^8.0.2":
+"@metamask/phishing-controller@^8.0.0", "@metamask/phishing-controller@^8.0.2":
version "8.0.2"
resolved "https://registry.yarnpkg.com/@metamask/phishing-controller/-/phishing-controller-8.0.2.tgz#612d2d2d0d5e8746289fda036b0382a63ea82ffb"
integrity sha512-aRnpv+AtjqffT7hnwF1XZahWs8K99dOd8DcAAaVJyWsdt0cT3o6ryZXHMcFTUGP8Fl88LmkLm1970zfUO8AxFg==
@@ -4265,6 +4290,19 @@
eth-phishing-detect "^1.2.0"
punycode "^2.1.1"
+"@metamask/polling-controller@^1.0.2":
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/@metamask/polling-controller/-/polling-controller-1.0.2.tgz#4e3e0c8ecaba5eb80b06c3c07d2930a0cbc7855f"
+ integrity sha512-9LrM7Qo/wM2cBOfZYI+ghsk0yGPzNxYRa75RwCCi9SlKlSQG4SScI4HIxa/qANTN9XFs+HnXPDCEthGyhYSvMw==
+ dependencies:
+ "@metamask/base-controller" "^3.2.3"
+ "@metamask/controller-utils" "^5.0.2"
+ "@metamask/network-controller" "^16.0.0"
+ "@metamask/utils" "^8.2.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.0.0":
version "8.0.0"
resolved "https://registry.yarnpkg.com/@metamask/post-message-stream/-/post-message-stream-8.0.0.tgz#101a54c2a4029879691ce83065638493d7822f48"
@@ -4356,7 +4394,7 @@
resolved "https://registry.yarnpkg.com/@metamask/safe-event-emitter/-/safe-event-emitter-3.0.0.tgz#8c2b9073fe0722d48693143b0dc8448840daa3bd"
integrity sha512-j6Z47VOmVyGMlnKXZmL0fyvWfEYtKWCA9yGZkU3FCsGZUT5lHGmvaV9JA5F2Y+010y7+ROtR3WMXIkvl/nVzqQ==
-"@metamask/scure-bip39@^2.0.3", "@metamask/scure-bip39@^2.1.0":
+"@metamask/scure-bip39@^2.1.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@metamask/scure-bip39/-/scure-bip39-2.1.0.tgz#13456884736e56ede15e471bd93c0aa0acdedd0b"
integrity sha512-Ndwdnld0SI6YaftEUUVq20sdoWcWNXsJXxvQkbiY42FKmrA16U6WoSh9Eq+NpugpKKwK6f5uvaTDusjndiEDGQ==
@@ -4646,14 +4684,7 @@
jsbi "^3.1.5"
sha.js "^2.4.11"
-"@noble/curves@1.1.0", "@noble/curves@~1.1.0":
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.1.0.tgz#f13fc667c89184bc04cccb9b11e8e7bae27d8c3d"
- integrity sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==
- dependencies:
- "@noble/hashes" "1.3.1"
-
-"@noble/curves@^1.2.0":
+"@noble/curves@1.3.0", "@noble/curves@^1.2.0", "@noble/curves@~1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.3.0.tgz#01be46da4fd195822dab821e72f71bf4aeec635e"
integrity sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==
@@ -4665,27 +4696,22 @@
resolved "https://registry.yarnpkg.com/@noble/ed25519/-/ed25519-1.7.3.tgz#57e1677bf6885354b466c38e2b620c62f45a7123"
integrity sha512-iR8GBkDt0Q3GyaVcIu7mSsVIqnFbkbRzGLWlvhwunacoLwt4J3swfKhfaM6rN6WY+TBGoYT1GtT1mIh2/jGbRQ==
-"@noble/hashes@1.2.0", "@noble/hashes@~1.2.0":
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.2.0.tgz#a3150eeb09cc7ab207ebf6d7b9ad311a9bdbed12"
- integrity sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ==
-
-"@noble/hashes@1.3.1":
- version "1.3.1"
- resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9"
- integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==
-
-"@noble/hashes@1.3.3", "@noble/hashes@^1.0.0", "@noble/hashes@^1.3.1", "@noble/hashes@^1.3.2", "@noble/hashes@~1.3.0", "@noble/hashes@~1.3.1":
+"@noble/hashes@1.3.3", "@noble/hashes@~1.3.2":
version "1.3.3"
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.3.tgz#39908da56a4adc270147bb07968bf3b16cfe1699"
integrity sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==
+"@noble/hashes@^1.0.0", "@noble/hashes@^1.1.2", "@noble/hashes@^1.3.1", "@noble/hashes@^1.3.2":
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426"
+ integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==
+
"@noble/hashes@~1.1.1":
version "1.1.5"
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.1.5.tgz#1a0377f3b9020efe2fae03290bd2a12140c95c11"
integrity sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ==
-"@noble/secp256k1@1.7.1", "@noble/secp256k1@^1.5.5", "@noble/secp256k1@~1.7.0":
+"@noble/secp256k1@^1.5.5":
version "1.7.1"
resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.1.tgz#b251c70f824ce3ca7f8dc3df08d58f005cc0507c"
integrity sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==
@@ -5636,44 +5662,27 @@
image-to-base64 "^2.2.0"
open "^8.2.0"
-"@scure/base@^1.0.0", "@scure/base@^1.1.1", "@scure/base@^1.1.3", "@scure/base@~1.1.0":
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.3.tgz#8584115565228290a6c6c4961973e0903bb3df2f"
- integrity sha512-/+SgoRjLq7Xlf0CWuLHq2LUZeL/w65kfzAPG5NH9pcmBhs+nunQTn4gvdwgMTIXnt9b2C/1SeL2XiysZEyIC9Q==
-
-"@scure/bip32@1.1.5":
- version "1.1.5"
- resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.1.5.tgz#d2ccae16dcc2e75bc1d75f5ef3c66a338d1ba300"
- integrity sha512-XyNh1rB0SkEqd3tXcXMi+Xe1fvg+kUIcoRIEujP1Jgv7DqW2r9lg3Ah0NkFaCs9sTkQAQA8kw7xiRXzENi9Rtw==
- dependencies:
- "@noble/hashes" "~1.2.0"
- "@noble/secp256k1" "~1.7.0"
- "@scure/base" "~1.1.0"
-
-"@scure/bip32@1.3.1":
- version "1.3.1"
- resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.3.1.tgz#7248aea723667f98160f593d621c47e208ccbb10"
- integrity sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==
- dependencies:
- "@noble/curves" "~1.1.0"
- "@noble/hashes" "~1.3.1"
- "@scure/base" "~1.1.0"
+"@scure/base@^1.0.0", "@scure/base@^1.1.1", "@scure/base@^1.1.3", "@scure/base@~1.1.0", "@scure/base@~1.1.4":
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.6.tgz#8ce5d304b436e4c84f896e0550c83e4d88cb917d"
+ integrity sha512-ok9AWwhcgYuGG3Zfhyqg+zwl+Wn5uE+dwC0NV/2qQkx4dABbb/bx96vWu8NSj+BNjjSjno+JRYRjle1jV08k3g==
-"@scure/bip39@1.1.1":
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.1.1.tgz#b54557b2e86214319405db819c4b6a370cf340c5"
- integrity sha512-t+wDck2rVkh65Hmv280fYdVdY25J9YeEUIgn2LG1WM6gxFkGzcksoDiUkWVpVp3Oex9xGC68JU2dSbUfwZ2jPg==
+"@scure/bip32@1.3.3":
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.3.3.tgz#a9624991dc8767087c57999a5d79488f48eae6c8"
+ integrity sha512-LJaN3HwRbfQK0X1xFSi0Q9amqOgzQnnDngIt+ZlsBC3Bm7/nE7K0kwshZHyaru79yIVRv/e1mQAjZyuZG6jOFQ==
dependencies:
- "@noble/hashes" "~1.2.0"
- "@scure/base" "~1.1.0"
+ "@noble/curves" "~1.3.0"
+ "@noble/hashes" "~1.3.2"
+ "@scure/base" "~1.1.4"
-"@scure/bip39@1.2.1":
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.2.1.tgz#5cee8978656b272a917b7871c981e0541ad6ac2a"
- integrity sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==
+"@scure/bip39@1.2.2":
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.2.2.tgz#f3426813f4ced11a47489cbcf7294aa963966527"
+ integrity sha512-HYf9TUXG80beW+hGAt3TRM8wU6pQoYur9iNypTROm42dorCGmLnFe3eWjz3gOq6G62H2WRh0FCzAR1PI+29zIA==
dependencies:
- "@noble/hashes" "~1.3.0"
- "@scure/base" "~1.1.0"
+ "@noble/hashes" "~1.3.2"
+ "@scure/base" "~1.1.4"
"@segment/analytics-react-native@^2.17.0":
version "2.17.0"
@@ -5962,12 +5971,15 @@
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"
- resolved "https://registry.yarnpkg.com/@spruceid/siwe-parser/-/siwe-parser-1.1.3.tgz#0eebe8bbd63c6de89cb44c06b6329b00b305df65"
- integrity sha512-oQ8PcwDqjGWJvLmvAF2yzd6iniiWxK0Qtz+Dw+gLD/W5zOQJiKIUXwslHOm8VB8OOOKW9vfR3dnPBhHaZDvRsw==
+"@spruceid/siwe-parser@1.1.3", "@spruceid/siwe-parser@2.1.0":
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/@spruceid/siwe-parser/-/siwe-parser-2.1.0.tgz#59859ccfd02403179bcf115d9e02a7dc953a0820"
+ integrity sha512-tFQwY2oQLa4qvHE6npKsVgVdVLQOCGP1zJM3yjZOHut43LqCwdSwitZndFLrJHZLpqru9FnmYHRakvsPvrI+qA==
dependencies:
+ "@noble/hashes" "^1.1.2"
apg-js "^4.1.1"
+ uri-js "^4.4.1"
+ valid-url "^1.0.9"
"@stablelib/aead@^1.0.1":
version "1.0.1"
@@ -10494,10 +10506,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.1.3"
- resolved "https://registry.yarnpkg.com/apg-js/-/apg-js-4.1.3.tgz#0cb9dc99f8830740d7a8f9fc0048fa618ae4d199"
- integrity sha512-XYyDcoBho8OpnWPRnedMwyL+76ovCtsESerHZEfY39dO4IrEqN97mdEYkOyHa0XTX5+3+U5FmpqPLttK0f7n6g==
+apg-js@4.2.0, 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==
app-root-dir@^1.0.2:
version "1.0.2"
@@ -12014,7 +12026,7 @@ bn.js@5.2.1, bn.js@^5.0.0, bn.js@^5.1.2, bn.js@^5.2.0, bn.js@^5.2.1:
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70"
integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==
-bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.10.0, bn.js@^4.11.0, bn.js@^4.11.8, bn.js@^4.11.9, bn.js@^4.12.0:
+bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.10.0, bn.js@^4.11.0, bn.js@^4.11.8, bn.js@^4.11.9:
version "4.12.0"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88"
integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==
@@ -13289,6 +13301,36 @@ content-type@^1.0.4, content-type@~1.0.4, content-type@~1.0.5:
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918"
integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
+contentful-resolve-response@^1.8.1:
+ version "1.8.1"
+ resolved "https://registry.yarnpkg.com/contentful-resolve-response/-/contentful-resolve-response-1.8.1.tgz#b44ff13e12fab7deb00ef6216d8a7171bdda0395"
+ integrity sha512-VXGK2c8dBIGcRCknqudKmkDr2PzsUYfjLN6hhx71T09UzoXOdA/c0kfDhsf/BBCBWPWcLaUgaJEFU0lCo45TSg==
+ dependencies:
+ fast-copy "^2.1.7"
+
+contentful-sdk-core@^8.1.0:
+ version "8.1.2"
+ resolved "https://registry.yarnpkg.com/contentful-sdk-core/-/contentful-sdk-core-8.1.2.tgz#a27ea57cfd631b4c6d58e5ca04fcde6d231e2c1b"
+ integrity sha512-XZvX2JMJF4YiICXLrHFv59KBHaQJ6ElqAP8gSNgnCu4x+pPG7Y1bC2JMNOiyAgJuGQGVUOcNZ5PmK+tsNEayYw==
+ dependencies:
+ fast-copy "^2.1.7"
+ lodash.isplainobject "^4.0.6"
+ lodash.isstring "^4.0.1"
+ 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==
+ dependencies:
+ "@contentful/rich-text-types" "^16.0.2"
+ axios "^1.6.7"
+ contentful-resolve-response "^1.8.1"
+ contentful-sdk-core "^8.1.0"
+ json-stringify-safe "^5.0.1"
+ type-fest "^4.0.0"
+
continuation-local-storage@3.x:
version "3.2.1"
resolved "https://registry.yarnpkg.com/continuation-local-storage/-/continuation-local-storage-3.2.1.tgz#11f613f74e914fe9b34c92ad2d28fe6ae1db7ffb"
@@ -15550,13 +15592,6 @@ eth-json-rpc-errors@^1.0.1:
dependencies:
fast-safe-stringify "^2.0.6"
-eth-json-rpc-errors@^2.0.2:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/eth-json-rpc-errors/-/eth-json-rpc-errors-2.0.2.tgz#c1965de0301fe941c058e928bebaba2e1285e3c4"
- integrity sha512-uBCRM2w2ewusRHGxN8JhcuOb2RN3ueAOYH/0BhqdFmQkZx5lj5+fLKTz0mIVOzd4FG5/kUksCzCD7eTEim6gaA==
- dependencies:
- fast-safe-stringify "^2.0.6"
-
eth-json-rpc-filters@4.2.2, eth-json-rpc-filters@^4.2.1:
version "4.2.2"
resolved "https://registry.yarnpkg.com/eth-json-rpc-filters/-/eth-json-rpc-filters-4.2.2.tgz#eb35e1dfe9357ace8a8908e7daee80b2cd60a10d"
@@ -15734,25 +15769,15 @@ ethereum-cryptography@^0.1.3:
secp256k1 "^4.0.1"
setimmediate "^1.0.5"
-ethereum-cryptography@^1.1.2:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/ethereum-cryptography/-/ethereum-cryptography-1.2.0.tgz#5ccfa183e85fdaf9f9b299a79430c044268c9b3a"
- integrity sha512-6yFQC9b5ug6/17CQpCyE3k9eKBMdhyVjzUy1WkiuY/E4vj/SXDBbCw8QEIaXqf0Mf2SnY6RmpDcwlUmBSS0EJw==
- dependencies:
- "@noble/hashes" "1.2.0"
- "@noble/secp256k1" "1.7.1"
- "@scure/bip32" "1.1.5"
- "@scure/bip39" "1.1.1"
-
ethereum-cryptography@^2.0.0, ethereum-cryptography@^2.1.2:
- version "2.1.2"
- resolved "https://registry.yarnpkg.com/ethereum-cryptography/-/ethereum-cryptography-2.1.2.tgz#18fa7108622e56481157a5cb7c01c0c6a672eb67"
- integrity sha512-Z5Ba0T0ImZ8fqXrJbpHcbpAvIswRte2wGNR/KePnu8GbbvgJ47lMxT/ZZPG6i9Jaht4azPDop4HaM00J0J59ug==
+ version "2.1.3"
+ resolved "https://registry.yarnpkg.com/ethereum-cryptography/-/ethereum-cryptography-2.1.3.tgz#1352270ed3b339fe25af5ceeadcf1b9c8e30768a"
+ integrity sha512-BlwbIL7/P45W8FGW2r7LGuvoEZ+7PWsniMvQ4p5s2xCyw9tmaDlpfsN9HjAucbF+t/qpVHwZUisgfK24TCW8aA==
dependencies:
- "@noble/curves" "1.1.0"
- "@noble/hashes" "1.3.1"
- "@scure/bip32" "1.3.1"
- "@scure/bip39" "1.2.1"
+ "@noble/curves" "1.3.0"
+ "@noble/hashes" "1.3.3"
+ "@scure/bip32" "1.3.3"
+ "@scure/bip39" "1.2.2"
ethereum-ens-network-map@^1.0.0:
version "1.0.2"
@@ -16438,6 +16463,11 @@ fast-base64-decode@^1.0.0:
resolved "https://registry.yarnpkg.com/fast-base64-decode/-/fast-base64-decode-1.0.0.tgz#b434a0dd7d92b12b43f26819300d2dafb83ee418"
integrity sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q==
+fast-copy@^2.1.7:
+ version "2.1.7"
+ resolved "https://registry.yarnpkg.com/fast-copy/-/fast-copy-2.1.7.tgz#affc9475cb4b555fb488572b2a44231d0c9fa39e"
+ integrity sha512-ozrGwyuCTAy7YgFCua8rmqmytECYk/JYAMXcswOcm0qvGoE3tPb7ivBeIHTOK2DiapBhDZgacIhzhQIKU5TCfA==
+
fast-deep-equal@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
@@ -20332,6 +20362,11 @@ lodash.isplainobject@^4.0.6:
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=
+lodash.isstring@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
+ integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==
+
lodash.memoize@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
@@ -22357,6 +22392,11 @@ p-map@^4.0.0:
dependencies:
aggregate-error "^3.0.0"
+p-throttle@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/p-throttle/-/p-throttle-4.1.1.tgz#80b1fbd358af40a8bfa1667f9dc8b72b714ad692"
+ integrity sha512-TuU8Ato+pRTPJoDzYD4s7ocJYcNSEZRvlxoq3hcPI2kZDZ49IQ1Wkj7/gDJc3X7XiEAAvRGtDzdXJI0tC3IL1g==
+
p-try@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
@@ -27246,6 +27286,11 @@ type-fest@^2.19.0:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b"
integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==
+type-fest@^4.0.0:
+ version "4.15.0"
+ resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.15.0.tgz#21da206b89c15774cc718c4f2d693e13a1a14a43"
+ integrity sha512-tB9lu0pQpX5KJq54g+oHOLumOx+pMep4RaM6liXh2PKmVRFF+/vAtUP0ZaJ0kOySfVNjF6doBWPHhBhISKdlIA==
+
type-is@^1.6.16, type-is@~1.6.18:
version "1.6.18"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
@@ -27551,7 +27596,7 @@ uqr@^0.1.2:
resolved "https://registry.yarnpkg.com/uqr/-/uqr-0.1.2.tgz#5c6cd5dcff9581f9bb35b982cb89e2c483a41d7d"
integrity sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA==
-uri-js@^4.2.2:
+uri-js@^4.2.2, uri-js@^4.4.1:
version "4.4.1"
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==
@@ -27794,10 +27839,10 @@ v8-to-istanbul@^9.0.1:
"@types/istanbul-lib-coverage" "^2.0.1"
convert-source-map "^1.6.0"
-valid-url@1.0.9:
+valid-url@1.0.9, valid-url@^1.0.9:
version "1.0.9"
resolved "https://registry.yarnpkg.com/valid-url/-/valid-url-1.0.9.tgz#1c14479b40f1397a75782f115e4086447433a200"
- integrity sha1-HBRHm0DxOXp1eC8RXkCGRHQzogA=
+ integrity sha512-QQDsV8OnSf5Uc30CKSwG9lnhMPe6exHtTXLRYX8uMwKENy640pU+2BgBL0LRbDh/eYRahNCS7aewCx0wf3NYVA==
validate-npm-package-license@^3.0.1:
version "3.0.4"