diff --git a/packages/accounts-controller/package.json b/packages/accounts-controller/package.json index 2c7551c346..ccc9e0c9a4 100644 --- a/packages/accounts-controller/package.json +++ b/packages/accounts-controller/package.json @@ -44,10 +44,10 @@ "dependencies": { "@ethereumjs/util": "^8.1.0", "@metamask/base-controller": "^7.0.0", - "@metamask/eth-snap-keyring": "^4.3.1", + "@metamask/eth-snap-keyring": "^4.3.3", "@metamask/keyring-api": "^8.1.0", - "@metamask/snaps-sdk": "^6.1.1", - "@metamask/snaps-utils": "^7.8.1", + "@metamask/snaps-sdk": "^6.5.0", + "@metamask/snaps-utils": "^8.1.1", "@metamask/utils": "^9.1.0", "deepmerge": "^4.2.2", "ethereum-cryptography": "^2.1.2", @@ -57,7 +57,7 @@ "devDependencies": { "@metamask/auto-changelog": "^3.4.4", "@metamask/keyring-controller": "^17.2.0", - "@metamask/snaps-controllers": "^9.3.1", + "@metamask/snaps-controllers": "^9.7.0", "@types/jest": "^27.4.1", "@types/readable-stream": "^2.3.0", "jest": "^27.5.1", @@ -68,7 +68,7 @@ }, "peerDependencies": { "@metamask/keyring-controller": "^17.0.0", - "@metamask/snaps-controllers": "^9.3.0" + "@metamask/snaps-controllers": "^9.7.0" }, "engines": { "node": "^18.18 || >=20" diff --git a/packages/chain-controller/package.json b/packages/chain-controller/package.json index d547e2dbc8..91c7eeef79 100644 --- a/packages/chain-controller/package.json +++ b/packages/chain-controller/package.json @@ -45,9 +45,9 @@ "@metamask/base-controller": "^7.0.0", "@metamask/chain-api": "^0.1.0", "@metamask/keyring-api": "^8.1.0", - "@metamask/snaps-controllers": "^9.3.1", - "@metamask/snaps-sdk": "^6.1.1", - "@metamask/snaps-utils": "^7.8.1", + "@metamask/snaps-controllers": "^9.7.0", + "@metamask/snaps-sdk": "^6.5.0", + "@metamask/snaps-utils": "^8.1.1", "@metamask/utils": "^9.1.0", "uuid": "^8.3.2" }, diff --git a/packages/profile-sync-controller/package.json b/packages/profile-sync-controller/package.json index 14f018e836..df50f5983d 100644 --- a/packages/profile-sync-controller/package.json +++ b/packages/profile-sync-controller/package.json @@ -70,8 +70,8 @@ }, "dependencies": { "@metamask/base-controller": "^7.0.0", - "@metamask/snaps-sdk": "^6.1.1", - "@metamask/snaps-utils": "^7.8.1", + "@metamask/snaps-sdk": "^6.5.0", + "@metamask/snaps-utils": "^8.1.1", "@noble/ciphers": "^0.5.2", "@noble/hashes": "^1.4.0", "immer": "^9.0.6", @@ -84,7 +84,8 @@ "@metamask/auto-changelog": "^3.4.4", "@metamask/keyring-api": "^8.1.0", "@metamask/keyring-controller": "^17.2.0", - "@metamask/snaps-controllers": "^9.3.1", + "@metamask/network-controller": "^21.0.0", + "@metamask/snaps-controllers": "^9.7.0", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "ethers": "^6.12.0", @@ -99,7 +100,7 @@ "peerDependencies": { "@metamask/accounts-controller": "^18.1.1", "@metamask/keyring-controller": "^17.2.0", - "@metamask/snaps-controllers": "^9.3.0" + "@metamask/snaps-controllers": "^9.7.0" }, "engines": { "node": "^18.18 || >=20" diff --git a/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.test.ts b/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.test.ts index 3780340eb4..164ab168b4 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.test.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.test.ts @@ -388,7 +388,7 @@ describe('user-storage/user-storage-controller - enableProfileSyncing() tests', }); describe('user-storage/user-storage-controller - syncInternalAccountsWithUserStorage() tests', () => { - it('rejects if UserStorage is not enabled', async () => { + it('returns void if UserStorage is not enabled', async () => { const arrangeMocks = async () => { return { messengerMocks: mockUserStorageMessenger(), @@ -409,9 +409,9 @@ describe('user-storage/user-storage-controller - syncInternalAccountsWithUserSto }, }); - await expect( - controller.syncInternalAccountsWithUserStorage(), - ).rejects.toThrow(expect.any(Error)); + await controller.syncInternalAccountsWithUserStorage(); + + expect(messengerMocks.mockAccountsListAccounts).not.toHaveBeenCalled(); }); it('returns void if account syncing feature flag is disabled', async () => { @@ -1083,7 +1083,7 @@ describe('user-storage/user-storage-controller - syncInternalAccountsWithUserSto }); describe('user-storage/user-storage-controller - saveInternalAccountToUserStorage() tests', () => { - it('rejects if UserStorage is not enabled', async () => { + it('returns void if UserStorage is not enabled', async () => { const arrangeMocks = async () => { return { messengerMocks: mockUserStorageMessenger(), @@ -1103,11 +1103,13 @@ describe('user-storage/user-storage-controller - saveInternalAccountToUserStorag }, }); - await expect( - controller.saveInternalAccountToUserStorage( - MOCK_INTERNAL_ACCOUNTS.ONE[0].address, - ), - ).rejects.toThrow(expect.any(Error)); + await controller.saveInternalAccountToUserStorage( + MOCK_INTERNAL_ACCOUNTS.ONE[0] as InternalAccount, + ); + + expect( + messengerMocks.mockAccountsGetAccountByAddress, + ).not.toHaveBeenCalled(); }); it('returns void if account syncing feature flag is disabled', async () => { @@ -1130,7 +1132,7 @@ describe('user-storage/user-storage-controller - saveInternalAccountToUserStorag }); await controller.saveInternalAccountToUserStorage( - MOCK_INTERNAL_ACCOUNTS.ONE[0].address, + MOCK_INTERNAL_ACCOUNTS.ONE[0] as InternalAccount, ); expect(mockAPI.isDone()).toBe(false); @@ -1156,7 +1158,7 @@ describe('user-storage/user-storage-controller - saveInternalAccountToUserStorag }); await controller.saveInternalAccountToUserStorage( - MOCK_INTERNAL_ACCOUNTS.ONE[0].address, + MOCK_INTERNAL_ACCOUNTS.ONE[0] as InternalAccount, ); expect(mockAPI.isDone()).toBe(true); @@ -1184,7 +1186,7 @@ describe('user-storage/user-storage-controller - saveInternalAccountToUserStorag await expect( controller.saveInternalAccountToUserStorage( - MOCK_INTERNAL_ACCOUNTS.ONE[0].address, + MOCK_INTERNAL_ACCOUNTS.ONE[0] as InternalAccount, ), ).rejects.toThrow(expect.any(Error)); }); @@ -1210,7 +1212,7 @@ describe('user-storage/user-storage-controller - saveInternalAccountToUserStorag ); expect(mockSaveInternalAccountToUserStorage).toHaveBeenCalledWith( - MOCK_INTERNAL_ACCOUNTS.ONE[0].address, + MOCK_INTERNAL_ACCOUNTS.ONE[0], ); }); @@ -1235,7 +1237,7 @@ describe('user-storage/user-storage-controller - saveInternalAccountToUserStorag ); expect(mockSaveInternalAccountToUserStorage).toHaveBeenCalledWith( - MOCK_INTERNAL_ACCOUNTS.ONE[0].address, + MOCK_INTERNAL_ACCOUNTS.ONE[0], ); }); }); @@ -1272,7 +1274,6 @@ function mockUserStorageMessenger(options?: { 'NotificationServicesController:selectIsNotificationServicesEnabled', 'AccountsController:listAccounts', 'AccountsController:updateAccountMetadata', - 'AccountsController:getAccountByAddress', 'KeyringController:addNewAccount', ], allowedEvents: [ diff --git a/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.ts b/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.ts index 661599098e..5af28ab924 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/UserStorageController.ts @@ -1,7 +1,6 @@ import type { AccountsControllerListAccountsAction, AccountsControllerUpdateAccountMetadataAction, - AccountsControllerGetAccountByAddressAction, AccountsControllerAccountRenamedEvent, AccountsControllerAccountAddedEvent, } from '@metamask/accounts-controller'; @@ -19,6 +18,7 @@ import type { KeyringControllerUnlockEvent, KeyringControllerAddNewAccountAction, } from '@metamask/keyring-controller'; +import type { NetworkConfiguration } from '@metamask/network-controller'; import type { HandleSnapRequest } from '@metamask/snaps-controllers'; import { createSnapSignMessageRequest } from '../authentication/auth-snap-requests'; @@ -35,6 +35,7 @@ import { mapInternalAccountToUserStorageAccount, } from './accounts/user-storage'; import { createSHA256Hash } from './encryption'; +import { startNetworkSyncing } from './network-syncing/controller-integration'; import type { UserStoragePathWithFeatureAndKey, UserStoragePathWithFeatureOnly, @@ -45,6 +46,27 @@ import { upsertUserStorage, } from './services'; +// TODO: add external NetworkController event +// Need to listen for when a network gets added +type NetworkControllerNetworkAddedEvent = { + type: 'NetworkController:networkAdded'; + payload: [networkConfiguration: NetworkConfiguration]; +}; + +// TODO: add external NetworkController event +// Need to listen for when a network is updated, or the default rpc/block explorer changes +type NetworkControllerNetworkChangedEvent = { + type: 'NetworkController:networkChanged'; + payload: [networkConfiguration: NetworkConfiguration]; +}; + +// TODO: add external NetworkController event +// Need to listen for when a network gets deleted +type NetworkControllerNetworkDeletedEvent = { + type: 'NetworkController:networkDeleted'; + payload: [networkConfiguration: NetworkConfiguration]; +}; + // TODO: fix external dependencies export declare type NotificationServicesControllerDisableNotificationServices = { @@ -137,13 +159,6 @@ export type UserStorageControllerSyncInternalAccountsWithUserStorage = export type UserStorageControllerSaveInternalAccountToUserStorage = ActionsObj['saveInternalAccountToUserStorage']; -export type UserStorageControllerStateChangeEvent = ControllerStateChangeEvent< - typeof controllerName, - UserStorageControllerState ->; -export type Events = UserStorageControllerStateChangeEvent; - -// Allowed Actions export type AllowedActions = // Keyring Requests | KeyringControllerGetStateAction @@ -160,12 +175,11 @@ export type AllowedActions = | NotificationServicesControllerSelectIsNotificationServicesEnabled // Account syncing | AccountsControllerListAccountsAction - | AccountsControllerGetAccountByAddressAction | AccountsControllerUpdateAccountMetadataAction | KeyringControllerAddNewAccountAction; // Messenger events -export type UserStorageControllerChangeEvent = ControllerStateChangeEvent< +export type UserStorageControllerStateChangeEvent = ControllerStateChangeEvent< typeof controllerName, UserStorageControllerState >; @@ -177,15 +191,24 @@ export type UserStorageControllerAccountSyncingComplete = { type: `${typeof controllerName}:accountSyncingComplete`; payload: [boolean]; }; +export type Events = + | UserStorageControllerStateChangeEvent + | UserStorageControllerAccountSyncingInProgress + | UserStorageControllerAccountSyncingComplete; export type AllowedEvents = - | UserStorageControllerChangeEvent + | UserStorageControllerStateChangeEvent | UserStorageControllerAccountSyncingInProgress | UserStorageControllerAccountSyncingComplete | KeyringControllerLockEvent | KeyringControllerUnlockEvent + // Account Syncing Events | AccountsControllerAccountAddedEvent - | AccountsControllerAccountRenamedEvent; + | AccountsControllerAccountRenamedEvent + // Network Syncing Events + | NetworkControllerNetworkAddedEvent + | NetworkControllerNetworkChangedEvent + | NetworkControllerNetworkDeletedEvent; // Messenger export type UserStorageControllerMessenger = RestrictedControllerMessenger< @@ -241,6 +264,17 @@ export default class UserStorageController extends BaseController< // We will remove this once the feature will be released isAccountSyncingEnabled: false, isAccountSyncingInProgress: false, + canSync: () => { + try { + this.#assertProfileSyncingEnabled(); + + return ( + this.#accounts.isAccountSyncingEnabled && this.#auth.isAuthEnabled() + ); + } catch { + return false; + } + }, setupAccountSyncingSubscriptions: () => { this.messagingSystem.subscribe( 'AccountsController:accountAdded', @@ -249,7 +283,7 @@ export default class UserStorageController extends BaseController< if (this.#accounts.isAccountSyncingInProgress) { return; } - await this.saveInternalAccountToUserStorage(account.address); + await this.saveInternalAccountToUserStorage(account); }, ); @@ -260,16 +294,10 @@ export default class UserStorageController extends BaseController< if (this.#accounts.isAccountSyncingInProgress) { return; } - await this.saveInternalAccountToUserStorage(account.address); + await this.saveInternalAccountToUserStorage(account); }, ); }, - getInternalAccountByAddress: async (address: string) => { - return this.messagingSystem.call( - 'AccountsController:getAccountByAddress', - address, - ); - }, getInternalAccountsList: async (): Promise => { return this.messagingSystem.call('AccountsController:listAccounts'); }, @@ -284,21 +312,15 @@ export default class UserStorageController extends BaseController< null ); }, - saveInternalAccountToUserStorage: async (address: string) => { - const internalAccount = await this.#accounts.getInternalAccountByAddress( - address, - ); - - if (!internalAccount) { - return; - } - + saveInternalAccountToUserStorage: async ( + internalAccount: InternalAccount, + ) => { // Map the internal account to the user storage account schema const mappedAccount = mapInternalAccountToUserStorageAccount(internalAccount); await this.performSetStorage( - `accounts.${address}`, + `accounts.${internalAccount.address}`, JSON.stringify(mappedAccount), ); }, @@ -372,6 +394,7 @@ export default class UserStorageController extends BaseController< state?: UserStorageControllerState; env?: { isAccountSyncingEnabled?: boolean; + isNetworkSyncingEnabled?: boolean; }; getMetaMetricsState: () => boolean; nativeScryptCrypto?: NativeScrypt; @@ -392,6 +415,22 @@ export default class UserStorageController extends BaseController< this.#registerMessageHandlers(); this.#nativeScryptCrypto = nativeScryptCrypto; this.#accounts.setupAccountSyncingSubscriptions(); + + // Network Syncing + if (env?.isNetworkSyncingEnabled) { + startNetworkSyncing({ + messenger, + getStorageConfig: async () => { + const { storageKey, bearerToken } = + await this.#getStorageKeyAndBearerToken(); + return { + storageKey, + bearerToken, + nativeScryptCrypto: this.#nativeScryptCrypto, + }; + }, + }); + } } /** @@ -676,13 +715,11 @@ export default class UserStorageController extends BaseController< * It will add new accounts to the internal accounts list, update/merge conflicting names and re-upload the results in some cases to the user storage. */ async syncInternalAccountsWithUserStorage(): Promise { - if (!this.#accounts.isAccountSyncingEnabled) { + if (!this.#accounts.canSync()) { return; } try { - this.#assertProfileSyncingEnabled(); - this.#accounts.isAccountSyncingInProgress = true; const userStorageAccountsList = @@ -731,7 +768,7 @@ export default class UserStorageController extends BaseController< if (!userStorageAccount) { await this.#accounts.saveInternalAccountToUserStorage( - internalAccount.address, + internalAccount, ); continue; } @@ -761,7 +798,7 @@ export default class UserStorageController extends BaseController< // Internal account has custom name but user storage account has default name if (isUserStorageAccountNameDefault) { await this.#accounts.saveInternalAccountToUserStorage( - internalAccount.address, + internalAccount, ); continue; } @@ -778,7 +815,7 @@ export default class UserStorageController extends BaseController< if (isInternalAccountNameNewer) { await this.#accounts.saveInternalAccountToUserStorage( - internalAccount.address, + internalAccount, ); continue; } @@ -796,7 +833,7 @@ export default class UserStorageController extends BaseController< continue; } else if (internalAccount.metadata.nameLastUpdatedAt !== undefined) { await this.#accounts.saveInternalAccountToUserStorage( - internalAccount.address, + internalAccount, ); continue; } @@ -813,17 +850,17 @@ export default class UserStorageController extends BaseController< /** * Saves an individual internal account to the user storage. - * @param address - The address of the internal account to save + * @param internalAccount - The internal account to save */ - async saveInternalAccountToUserStorage(address: string): Promise { - if (!this.#accounts.isAccountSyncingEnabled) { + async saveInternalAccountToUserStorage( + internalAccount: InternalAccount, + ): Promise { + if (!this.#accounts.canSync()) { return; } try { - this.#assertProfileSyncingEnabled(); - - await this.#accounts.saveInternalAccountToUserStorage(address); + await this.#accounts.saveInternalAccountToUserStorage(internalAccount); } catch (e) { const errorMessage = e instanceof Error ? e.message : JSON.stringify(e); throw new Error( diff --git a/packages/profile-sync-controller/src/controllers/user-storage/__fixtures__/mockResponses.ts b/packages/profile-sync-controller/src/controllers/user-storage/__fixtures__/mockResponses.ts index 3e47c0b151..3d252c6086 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/__fixtures__/mockResponses.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/__fixtures__/mockResponses.ts @@ -8,7 +8,11 @@ import type { GetUserStorageResponse, } from '../services'; import { USER_STORAGE_ENDPOINT } from '../services'; -import { MOCK_ENCRYPTED_STORAGE_DATA, MOCK_STORAGE_KEY } from './mockStorage'; +import { + MOCK_ENCRYPTED_STORAGE_DATA, + MOCK_STORAGE_DATA, + MOCK_STORAGE_KEY, +} from './mockStorage'; type MockResponse = { url: string; @@ -29,19 +33,22 @@ export const getMockUserStorageEndpoint = ( )}`; }; -const MOCK_GET_USER_STORAGE_RESPONSE = - async (): Promise => ({ - HashedKey: 'HASHED_KEY', - Data: await MOCK_ENCRYPTED_STORAGE_DATA(), - }); +export const createMockGetStorageResponse = async ( + data?: string, +): Promise => ({ + HashedKey: 'HASHED_KEY', + Data: await MOCK_ENCRYPTED_STORAGE_DATA(data), +}); -const MOCK_GET_USER_STORAGE_ALL_FEATURE_ENTRIES_RESPONSE = - async (): Promise => [ - { +export const createMockAllFeatureEntriesResponse = async ( + dataArr: string[] = [MOCK_STORAGE_DATA], +): Promise => + Promise.all( + dataArr.map(async (d) => ({ HashedKey: 'HASHED_KEY', - Data: await MOCK_ENCRYPTED_STORAGE_DATA(), - }, - ]; + Data: await MOCK_ENCRYPTED_STORAGE_DATA(d), + })), + ); export const getMockUserStorageGetResponse = async ( path: UserStoragePathWithFeatureAndKey = 'notifications.notificationSettings', @@ -49,17 +56,18 @@ export const getMockUserStorageGetResponse = async ( return { url: getMockUserStorageEndpoint(path), requestMethod: 'GET', - response: await MOCK_GET_USER_STORAGE_RESPONSE(), + response: await createMockGetStorageResponse(), } satisfies MockResponse; }; export const getMockUserStorageAllFeatureEntriesResponse = async ( path: UserStoragePathWithFeatureOnly = 'notifications', + dataArr?: string[], ) => { return { url: getMockUserStorageEndpoint(path), requestMethod: 'GET', - response: await MOCK_GET_USER_STORAGE_ALL_FEATURE_ENTRIES_RESPONSE(), + response: await createMockAllFeatureEntriesResponse(dataArr), } satisfies MockResponse; }; diff --git a/packages/profile-sync-controller/src/controllers/user-storage/__fixtures__/mockStorage.ts b/packages/profile-sync-controller/src/controllers/user-storage/__fixtures__/mockStorage.ts index 719bbf45ac..628f45d05f 100644 --- a/packages/profile-sync-controller/src/controllers/user-storage/__fixtures__/mockStorage.ts +++ b/packages/profile-sync-controller/src/controllers/user-storage/__fixtures__/mockStorage.ts @@ -7,8 +7,8 @@ export const MOCK_STORAGE_DATA = JSON.stringify({ hello: 'world' }); // NOTE - using encryption.encryptString directly in fixtures causes issues on mobile. // This is because this fixture is getting added in at run time. Will be improved once we support multiple exports let cachedMockEncryptedData: string; -export const MOCK_ENCRYPTED_STORAGE_DATA = async () => +export const MOCK_ENCRYPTED_STORAGE_DATA = async (data?: string) => (cachedMockEncryptedData ??= await encryption.encryptString( - MOCK_STORAGE_DATA, + data ?? MOCK_STORAGE_DATA, MOCK_STORAGE_KEY, )); diff --git a/packages/profile-sync-controller/src/controllers/user-storage/__fixtures__/test-utils.ts b/packages/profile-sync-controller/src/controllers/user-storage/__fixtures__/test-utils.ts new file mode 100644 index 0000000000..6c0983fd23 --- /dev/null +++ b/packages/profile-sync-controller/src/controllers/user-storage/__fixtures__/test-utils.ts @@ -0,0 +1,35 @@ +type WaitForOptions = { + intervalMs?: number; + timeoutMs?: number; +}; + +/** + * Testing Utility - waitFor. Waits for and checks (at an interval) if assertion is reached. + * + * @param assertionFn - assertion function + * @param options - set wait for options + * @returns promise that you need to await in tests + */ +export const waitFor = async ( + assertionFn: () => void, + options: WaitForOptions = {}, +): Promise => { + const { intervalMs = 50, timeoutMs = 2000 } = options; + + const startTime = Date.now(); + + return new Promise((resolve, reject) => { + const intervalId = setInterval(() => { + try { + assertionFn(); + clearInterval(intervalId); + resolve(); + } catch (error) { + if (Date.now() - startTime >= timeoutMs) { + clearInterval(intervalId); + reject(new Error(`waitFor: timeout reached after ${timeoutMs}ms`)); + } + } + }, intervalMs); + }); +}; diff --git a/packages/profile-sync-controller/src/controllers/user-storage/network-syncing/__fixtures__/mockNetwork.ts b/packages/profile-sync-controller/src/controllers/user-storage/network-syncing/__fixtures__/mockNetwork.ts new file mode 100644 index 0000000000..417af73d5b --- /dev/null +++ b/packages/profile-sync-controller/src/controllers/user-storage/network-syncing/__fixtures__/mockNetwork.ts @@ -0,0 +1,28 @@ +import type { NetworkConfiguration } from '@metamask/network-controller'; + +import type { RemoteNetworkConfiguration } from '../types'; + +export const createMockNetworkConfiguration = ( + override?: Partial, +): NetworkConfiguration => { + return { + chainId: '0x1337', + blockExplorerUrls: [], + defaultRpcEndpointIndex: 0, + name: 'Mock Network', + nativeCurrency: 'MOCK TOKEN', + rpcEndpoints: [], + defaultBlockExplorerUrlIndex: 0, + ...override, + }; +}; + +export const createMockRemoteNetworkConfiguration = ( + override?: Partial, +): RemoteNetworkConfiguration => { + return { + v: '1', + ...createMockNetworkConfiguration(), + ...override, + }; +}; diff --git a/packages/profile-sync-controller/src/controllers/user-storage/network-syncing/controller-integration.test.ts b/packages/profile-sync-controller/src/controllers/user-storage/network-syncing/controller-integration.test.ts new file mode 100644 index 0000000000..827f0b685c --- /dev/null +++ b/packages/profile-sync-controller/src/controllers/user-storage/network-syncing/controller-integration.test.ts @@ -0,0 +1,150 @@ +import type { NotNamespacedBy } from '@metamask/base-controller'; +import { ControllerMessenger } from '@metamask/base-controller'; +import log from 'loglevel'; + +import type { AllowedActions, AllowedEvents } from '..'; +import { MOCK_STORAGE_KEY } from '../__fixtures__'; +import { waitFor } from '../__fixtures__/test-utils'; +import type { UserStorageBaseOptions } from '../services'; +import { createMockNetworkConfiguration } from './__fixtures__/mockNetwork'; +import { startNetworkSyncing } from './controller-integration'; +import * as SyncModule from './sync'; + +jest.mock('loglevel', () => { + const actual = jest.requireActual('loglevel'); + return { + ...actual, + default: { + ...actual.default, + warn: jest.fn(), + }, + // Mocking an ESModule. + // eslint-disable-next-line @typescript-eslint/naming-convention + __esModule: true, + }; +}); +const warnMock = jest.mocked(log.warn); + +const storageOpts: UserStorageBaseOptions = { + bearerToken: 'MOCK_TOKEN', + storageKey: MOCK_STORAGE_KEY, +}; + +type ExternalEvents = NotNamespacedBy< + 'UserStorageController', + AllowedEvents['type'] +>; +const getEvents = (): ExternalEvents[] => [ + 'NetworkController:networkAdded', + 'NetworkController:networkChanged', + 'NetworkController:networkDeleted', +]; + +const testMatrix = [ + { + event: 'NetworkController:networkAdded' as const, + arrangeSyncFnMock: () => + jest.spyOn(SyncModule, 'addNetwork').mockResolvedValue(), + }, + { + event: 'NetworkController:networkChanged' as const, + arrangeSyncFnMock: () => + jest.spyOn(SyncModule, 'updateNetwork').mockResolvedValue(), + }, + { + event: 'NetworkController:networkDeleted' as const, + arrangeSyncFnMock: () => + jest.spyOn(SyncModule, 'deleteNetwork').mockResolvedValue(), + }, +]; + +describe.each(testMatrix)( + 'network-syncing/controller-integration - $event', + ({ event, arrangeSyncFnMock }) => { + it(`should successfully sync when ${event} is emitted`, async () => { + const syncFnMock = arrangeSyncFnMock(); + const { baseMessenger, messenger, getStorageConfig } = arrangeMocks(); + startNetworkSyncing({ messenger, getStorageConfig }); + baseMessenger.publish(event, createMockNetworkConfiguration()); + + await waitFor(() => { + expect(getStorageConfig).toHaveBeenCalled(); + expect(syncFnMock).toHaveBeenCalled(); + }); + }); + + it('should silently fail is unable to authenticate or get storage key', async () => { + const syncFnMock = arrangeSyncFnMock(); + const { baseMessenger, messenger, getStorageConfig } = arrangeMocks(); + getStorageConfig.mockRejectedValue(new Error('Mock Error')); + startNetworkSyncing({ messenger, getStorageConfig }); + baseMessenger.publish(event, createMockNetworkConfiguration()); + + expect(getStorageConfig).toHaveBeenCalled(); + expect(syncFnMock).not.toHaveBeenCalled(); + }); + + it(`should emit a warning if controller messenger is missing the ${event} event`, async () => { + const { baseMessenger, getStorageConfig } = arrangeMocks(); + + const eventsWithoutNetworkAdded = getEvents().filter((e) => e !== event); + const messenger = mockUserStorageMessenger( + baseMessenger, + eventsWithoutNetworkAdded, + ); + + startNetworkSyncing({ messenger, getStorageConfig }); + expect(warnMock).toHaveBeenCalled(); + }); + }, +); + +/** + * Test Utility - arrange mocks and parameters + * @returns the mocks and parameters used when testing `startNetworkSyncing()` + */ +function arrangeMocks() { + const baseMessenger = mockBaseMessenger(); + const messenger = mockUserStorageMessenger(baseMessenger); + const getStorageConfigMock = jest.fn().mockResolvedValue(storageOpts); + + return { + getStorageConfig: getStorageConfigMock, + baseMessenger, + messenger, + }; +} + +/** + * Test Utility - creates a base messenger so we can invoke/publish events + * @returns Base messenger for publishing events + */ +function mockBaseMessenger() { + const baseMessenger = new ControllerMessenger< + AllowedActions, + AllowedEvents + >(); + + return baseMessenger; +} + +/** + * Test Utility - creates a UserStorageMessenger to simulate the messenger used inside the UserStorageController + * @param baseMessenger - base messenger to restrict + * @param eventsOverride - provide optional override events + * @returns UserStorageMessenger + */ +function mockUserStorageMessenger( + baseMessenger: ReturnType, + eventsOverride?: ExternalEvents[], +) { + const allowedEvents = eventsOverride ?? getEvents(); + + const messenger = baseMessenger.getRestricted({ + name: 'UserStorageController', + allowedActions: [], + allowedEvents, + }); + + return messenger; +} diff --git a/packages/profile-sync-controller/src/controllers/user-storage/network-syncing/controller-integration.ts b/packages/profile-sync-controller/src/controllers/user-storage/network-syncing/controller-integration.ts new file mode 100644 index 0000000000..a7b77fe91a --- /dev/null +++ b/packages/profile-sync-controller/src/controllers/user-storage/network-syncing/controller-integration.ts @@ -0,0 +1,69 @@ +import log from 'loglevel'; + +import type { UserStorageBaseOptions } from '../services'; +import type { UserStorageControllerMessenger } from '../UserStorageController'; +import { addNetwork, deleteNetwork, updateNetwork } from './sync'; + +type SetupNetworkSyncingProps = { + messenger: UserStorageControllerMessenger; + getStorageConfig: () => Promise; +}; + +/** + * Initialize and setup events to listen to for network syncing + * @param props - parameters used for initializing and enabling network syncing + */ +export function startNetworkSyncing(props: SetupNetworkSyncingProps) { + const { messenger, getStorageConfig } = props; + + try { + messenger.subscribe( + 'NetworkController:networkAdded', + // eslint-disable-next-line @typescript-eslint/no-misused-promises + async (networkConfiguration) => { + try { + const opts = await getStorageConfig(); + await addNetwork(networkConfiguration, opts); + } catch { + // Silently fail sync + } + }, + ); + } catch (e) { + log.warn('NetworkSyncing, event subscription failed', e); + } + + try { + messenger.subscribe( + 'NetworkController:networkDeleted', + // eslint-disable-next-line @typescript-eslint/no-misused-promises + async (networkConfiguration) => { + try { + const opts = await getStorageConfig(); + await deleteNetwork(networkConfiguration, opts); + } catch { + // Silently fail sync + } + }, + ); + } catch (e) { + log.warn('NetworkSyncing, event subscription failed', e); + } + + try { + messenger.subscribe( + 'NetworkController:networkChanged', + // eslint-disable-next-line @typescript-eslint/no-misused-promises + async (networkConfiguration) => { + try { + const opts = await getStorageConfig(); + await updateNetwork(networkConfiguration, opts); + } catch { + // Silently fail sync + } + }, + ); + } catch (e) { + log.warn('NetworkSyncing, event subscription failed', e); + } +} diff --git a/packages/profile-sync-controller/src/controllers/user-storage/network-syncing/services.test.ts b/packages/profile-sync-controller/src/controllers/user-storage/network-syncing/services.test.ts new file mode 100644 index 0000000000..69cb8c6c14 --- /dev/null +++ b/packages/profile-sync-controller/src/controllers/user-storage/network-syncing/services.test.ts @@ -0,0 +1,143 @@ +import { + MOCK_STORAGE_KEY, + createMockAllFeatureEntriesResponse, +} from '../__fixtures__'; +import { + mockEndpointGetUserStorageAllFeatureEntries, + mockEndpointUpsertUserStorage, +} from '../__fixtures__/mockServices'; +import type { UserStorageBaseOptions } from '../services'; +import { createMockRemoteNetworkConfiguration } from './__fixtures__/mockNetwork'; +import { + batchUpsertRemoteNetworks, + getAllRemoteNetworks, + upsertRemoteNetwork, +} from './services'; +import type { RemoteNetworkConfiguration } from './types'; + +const storageOpts: UserStorageBaseOptions = { + bearerToken: 'MOCK_TOKEN', + storageKey: MOCK_STORAGE_KEY, +}; + +describe('network-syncing/services - getAllRemoteNetworks()', () => { + const arrangeMockNetwork = () => { + const mockNetwork = createMockRemoteNetworkConfiguration({ + chainId: '0x1337', + }); + return { + mockNetwork, + }; + }; + + const arrangeMockGetAllAPI = async ( + network: RemoteNetworkConfiguration, + status: 200 | 500 = 200, + ) => { + const payload = { + status, + body: + status === 200 + ? await createMockAllFeatureEntriesResponse([JSON.stringify(network)]) + : undefined, + }; + + return { + mockGetAllAPI: await mockEndpointGetUserStorageAllFeatureEntries( + 'networks', + payload, + ), + }; + }; + + it('should return list of remote networks', async () => { + const { mockNetwork } = arrangeMockNetwork(); + const { mockGetAllAPI } = await arrangeMockGetAllAPI(mockNetwork); + + const result = await getAllRemoteNetworks(storageOpts); + expect(mockGetAllAPI.isDone()).toBe(true); + + expect(result).toHaveLength(1); + expect(result[0].chainId).toBe(mockNetwork.chainId); + }); + + it('should return an empty list if fails to get networks', async () => { + const { mockNetwork } = arrangeMockNetwork(); + const { mockGetAllAPI } = await arrangeMockGetAllAPI(mockNetwork, 500); + + const result = await getAllRemoteNetworks(storageOpts); + expect(mockGetAllAPI.isDone()).toBe(true); + + expect(result).toHaveLength(0); + }); + + it('should return empty list if unable to parse retrieved networks', async () => { + const { mockNetwork } = arrangeMockNetwork(); + const { mockGetAllAPI } = await arrangeMockGetAllAPI(mockNetwork); + const realParse = JSON.parse; + jest.spyOn(JSON, 'parse').mockImplementation((data) => { + if (data === JSON.stringify(mockNetwork)) { + throw new Error('MOCK FAIL TO PARSE STRING'); + } + + return realParse(data); + }); + + const result = await getAllRemoteNetworks(storageOpts); + expect(mockGetAllAPI.isDone()).toBe(true); + + expect(result).toHaveLength(0); + + JSON.parse = realParse; + }); +}); + +describe('network-syncing/services - upsertRemoteNetwork()', () => { + const arrangeMocks = () => { + const mockNetwork = createMockRemoteNetworkConfiguration({ + chainId: '0x1337', + }); + + return { + storageOps: storageOpts, + mockNetwork, + mockUpsertAPI: mockEndpointUpsertUserStorage('networks.0x1337'), + }; + }; + + it('should call upsert storage API with mock network', async () => { + const { mockNetwork, mockUpsertAPI } = arrangeMocks(); + await upsertRemoteNetwork(mockNetwork, storageOpts); + expect(mockUpsertAPI.isDone()).toBe(true); + }); +}); + +/** + * TODO - the batch endpoint has not been made in the backend yet. + * Mock endpoints will need to be updated in future + */ +describe('network-syncing/services - batchUpsertRemoteNetworks()', () => { + const arrangeMocks = () => { + const mockNetworks = [ + createMockRemoteNetworkConfiguration({ chainId: '0x1337' }), + createMockRemoteNetworkConfiguration({ chainId: '0x1338' }), + ]; + + const mockAPI = (key: string) => + mockEndpointUpsertUserStorage(`networks.${key}`); + + return { + storageOps: storageOpts, + mockNetworks, + mockUpsertAPI1: mockAPI('0x1337'), + mockUpsertAPI2: mockAPI('0x1338'), + }; + }; + + it('should call upsert storage API with mock network', async () => { + const { mockNetworks, mockUpsertAPI1, mockUpsertAPI2 } = arrangeMocks(); + await batchUpsertRemoteNetworks(mockNetworks, storageOpts); + expect(mockUpsertAPI1.isDone()).toBe(true); + expect(mockUpsertAPI2.isDone()).toBe(true); + }); +}); diff --git a/packages/profile-sync-controller/src/controllers/user-storage/network-syncing/services.ts b/packages/profile-sync-controller/src/controllers/user-storage/network-syncing/services.ts new file mode 100644 index 0000000000..e464d55463 --- /dev/null +++ b/packages/profile-sync-controller/src/controllers/user-storage/network-syncing/services.ts @@ -0,0 +1,78 @@ +import type { UserStorageBaseOptions } from '../services'; +import { + getUserStorageAllFeatureEntries, + upsertUserStorage, +} from '../services'; +import type { RemoteNetworkConfiguration } from './types'; + +// TODO - parse type, and handle version changes +/** + * parses the raw remote data to the NetworkConfiguration shape + * @todo - improve parsing instead of asserting + * @todo - improve version handling + * @param rawData - raw remote user storage data + * @returns NetworkConfiguration or undefined if failed to parse + */ +function parseNetworkConfiguration(rawData: string) { + try { + return JSON.parse(rawData) as RemoteNetworkConfiguration; + } catch { + return undefined; + } +} + +const isDefined = (value: Value | null | undefined): value is Value => + value !== undefined && value !== null; + +/** + * gets all remote networks from user storage + * @param opts - user storage options/configuration + * @returns array of all remote networks + */ +export async function getAllRemoteNetworks( + opts: UserStorageBaseOptions, +): Promise { + const rawResults = + (await getUserStorageAllFeatureEntries({ + ...opts, + path: 'networks', + })) ?? []; + + const results = rawResults + .map((rawData) => parseNetworkConfiguration(rawData)) + .filter(isDefined); + + return results; +} + +/** + * Upserts a remote network to user storage + * @param network - network we are updating or inserting + * @param opts - user storage options/configuration + */ +export async function upsertRemoteNetwork( + network: RemoteNetworkConfiguration, + opts: UserStorageBaseOptions, +) { + const chainId: string = network.chainId.toString(); + const data = JSON.stringify(network); + return await upsertUserStorage(data, { + ...opts, + path: `networks.${chainId}`, + }); +} + +/** + * Batch upsert a list of remote networks into user storage + * @param networks - a list of networks to update or insert + * @param opts - user storage options/configuration + */ +export async function batchUpsertRemoteNetworks( + networks: RemoteNetworkConfiguration[], + opts: UserStorageBaseOptions, +): Promise { + // TODO - this has not yet been provided by the backend team + // we will replace this with a batch endpoint in near future + const promises = networks.map((n) => upsertRemoteNetwork(n, opts)); + await Promise.allSettled(promises); +} diff --git a/packages/profile-sync-controller/src/controllers/user-storage/network-syncing/sync.test.ts b/packages/profile-sync-controller/src/controllers/user-storage/network-syncing/sync.test.ts new file mode 100644 index 0000000000..924a88caaa --- /dev/null +++ b/packages/profile-sync-controller/src/controllers/user-storage/network-syncing/sync.test.ts @@ -0,0 +1,92 @@ +import type { NetworkConfiguration } from '@metamask/network-controller'; + +import { MOCK_STORAGE_KEY } from '../__fixtures__'; +import { mockEndpointUpsertUserStorage } from '../__fixtures__/mockServices'; +import type { UserStorageBaseOptions } from '../services'; +import { createMockNetworkConfiguration } from './__fixtures__/mockNetwork'; +import { + addNetwork, + batchUpdateNetworks, + deleteNetwork, + updateNetwork, +} from './sync'; + +const storageOpts: UserStorageBaseOptions = { + bearerToken: 'MOCK_TOKEN', + storageKey: MOCK_STORAGE_KEY, +}; + +const arrangeMockNetwork = () => + createMockNetworkConfiguration({ chainId: '0x1337' }); + +const testMatrix = [ + { + fnName: 'updateNetwork()', + act: (n: NetworkConfiguration) => updateNetwork(n, storageOpts), + }, + { + fnName: 'addNetwork()', + act: (n: NetworkConfiguration) => addNetwork(n, storageOpts), + }, + { + fnName: 'deleteNetwork()', + act: (n: NetworkConfiguration) => deleteNetwork(n, storageOpts), + }, +]; + +describe('network-syncing/sync - updateNetwork() / addNetwork() / deleteNetwork()', () => { + it.each(testMatrix)('should successfully call $fnName', async ({ act }) => { + const mockNetwork = arrangeMockNetwork(); + const mockUpsertAPI = mockEndpointUpsertUserStorage('networks.0x1337'); + await act(mockNetwork); + expect(mockUpsertAPI.isDone()).toBe(true); + }); + + it.each(testMatrix)( + 'should throw error when calling $fnName when API fails', + async ({ act }) => { + const mockNetwork = arrangeMockNetwork(); + const mockUpsertAPI = mockEndpointUpsertUserStorage('networks.0x1337', { + status: 500, + }); + await expect(async () => await act(mockNetwork)).rejects.toThrow( + expect.any(Error), + ); + expect(mockUpsertAPI.isDone()).toBe(true); + }, + ); +}); + +/** + * TODO - the batch endpoint has not been made in the backend yet. + * Mock endpoints will need to be updated in future + */ +describe('network-syncing/sync - batchUpdateNetworks()', () => { + const arrangeMocks = () => { + const mockNetworks = [ + createMockNetworkConfiguration({ chainId: '0x1337' }), + createMockNetworkConfiguration({ chainId: '0x1338' }), + ]; + + const mockAPI = (key: string) => + mockEndpointUpsertUserStorage(`networks.${key}`); + + return { + storageOps: storageOpts, + mockNetworks, + mockUpsertAPI1: mockAPI('0x1337'), + mockUpsertAPI2: mockAPI('0x1338'), + }; + }; + + it('should call upsert storage API with mock network', async () => { + const { mockNetworks, mockUpsertAPI1, mockUpsertAPI2 } = arrangeMocks(); + // Example where we can batch normal adds/updates with deletes + await batchUpdateNetworks( + [mockNetworks[0], { ...mockNetworks[1], deleted: true }], + storageOpts, + ); + expect(mockUpsertAPI1.isDone()).toBe(true); + expect(mockUpsertAPI2.isDone()).toBe(true); + }); +}); diff --git a/packages/profile-sync-controller/src/controllers/user-storage/network-syncing/sync.ts b/packages/profile-sync-controller/src/controllers/user-storage/network-syncing/sync.ts new file mode 100644 index 0000000000..0113b7b189 --- /dev/null +++ b/packages/profile-sync-controller/src/controllers/user-storage/network-syncing/sync.ts @@ -0,0 +1,33 @@ +import type { NetworkConfiguration } from '@metamask/network-controller'; + +import type { UserStorageBaseOptions } from '../services'; +import { batchUpsertRemoteNetworks, upsertRemoteNetwork } from './services'; +import type { RemoteNetworkConfiguration } from './types'; + +export const updateNetwork = async ( + network: NetworkConfiguration, + opts: UserStorageBaseOptions, +) => { + return await upsertRemoteNetwork({ v: '1', ...network, d: false }, opts); +}; + +export const addNetwork = updateNetwork; + +export const deleteNetwork = async ( + network: NetworkConfiguration, + opts: UserStorageBaseOptions, +) => { + // we are soft deleting, as we need to consider devices that have not yet synced + return await upsertRemoteNetwork({ v: '1', ...network, d: true }, opts); +}; + +export const batchUpdateNetworks = async ( + networks: (NetworkConfiguration & { deleted?: boolean })[], + opts: UserStorageBaseOptions, +) => { + const remoteNetworks: RemoteNetworkConfiguration[] = networks.map((n) => ({ + v: '1', + ...n, + })); + return await batchUpsertRemoteNetworks(remoteNetworks, opts); +}; diff --git a/packages/profile-sync-controller/src/controllers/user-storage/network-syncing/types.ts b/packages/profile-sync-controller/src/controllers/user-storage/network-syncing/types.ts new file mode 100644 index 0000000000..6cc5c6b37b --- /dev/null +++ b/packages/profile-sync-controller/src/controllers/user-storage/network-syncing/types.ts @@ -0,0 +1,13 @@ +import type { NetworkConfiguration } from '@metamask/network-controller'; + +export type RemoteNetworkConfiguration = NetworkConfiguration & { + /** + * `version` property. Enables future versioning of the `NetworkConfiguration` shape + */ + v: '1'; + /** + * isDeleted property, used for soft deletion & for correct syncing + * (delete vs upload network) + */ + d?: boolean; +}; diff --git a/packages/profile-sync-controller/tsconfig.build.json b/packages/profile-sync-controller/tsconfig.build.json index cc1d89ad98..14d9783526 100644 --- a/packages/profile-sync-controller/tsconfig.build.json +++ b/packages/profile-sync-controller/tsconfig.build.json @@ -9,7 +9,8 @@ "references": [ { "path": "../base-controller/tsconfig.build.json" }, { "path": "../keyring-controller/tsconfig.build.json" }, - { "path": "../accounts-controller/tsconfig.build.json" } + { "path": "../accounts-controller/tsconfig.build.json" }, + { "path": "../network-controller/tsconfig.build.json" } ], "include": ["../../types", "./src"] } diff --git a/packages/profile-sync-controller/tsconfig.json b/packages/profile-sync-controller/tsconfig.json index e766ef509b..8e86565b1e 100644 --- a/packages/profile-sync-controller/tsconfig.json +++ b/packages/profile-sync-controller/tsconfig.json @@ -6,7 +6,8 @@ "references": [ { "path": "../base-controller" }, { "path": "../keyring-controller" }, - { "path": "../accounts-controller" } + { "path": "../accounts-controller" }, + { "path": "../network-controller" } ], "include": ["../../types", "./src"] } diff --git a/yarn.lock b/yarn.lock index acf165d2a3..3e638e47c0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2195,12 +2195,12 @@ __metadata: "@ethereumjs/util": "npm:^8.1.0" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^7.0.0" - "@metamask/eth-snap-keyring": "npm:^4.3.1" + "@metamask/eth-snap-keyring": "npm:^4.3.3" "@metamask/keyring-api": "npm:^8.1.0" "@metamask/keyring-controller": "npm:^17.2.0" - "@metamask/snaps-controllers": "npm:^9.3.1" - "@metamask/snaps-sdk": "npm:^6.1.1" - "@metamask/snaps-utils": "npm:^7.8.1" + "@metamask/snaps-controllers": "npm:^9.7.0" + "@metamask/snaps-sdk": "npm:^6.5.0" + "@metamask/snaps-utils": "npm:^8.1.1" "@metamask/utils": "npm:^9.1.0" "@types/jest": "npm:^27.4.1" "@types/readable-stream": "npm:^2.3.0" @@ -2215,7 +2215,7 @@ __metadata: uuid: "npm:^8.3.2" peerDependencies: "@metamask/keyring-controller": ^17.0.0 - "@metamask/snaps-controllers": ^9.3.0 + "@metamask/snaps-controllers": ^9.7.0 languageName: unknown linkType: soft @@ -2446,9 +2446,9 @@ __metadata: "@metamask/base-controller": "npm:^7.0.0" "@metamask/chain-api": "npm:^0.1.0" "@metamask/keyring-api": "npm:^8.1.0" - "@metamask/snaps-controllers": "npm:^9.3.1" - "@metamask/snaps-sdk": "npm:^6.1.1" - "@metamask/snaps-utils": "npm:^7.8.1" + "@metamask/snaps-controllers": "npm:^9.7.0" + "@metamask/snaps-sdk": "npm:^6.5.0" + "@metamask/snaps-utils": "npm:^8.1.1" "@metamask/utils": "npm:^9.1.0" "@types/jest": "npm:^27.4.1" "@types/readable-stream": "npm:^2.3.0" @@ -2792,21 +2792,21 @@ __metadata: languageName: node linkType: hard -"@metamask/eth-snap-keyring@npm:^4.3.1": - version: 4.3.2 - resolution: "@metamask/eth-snap-keyring@npm:4.3.2" +"@metamask/eth-snap-keyring@npm:^4.3.3": + version: 4.3.3 + resolution: "@metamask/eth-snap-keyring@npm:4.3.3" dependencies: "@ethereumjs/tx": "npm:^4.2.0" "@metamask/eth-sig-util": "npm:^7.0.3" - "@metamask/keyring-api": "npm:^8.0.1" - "@metamask/snaps-controllers": "npm:^9.3.0" - "@metamask/snaps-sdk": "npm:^6.1.0" + "@metamask/keyring-api": "npm:^8.1.0" + "@metamask/snaps-controllers": "npm:^9.6.0" + "@metamask/snaps-sdk": "npm:^6.4.0" "@metamask/snaps-utils": "npm:^7.8.0" "@metamask/superstruct": "npm:^3.1.0" - "@metamask/utils": "npm:^9.1.0" + "@metamask/utils": "npm:^9.2.1" "@types/uuid": "npm:^9.0.1" uuid: "npm:^9.0.0" - checksum: 10/c6a38be757d31ef2d88568397dc59635ba993bae7e732f16d3c3584ae6d7bf76a3ef05998dc2c8ca1c9d8db79573b6bf2b85b0af68bc2906183cc806845d6677 + checksum: 10/035c82afef82a4cee7bc63b5c4f152a132b683017ec90a4b614764a4bc7adcca8faccf78c25adcddca2d29eee2fed08706f07d72afb93640956b86e862d4f555 languageName: node linkType: hard @@ -3052,7 +3052,7 @@ __metadata: languageName: node linkType: hard -"@metamask/keyring-api@npm:^8.0.1, @metamask/keyring-api@npm:^8.1.0": +"@metamask/keyring-api@npm:^8.1.0": version: 8.1.0 resolution: "@metamask/keyring-api@npm:8.1.0" dependencies: @@ -3349,21 +3349,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/phishing-controller@npm:^10.1.1": - version: 10.1.1 - resolution: "@metamask/phishing-controller@npm:10.1.1" - dependencies: - "@metamask/base-controller": "npm:^6.0.2" - "@metamask/controller-utils": "npm:^11.0.2" - "@types/punycode": "npm:^2.1.0" - eth-phishing-detect: "npm:^1.2.0" - fastest-levenshtein: "npm:^1.0.16" - punycode: "npm:^2.1.1" - checksum: 10/4c6723d62a3a0b071fbf4c2b227a2eef6daa5f742bce80677bdf96312393c427d0d3c183ffcc13e065464c6c644f2b556c56e79161757bbccb525d4b34ee46b0 - languageName: node - linkType: hard - -"@metamask/phishing-controller@workspace:packages/phishing-controller": +"@metamask/phishing-controller@npm:^12.0.2, @metamask/phishing-controller@workspace:packages/phishing-controller": version: 0.0.0-use.local resolution: "@metamask/phishing-controller@workspace:packages/phishing-controller" dependencies: @@ -3413,7 +3399,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/post-message-stream@npm:^8.1.0": +"@metamask/post-message-stream@npm:^8.1.1": version: 8.1.1 resolution: "@metamask/post-message-stream@npm:8.1.1" dependencies: @@ -3454,9 +3440,10 @@ __metadata: "@metamask/base-controller": "npm:^7.0.0" "@metamask/keyring-api": "npm:^8.1.0" "@metamask/keyring-controller": "npm:^17.2.0" - "@metamask/snaps-controllers": "npm:^9.3.1" - "@metamask/snaps-sdk": "npm:^6.1.1" - "@metamask/snaps-utils": "npm:^7.8.1" + "@metamask/network-controller": "npm:^21.0.0" + "@metamask/snaps-controllers": "npm:^9.7.0" + "@metamask/snaps-sdk": "npm:^6.5.0" + "@metamask/snaps-utils": "npm:^8.1.1" "@noble/ciphers": "npm:^0.5.2" "@noble/hashes": "npm:^1.4.0" "@types/jest": "npm:^27.4.1" @@ -3475,7 +3462,7 @@ __metadata: peerDependencies: "@metamask/accounts-controller": ^18.1.1 "@metamask/keyring-controller": ^17.2.0 - "@metamask/snaps-controllers": ^9.3.0 + "@metamask/snaps-controllers": ^9.7.0 languageName: unknown linkType: soft @@ -3644,9 +3631,9 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-controllers@npm:^9.3.0, @metamask/snaps-controllers@npm:^9.3.1": - version: 9.4.0 - resolution: "@metamask/snaps-controllers@npm:9.4.0" +"@metamask/snaps-controllers@npm:^9.6.0, @metamask/snaps-controllers@npm:^9.7.0": + version: 9.7.0 + resolution: "@metamask/snaps-controllers@npm:9.7.0" dependencies: "@metamask/approval-controller": "npm:^7.0.2" "@metamask/base-controller": "npm:^6.0.2" @@ -3654,14 +3641,14 @@ __metadata: "@metamask/json-rpc-middleware-stream": "npm:^8.0.2" "@metamask/object-multiplex": "npm:^2.0.0" "@metamask/permission-controller": "npm:^11.0.0" - "@metamask/phishing-controller": "npm:^10.1.1" - "@metamask/post-message-stream": "npm:^8.1.0" + "@metamask/phishing-controller": "npm:^12.0.2" + "@metamask/post-message-stream": "npm:^8.1.1" "@metamask/rpc-errors": "npm:^6.3.1" "@metamask/snaps-registry": "npm:^3.2.1" - "@metamask/snaps-rpc-methods": "npm:^11.0.0" - "@metamask/snaps-sdk": "npm:^6.2.0" - "@metamask/snaps-utils": "npm:^8.0.0" - "@metamask/utils": "npm:^9.1.0" + "@metamask/snaps-rpc-methods": "npm:^11.1.1" + "@metamask/snaps-sdk": "npm:^6.5.0" + "@metamask/snaps-utils": "npm:^8.1.1" + "@metamask/utils": "npm:^9.2.1" "@xstate/fsm": "npm:^2.0.0" browserify-zlib: "npm:^0.2.0" concat-stream: "npm:^2.0.0" @@ -3673,11 +3660,11 @@ __metadata: readable-web-to-node-stream: "npm:^3.0.2" tar-stream: "npm:^3.1.7" peerDependencies: - "@metamask/snaps-execution-environments": ^6.6.2 + "@metamask/snaps-execution-environments": ^6.7.1 peerDependenciesMeta: "@metamask/snaps-execution-environments": optional: true - checksum: 10/36c9fb83c675443b1dd9f2bf9eccd1eda1648c5c451bb93e432a358377557f7953b96579d2b5d9605f39bfb96062968a8e612b9500c14fb1fb7bb15d176bd7a1 + checksum: 10/8a353819e60330ef3e338a40b1115d4c830b92b1cc0c92afb2b34bf46fbc906e6da5f905654e1d486cacd40b7025ec74d3cd01cb935090035ce9f1021ce5469f languageName: node linkType: hard @@ -3693,36 +3680,36 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-rpc-methods@npm:^11.0.0": - version: 11.0.0 - resolution: "@metamask/snaps-rpc-methods@npm:11.0.0" +"@metamask/snaps-rpc-methods@npm:^11.1.1": + version: 11.1.1 + resolution: "@metamask/snaps-rpc-methods@npm:11.1.1" dependencies: "@metamask/key-tree": "npm:^9.1.2" "@metamask/permission-controller": "npm:^11.0.0" "@metamask/rpc-errors": "npm:^6.3.1" - "@metamask/snaps-sdk": "npm:^6.2.0" - "@metamask/snaps-utils": "npm:^8.0.0" + "@metamask/snaps-sdk": "npm:^6.5.0" + "@metamask/snaps-utils": "npm:^8.1.1" "@metamask/superstruct": "npm:^3.1.0" - "@metamask/utils": "npm:^9.1.0" + "@metamask/utils": "npm:^9.2.1" "@noble/hashes": "npm:^1.3.1" - checksum: 10/2e594d7f9fde910e87525a6ded9a162d2ea8631249b7868ca710df0d8d25c127674079956976c7443e30c96bedb84bf818748a01aad41cfe69f88524781b994f + checksum: 10/e23279dabc6f4ffe2c6c4a7003a624cd5e79b558d7981ec12c23e54a5da25cb7be9bc7bddfa8b2ce84af28a89b42076a2c14ab004b7a976a4426bf1e1de71b5b languageName: node linkType: hard -"@metamask/snaps-sdk@npm:^6.1.0, @metamask/snaps-sdk@npm:^6.1.1, @metamask/snaps-sdk@npm:^6.2.0, @metamask/snaps-sdk@npm:^6.2.1": - version: 6.2.1 - resolution: "@metamask/snaps-sdk@npm:6.2.1" +"@metamask/snaps-sdk@npm:^6.1.0, @metamask/snaps-sdk@npm:^6.4.0, @metamask/snaps-sdk@npm:^6.5.0": + version: 6.5.0 + resolution: "@metamask/snaps-sdk@npm:6.5.0" dependencies: "@metamask/key-tree": "npm:^9.1.2" "@metamask/providers": "npm:^17.1.2" "@metamask/rpc-errors": "npm:^6.3.1" "@metamask/superstruct": "npm:^3.1.0" - "@metamask/utils": "npm:^9.1.0" - checksum: 10/95728ff7cb5646d04221230c1368f84b1aef931a361ef79689d32426f9f86c6524345e3bd387221a8e7ee0d47e15d1797665bf244566b94cacc09bd643567f05 + "@metamask/utils": "npm:^9.2.1" + checksum: 10/e3da25d20d6adae2cd4845552fe973d0571303d1b9dffdce5d29202707278afac949293aec4470954620d852f30adebd1e54713f2b07c1c341dab09e908e0368 languageName: node linkType: hard -"@metamask/snaps-utils@npm:^7.8.0, @metamask/snaps-utils@npm:^7.8.1": +"@metamask/snaps-utils@npm:^7.8.0": version: 7.8.1 resolution: "@metamask/snaps-utils@npm:7.8.1" dependencies: @@ -3753,9 +3740,9 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-utils@npm:^8.0.0": - version: 8.0.1 - resolution: "@metamask/snaps-utils@npm:8.0.1" +"@metamask/snaps-utils@npm:^8.1.1": + version: 8.1.1 + resolution: "@metamask/snaps-utils@npm:8.1.1" dependencies: "@babel/core": "npm:^7.23.2" "@babel/types": "npm:^7.23.0" @@ -3765,9 +3752,9 @@ __metadata: "@metamask/rpc-errors": "npm:^6.3.1" "@metamask/slip44": "npm:^4.0.0" "@metamask/snaps-registry": "npm:^3.2.1" - "@metamask/snaps-sdk": "npm:^6.2.1" + "@metamask/snaps-sdk": "npm:^6.5.0" "@metamask/superstruct": "npm:^3.1.0" - "@metamask/utils": "npm:^9.1.0" + "@metamask/utils": "npm:^9.2.1" "@noble/hashes": "npm:^1.3.1" "@scure/base": "npm:^1.1.1" chalk: "npm:^4.1.2" @@ -3780,7 +3767,7 @@ __metadata: semver: "npm:^7.5.4" ses: "npm:^1.1.0" validate-npm-package-name: "npm:^5.0.0" - checksum: 10/4c0d58ad04f1e4c625dd01aaf171de0f538afc2003ac928159deee5ebed6d490ccd50575ad585b4f6846aa8d00bccd8f1ea4b32f124e6427873fb985cae8513f + checksum: 10/f4ceb52a1f9578993c88c82a67f4f041309af51c83ff5caa3fed080f36b54d14ea7da807ce1cf19a13600dd0e77c51af70398e8c7bb78f0ba99a037f4d22610f languageName: node linkType: hard @@ -3905,9 +3892,9 @@ __metadata: languageName: node linkType: hard -"@metamask/utils@npm:^9.0.0, @metamask/utils@npm:^9.1.0": - version: 9.1.0 - resolution: "@metamask/utils@npm:9.1.0" +"@metamask/utils@npm:^9.0.0, @metamask/utils@npm:^9.1.0, @metamask/utils@npm:^9.2.1": + version: 9.2.1 + resolution: "@metamask/utils@npm:9.2.1" dependencies: "@ethereumjs/tx": "npm:^4.2.0" "@metamask/superstruct": "npm:^3.1.0" @@ -3918,7 +3905,7 @@ __metadata: pony-cause: "npm:^2.1.10" semver: "npm:^7.5.4" uuid: "npm:^9.0.1" - checksum: 10/7335e151a51be92e86868dc48b3ee78c376d4edd5d758d334176027247637ab22839d8f663bd02542c0a19b05ecec456bedab5f36436689cf3d953ca36d91781 + checksum: 10/2192797afd91af19898e107afeaf63e89b61dc7285e0a75d0cc814b5b288e4cdfc856781b01904034c4d2c1efd9bdab512af24c7e4dfe7b77a03f1f3d9dec7e8 languageName: node linkType: hard