diff --git a/app/actions/notification/helpers/index.ts b/app/actions/notification/helpers/index.ts index 9f47da07f88..b1c4c881eb9 100644 --- a/app/actions/notification/helpers/index.ts +++ b/app/actions/notification/helpers/index.ts @@ -171,6 +171,15 @@ export const markMetamaskNotificationsAsRead = async ( return getErrorMessage(error); } }; + +export const syncInternalAccountsWithUserStorage = async () => { + try { + await Engine.context.UserStorageController.syncInternalAccountsWithUserStorage(); + } catch (error) { + return getErrorMessage(error); + } +}; + /** * Perform the deletion of the notifications storage key and the creation of on chain triggers to reset the notifications. * @@ -178,12 +187,12 @@ export const markMetamaskNotificationsAsRead = async ( */ export const performDeleteStorage = async (): Promise => { try { - await Engine.context.UserStorageController.performDeleteStorage('notifications.notification_settings'); - await Engine.context.NotificationServicesController.createOnChainTriggers( - { + await Engine.context.UserStorageController.performDeleteStorage( + 'notifications.notification_settings', + ); + await Engine.context.NotificationServicesController.createOnChainTriggers({ resetNotifications: true, - }, - ); + }); } catch (error) { return getErrorMessage(error); } diff --git a/app/components/Views/Wallet/index.test.tsx b/app/components/Views/Wallet/index.test.tsx index 9b008db9496..87ef6fe0502 100644 --- a/app/components/Views/Wallet/index.test.tsx +++ b/app/components/Views/Wallet/index.test.tsx @@ -1,13 +1,15 @@ import React from 'react'; import Wallet from './'; import { renderScreen } from '../../../util/test/renderWithProvider'; -import { screen } from '@testing-library/react-native'; +import { act, screen } from '@testing-library/react-native'; import ScrollableTabView from 'react-native-scrollable-tab-view'; import Routes from '../../../constants/navigation/Routes'; import { backgroundState } from '../../../util/test/initial-root-state'; import { createMockAccountsControllerState } from '../../../util/test/accountsControllerTestUtils'; import { WalletViewSelectorsIDs } from '../../../../e2e/selectors/wallet/WalletView.selectors'; import { CommonSelectorsIDs } from '../../../../e2e/selectors/Common.selectors'; +import { useAccountSyncing } from '../../../util/notifications/hooks/useAccountSyncing'; +import { AppState } from 'react-native'; const MOCK_ADDRESS = '0xc4955c0d639d99699bfd7ec54d9fafee40e4d272'; @@ -108,6 +110,13 @@ jest.mock('react-native-scrollable-tab-view', () => { return ScrollableTabViewMock; }); +jest.mock('../../../util/notifications/hooks/useAccountSyncing', () => ({ + useAccountSyncing: jest.fn().mockReturnValue({ + dispatchAccountSyncing: jest.fn(), + error: undefined, + }), +})); + const render = (Component: React.ComponentType) => renderScreen( Component, @@ -144,4 +153,40 @@ describe('Wallet', () => { const foxIcon = screen.getByTestId(CommonSelectorsIDs.FOX_ICON); expect(foxIcon).toBeDefined(); }); + it('dispatches account syncing on mount', () => { + jest.clearAllMocks(); + //@ts-expect-error we are ignoring the navigation params on purpose because we do not want to mock setOptions to test the navbar + render(Wallet); + expect(useAccountSyncing().dispatchAccountSyncing).toHaveBeenCalledTimes(1); + }); + it('dispatches account syncing when appState switches from inactive|background to active', () => { + jest.clearAllMocks(); + + const addEventListener = jest.spyOn(AppState, 'addEventListener'); + + //@ts-expect-error we are ignoring the navigation params on purpose because we do not want to mock setOptions to test the navbar + render(Wallet); + + expect(addEventListener).toHaveBeenCalledWith( + 'change', + expect.any(Function), + ); + const handleAppStateChange = ( + addEventListener as jest.Mock + ).mock.calls.find(([event]) => event === 'change')[1]; + + act(() => { + handleAppStateChange('background'); + handleAppStateChange('active'); + }); + + expect(useAccountSyncing().dispatchAccountSyncing).toHaveBeenCalledTimes(2); + + act(() => { + handleAppStateChange('inactive'); + handleAppStateChange('active'); + }); + + expect(useAccountSyncing().dispatchAccountSyncing).toHaveBeenCalledTimes(3); + }); }); diff --git a/app/components/Views/Wallet/index.tsx b/app/components/Views/Wallet/index.tsx index 4ecc384baa6..a188ebb5340 100644 --- a/app/components/Views/Wallet/index.tsx +++ b/app/components/Views/Wallet/index.tsx @@ -94,6 +94,8 @@ import { } from '../../../selectors/notifications'; import { ButtonVariants } from '../../../component-library/components/Buttons/Button'; import { useListNotifications } from '../../../util/notifications/hooks/useNotifications'; +import { useAccountSyncing } from '../../../util/notifications/hooks/useAccountSyncing'; + import { PortfolioBalance } from '../../UI/Tokens/TokenList/PortfolioBalance'; import useCheckNftAutoDetectionModal from '../../hooks/useCheckNftAutoDetectionModal'; import useCheckMultiRpcModal from '../../hooks/useCheckMultiRpcModal'; @@ -161,6 +163,7 @@ const Wallet = ({ const appState = useRef(AppState.currentState); const { navigate } = useNavigation(); const { listNotifications } = useListNotifications(); + const { dispatchAccountSyncing } = useAccountSyncing(); const walletRef = useRef(null); const theme = useTheme(); const { toastRef } = useContext(ToastContext); @@ -401,13 +404,17 @@ const Wallet = ({ [navigation, providerConfig.chainId], ); + // Layout effect when component/view is visible + // - fetches notifications + // - dispatches account syncing useLayoutEffect(() => { const handleAppStateChange = (nextAppState: AppStateStatus) => { if ( - appState.current.match(/inactive|background/) && + appState.current?.match(/inactive|background/) && nextAppState === 'active' ) { listNotifications(); + dispatchAccountSyncing(); } appState.current = nextAppState; @@ -417,11 +424,14 @@ const Wallet = ({ 'change', handleAppStateChange, ); + listNotifications(); + dispatchAccountSyncing(); + return () => { subscription.remove(); }; - }, [listNotifications]); + }, [listNotifications, dispatchAccountSyncing]); useEffect(() => { navigation.setOptions( diff --git a/app/core/Analytics/MetaMetrics.events.ts b/app/core/Analytics/MetaMetrics.events.ts index cbfae65629e..c2cc5e016c1 100644 --- a/app/core/Analytics/MetaMetrics.events.ts +++ b/app/core/Analytics/MetaMetrics.events.ts @@ -374,6 +374,9 @@ enum EVENT_NAME { PRIMARY_CURRENCY_TOGGLE = 'primary_currency_toggle', LOGIN_DOWNLOAD_LOGS = 'Download State Logs Button Clicked', + // Profile Syncing + ACCOUNTS_SYNC_ADDED = 'Accounts Sync Added', + ACCOUNTS_SYNC_NAME_UPDATED = 'Accounts Sync Name Updated', // network MULTI_RPC_MIGRATION_MODAL_ACCEPTED = 'multi_rpc_migration_modal_accepted', @@ -865,7 +868,11 @@ const events = { ), PRIMARY_CURRENCY_TOGGLE: generateOpt(EVENT_NAME.PRIMARY_CURRENCY_TOGGLE), LOGIN_DOWNLOAD_LOGS: generateOpt(EVENT_NAME.LOGIN_DOWNLOAD_LOGS), - + // Profile Syncing + ACCOUNTS_SYNC_ADDED: generateOpt(EVENT_NAME.ACCOUNTS_SYNC_ADDED), + ACCOUNTS_SYNC_NAME_UPDATED: generateOpt( + EVENT_NAME.ACCOUNTS_SYNC_NAME_UPDATED, + ), // Connection CONNECTION_DROPPED: generateOpt(EVENT_NAME.CONNECTION_DROPPED), CONNECTION_RESTORED: generateOpt(EVENT_NAME.CONNECTION_RESTORED), diff --git a/app/core/Engine.ts b/app/core/Engine.ts index 3d72ab1a406..15e430d88b3 100644 --- a/app/core/Engine.ts +++ b/app/core/Engine.ts @@ -135,7 +135,7 @@ import { } from '@metamask/snaps-controllers'; import { WebViewExecutionService } from '@metamask/snaps-controllers/react-native'; -import { NotificationArgs } from '@metamask/snaps-rpc-methods/dist/types/restricted/notify'; +import { NotificationParameters } from '@metamask/snaps-rpc-methods/dist/restricted/notify.cjs'; import { getSnapsWebViewPromise } from '../lib/snaps'; import { buildSnapEndowmentSpecifications, @@ -894,7 +894,7 @@ export class Engine { type, requestData: { content, placeholder }, }), - showInAppNotification: (origin: string, args: NotificationArgs) => { + showInAppNotification: (origin: string, args: NotificationParameters) => { Logger.log( 'Snaps/ showInAppNotification called with args: ', args, @@ -1218,12 +1218,42 @@ export class Engine { const userStorageController = new UserStorageController.Controller({ getMetaMetricsState: () => MetaMetrics.getInstance().isEnabled(), + env: { + isAccountSyncingEnabled: Boolean(process.env.IS_TEST), + }, + config: { + accountSyncing: { + onAccountAdded: (profileId) => { + MetaMetrics.getInstance().trackEvent( + MetricsEventBuilder.createEventBuilder( + MetaMetricsEvents.ACCOUNTS_SYNC_ADDED, + ) + .addProperties({ + profile_id: profileId, + }) + .build(), + ); + }, + onAccountNameUpdated: (profileId) => { + MetaMetrics.getInstance().trackEvent( + MetricsEventBuilder.createEventBuilder( + MetaMetricsEvents.ACCOUNTS_SYNC_NAME_UPDATED, + ) + .addProperties({ + profile_id: profileId, + }) + .build(), + ); + }, + }, + }, state: initialState.UserStorageController, messenger: this.controllerMessenger.getRestricted({ name: 'UserStorageController', allowedActions: [ 'SnapController:handleRequest', 'KeyringController:getState', + 'KeyringController:addNewAccount', 'AuthenticationController:getBearerToken', 'AuthenticationController:getSessionProfile', 'AuthenticationController:isSignedIn', @@ -1231,13 +1261,12 @@ export class Engine { 'AuthenticationController:performSignIn', 'NotificationServicesController:disableNotificationServices', 'NotificationServicesController:selectIsNotificationServicesEnabled', - 'KeyringController:addNewAccount', 'AccountsController:listAccounts', 'AccountsController:updateAccountMetadata', ], allowedEvents: [ - 'KeyringController:lock', 'KeyringController:unlock', + 'KeyringController:lock', 'AccountsController:accountAdded', 'AccountsController:accountRenamed', ], diff --git a/app/util/notifications/hooks/useAccountSyncing.test.tsx b/app/util/notifications/hooks/useAccountSyncing.test.tsx new file mode 100644 index 00000000000..f9fdb72cb40 --- /dev/null +++ b/app/util/notifications/hooks/useAccountSyncing.test.tsx @@ -0,0 +1,102 @@ +/* eslint-disable import/no-namespace */ +/* eslint-disable react/prop-types */ +/* eslint-disable react/display-name */ +/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable @typescript-eslint/no-require-imports */ +import { act, renderHook } from '@testing-library/react-hooks'; +import React from 'react'; +import { Provider } from 'react-redux'; +import createMockStore from 'redux-mock-store'; +import * as Actions from '../../../actions/notification/helpers'; +import initialRootState from '../../../util/test/initial-root-state'; +import { useAccountSyncing } from './useAccountSyncing'; + +function arrangeStore() { + const store = createMockStore()(initialRootState); + + // Ensure dispatch mocks are handled correctly + store.dispatch = jest.fn().mockImplementation((action) => { + if (typeof action === 'function') { + return action(store.dispatch, store.getState); + } + return Promise.resolve(); + }); + + return store; +} + +describe('useAccountSyncing', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + function arrangeHook() { + const store = arrangeStore(); + const hook = renderHook(() => useAccountSyncing(), { + wrapper: ({ children }) => {children}, + }); + + return hook; + } + + function arrangeActions() { + const syncInternalAccountsWithUserStorageAction = jest + .spyOn(Actions, 'syncInternalAccountsWithUserStorage') + .mockResolvedValue(undefined); + + return { + syncInternalAccountsWithUserStorageAction, + }; + } + + it('dispatches account syncing and error as undefined', async () => { + const mockActions = arrangeActions(); + + const { result } = arrangeHook(); + await act(async () => { + await result.current.dispatchAccountSyncing(); + }); + + expect( + mockActions.syncInternalAccountsWithUserStorageAction, + ).toHaveBeenCalledTimes(1); + expect(result.current.error).toBeUndefined(); + }); + + it('sets error message when syncInternalAccountsWithUserStorageAction returns an error', async () => { + const mockActions = arrangeActions(); + mockActions.syncInternalAccountsWithUserStorageAction.mockRejectedValueOnce( + new Error('MOCK - failed to sync internal account with user storage'), + ); + + const { result } = arrangeHook(); + await act(async () => { + await result.current.dispatchAccountSyncing(); + }); + + expect( + mockActions.syncInternalAccountsWithUserStorageAction, + ).toHaveBeenCalledTimes(1); + expect(result.current.error).toBeDefined(); + expect(result.current.error).toEqual( + 'MOCK - failed to sync internal account with user storage', + ); + }); + + it('sets error message when an error occurs during dispatchAccountSyncing', async () => { + const mockActions = arrangeActions(); + mockActions.syncInternalAccountsWithUserStorageAction.mockRejectedValueOnce( + new Error('MOCK - failed to sync internal account with user storage'), + ); + + const { result } = arrangeHook(); + await act(async () => { + await result.current.dispatchAccountSyncing(); + }); + + expect(result.current.error).toBeDefined(); + expect(result.current.error).toEqual( + 'MOCK - failed to sync internal account with user storage', + ); + }); +}); diff --git a/app/util/notifications/hooks/useAccountSyncing.ts b/app/util/notifications/hooks/useAccountSyncing.ts new file mode 100644 index 00000000000..7c309e81deb --- /dev/null +++ b/app/util/notifications/hooks/useAccountSyncing.ts @@ -0,0 +1,32 @@ +import { useState, useCallback } from 'react'; +import { getErrorMessage } from '../../../util/errorHandling'; +import { syncInternalAccountsWithUserStorage as syncInternalAccountsWithUserStorageAction } from '../../../actions/notification/helpers'; + +/** + * Custom hook to dispatch account syncing. + * + * @returns An object containing the `dispatchAccountSyncing` function, and error state. + */ +export const useAccountSyncing = () => { + const [error, setError] = useState(); + + const dispatchAccountSyncing = useCallback(async () => { + setError(undefined); + try { + const errorMessage = await syncInternalAccountsWithUserStorageAction(); + if (errorMessage) { + setError(getErrorMessage(errorMessage)); + return errorMessage; + } + } catch (e) { + const errorMessage = getErrorMessage(e); + setError(errorMessage); + return errorMessage; + } + }, []); + + return { + dispatchAccountSyncing, + error, + }; +}; diff --git a/app/util/notifications/hooks/useProfileSyncing.ts b/app/util/notifications/hooks/useProfileSyncing.ts index 33255b0b590..5f0d742dd22 100644 --- a/app/util/notifications/hooks/useProfileSyncing.ts +++ b/app/util/notifications/hooks/useProfileSyncing.ts @@ -1,4 +1,3 @@ -/* eslint-disable import/prefer-default-export */ import { useState, useCallback } from 'react'; import { ProfileSyncingReturn } from './types'; import { getErrorMessage } from '../../../util/errorHandling'; diff --git a/bitrise.yml b/bitrise.yml index 67dd1873222..99321b209ff 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -135,6 +135,8 @@ stages: - run_ios_api_specs: {} - run_tag_smoke_accounts_ios: {} - run_tag_smoke_accounts_android: {} + - run_tag_smoke_notifications_ios: {} + - run_tag_smoke_notifications_android: {} # - run_tag_smoke_assets_ios: {} - run_tag_smoke_assets_android: {} - run_tag_smoke_confirmations_ios: {} @@ -159,6 +161,8 @@ stages: - run_tag_smoke_confirmations_android: {} - run_tag_smoke_accounts_ios: {} - run_tag_smoke_accounts_android: {} + - run_tag_smoke_notifications_ios: {} + - run_tag_smoke_notifications_android: {} - run_tag_smoke_assets_ios: {} - run_tag_smoke_assets_android: {} - run_tag_smoke_swaps_ios: {} @@ -522,6 +526,22 @@ workflows: - TEST_SUITE_TAG: '.*SmokeAccounts.*' after_run: - android_e2e_test + run_tag_smoke_notifications_ios: + envs: + - TEST_SUITE_FOLDER: './e2e/specs/notifications/*' + - TEST_SUITE_TAG: '.*SmokeNotifications.*' + after_run: + - ios_e2e_test + run_tag_smoke_notifications_android: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + envs: + - TEST_SUITE_FOLDER: './e2e/specs/notifications/*' + - TEST_SUITE_TAG: '.*SmokeNotifications.*' + after_run: + - android_e2e_test run_tag_smoke_assets_ios: envs: - TEST_SUITE_FOLDER: './e2e/specs/assets/*' diff --git a/e2e/fixtures/fixture-builder.js b/e2e/fixtures/fixture-builder.js index 553cc1882b7..5cf74036981 100644 --- a/e2e/fixtures/fixture-builder.js +++ b/e2e/fixtures/fixture-builder.js @@ -421,6 +421,7 @@ class FixtureBuilder { pendingApprovalCount: 0, approvalFlows: [], }, + UserStorageController: {}, NotificationServicesController: { subscriptionAccountsSeen: [], isMetamaskNotificationsFeatureSeen: false, diff --git a/e2e/pages/AccountListView.js b/e2e/pages/AccountListView.js index c06efe7df19..f42dbc96b50 100644 --- a/e2e/pages/AccountListView.js +++ b/e2e/pages/AccountListView.js @@ -46,6 +46,13 @@ class AccountListView { return Matchers.getElementByID(CellModalSelectorsIDs.BASE_TITLE, index); } + getAccountElementByAccountName(accountName) { + return Matchers.getElementByIDAndLabel( + CellModalSelectorsIDs.BASE_TITLE, + accountName, + ); + } + getSelectElement(index) { return Matchers.getElementByID(CellModalSelectorsIDs.SELECT, index); } diff --git a/e2e/pages/modals/AccountActionsModal.js b/e2e/pages/modals/AccountActionsModal.js index 0fdfc25ef84..5ca700ed16a 100644 --- a/e2e/pages/modals/AccountActionsModal.js +++ b/e2e/pages/modals/AccountActionsModal.js @@ -1,6 +1,9 @@ import { AccountActionsModalSelectorsIDs } from '../../selectors/Modals/AccountActionsModal.selectors.js'; import Matchers from '../../utils/Matchers'; import Gestures from '../../utils/Gestures'; +import EditAccountNameSelectorIDs from '../../selectors/EditAccountName.selectors.js'; +import TestHelpers from '../../helpers.js'; +import EditAccountNameView from '../EditAccountNameView.js'; class AccountActionsModal { get editAccount() { @@ -22,6 +25,16 @@ class AccountActionsModal { async tapShowPrivateKey() { await Gestures.waitAndTap(await this.showPrivateKey); } + + async renameActiveAccount(newName) { + await this.tapEditAccount(); + await Gestures.clearField(EditAccountNameView.accountNameInput); + await TestHelpers.typeTextAndHideKeyboard( + EditAccountNameSelectorIDs.ACCOUNT_NAME_INPUT, + newName, + ); + await EditAccountNameView.tapSave(); + } } export default new AccountActionsModal(); diff --git a/e2e/specs/notifications/account-syncing/mockData.js b/e2e/specs/notifications/account-syncing/mockData.js new file mode 100644 index 00000000000..96e92ecd849 --- /dev/null +++ b/e2e/specs/notifications/account-syncing/mockData.js @@ -0,0 +1,12 @@ +export const accountsSyncMockResponse = [ + { + HashedKey: + '997050281e559a2bb40d1c2e73d9f0887cbea1b81ff9dd7815917949e37f4f2f', + Data: '{"v":"1","t":"scrypt","d":"1yC/ZXarV57HbqEZ46nH0JWgXfPl86nTHD7kai2g5gm290FM9tw5QjOaAAwIuQESEE8TIM/J9pIj7nmlGi+BZrevTtK3DXWXwnUQsCP7amKd5Q4gs3EEQgXpA0W+WJUgyElj869rwIv/C6tl5E2pK4j/0EAjMSIm1TGoj9FPohyRgZsOIt8VhZfb7w0GODsjPwPIkN6zazvJ3gAFYFPh7yRtebFs86z3fzqCWZ9zakdCHntchC2oZiaApXR9yzaPlGgnPg==","o":{"N":131072,"r":8,"p":1,"dkLen":16},"saltLen":16}', + }, + { + HashedKey: + 'e53d8feb65b4cf0c339e57bee2a81b155e056622f9192c54b707f928c8a42a7a', + Data: '{"v":"1","t":"scrypt","d":"O7QEtUo7q/jG+UNkD/HOxQARGGRXsGPrLsDlkwDfgfoYlPI0To/M3pJRBlKD0RLEFIPHtHBEA5bv/2izB21VljvhMnhHfo0KgQ+e8Uq1t7grwa+r+ge3qbPNY+w78Xt8GtC+Hkrw5fORKvCn+xjzaCHYV6RxKYbp1TpyCJq7hDrr1XiyL8kqbpE0hAHALrrQOoV9/WXJi9pC5J118kquXx8CNA1P5wO/BXKp1AbryGR6kVW3lsp1sy3lYE/TApa5lTj+","o":{"N":131072,"r":8,"p":1,"dkLen":16},"saltLen":16}', + }, +]; diff --git a/e2e/specs/notifications/account-syncing/sync-after-adding-custom-name-account.spec.js b/e2e/specs/notifications/account-syncing/sync-after-adding-custom-name-account.spec.js new file mode 100644 index 00000000000..d4b825ec117 --- /dev/null +++ b/e2e/specs/notifications/account-syncing/sync-after-adding-custom-name-account.spec.js @@ -0,0 +1,98 @@ +import { SDK } from '@metamask/profile-sync-controller'; +import { + NOTIFICATIONS_TEAM_PASSWORD, + NOTIFICATIONS_TEAM_SEED_PHRASE, + NOTIFICATIONS_TEAM_STORAGE_KEY, +} from '../utils/constants'; +import { + startMockServer, + stopMockServer, +} from '../../../api-mocking/mock-server'; +import { accountsSyncMockResponse } from './mockData'; +import { importWalletWithRecoveryPhrase } from '../../../viewHelper'; +import TestHelpers from '../../../helpers'; +import WalletView from '../../../pages/wallet/WalletView'; +import AccountListView from '../../../pages/AccountListView'; +import Assertions from '../../../utils/Assertions'; +import AddAccountModal from '../../../pages/modals/AddAccountModal'; +import AccountActionsModal from '../../../pages/modals/AccountActionsModal'; +import { mockNotificationServices } from '../utils/mocks'; +import { SmokeNotifications } from '../../../tags'; + +describe(SmokeNotifications('Account syncing'), () => { + const NEW_ACCOUNT_NAME = 'My third account'; + + it('syncs newly added accounts with custom names and retrieves them after importing the same SRP', async () => { + jest.setTimeout(200000); + await TestHelpers.reverseServerPort(); + + const mockServer = await startMockServer(); + + const { userStorageMockttpControllerInstance } = + await mockNotificationServices(mockServer); + + userStorageMockttpControllerInstance.setupPath('accounts', mockServer, { + getResponse: accountsSyncMockResponse, + }); + + const decryptedAccountNames = await Promise.all( + accountsSyncMockResponse.map(async (response) => { + const decryptedAccountName = await SDK.Encryption.decryptString( + response.Data, + NOTIFICATIONS_TEAM_STORAGE_KEY, + ); + return JSON.parse(decryptedAccountName).n; + }), + ); + + await device.launchApp({ + newInstance: true, + delete: true, + }); + + await importWalletWithRecoveryPhrase( + NOTIFICATIONS_TEAM_SEED_PHRASE, + NOTIFICATIONS_TEAM_PASSWORD, + ); + + await WalletView.tapIdenticon(); + await Assertions.checkIfVisible(AccountListView.accountList); + + for (const accountName of decryptedAccountNames) { + await Assertions.checkIfVisible( + await AccountListView.getAccountElementByAccountName(accountName), + ); + } + + await AccountListView.tapAddAccountButton(); + await AddAccountModal.tapCreateAccount(); + await AccountListView.swipeToDismissAccountsModal(); + + await WalletView.tapMainWalletAccountActions(); + await AccountActionsModal.renameActiveAccount(NEW_ACCOUNT_NAME); + + await Assertions.checkIfElementToHaveText( + WalletView.accountName, + NEW_ACCOUNT_NAME, + ); + + await device.launchApp({ + newInstance: true, + delete: true, + }); + + await importWalletWithRecoveryPhrase( + NOTIFICATIONS_TEAM_SEED_PHRASE, + NOTIFICATIONS_TEAM_PASSWORD, + ); + + await WalletView.tapIdenticon(); + await Assertions.checkIfVisible(AccountListView.accountList); + + await Assertions.checkIfVisible( + await AccountListView.getAccountElementByAccountName(NEW_ACCOUNT_NAME), + ); + + await stopMockServer(); + }); +}); diff --git a/e2e/specs/notifications/account-syncing/sync-after-onboarding.spec.js b/e2e/specs/notifications/account-syncing/sync-after-onboarding.spec.js new file mode 100644 index 00000000000..3be2697e1e3 --- /dev/null +++ b/e2e/specs/notifications/account-syncing/sync-after-onboarding.spec.js @@ -0,0 +1,67 @@ +import { SDK } from '@metamask/profile-sync-controller'; +import { + NOTIFICATIONS_TEAM_PASSWORD, + NOTIFICATIONS_TEAM_SEED_PHRASE, + NOTIFICATIONS_TEAM_STORAGE_KEY, +} from '../utils/constants'; +import { + startMockServer, + stopMockServer, +} from '../../../api-mocking/mock-server'; +import { accountsSyncMockResponse } from './mockData'; +import { importWalletWithRecoveryPhrase } from '../../../viewHelper'; +import TestHelpers from '../../../helpers'; +import WalletView from '../../../pages/wallet/WalletView'; +import AccountListView from '../../../pages/AccountListView'; +import Assertions from '../../../utils/Assertions'; +import { mockNotificationServices } from '../utils/mocks'; +import { SmokeNotifications } from '../../../tags'; + +describe(SmokeNotifications('Account syncing'), () => { + it('retrieves all previously synced accounts', async () => { + const decryptedAccountNames = await Promise.all( + accountsSyncMockResponse.map(async (response) => { + const decryptedAccountName = await SDK.Encryption.decryptString( + response.Data, + NOTIFICATIONS_TEAM_STORAGE_KEY, + ); + return JSON.parse(decryptedAccountName).n; + }), + ); + + const mockServer = await startMockServer({ + mockUrl: 'https://user-storage.api.cx.metamask.io/api/v1/userstorage', + }); + + const { userStorageMockttpControllerInstance } = + await mockNotificationServices(mockServer); + + userStorageMockttpControllerInstance.setupPath('accounts', mockServer, { + getResponse: accountsSyncMockResponse, + }); + + jest.setTimeout(200000); + await TestHelpers.reverseServerPort(); + + await device.launchApp({ + newInstance: true, + delete: true, + }); + + await importWalletWithRecoveryPhrase( + NOTIFICATIONS_TEAM_SEED_PHRASE, + NOTIFICATIONS_TEAM_PASSWORD, + ); + + await WalletView.tapIdenticon(); + await Assertions.checkIfVisible(AccountListView.accountList); + + for (const accountName of decryptedAccountNames) { + await Assertions.checkIfVisible( + await AccountListView.getAccountElementByAccountName(accountName), + ); + } + + await stopMockServer(); + }); +}); diff --git a/e2e/specs/notifications/utils/constants.js b/e2e/specs/notifications/utils/constants.js new file mode 100644 index 00000000000..8c611dcd4f4 --- /dev/null +++ b/e2e/specs/notifications/utils/constants.js @@ -0,0 +1,7 @@ +// As we rely on profile syncing for most of our features, we need to use the same SRP for all of our tests +export const NOTIFICATIONS_TEAM_SEED_PHRASE = + 'leisure swallow trip elbow prison wait rely keep supply hole general mountain'; +export const NOTIFICATIONS_TEAM_PASSWORD = 'notify_password'; +// You can use the storage key below to generate mock data +export const NOTIFICATIONS_TEAM_STORAGE_KEY = + '0d55d30da233959674d14076737198c05ae3fb8631a17e20d3c28c60dddd82f7'; diff --git a/e2e/specs/notifications/utils/helpers.js b/e2e/specs/notifications/utils/helpers.js new file mode 100644 index 00000000000..7eed1dd5382 --- /dev/null +++ b/e2e/specs/notifications/utils/helpers.js @@ -0,0 +1,10 @@ +export const determineIfFeatureEntryFromURL = (url) => { + const decodedUrl = decodeURIComponent(url); + return ( + decodedUrl.substring(decodedUrl.lastIndexOf('userstorage') + 12).split('/') + .length === 2 + ); +}; + +export const getDecodedProxiedURL = (url) => + decodeURIComponent(String(new URL(url).searchParams.get('url'))); diff --git a/e2e/specs/notifications/utils/mocks.js b/e2e/specs/notifications/utils/mocks.js new file mode 100644 index 00000000000..155e84a325a --- /dev/null +++ b/e2e/specs/notifications/utils/mocks.js @@ -0,0 +1,83 @@ +import { AuthenticationController } from '@metamask/profile-sync-controller'; +import { + NotificationServicesController, + NotificationServicesPushController, +} from '@metamask/notification-services-controller'; +import { UserStorageMockttpController } from './user-storage/userStorageMockttpController'; +import { getDecodedProxiedURL } from './helpers'; + +const AuthMocks = AuthenticationController.Mocks; +const NotificationMocks = NotificationServicesController.Mocks; +const PushMocks = NotificationServicesPushController.Mocks; + +/** + * E2E mock setup for notification APIs (Auth, UserStorage, Notifications, Push Notifications, Profile syncing) + * + * @param server - server obj used to mock our endpoints + * @param userStorageMockttpController - optional controller to mock user storage endpoints + */ +export async function mockNotificationServices(server) { + // Auth + mockAPICall(server, AuthMocks.getMockAuthNonceResponse()); + mockAPICall(server, AuthMocks.getMockAuthLoginResponse()); + mockAPICall(server, AuthMocks.getMockAuthAccessTokenResponse()); + + // Storage + const userStorageMockttpControllerInstance = + new UserStorageMockttpController(); + + userStorageMockttpControllerInstance.setupPath('accounts', server); + userStorageMockttpControllerInstance.setupPath('networks', server); + userStorageMockttpControllerInstance.setupPath('notifications', server); + + // Notifications + mockAPICall(server, NotificationMocks.getMockFeatureAnnouncementResponse()); + mockAPICall(server, NotificationMocks.getMockBatchCreateTriggersResponse()); + mockAPICall(server, NotificationMocks.getMockBatchDeleteTriggersResponse()); + mockAPICall(server, NotificationMocks.getMockListNotificationsResponse()); + mockAPICall( + server, + NotificationMocks.getMockMarkNotificationsAsReadResponse(), + ); + + // Push Notifications + mockAPICall(server, PushMocks.getMockRetrievePushNotificationLinksResponse()); + mockAPICall(server, PushMocks.getMockUpdatePushNotificationLinksResponse()); + mockAPICall(server, PushMocks.getMockCreateFCMRegistrationTokenResponse()); + mockAPICall(server, PushMocks.getMockDeleteFCMRegistrationTokenResponse()); + + return { + userStorageMockttpControllerInstance, + }; +} + +function mockAPICall(server, response) { + let requestRuleBuilder; + + if (response.requestMethod === 'GET') { + requestRuleBuilder = server.forGet('/proxy'); + } + + if (response.requestMethod === 'POST') { + requestRuleBuilder = server.forPost('/proxy'); + } + + if (response.requestMethod === 'PUT') { + requestRuleBuilder = server.forPut('/proxy'); + } + + if (response.requestMethod === 'DELETE') { + requestRuleBuilder = server.forDelete('/proxy'); + } + + requestRuleBuilder + ?.matching((request) => { + const url = getDecodedProxiedURL(request.url); + + return url.includes(String(response.url)); + }) + .thenCallback(() => ({ + statusCode: 200, + json: response.response, + })); +} diff --git a/e2e/specs/notifications/utils/user-storage/userStorageMockttpController.js b/e2e/specs/notifications/utils/user-storage/userStorageMockttpController.js new file mode 100644 index 00000000000..9438e2a7fc7 --- /dev/null +++ b/e2e/specs/notifications/utils/user-storage/userStorageMockttpController.js @@ -0,0 +1,171 @@ +import { + determineIfFeatureEntryFromURL, + getDecodedProxiedURL, +} from '../helpers'; + +// TODO: Export user storage schema from @metamask/profile-sync-controller +export const pathRegexps = { + accounts: + /https:\/\/user-storage\.api\.cx\.metamask\.io\/api\/v1\/userstorage\/accounts/u, + networks: + /https:\/\/user-storage\.api\.cx\.metamask\.io\/api\/v1\/userstorage\/networks/u, + notifications: + /https:\/\/user-storage\.api\.cx\.metamask\.io\/api\/v1\/userstorage\/notifications/u, +}; + +export class UserStorageMockttpController { + paths = new Map(); + + async onGet(path, request, statusCode = 200) { + const internalPathData = this.paths.get(path); + + if (!internalPathData) { + return { + statusCode, + json: null, + }; + } + + const isFeatureEntry = determineIfFeatureEntryFromURL(request.url); + + if (isFeatureEntry) { + const json = + internalPathData.response?.find( + (entry) => + entry.HashedKey === + getDecodedProxiedURL(request.url).split('/').pop(), + ) || null; + + return { + statusCode, + json, + }; + } + + const json = internalPathData?.response.length + ? internalPathData.response + : null; + + return { + statusCode, + json, + }; + } + + async onPut(path, request, statusCode = 204) { + const isFeatureEntry = determineIfFeatureEntryFromURL(request.url); + + const data = await request.body.getJson(); + + const newOrUpdatedSingleOrBatchEntries = + isFeatureEntry && typeof data?.data === 'string' + ? [ + { + HashedKey: getDecodedProxiedURL(request.url).split('/').pop(), + Data: data?.data, + }, + ] + : Object.entries(data?.data).map(([key, value]) => ({ + HashedKey: key, + Data: value, + })); + + newOrUpdatedSingleOrBatchEntries.forEach((entry) => { + const internalPathData = this.paths.get(path); + + if (!internalPathData) { + return; + } + + const doesThisEntryExist = internalPathData.response?.find( + (existingEntry) => existingEntry.HashedKey === entry.HashedKey, + ); + + if (doesThisEntryExist) { + this.paths.set(path, { + ...internalPathData, + response: internalPathData.response.map((existingEntry) => + existingEntry.HashedKey === entry.HashedKey ? entry : existingEntry, + ), + }); + } else { + this.paths.set(path, { + ...internalPathData, + response: [...(internalPathData?.response || []), entry], + }); + } + }); + + return { + statusCode, + }; + } + + async onDelete(path, request, statusCode = 204) { + const internalPathData = this.paths.get(path); + + if (!internalPathData) { + return { + statusCode, + }; + } + + const isFeatureEntry = determineIfFeatureEntryFromURL(request.url); + + if (isFeatureEntry) { + this.paths.set(path, { + ...internalPathData, + response: internalPathData?.response.filter( + (entry) => + entry.HashedKey !== + getDecodedProxiedURL(request.url).split('/').pop(), + ), + }); + } else { + this.paths.set(path, { + ...internalPathData, + response: [], + }); + } + + return { + statusCode, + }; + } + + async setupPath(path, server, overrides) { + const previouslySetupPath = this.paths.get(path); + + this.paths.set(path, { + response: overrides?.getResponse || previouslySetupPath?.response || [], + }); + + await server + .forGet('/proxy') + .matching((request) => + pathRegexps[path].test(getDecodedProxiedURL(request.url)), + ) + .always() + .thenCallback((request) => + this.onGet(path, request, overrides?.getStatusCode), + ); + await server + .forPut('/proxy') + .matching((request) => + pathRegexps[path].test(getDecodedProxiedURL(request.url)), + ) + .always() + .thenCallback((request) => + this.onPut(path, request, overrides?.putStatusCode), + ); + await server + .forDelete('/proxy') + .matching((request) => + pathRegexps[path].test(getDecodedProxiedURL(request.url)), + ) + .always() + .thenCallback((request) => + this.onDelete(path, request, overrides?.deleteStatusCode), + ); + } +} diff --git a/e2e/specs/notifications/utils/user-storage/userStorageMockttpController.test.js b/e2e/specs/notifications/utils/user-storage/userStorageMockttpController.test.js new file mode 100644 index 00000000000..5ea0f1603ca --- /dev/null +++ b/e2e/specs/notifications/utils/user-storage/userStorageMockttpController.test.js @@ -0,0 +1,305 @@ +import { getLocal } from 'mockttp'; +import { UserStorageMockttpController } from './userStorageMockttpController'; + +describe('UserStorageMockttpController', () => { + let mockServer; + + const baseUrl = + 'http://localhost/proxy?url=https://user-storage.api.cx.metamask.io/api/v1/userstorage'; + + describe('mimics user storage behaviour', () => { + mockServer = getLocal({ cors: true }); + + it('handles GET requests that have empty response', async () => { + const controller = new UserStorageMockttpController(); + + await controller.setupPath('accounts', mockServer); + + const request = await controller.onGet('accounts', { + url: `${baseUrl}/accounts`, + }); + + expect(request.json).toEqual(null); + }); + + it('handles GET requests that have a pre-defined response', async () => { + const controller = new UserStorageMockttpController(); + const mockedData = [ + { + HashedKey: + '7f8a7963423985c50f75f6ad42a6cf4e7eac43a6c55e3c6fcd49d73f01c1471b', + Data: 'data1', + }, + { + HashedKey: + 'c236b92ea7d513b2beda062cb546986961dfa7ca4334a2913f7837e43d050468', + Data: 'data2', + }, + ]; + + await controller.setupPath('accounts', mockServer, { + getResponse: mockedData, + }); + + const request = await controller.onGet('accounts', { + url: `${baseUrl}/accounts`, + }); + + expect(request.json).toEqual(mockedData); + }); + + it('handles batch GET requests', async () => { + const controller = new UserStorageMockttpController(); + const mockedData = [ + { + HashedKey: + '7f8a7963423985c50f75f6ad42a6cf4e7eac43a6c55e3c6fcd49d73f01c1471b', + Data: 'data1', + }, + { + HashedKey: + 'c236b92ea7d513b2beda062cb546986961dfa7ca4334a2913f7837e43d050468', + Data: 'data2', + }, + ]; + + await controller.setupPath('accounts', mockServer, { + getResponse: mockedData, + }); + + const request = await controller.onGet('accounts', { + url: `${baseUrl}/accounts`, + }); + + expect(request.json).toEqual(mockedData); + }); + + it('handles GET requests for feature entries', async () => { + const controller = new UserStorageMockttpController(); + const mockedData = [ + { + HashedKey: + '7f8a7963423985c50f75f6ad42a6cf4e7eac43a6c55e3c6fcd49d73f01c1471b', + Data: 'data1', + }, + { + HashedKey: + 'c236b92ea7d513b2beda062cb546986961dfa7ca4334a2913f7837e43d050468', + Data: 'data2', + }, + ]; + + await controller.setupPath('accounts', mockServer, { + getResponse: mockedData, + }); + + const request = await controller.onGet('accounts', { + url: `${baseUrl}/accounts/7f8a7963423985c50f75f6ad42a6cf4e7eac43a6c55e3c6fcd49d73f01c1471b`, + }); + + expect(request.json).toEqual(mockedData[0]); + }); + + it('handles PUT requests to create new entries', async () => { + const controller = new UserStorageMockttpController(); + const mockedData = [ + { + HashedKey: + '7f8a7963423985c50f75f6ad42a6cf4e7eac43a6c55e3c6fcd49d73f01c1471b', + Data: 'data1', + }, + { + HashedKey: + 'c236b92ea7d513b2beda062cb546986961dfa7ca4334a2913f7837e43d050468', + Data: 'data2', + }, + ]; + const mockedAddedData = { + HashedKey: + '6afbe024087495b4e0d56c4bdfc981c84eba44a7c284d4f455b5db4fcabc2173', + Data: 'data3', + }; + + await controller.setupPath('accounts', mockServer, { + getResponse: mockedData, + }); + + const putRequest = await controller.onPut('accounts', { + url: `${baseUrl}/accounts/6afbe024087495b4e0d56c4bdfc981c84eba44a7c284d4f455b5db4fcabc2173`, + body: { + getJson: async () => ({ + data: mockedAddedData.Data, + }), + }, + }); + + expect(putRequest.statusCode).toEqual(204); + + const getRequest = await controller.onGet('accounts', { + url: `${baseUrl}/accounts`, + }); + + expect(getRequest.json).toEqual([...mockedData, mockedAddedData]); + }); + + it('handles PUT requests to update existing entries', async () => { + const controller = new UserStorageMockttpController(); + const mockedData = [ + { + HashedKey: + '7f8a7963423985c50f75f6ad42a6cf4e7eac43a6c55e3c6fcd49d73f01c1471b', + Data: 'data1', + }, + { + HashedKey: + 'c236b92ea7d513b2beda062cb546986961dfa7ca4334a2913f7837e43d050468', + Data: 'data2', + }, + ]; + const mockedUpdatedData = { + HashedKey: + 'c236b92ea7d513b2beda062cb546986961dfa7ca4334a2913f7837e43d050468', + Data: 'data3', + }; + + await controller.setupPath('accounts', mockServer, { + getResponse: mockedData, + }); + + const putRequest = await controller.onPut('accounts', { + url: `${baseUrl}/accounts/c236b92ea7d513b2beda062cb546986961dfa7ca4334a2913f7837e43d050468`, + body: { + getJson: async () => ({ + data: mockedUpdatedData.Data, + }), + }, + }); + + expect(putRequest.statusCode).toEqual(204); + + const getRequest = await controller.onGet('accounts', { + url: `${baseUrl}/accounts`, + }); + + expect(getRequest.json).toEqual([mockedData[0], mockedUpdatedData]); + }); + + it('handles batch PUT requests', async () => { + const controller = new UserStorageMockttpController(); + const mockedData = [ + { + HashedKey: + '7f8a7963423985c50f75f6ad42a6cf4e7eac43a6c55e3c6fcd49d73f01c1471b', + Data: 'data1', + }, + { + HashedKey: + 'c236b92ea7d513b2beda062cb546986961dfa7ca4334a2913f7837e43d050468', + Data: 'data2', + }, + ]; + const mockedUpdatedData = [ + { + HashedKey: + '7f8a7963423985c50f75f6ad42a6cf4e7eac43a6c55e3c6fcd49d73f01c1471b', + Data: 'data3', + }, + { + HashedKey: + 'c236b92ea7d513b2beda062cb546986961dfa7ca4334a2913f7837e43d050468', + Data: 'data4', + }, + ]; + + await controller.setupPath('accounts', mockServer, { + getResponse: mockedData, + }); + + const putData = {}; + mockedUpdatedData.forEach((entry) => { + putData[entry.HashedKey] = entry.Data; + }); + + const putRequest = await controller.onPut('accounts', { + url: `${baseUrl}/accounts`, + body: { + getJson: async () => ({ + data: putData, + }), + }, + }); + + expect(putRequest.statusCode).toEqual(204); + + const getRequest = await controller.onGet('accounts', { + url: `${baseUrl}/accounts`, + }); + + expect(getRequest.json).toEqual(mockedUpdatedData); + }); + + it('handles DELETE requests', async () => { + const controller = new UserStorageMockttpController(); + const mockedData = [ + { + HashedKey: + '7f8a7963423985c50f75f6ad42a6cf4e7eac43a6c55e3c6fcd49d73f01c1471b', + Data: 'data1', + }, + { + HashedKey: + 'c236b92ea7d513b2beda062cb546986961dfa7ca4334a2913f7837e43d050468', + Data: 'data2', + }, + ]; + + await controller.setupPath('accounts', mockServer, { + getResponse: mockedData, + }); + + const deleteRequest = await controller.onDelete('accounts', { + url: `${baseUrl}/accounts/c236b92ea7d513b2beda062cb546986961dfa7ca4334a2913f7837e43d050468`, + }); + + expect(deleteRequest.statusCode).toEqual(204); + + const getRequest = await controller.onGet('accounts', { + url: `${baseUrl}/accounts`, + }); + + expect(getRequest.json).toEqual([mockedData[0]]); + }); + + it('handles batch DELETE requests', async () => { + const controller = new UserStorageMockttpController(); + const mockedData = [ + { + HashedKey: + '7f8a7963423985c50f75f6ad42a6cf4e7eac43a6c55e3c6fcd49d73f01c1471b', + Data: 'data1', + }, + { + HashedKey: + 'c236b92ea7d513b2beda062cb546986961dfa7ca4334a2913f7837e43d050468', + Data: 'data2', + }, + ]; + + await controller.setupPath('accounts', mockServer, { + getResponse: mockedData, + }); + + const deleteRequest = await controller.onDelete('accounts', { + url: `${baseUrl}/accounts`, + }); + + expect(deleteRequest.statusCode).toEqual(204); + + const getRequest = await controller.onGet('accounts', { + url: `${baseUrl}/accounts`, + }); + + expect(getRequest.json).toEqual(null); + }); + }); +}); diff --git a/e2e/tags.js b/e2e/tags.js index 8e78b934e57..503188a317e 100644 --- a/e2e/tags.js +++ b/e2e/tags.js @@ -6,6 +6,7 @@ const tags = { SmokeSwaps: 'SmokeSwaps', SmokeRest: 'SmokeRest', smokeAssets: 'smokeAssets', + smokeNotifications: 'smokeNotifications', }; const Regression = (testName) => `${tags.regression} ${testName}`; @@ -15,6 +16,8 @@ const SmokeConfirmations = (testName) => `${tags.smokeConfirmations} ${testName}`; const SmokeSwaps = (testName) => `${tags.SmokeSwaps} ${testName}`; const SmokeAssets = (testName) => `${tags.smokeAssets} ${testName}`; +const SmokeNotifications = (testName) => + `${tags.smokeNotifications} ${testName}`; export { Regression, @@ -23,4 +26,5 @@ export { SmokeConfirmations, SmokeSwaps, SmokeAssets, + SmokeNotifications, }; diff --git a/e2e/viewHelper.js b/e2e/viewHelper.js index 8c3db7feba3..2439f062d06 100644 --- a/e2e/viewHelper.js +++ b/e2e/viewHelper.js @@ -87,7 +87,7 @@ export const skipNotificationsDeviceSettings = async () => { } }; -export const importWalletWithRecoveryPhrase = async () => { +export const importWalletWithRecoveryPhrase = async (seedPhrase, password) => { // tap on import seed phrase button await Assertions.checkIfVisible(OnboardingCarouselView.container); await OnboardingCarouselView.tapOnGetStartedButton(); @@ -98,9 +98,11 @@ export const importWalletWithRecoveryPhrase = async () => { await acceptTermOfUse(); // should import wallet with secret recovery phrase await ImportWalletView.clearSecretRecoveryPhraseInputBox(); - await ImportWalletView.enterSecretRecoveryPhrase(validAccount.seedPhrase); - await ImportWalletView.enterPassword(validAccount.password); - await ImportWalletView.reEnterPassword(validAccount.password); + await ImportWalletView.enterSecretRecoveryPhrase( + seedPhrase ?? validAccount.seedPhrase, + ); + await ImportWalletView.enterPassword(password ?? validAccount.password); + await ImportWalletView.reEnterPassword(password ?? validAccount.password); //'Should dismiss Enable device Notifications checks alert' await TestHelpers.delay(3500); diff --git a/package.json b/package.json index 7775052b2a0..719628574b8 100644 --- a/package.json +++ b/package.json @@ -106,7 +106,6 @@ "**/xml2js": ">=0.5.0", "react-native-level-fs/**/bl": "^1.2.3", "react-native-level-fs/levelup/semver": "^5.7.2", - "@metamask/accounts-controller": "^18.1.0", "@metamask/contract-metadata": "^2.1.0", "@metamask/react-native-payments/validator": "^13.7.0", "**/minimist": "1.2.6", @@ -185,8 +184,8 @@ "@metamask/smart-transactions-controller": "^13.1.0", "@metamask/snaps-controllers": "^9.8.0", "@metamask/snaps-execution-environments": "^6.7.2", - "@metamask/snaps-rpc-methods": "^9.1.4", - "@metamask/snaps-sdk": "^6.5.0", + "@metamask/snaps-rpc-methods": "^11.1.1", + "@metamask/snaps-sdk": "^6.5.1", "@metamask/snaps-utils": "^8.1.1", "@metamask/stake-sdk": "^0.2.13", "@metamask/swappable-obj-proxy": "^2.1.0", diff --git a/yarn.lock b/yarn.lock index 9ffae1c415c..c01533e41e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4302,7 +4302,25 @@ "@metamask/superstruct" "^3.1.0" "@metamask/utils" "^9.0.0" -"@metamask/accounts-controller@^17.2.0", "@metamask/accounts-controller@^18.1.0", "@metamask/accounts-controller@^18.2.1": +"@metamask/accounts-controller@^17.2.0": + version "17.2.0" + resolved "https://registry.yarnpkg.com/@metamask/accounts-controller/-/accounts-controller-17.2.0.tgz#b74918444a9d8e6e69b9b761dc58e64aac6b466c" + integrity sha512-hfdfRV7mxxnyG1tri3CatV63WWtwPkUSl0zTz7Mq3psSXqOFr+08f1Elw4sX7pP1V/rCxZKeotoluIjUeu1Q9Q== + dependencies: + "@ethereumjs/util" "^8.1.0" + "@metamask/base-controller" "^6.0.0" + "@metamask/eth-snap-keyring" "^4.3.1" + "@metamask/keyring-api" "^8.0.0" + "@metamask/keyring-controller" "^17.1.0" + "@metamask/snaps-sdk" "^4.2.0" + "@metamask/snaps-utils" "^7.4.0" + "@metamask/utils" "^8.3.0" + deepmerge "^4.2.2" + ethereum-cryptography "^2.1.2" + immer "^9.0.6" + uuid "^8.3.2" + +"@metamask/accounts-controller@^18.2.1": version "18.2.1" resolved "https://registry.yarnpkg.com/@metamask/accounts-controller/-/accounts-controller-18.2.1.tgz#8e4a842316e9b7bbd0409b36129f7123ba4a4c79" integrity sha512-BEvux+ZFpTOQa6HbRl7i7Tq24ztqrZIsX+H0ePh47lU+N8RWq1q0JCItV+zbsgdcYnwhtcMZTsp4jJPQwPe2og== @@ -4392,12 +4410,12 @@ "@metamask/utils" "^9.1.0" immer "^9.0.6" -"@metamask/base-controller@^7.0.1": - version "7.0.1" - resolved "https://registry.yarnpkg.com/@metamask/base-controller/-/base-controller-7.0.1.tgz#78eef77c2cd980e1f86bd5077c229bbaa5fd2b9d" - integrity sha512-U1strOKT4v/kSJ2h4tgn9iUVVuv5Ja64J+OR145ITHd4574FaUYVJR80/imn6WmCbo8B6AYwHwEovdZ5qLGSKw== +"@metamask/base-controller@^7.0.1", "@metamask/base-controller@^7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@metamask/base-controller/-/base-controller-7.0.2.tgz#bf908858215cd4f7d072b3b0f7f0946cf886ee49" + integrity sha512-zeZ5QPKedGT/r2M1NsT4lE7z4u9ciSNcOXG2vUdmfA+QT9YLwIm5+t56UGku3ZTjKGxDn9Ukca3BEkRc57Gt0A== dependencies: - "@metamask/utils" "^9.1.0" + "@metamask/utils" "^10.0.0" immer "^9.0.6" "@metamask/browser-passworder@^4.3.0": @@ -4434,17 +4452,18 @@ resolved "https://registry.yarnpkg.com/@metamask/contract-metadata/-/contract-metadata-2.2.0.tgz#277764d0d56e37180ae7644a9d11eb96295b36fc" integrity sha512-SM6A4C7vXNbVpgMTX67kfW8QWvu3eSXxMZlY5PqZBTkvri1s9zgQ0uwRkK5r2VXNEoVmXCDnnEX/tX5EzzgNUQ== -"@metamask/controller-utils@^11.0.0", "@metamask/controller-utils@^11.0.2", "@metamask/controller-utils@^11.3.0": - version "11.3.0" - resolved "https://registry.yarnpkg.com/@metamask/controller-utils/-/controller-utils-11.3.0.tgz#530fd22289f717b752b4a7b6e504e1f2911b30a4" - integrity sha512-5b+Jg9sKKESzvQcuipHC1D7KSh98MVIi7hXQUk7iX+YVMl4KoKDv94Bl+li8g+jCBshMOV9bRMRh25/hdEvTZQ== +"@metamask/controller-utils@^11.0.0", "@metamask/controller-utils@^11.0.2", "@metamask/controller-utils@^11.3.0", "@metamask/controller-utils@^11.4.2": + version "11.4.2" + resolved "https://registry.yarnpkg.com/@metamask/controller-utils/-/controller-utils-11.4.2.tgz#0186c62c841ec94f60a67d0764dc7ab59c176c51" + integrity sha512-4zGfTTpjDgvRxo/tb3v1NBu0V/b2bqaDpSQC0noi5A4sm1yiZhyzqBY9+Zg11s8G/imp7+jvYRP1QM6OjFgIjQ== dependencies: "@ethereumjs/util" "^8.1.0" "@metamask/eth-query" "^4.0.0" "@metamask/ethjs-unit" "^0.3.0" - "@metamask/utils" "^9.1.0" + "@metamask/utils" "^10.0.0" "@spruceid/siwe-parser" "2.1.0" "@types/bn.js" "^5.1.5" + bignumber.js "^9.1.2" bn.js "^5.2.1" eth-ens-namehash "^2.0.8" fast-deep-equal "^3.1.3" @@ -4685,22 +4704,20 @@ ethereum-cryptography "^2.1.2" randombytes "^2.1.0" -"@metamask/eth-snap-keyring@^4.3.3": - version "4.3.4" - resolved "https://registry.yarnpkg.com/@metamask/eth-snap-keyring/-/eth-snap-keyring-4.3.4.tgz#78308770d421f6f3c9d24dba925acf87a54012e8" - integrity sha512-y68ksrkwZJ7J5Edej+XvTDR04CwZs0BHjOVpJbnA/328P75Gs7jSHWqOE7DtziMbwpVsFlgBMTVFPsSyrh8Fmg== +"@metamask/eth-snap-keyring@^4.3.1", "@metamask/eth-snap-keyring@^4.3.3": + version "4.3.6" + resolved "https://registry.yarnpkg.com/@metamask/eth-snap-keyring/-/eth-snap-keyring-4.3.6.tgz#c010c644575d1d2dd7c47953f6a1392fe17e38d9" + integrity sha512-jds0NdBDWM99FnO7WjODnRo+fnRoo11lJZlFS+cIa4ol7TMQmJ0HQdpno7R2LNfweoTioDMQd1LY1mCBq4zXnA== dependencies: "@ethereumjs/tx" "^4.2.0" "@metamask/eth-sig-util" "^7.0.3" - "@metamask/keyring-api" "^8.1.0" - "@metamask/snaps-controllers" "^9.6.0" - "@metamask/snaps-sdk" "^6.4.0" - "@metamask/snaps-utils" "^7.8.0" + "@metamask/snaps-controllers" "^9.7.0" + "@metamask/snaps-sdk" "^6.5.1" + "@metamask/snaps-utils" "^7.8.1" "@metamask/superstruct" "^3.1.0" - "@metamask/utils" "^9.1.0" - "@types/uuid" "^9.0.1" - deepmerge "^4.2.2" - uuid "^9.0.0" + "@metamask/utils" "^9.2.1" + "@types/uuid" "^9.0.8" + uuid "^9.0.1" "@metamask/etherscan-link@^2.0.0": version "2.1.0" @@ -4866,7 +4883,7 @@ "@noble/hashes" "^1.3.2" "@scure/base" "^1.0.0" -"@metamask/keyring-api@^8.1.0", "@metamask/keyring-api@^8.1.3": +"@metamask/keyring-api@^8.0.0", "@metamask/keyring-api@^8.1.0", "@metamask/keyring-api@^8.1.3": version "8.1.3" resolved "https://registry.yarnpkg.com/@metamask/keyring-api/-/keyring-api-8.1.3.tgz#53e6a68236b88592db5bd43cf7e0d7e97dfad818" integrity sha512-Ztm4G/U5hc+GKS/VOnqLWYVh2O26lFQ03bNpeufrfKq7regydIqYuHFcSowbQyj7xCZqKPsvl9jxhKdYIxvCXQ== @@ -4878,21 +4895,21 @@ bech32 "^2.0.0" uuid "^9.0.1" -"@metamask/keyring-controller@^17.2.1", "@metamask/keyring-controller@^17.2.2": - version "17.2.2" - resolved "https://registry.yarnpkg.com/@metamask/keyring-controller/-/keyring-controller-17.2.2.tgz#944bc693305b4a6e4f1e73739a82c4bc6573dd9a" - integrity sha512-Shqk0ybcTPrHQLlBJ1V+InuYC7nD3/a6Ws0XCcBCOfkLTXvtSooKIWBioK83XlHMHkfsM6+bySxSqXJVgJvBZw== +"@metamask/keyring-controller@^17.1.0", "@metamask/keyring-controller@^17.2.1", "@metamask/keyring-controller@^17.2.2": + version "17.3.1" + resolved "https://registry.yarnpkg.com/@metamask/keyring-controller/-/keyring-controller-17.3.1.tgz#1a498dd165df5b908761e62fc9e194b8a4f9a074" + integrity sha512-+R4tD0KtXjjAts5xOo+CKETPQVa+RJDC98L2qU2iGHyFKN05gFYt4M8HMcK4gq2GhGxm+0r6SYOUw2jK/wjD5g== dependencies: "@ethereumjs/util" "^8.1.0" "@keystonehq/metamask-airgapped-keyring" "^0.14.1" - "@metamask/base-controller" "^7.0.1" + "@metamask/base-controller" "^7.0.2" "@metamask/browser-passworder" "^4.3.0" "@metamask/eth-hd-keyring" "^7.0.4" - "@metamask/eth-sig-util" "^7.0.1" + "@metamask/eth-sig-util" "^8.0.0" "@metamask/eth-simple-keyring" "^6.0.5" "@metamask/keyring-api" "^8.1.3" - "@metamask/message-manager" "^10.1.1" - "@metamask/utils" "^9.1.0" + "@metamask/message-manager" "^11.0.1" + "@metamask/utils" "^10.0.0" async-mutex "^0.5.0" ethereumjs-wallet "^1.0.1" immer "^9.0.6" @@ -4906,15 +4923,15 @@ "@metamask/controller-utils" "^11.3.0" uuid "^8.3.2" -"@metamask/message-manager@^10.1.1": - version "10.1.1" - resolved "https://registry.yarnpkg.com/@metamask/message-manager/-/message-manager-10.1.1.tgz#390acc4dff9f7c72aaf5c183397c872e53ae7f12" - integrity sha512-VFFqEPKOyo59P79CP/vlPDpMng1a1mMHIaXuvEJYTOf/UOqeVpw77G5IHVfjuG+tZNlAQIHYp7sEmPkob+rzcA== +"@metamask/message-manager@^11.0.1": + version "11.0.1" + resolved "https://registry.yarnpkg.com/@metamask/message-manager/-/message-manager-11.0.1.tgz#7ffa6ea5a0daebb0ccb78dbd75774bf3aa0b9d69" + integrity sha512-dPkx6v14MyBPqdnKSlBPR97/BCx8KLuGudK9u0U3CmqI5dpO3mXvwXNspu5lnBrnjAoYqQVb+/e4vqkOx4/DlQ== dependencies: - "@metamask/base-controller" "^7.0.1" - "@metamask/controller-utils" "^11.3.0" - "@metamask/eth-sig-util" "^7.0.1" - "@metamask/utils" "^9.1.0" + "@metamask/base-controller" "^7.0.2" + "@metamask/controller-utils" "^11.4.2" + "@metamask/eth-sig-util" "^8.0.0" + "@metamask/utils" "^10.0.0" "@types/uuid" "^8.3.0" jsonschema "^1.2.4" uuid "^8.3.2" @@ -5260,10 +5277,10 @@ readable-stream "^3.6.2" webextension-polyfill "^0.10.0" -"@metamask/providers@^17.1.2": - version "17.1.2" - resolved "https://registry.yarnpkg.com/@metamask/providers/-/providers-17.1.2.tgz#bb29c9cbf66be4c3f83d3e24ffea93f750b3db39" - integrity sha512-hACtF02yaUYThvWrRtVU4JAc+ZLCZ4PJUYBw6dK9Rze50J7zCxtss2mB7H8w76iLx//b5hjgXx6y92gPVjuYWg== +"@metamask/providers@^17.0.0", "@metamask/providers@^17.1.2": + version "17.2.1" + resolved "https://registry.yarnpkg.com/@metamask/providers/-/providers-17.2.1.tgz#e57105deddd0aab7c7a7efc44ce91f3bd9737906" + integrity sha512-xnF48ULB0uZ4mOPLMv5xzLWenMs1zbAUNP+wiBofetzIqnD/i6S8u9axIkAwEXBsb0JXtDI1lBPiTBJ5HUxRdw== dependencies: "@metamask/json-rpc-engine" "^9.0.1" "@metamask/json-rpc-middleware-stream" "^8.0.1" @@ -5404,10 +5421,10 @@ fast-json-patch "^3.1.0" lodash "^4.17.21" -"@metamask/snaps-controllers@^9.6.0", "@metamask/snaps-controllers@^9.8.0": - version "9.8.0" - resolved "https://registry.yarnpkg.com/@metamask/snaps-controllers/-/snaps-controllers-9.8.0.tgz#576de64fcaec7d393cc5c39f7d94dec068f3e2bf" - integrity sha512-Ey5bHhCsODF7NIkFvr3773ZYrxhbErLE1M12AUBIiLo7p5h/h8nWY+ilObQjTj3Au58oQ5QNYK1juvHMogFgdA== +"@metamask/snaps-controllers@^9.7.0", "@metamask/snaps-controllers@^9.8.0": + version "9.11.0" + resolved "https://registry.yarnpkg.com/@metamask/snaps-controllers/-/snaps-controllers-9.11.0.tgz#06cd0976994bfa12413c1326b60d50afe6a65640" + integrity sha512-4LrwAGnYVL125OqC7TRCFLdxtTfZLUbw/xyL0h/VWeT1+wt8uc6imN7gVyCudxM6GY2ujLaBcyp0NvmLEDDrfQ== dependencies: "@metamask/approval-controller" "^7.0.2" "@metamask/base-controller" "^6.0.2" @@ -5419,9 +5436,9 @@ "@metamask/post-message-stream" "^8.1.1" "@metamask/rpc-errors" "^6.3.1" "@metamask/snaps-registry" "^3.2.1" - "@metamask/snaps-rpc-methods" "^11.1.1" - "@metamask/snaps-sdk" "^6.5.1" - "@metamask/snaps-utils" "^8.1.1" + "@metamask/snaps-rpc-methods" "^11.4.0" + "@metamask/snaps-sdk" "^6.8.0" + "@metamask/snaps-utils" "^8.4.0" "@metamask/utils" "^9.2.1" "@xstate/fsm" "^2.0.0" browserify-zlib "^0.2.0" @@ -5461,34 +5478,20 @@ "@noble/curves" "^1.2.0" "@noble/hashes" "^1.3.2" -"@metamask/snaps-rpc-methods@^11.1.1": - version "11.1.1" - resolved "https://registry.yarnpkg.com/@metamask/snaps-rpc-methods/-/snaps-rpc-methods-11.1.1.tgz#d01a82cb1d8af60ade5f3d3da3adf8a275beda48" - integrity sha512-j4oJyMSBLTLg0RA28wgsCjdbC/YZUyRB/U6/tLCsaGXDKIPnOR17yjyeOx/IKLQR91ZC2+y4dMkgFbyOVSDI3Q== +"@metamask/snaps-rpc-methods@^11.1.1", "@metamask/snaps-rpc-methods@^11.4.0": + version "11.4.0" + resolved "https://registry.yarnpkg.com/@metamask/snaps-rpc-methods/-/snaps-rpc-methods-11.4.0.tgz#6cbf6cd6d91b65a5156882b4c10b1bdbc026e4cc" + integrity sha512-vFX4NFV1y7sj7uCDH6lJdDbRSLhELQdrv1FM0x/S8UptKYLGMhKafplhfx7KcQm0DBugSIXd42rmpgE8addunQ== dependencies: "@metamask/key-tree" "^9.1.2" "@metamask/permission-controller" "^11.0.0" "@metamask/rpc-errors" "^6.3.1" - "@metamask/snaps-sdk" "^6.5.0" - "@metamask/snaps-utils" "^8.1.1" + "@metamask/snaps-sdk" "^6.8.0" + "@metamask/snaps-utils" "^8.4.0" "@metamask/superstruct" "^3.1.0" "@metamask/utils" "^9.2.1" "@noble/hashes" "^1.3.1" -"@metamask/snaps-rpc-methods@^9.1.4": - version "9.1.4" - resolved "https://registry.yarnpkg.com/@metamask/snaps-rpc-methods/-/snaps-rpc-methods-9.1.4.tgz#d0aa695b89326eece7b23d188d8dce7d35a78d9c" - integrity sha512-eYcfJDYlXE4RqtRTyp8n7SsJ0aAQlz5H335/5jch3L3CwQwC3z8KlKm258iXPiyT7fa9VVxdZqGs/rBaivx0Iw== - dependencies: - "@metamask/key-tree" "^9.1.1" - "@metamask/permission-controller" "^10.0.0" - "@metamask/rpc-errors" "^6.2.1" - "@metamask/snaps-sdk" "^6.0.0" - "@metamask/snaps-utils" "^7.7.0" - "@metamask/utils" "^8.3.0" - "@noble/hashes" "^1.3.1" - superstruct "^1.0.3" - "@metamask/snaps-sdk@^3.1.1": version "3.2.0" resolved "https://registry.yarnpkg.com/@metamask/snaps-sdk/-/snaps-sdk-3.2.0.tgz#66d60869697a479a3484adc9532d82c3b568fc19" @@ -5501,10 +5504,22 @@ fast-xml-parser "^4.3.4" superstruct "^1.0.3" -"@metamask/snaps-sdk@^6.0.0", "@metamask/snaps-sdk@^6.1.0", "@metamask/snaps-sdk@^6.4.0", "@metamask/snaps-sdk@^6.5.0", "@metamask/snaps-sdk@^6.5.1": - version "6.5.1" - resolved "https://registry.yarnpkg.com/@metamask/snaps-sdk/-/snaps-sdk-6.5.1.tgz#527691767d98c08c802656b020d5d94d6336623e" - integrity sha512-uQEZZNjKwHZZfu9StwlmvTFle5MqiheO6AQctVhpYGJ1kjJ7Qwa+vJhMd0Ox1QI9C3qaUCqxOCDV0mpd1jPRKg== +"@metamask/snaps-sdk@^4.2.0": + version "4.4.2" + resolved "https://registry.yarnpkg.com/@metamask/snaps-sdk/-/snaps-sdk-4.4.2.tgz#6d7654ca3ecbcda5cd8689f49721c084241a4495" + integrity sha512-V6d1kQdkCTYQ7Z3+ZVnMWjwsS2TRaKNnSRtSHgjATdSacW5d/1td2KbTs+1x1/cSe58ULKW1SBwRNy0i0c95hA== + dependencies: + "@metamask/key-tree" "^9.1.1" + "@metamask/providers" "^17.0.0" + "@metamask/rpc-errors" "^6.2.1" + "@metamask/utils" "^8.3.0" + fast-xml-parser "^4.3.4" + superstruct "^1.0.3" + +"@metamask/snaps-sdk@^6.1.0", "@metamask/snaps-sdk@^6.5.0", "@metamask/snaps-sdk@^6.5.1", "@metamask/snaps-sdk@^6.8.0": + version "6.9.0" + resolved "https://registry.yarnpkg.com/@metamask/snaps-sdk/-/snaps-sdk-6.9.0.tgz#409a36cdecf46da4825c437091c9d1541bea7bce" + integrity sha512-wPiZNvmkUXTGJ5yLIN9s+KZuB9zx4bz5LurUpufQ8geaMenQ2ZSyg8vqx7hj25q3yk3W2KncV3wnBmtixjucRA== dependencies: "@metamask/key-tree" "^9.1.2" "@metamask/providers" "^17.1.2" @@ -5512,7 +5527,7 @@ "@metamask/superstruct" "^3.1.0" "@metamask/utils" "^9.2.1" -"@metamask/snaps-utils@^7.7.0", "@metamask/snaps-utils@^7.8.0": +"@metamask/snaps-utils@^7.4.0", "@metamask/snaps-utils@^7.8.1": version "7.8.1" resolved "https://registry.yarnpkg.com/@metamask/snaps-utils/-/snaps-utils-7.8.1.tgz#d18f56ece8a1d4e9ff2e8e7645c3349cf08937bc" integrity sha512-v0xNoiWeJGHvtJqP0aU5dj+phqpV6vKCJoV5tNBXl8/AvMTaV2YL4SLO/z+PTo0RWZFplgAuuDsY254kAXi9Fw== @@ -5541,10 +5556,10 @@ ses "^1.1.0" validate-npm-package-name "^5.0.0" -"@metamask/snaps-utils@^8.1.1": - version "8.1.1" - resolved "https://registry.yarnpkg.com/@metamask/snaps-utils/-/snaps-utils-8.1.1.tgz#b32c403c0b5419dae3a22e4c6119099e9b3466ba" - integrity sha512-oKwm+0r2WIFinMo07RYAQVb2Khr/T+c8+V0m4iT9WfJHVNG8mHUMonH3ae2jksZ0/327na0mYVj804PLQ88SNA== +"@metamask/snaps-utils@^8.1.1", "@metamask/snaps-utils@^8.4.0": + version "8.4.0" + resolved "https://registry.yarnpkg.com/@metamask/snaps-utils/-/snaps-utils-8.4.0.tgz#aefedf45e6fe7b87a6b646193b7c0150caa5e51d" + integrity sha512-xxrAsxKf+l8Z3RP9oriNp6Jboh2tUB3mYZDw/kAvzBRX0tr28KgU/sKhRj4jF8GHEL7/jJmw9OIwOlhuXxD+Kg== dependencies: "@babel/core" "^7.23.2" "@babel/types" "^7.23.0" @@ -5554,7 +5569,7 @@ "@metamask/rpc-errors" "^6.3.1" "@metamask/slip44" "^4.0.0" "@metamask/snaps-registry" "^3.2.1" - "@metamask/snaps-sdk" "^6.5.0" + "@metamask/snaps-sdk" "^6.8.0" "@metamask/superstruct" "^3.1.0" "@metamask/utils" "^9.2.1" "@noble/hashes" "^1.3.1" @@ -5575,7 +5590,7 @@ resolved "https://registry.yarnpkg.com/@metamask/stake-sdk/-/stake-sdk-0.2.13.tgz#368749d698353f1ec9f9119ebc5bf9980582cc2b" integrity sha512-F6JIfiCmQ+6xg9MA2HXJw7MVbEPK5HWy4IkI/2lmb1+tBGnK/9PddhhllNn50aU3Af/T0gPUQtLSVUXErfBHfg== -"@metamask/superstruct@^3.1.0": +"@metamask/superstruct@^3.0.0", "@metamask/superstruct@^3.1.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@metamask/superstruct/-/superstruct-3.1.0.tgz#148f786a674fba3ac885c1093ab718515bf7f648" integrity sha512-N08M56HdOgBfRKkrgCMZvQppkZGcArEop3kixNEtVbJKm6P9Cfg0YkI6X0s1g78sNrj2fWUwvJADdZuzJgFttA== @@ -5662,6 +5677,21 @@ lodash "^4.17.21" uuid "^8.3.2" +"@metamask/utils@^10.0.0": + version "10.0.0" + resolved "https://registry.yarnpkg.com/@metamask/utils/-/utils-10.0.0.tgz#9285e6e195810e8b7c875147ac64981b4be51733" + integrity sha512-EoNZJijLqBbir8ikuiHBHfhCqE1s8Odae3bhtRAd8itJB109xmfFF84djY/iaQI+EAp59Sy7iwengfRohaTK8A== + dependencies: + "@ethereumjs/tx" "^4.2.0" + "@metamask/superstruct" "^3.1.0" + "@noble/hashes" "^1.3.1" + "@scure/base" "^1.1.3" + "@types/debug" "^4.1.7" + debug "^4.3.4" + pony-cause "^2.1.10" + semver "^7.5.4" + uuid "^9.0.1" + "@metamask/utils@^3.4.1": version "3.6.0" resolved "https://registry.yarnpkg.com/@metamask/utils/-/utils-3.6.0.tgz#b218b969a05ca7a8093b5d1670f6625061de707d" @@ -5684,18 +5714,18 @@ superstruct "^1.0.3" "@metamask/utils@^8.1.0", "@metamask/utils@^8.2.0", "@metamask/utils@^8.3.0": - version "8.4.0" - resolved "https://registry.yarnpkg.com/@metamask/utils/-/utils-8.4.0.tgz#f44812c96467a4e1b70b2edff6ee89a9caa4e354" - integrity sha512-dbIc3C7alOe0agCuBHM1h71UaEaEqOk2W8rAtEn8QGz4haH2Qq7MoK6i7v2guzvkJVVh79c+QCzIqphC3KvrJg== + version "8.5.0" + resolved "https://registry.yarnpkg.com/@metamask/utils/-/utils-8.5.0.tgz#ddd0d4012d5191809404c97648a837ea9962cceb" + integrity sha512-I6bkduevXb72TIM9q2LRO63JSsF9EXduh3sBr9oybNX2hNNpr/j1tEjXrsG0Uabm4MJ1xkGAQEMwifvKZIkyxQ== dependencies: "@ethereumjs/tx" "^4.2.0" + "@metamask/superstruct" "^3.0.0" "@noble/hashes" "^1.3.1" "@scure/base" "^1.1.3" "@types/debug" "^4.1.7" debug "^4.3.4" pony-cause "^2.1.10" semver "^7.5.4" - superstruct "^1.0.3" uuid "^9.0.1" "@metamask/utils@^9.0.0", "@metamask/utils@^9.1.0", "@metamask/utils@^9.2.1": @@ -10302,7 +10332,7 @@ resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.2.tgz#ede1d1b1e451548d44919dc226253e32a6952c4b" integrity sha512-kNnC1GFBLuhImSnV7w4njQkUiJi0ZXUycu1rUaouPqiKlXkh77JKgdRnTAp1x5eBwcIwbtI+3otwzuIDEuDoxQ== -"@types/uuid@^9.0.1", "@types/uuid@^9.0.8": +"@types/uuid@^9.0.8": version "9.0.8" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.8.tgz#7545ba4fc3c003d6c756f651f3bf163d8f0f29ba" integrity sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA== @@ -13297,7 +13327,7 @@ bignumber.js@^7.2.1: resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-7.2.1.tgz#80c048759d826800807c4bfd521e50edbba57a5f" integrity sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ== -bignumber.js@^9.0.1, bignumber.js@^9.0.2: +bignumber.js@^9.0.1, bignumber.js@^9.0.2, bignumber.js@^9.1.2: version "9.1.2" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c" integrity sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==