diff --git a/packages/claims-controller/CHANGELOG.md b/packages/claims-controller/CHANGELOG.md index faf6bdcc678..f97f51e7f9a 100644 --- a/packages/claims-controller/CHANGELOG.md +++ b/packages/claims-controller/CHANGELOG.md @@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Expose all public `ClaimsController` methods through its messenger ([#8219](https://github.com/MetaMask/core/pull/8219)) + - The following actions are now available: + - `ClaimsController:fetchClaimsConfigurations` + - `ClaimsController:getSubmitClaimConfig` + - `ClaimsController:generateClaimSignature` + - `ClaimsController:getClaims` + - `ClaimsController:saveOrUpdateClaimDraft` + - `ClaimsController:getClaimDrafts` + - `ClaimsController:deleteClaimDraft` + - `ClaimsController:deleteAllClaimDrafts` + - `ClaimsController:clearState` + - Corresponding action types are now exported (e.g. `ClaimsControllerGetClaimsAction`) + ### Changed - Update dependencies ([#8236](https://github.com/MetaMask/core/pull/8236)) diff --git a/packages/claims-controller/package.json b/packages/claims-controller/package.json index f3511475849..d5b59fe8bf9 100644 --- a/packages/claims-controller/package.json +++ b/packages/claims-controller/package.json @@ -40,6 +40,7 @@ "build:docs": "typedoc", "changelog:update": "../../scripts/update-changelog.sh @metamask/claims-controller", "changelog:validate": "../../scripts/validate-changelog.sh @metamask/claims-controller", + "generate-method-action-types": "tsx ../../scripts/generate-method-action-types.ts", "since-latest-release": "../../scripts/since-latest-release.sh", "test": "NODE_OPTIONS=--experimental-vm-modules jest --reporters=jest-silent-reporter", "test:clean": "NODE_OPTIONS=--experimental-vm-modules jest --clearCache", @@ -61,6 +62,7 @@ "deepmerge": "^4.2.2", "jest": "^29.7.0", "ts-jest": "^29.2.5", + "tsx": "^4.20.5", "typedoc": "^0.25.13", "typedoc-plugin-missing-exports": "^2.0.0", "typescript": "~5.3.3" diff --git a/packages/claims-controller/src/ClaimsController-method-action-types.ts b/packages/claims-controller/src/ClaimsController-method-action-types.ts new file mode 100644 index 00000000000..5a33bd54cb3 --- /dev/null +++ b/packages/claims-controller/src/ClaimsController-method-action-types.ts @@ -0,0 +1,112 @@ +/** + * This file is auto generated by `scripts/generate-method-action-types.ts`. + * Do not edit manually. + */ + +import type { ClaimsController } from './ClaimsController'; + +/** + * Fetch the required configurations for the claims service. + * + * @returns The required configurations for the claims service. + */ +export type ClaimsControllerFetchClaimsConfigurationsAction = { + type: `ClaimsController:fetchClaimsConfigurations`; + handler: ClaimsController['fetchClaimsConfigurations']; +}; + +/** + * Get required config for submitting a claim. + * + * @param claim - The claim request to get the required config for. + * @returns The required config for submitting the claim. + */ +export type ClaimsControllerGetSubmitClaimConfigAction = { + type: `ClaimsController:getSubmitClaimConfig`; + handler: ClaimsController['getSubmitClaimConfig']; +}; + +/** + * Generate a signature for a claim. + * + * @param chainId - The chain id of the claim. + * @param walletAddress - The impacted wallet address of the claim. + * @returns The signature for the claim. + */ +export type ClaimsControllerGenerateClaimSignatureAction = { + type: `ClaimsController:generateClaimSignature`; + handler: ClaimsController['generateClaimSignature']; +}; + +/** + * Get the list of claims for the current user. + * + * @returns The list of claims for the current user. + */ +export type ClaimsControllerGetClaimsAction = { + type: `ClaimsController:getClaims`; + handler: ClaimsController['getClaims']; +}; + +/** + * Save a claim draft to the state. + * If the draft name is not provided, a default name will be generated. + * If the draft with the same id already exists, it will be updated. + * + * @param draft - The draft to save. + * @returns The saved draft. + */ +export type ClaimsControllerSaveOrUpdateClaimDraftAction = { + type: `ClaimsController:saveOrUpdateClaimDraft`; + handler: ClaimsController['saveOrUpdateClaimDraft']; +}; + +/** + * Get the list of claim drafts. + * + * @returns The list of claim drafts. + */ +export type ClaimsControllerGetClaimDraftsAction = { + type: `ClaimsController:getClaimDrafts`; + handler: ClaimsController['getClaimDrafts']; +}; + +/** + * Delete a claim draft from the state. + * + * @param draftId - The ID of the draft to delete. + */ +export type ClaimsControllerDeleteClaimDraftAction = { + type: `ClaimsController:deleteClaimDraft`; + handler: ClaimsController['deleteClaimDraft']; +}; + +/** + * Delete all claim drafts from the state. + */ +export type ClaimsControllerDeleteAllClaimDraftsAction = { + type: `ClaimsController:deleteAllClaimDrafts`; + handler: ClaimsController['deleteAllClaimDrafts']; +}; + +/** + * Clears the claims state and resets to default values. + */ +export type ClaimsControllerClearStateAction = { + type: `ClaimsController:clearState`; + handler: ClaimsController['clearState']; +}; + +/** + * Union of all ClaimsController action types. + */ +export type ClaimsControllerMethodActions = + | ClaimsControllerFetchClaimsConfigurationsAction + | ClaimsControllerGetSubmitClaimConfigAction + | ClaimsControllerGenerateClaimSignatureAction + | ClaimsControllerGetClaimsAction + | ClaimsControllerSaveOrUpdateClaimDraftAction + | ClaimsControllerGetClaimDraftsAction + | ClaimsControllerDeleteClaimDraftAction + | ClaimsControllerDeleteAllClaimDraftsAction + | ClaimsControllerClearStateAction; diff --git a/packages/claims-controller/src/ClaimsController.test.ts b/packages/claims-controller/src/ClaimsController.test.ts index 9bf845df5b8..6cbf95535a1 100644 --- a/packages/claims-controller/src/ClaimsController.test.ts +++ b/packages/claims-controller/src/ClaimsController.test.ts @@ -104,9 +104,11 @@ describe('ClaimsController', () => { }); it('should fetch claims configurations successfully', async () => { - await withController(async ({ controller }) => { + await withController(async ({ controller, rootMessenger }) => { const initialState = controller.state; - const configurations = await controller.fetchClaimsConfigurations(); + const configurations = await rootMessenger.call( + 'ClaimsController:fetchClaimsConfigurations', + ); expect(configurations).toBeDefined(); const expectedConfigurations = { @@ -153,9 +155,11 @@ describe('ClaimsController', () => { }); it('should be able to generate valid submit claim config', async () => { - await withController(async ({ controller }) => { - const submitClaimConfig = - await controller.getSubmitClaimConfig(MOCK_CLAIM); + await withController(async ({ rootMessenger }) => { + const submitClaimConfig = await rootMessenger.call( + 'ClaimsController:getSubmitClaimConfig', + MOCK_CLAIM, + ); expect(mockClaimServiceRequestHeaders).toHaveBeenCalledTimes(1); expect(mockClaimServiceGetClaimsApiUrl).toHaveBeenCalledTimes(1); @@ -183,9 +187,12 @@ describe('ClaimsController', () => { ], }, }, - async ({ controller }) => { + async ({ rootMessenger }) => { await expect( - controller.getSubmitClaimConfig(MOCK_CLAIM), + rootMessenger.call( + 'ClaimsController:getSubmitClaimConfig', + MOCK_CLAIM, + ), ).rejects.toThrow( ClaimsControllerErrorMessages.CLAIM_ALREADY_SUBMITTED, ); @@ -213,8 +220,9 @@ describe('ClaimsController', () => { }); it('should generate a message and signature successfully', async () => { - await withController(async ({ controller }) => { - const signature = await controller.generateClaimSignature( + await withController(async ({ rootMessenger }) => { + const signature = await rootMessenger.call( + 'ClaimsController:generateClaimSignature', 1, MOCK_WALLET_ADDRESS, ); @@ -226,14 +234,18 @@ describe('ClaimsController', () => { }); it('should throw an error if claims API response with invalid SIWE message', async () => { - await withController(async ({ controller }) => { + await withController(async ({ rootMessenger }) => { mockClaimServiceGenerateMessageForClaimSignature.mockRestore(); mockClaimServiceGenerateMessageForClaimSignature.mockResolvedValueOnce({ message: 'invalid SIWE message', nonce: 'B4Y8k8lGdMml0nrqk', }); await expect( - controller.generateClaimSignature(1, MOCK_WALLET_ADDRESS), + rootMessenger.call( + 'ClaimsController:generateClaimSignature', + 1, + MOCK_WALLET_ADDRESS, + ), ).rejects.toThrow( ClaimsControllerErrorMessages.INVALID_SIGNATURE_MESSAGE, ); @@ -243,12 +255,12 @@ describe('ClaimsController', () => { describe('getClaims', () => { it('should be able to get the list of claims', async () => { - await withController(async ({ controller }) => { + await withController(async ({ controller, rootMessenger }) => { mockClaimsServiceGetClaims.mockResolvedValueOnce([ MOCK_CLAIM_1, MOCK_CLAIM_2, ]); - const claims = await controller.getClaims(); + const claims = await rootMessenger.call('ClaimsController:getClaims'); expect(claims).toBeDefined(); expect(claims).toStrictEqual([MOCK_CLAIM_1, MOCK_CLAIM_2]); expect(mockClaimsServiceGetClaims).toHaveBeenCalledTimes(1); @@ -294,9 +306,12 @@ describe('ClaimsController', () => { ]; it('should be able to save a claim draft', async () => { - await withController(async ({ controller }) => { + await withController(async ({ controller, rootMessenger }) => { const initialState = controller.state; - controller.saveOrUpdateClaimDraft(MOCK_DRAFT); + rootMessenger.call( + 'ClaimsController:saveOrUpdateClaimDraft', + MOCK_DRAFT, + ); const updatedState = controller.state; expect(updatedState).not.toBe(initialState); expect(updatedState.drafts).toHaveLength(1); @@ -316,8 +331,10 @@ describe('ClaimsController', () => { drafts: MOCK_CLAIM_DRAFTS, }, }, - async ({ controller }) => { - const claimDrafts = controller.getClaimDrafts(); + async ({ rootMessenger }) => { + const claimDrafts = rootMessenger.call( + 'ClaimsController:getClaimDrafts', + ); expect(claimDrafts).toBeDefined(); expect(claimDrafts).toStrictEqual(MOCK_CLAIM_DRAFTS); }, @@ -331,8 +348,8 @@ describe('ClaimsController', () => { drafts: MOCK_CLAIM_DRAFTS, }, }, - async ({ controller }) => { - controller.saveOrUpdateClaimDraft({ + async ({ controller, rootMessenger }) => { + rootMessenger.call('ClaimsController:saveOrUpdateClaimDraft', { draftId: 'mock-draft-1', chainId: '0x1', email: 'test@test.com', @@ -356,10 +373,13 @@ describe('ClaimsController', () => { drafts: MOCK_CLAIM_DRAFTS, }, }, - async ({ controller }) => { + async ({ controller, rootMessenger }) => { const initialState = controller.state; expect(initialState.drafts).toHaveLength(2); - controller.deleteClaimDraft('mock-draft-1'); + rootMessenger.call( + 'ClaimsController:deleteClaimDraft', + 'mock-draft-1', + ); const updatedState = controller.state; expect(updatedState.drafts).toHaveLength(1); expect(updatedState.drafts[0].draftId).toBe('mock-draft-2'); @@ -374,10 +394,10 @@ describe('ClaimsController', () => { drafts: MOCK_CLAIM_DRAFTS, }, }, - async ({ controller }) => { + async ({ controller, rootMessenger }) => { const initialState = controller.state; expect(initialState.drafts).toHaveLength(2); - controller.deleteAllClaimDrafts(); + rootMessenger.call('ClaimsController:deleteAllClaimDrafts'); const updatedState = controller.state; expect(updatedState.drafts).toHaveLength(0); }, @@ -397,11 +417,11 @@ describe('ClaimsController', () => { })), }, }, - async ({ controller }) => { + async ({ controller, rootMessenger }) => { expect(controller.state.claims).toHaveLength(2); expect(controller.state.drafts).toHaveLength(2); - controller.clearState(); + rootMessenger.call('ClaimsController:clearState'); expect(controller.state).toStrictEqual( getDefaultClaimsControllerState(), diff --git a/packages/claims-controller/src/ClaimsController.ts b/packages/claims-controller/src/ClaimsController.ts index 0e97a5854e7..f9ce5070afe 100644 --- a/packages/claims-controller/src/ClaimsController.ts +++ b/packages/claims-controller/src/ClaimsController.ts @@ -9,6 +9,7 @@ import type { KeyringControllerSignPersonalMessageAction } from '@metamask/keyri import type { Messenger } from '@metamask/messenger'; import { bytesToHex, stringToBytes } from '@metamask/utils'; +import type { ClaimsControllerMethodActions } from './ClaimsController-method-action-types'; import type { ClaimsServiceFetchClaimsConfigurationsAction, ClaimsServiceGenerateMessageForClaimSignatureAction, @@ -37,7 +38,9 @@ export type ClaimsControllerGetStateAction = ControllerGetStateAction< ClaimsControllerState >; -export type ClaimsControllerActions = ClaimsControllerGetStateAction; +export type ClaimsControllerActions = + | ClaimsControllerGetStateAction + | ClaimsControllerMethodActions; export type AllowedActions = | ClaimsServiceFetchClaimsConfigurationsAction @@ -99,6 +102,18 @@ export function getDefaultClaimsControllerState(): ClaimsControllerState { }; } +const MESSENGER_EXPOSED_METHODS = [ + 'fetchClaimsConfigurations', + 'getSubmitClaimConfig', + 'generateClaimSignature', + 'getClaims', + 'saveOrUpdateClaimDraft', + 'getClaimDrafts', + 'deleteClaimDraft', + 'deleteAllClaimDrafts', + 'clearState', +] as const; + export class ClaimsController extends BaseController< typeof CONTROLLER_NAME, ClaimsControllerState, @@ -111,6 +126,11 @@ export class ClaimsController extends BaseController< name: CONTROLLER_NAME, state: { ...getDefaultClaimsControllerState(), ...state }, }); + + this.messenger.registerMethodActionHandlers( + this, + MESSENGER_EXPOSED_METHODS, + ); } /** diff --git a/packages/claims-controller/src/index.ts b/packages/claims-controller/src/index.ts index 497d70f4bff..6675c21c470 100644 --- a/packages/claims-controller/src/index.ts +++ b/packages/claims-controller/src/index.ts @@ -10,6 +10,18 @@ export type { ClaimsControllerMessenger, } from './ClaimsController'; +export type { + ClaimsControllerFetchClaimsConfigurationsAction, + ClaimsControllerGetSubmitClaimConfigAction, + ClaimsControllerGenerateClaimSignatureAction, + ClaimsControllerGetClaimsAction, + ClaimsControllerSaveOrUpdateClaimDraftAction, + ClaimsControllerGetClaimDraftsAction, + ClaimsControllerDeleteClaimDraftAction, + ClaimsControllerDeleteAllClaimDraftsAction, + ClaimsControllerClearStateAction, +} from './ClaimsController-method-action-types'; + export type { Claim, ClaimsControllerState, diff --git a/packages/seedless-onboarding-controller/CHANGELOG.md b/packages/seedless-onboarding-controller/CHANGELOG.md index 4d6f884c037..4e60a31a4fe 100644 --- a/packages/seedless-onboarding-controller/CHANGELOG.md +++ b/packages/seedless-onboarding-controller/CHANGELOG.md @@ -7,6 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Expose all public `SeedlessOnboardingController` methods through its messenger ([#8219](https://github.com/MetaMask/core/pull/8219)) + - The following actions are now available: + - `SeedlessOnboardingController:fetchMetadataAccessCreds` + - `SeedlessOnboardingController:preloadToprfNodeDetails` + - `SeedlessOnboardingController:authenticate` + - `SeedlessOnboardingController:createToprfKeyAndBackupSeedPhrase` + - `SeedlessOnboardingController:addNewSecretData` + - `SeedlessOnboardingController:fetchAllSecretData` + - `SeedlessOnboardingController:changePassword` + - `SeedlessOnboardingController:updateBackupMetadataState` + - `SeedlessOnboardingController:verifyVaultPassword` + - `SeedlessOnboardingController:getSecretDataBackupState` + - `SeedlessOnboardingController:submitPassword` + - `SeedlessOnboardingController:setLocked` + - `SeedlessOnboardingController:syncLatestGlobalPassword` + - `SeedlessOnboardingController:submitGlobalPassword` + - `SeedlessOnboardingController:checkIsPasswordOutdated` + - `SeedlessOnboardingController:getIsUserAuthenticated` + - `SeedlessOnboardingController:clearState` + - `SeedlessOnboardingController:storeKeyringEncryptionKey` + - `SeedlessOnboardingController:loadKeyringEncryptionKey` + - `SeedlessOnboardingController:refreshAuthTokens` + - `SeedlessOnboardingController:revokePendingRefreshTokens` + - `SeedlessOnboardingController:rotateRefreshToken` + - `SeedlessOnboardingController:checkNodeAuthTokenExpired` + - `SeedlessOnboardingController:checkMetadataAccessTokenExpired` + - `SeedlessOnboardingController:checkAccessTokenExpired` + - Corresponding action types are now exported (e.g. `SeedlessOnboardingControllerGetAccessTokenAction`) + ## [9.0.0] ### Changed diff --git a/packages/seedless-onboarding-controller/package.json b/packages/seedless-onboarding-controller/package.json index 6cf76bd3f28..4850d487bc4 100644 --- a/packages/seedless-onboarding-controller/package.json +++ b/packages/seedless-onboarding-controller/package.json @@ -40,6 +40,7 @@ "build:docs": "typedoc", "changelog:update": "../../scripts/update-changelog.sh @metamask/seedless-onboarding-controller", "changelog:validate": "../../scripts/validate-changelog.sh @metamask/seedless-onboarding-controller", + "generate-method-action-types": "tsx ../../scripts/generate-method-action-types.ts", "since-latest-release": "../../scripts/since-latest-release.sh", "test": "NODE_OPTIONS=--experimental-vm-modules jest --reporters=jest-silent-reporter", "test:clean": "NODE_OPTIONS=--experimental-vm-modules jest --clearCache", @@ -72,6 +73,7 @@ "jest-environment-node": "^29.7.0", "nock": "^13.3.1", "ts-jest": "^29.2.5", + "tsx": "^4.20.5", "typedoc": "^0.25.13", "typedoc-plugin-missing-exports": "^2.0.0", "typescript": "~5.3.3" diff --git a/packages/seedless-onboarding-controller/src/SeedlessOnboardingController-method-action-types.ts b/packages/seedless-onboarding-controller/src/SeedlessOnboardingController-method-action-types.ts new file mode 100644 index 00000000000..f63e0167656 --- /dev/null +++ b/packages/seedless-onboarding-controller/src/SeedlessOnboardingController-method-action-types.ts @@ -0,0 +1,373 @@ +/** + * This file is auto generated by `scripts/generate-method-action-types.ts`. + * Do not edit manually. + */ + +import type { SeedlessOnboardingController } from './SeedlessOnboardingController'; + +export type SeedlessOnboardingControllerFetchMetadataAccessCredsAction = { + type: `SeedlessOnboardingController:fetchMetadataAccessCreds`; + handler: SeedlessOnboardingController['fetchMetadataAccessCreds']; +}; + +/** + * Gets the node details for the TOPRF operations. + * This function can be called to get the node endpoints, indexes and pubkeys and cache them locally. + */ +export type SeedlessOnboardingControllerPreloadToprfNodeDetailsAction = { + type: `SeedlessOnboardingController:preloadToprfNodeDetails`; + handler: SeedlessOnboardingController['preloadToprfNodeDetails']; +}; + +/** + * Authenticate OAuth user using the seedless onboarding flow + * and determine if the user is already registered or not. + * + * @param params - The parameters for authenticate OAuth user. + * @param params.idTokens - The ID token(s) issued by OAuth verification service. Currently this array only contains a single idToken which is verified by all the nodes, in future we are considering to issue a unique idToken for each node. + * @param params.authConnection - The social login provider. + * @param params.authConnectionId - OAuth authConnectionId from dashboard + * @param params.userId - user email or id from Social login + * @param params.groupedAuthConnectionId - Optional grouped authConnectionId to be used for the authenticate request. + * @param params.socialLoginEmail - The user email from Social login. + * @param params.refreshToken - Refresh token issued during OAuth login. Written to state when provided. + * @param params.revokeToken - revoke token for revoking refresh token and get new refresh token and new revoke token. + * @param params.accessToken - Access token for pairing with profile sync auth service and to access other services. + * @param params.metadataAccessToken - Metadata access token for accessing the metadata service before the vault is created or unlocked. + * @param params.skipLock - Optional flag to skip acquiring the controller lock. (to prevent deadlock in case the caller already acquired the lock) + * @returns A promise that resolves to the authentication result. + */ +export type SeedlessOnboardingControllerAuthenticateAction = { + type: `SeedlessOnboardingController:authenticate`; + handler: SeedlessOnboardingController['authenticate']; +}; + +/** + * Create a new TOPRF encryption key using given password and backups the provided seed phrase. + * + * @param password - The password used to create new wallet and seedphrase + * @param seedPhrase - The initial seed phrase (Mnemonic) created together with the wallet. + * @param keyringId - The keyring id of the backup seed phrase + * @returns A promise that resolves to the encrypted seed phrase and the encryption key. + */ +export type SeedlessOnboardingControllerCreateToprfKeyAndBackupSeedPhraseAction = + { + type: `SeedlessOnboardingController:createToprfKeyAndBackupSeedPhrase`; + handler: SeedlessOnboardingController['createToprfKeyAndBackupSeedPhrase']; + }; + +/** + * encrypt and add a new secret data to the metadata store. + * + * @param data - The data to add. + * @param type - The type of the secret data. + * @param options - Optional options object, which includes optional data to be added to the metadata store. + * @param options.keyringId - The keyring id of the backup keyring (SRP). + * @returns A promise that resolves to the success of the operation. + */ +export type SeedlessOnboardingControllerAddNewSecretDataAction = { + type: `SeedlessOnboardingController:addNewSecretData`; + handler: SeedlessOnboardingController['addNewSecretData']; +}; + +/** + * Fetches all encrypted secret data and metadata for user's account from the metadata store. + * + * Decrypts the secret data and returns the decrypted secret data using the recovered encryption key from the password. + * + * @param password - The optional password used to create new wallet. If not provided, `cached Encryption Key` will be used. + * @returns A promise that resolves to the secret data. + */ +export type SeedlessOnboardingControllerFetchAllSecretDataAction = { + type: `SeedlessOnboardingController:fetchAllSecretData`; + handler: SeedlessOnboardingController['fetchAllSecretData']; +}; + +/** + * Update the password of the seedless onboarding flow. + * + * Changing password will also update the encryption key, metadata store and the vault with new encrypted values. + * + * @param newPassword - The new password to update. + * @param oldPassword - The old password to verify. + * @returns A promise that resolves to the success of the operation. + */ +export type SeedlessOnboardingControllerChangePasswordAction = { + type: `SeedlessOnboardingController:changePassword`; + handler: SeedlessOnboardingController['changePassword']; +}; + +/** + * Update the backup metadata state for the given secret data. + * + * @param secretData - The data to backup, can be a single backup or array of backups. + * @param secretData.keyringId - The keyring id associated with the backup secret data. + * @param secretData.data - The secret data to update the backup metadata state. + */ +export type SeedlessOnboardingControllerUpdateBackupMetadataStateAction = { + type: `SeedlessOnboardingController:updateBackupMetadataState`; + handler: SeedlessOnboardingController['updateBackupMetadataState']; +}; + +/** + * Verify the password validity by decrypting the vault. + * + * @param password - The password to verify. + * @param options - Optional options object. + * @param options.skipLock - Whether to skip the lock acquisition. (to prevent deadlock in case the caller already acquired the lock) + * @returns A promise that resolves to the success of the operation. + * @throws {Error} If the password is invalid or the vault is not initialized. + */ +export type SeedlessOnboardingControllerVerifyVaultPasswordAction = { + type: `SeedlessOnboardingController:verifyVaultPassword`; + handler: SeedlessOnboardingController['verifyVaultPassword']; +}; + +/** + * Get backup state of the given secret data, from the controller state. + * + * If the given secret data is not backed up and not found in the state, it will return `undefined`. + * + * @param data - The data to get the backup state of. + * @param type - The type of the secret data. + * @returns The backup state of the given secret data. + */ +export type SeedlessOnboardingControllerGetSecretDataBackupStateAction = { + type: `SeedlessOnboardingController:getSecretDataBackupState`; + handler: SeedlessOnboardingController['getSecretDataBackupState']; +}; + +/** + * Submit the password to the controller, verify the password validity and unlock the controller. + * + * This method will be used especially when user unlock the wallet. + * The provided password will be verified against the encrypted vault, encryption key will be derived and saved in the controller state. + * + * This operation is useful when user performs some actions that requires the user password/encryption key. e.g. add new srp backup + * + * @param password - The password to submit. + * @returns A promise that resolves to the success of the operation. + */ +export type SeedlessOnboardingControllerSubmitPasswordAction = { + type: `SeedlessOnboardingController:submitPassword`; + handler: SeedlessOnboardingController['submitPassword']; +}; + +/** + * Set the controller to locked state, and deallocate the secrets (vault encryption key and salt). + * + * When the controller is locked, the user will not be able to perform any operations on the controller/vault. + * + * @returns A promise that resolves to the success of the operation. + */ +export type SeedlessOnboardingControllerSetLockedAction = { + type: `SeedlessOnboardingController:setLocked`; + handler: SeedlessOnboardingController['setLocked']; +}; + +/** + * Sync the latest global password to the controller. + * reset vault with latest globalPassword, + * persist the latest global password authPubKey + * + * @param params - The parameters for syncing the latest global password. + * @param params.globalPassword - The latest global password. + * @returns A promise that resolves to the success of the operation. + */ +export type SeedlessOnboardingControllerSyncLatestGlobalPasswordAction = { + type: `SeedlessOnboardingController:syncLatestGlobalPassword`; + handler: SeedlessOnboardingController['syncLatestGlobalPassword']; +}; + +/** + * @description Unlock the controller with the latest global password. + * + * @param params - The parameters for unlocking the controller. + * @param params.maxKeyChainLength - The maximum chain length of the pwd encryption keys. + * @param params.globalPassword - The latest global password. + * @returns A promise that resolves to the success of the operation. + */ +export type SeedlessOnboardingControllerSubmitGlobalPasswordAction = { + type: `SeedlessOnboardingController:submitGlobalPassword`; + handler: SeedlessOnboardingController['submitGlobalPassword']; +}; + +/** + * @description Check if the current password is outdated compare to the global password. + * + * @param options - Optional options object. + * @param options.globalAuthPubKey - The global auth public key to compare with the current auth public key. + * If not provided, the global auth public key will be fetched from the backend. + * @param options.skipCache - If true, bypass the cache and force a fresh check. + * @param options.skipLock - Whether to skip the lock acquisition. (to prevent deadlock in case the caller already acquired the lock) + * @returns A promise that resolves to true if the password is outdated, false otherwise. + */ +export type SeedlessOnboardingControllerCheckIsPasswordOutdatedAction = { + type: `SeedlessOnboardingController:checkIsPasswordOutdated`; + handler: SeedlessOnboardingController['checkIsPasswordOutdated']; +}; + +/** + * Check if the user is authenticated with the seedless onboarding flow by checking the token values in the state. + * + * This method will check the `accessToken` and `revokeToken` in the state, besides the social login authentication details. + * If both are present, the user is authenticated. + * If either is missing, the user is not authenticated. + * + * This method is useful when we want to check if the state has valid authenticated user details to perform vault creations. + * + * @returns True if the user is authenticated, false otherwise. + */ +export type SeedlessOnboardingControllerGetIsUserAuthenticatedAction = { + type: `SeedlessOnboardingController:getIsUserAuthenticated`; + handler: SeedlessOnboardingController['getIsUserAuthenticated']; +}; + +/** + * Clears the current state of the SeedlessOnboardingController. + */ +export type SeedlessOnboardingControllerClearStateAction = { + type: `SeedlessOnboardingController:clearState`; + handler: SeedlessOnboardingController['clearState']; +}; + +/** + * Store the keyring encryption key in state, encrypted under the current + * encryption key. + * + * @param keyringEncryptionKey - The keyring encryption key. + */ +export type SeedlessOnboardingControllerStoreKeyringEncryptionKeyAction = { + type: `SeedlessOnboardingController:storeKeyringEncryptionKey`; + handler: SeedlessOnboardingController['storeKeyringEncryptionKey']; +}; + +/** + * Load the keyring encryption key from state, decrypted under the current + * encryption key. + * + * @returns The keyring encryption key. + */ +export type SeedlessOnboardingControllerLoadKeyringEncryptionKeyAction = { + type: `SeedlessOnboardingController:loadKeyringEncryptionKey`; + handler: SeedlessOnboardingController['loadKeyringEncryptionKey']; +}; + +/** + * Refresh expired nodeAuthTokens, accessToken, and metadataAccessToken using + * the stored refresh token. + * + * Concurrent callers share a single in-flight HTTP request — if a refresh is + * already in-progress the returned promise resolves when that request settles + * rather than firing a duplicate request with the same token. + * + * @returns A promise that resolves when the tokens have been refreshed. + */ +export type SeedlessOnboardingControllerRefreshAuthTokensAction = { + type: `SeedlessOnboardingController:refreshAuthTokens`; + handler: SeedlessOnboardingController['refreshAuthTokens']; +}; + +/** + * Rotate the refresh token — fetch a new refresh/revoke token pair from the + * auth service and persist the new revoke token in the vault. + * + * This method should be called after a successful JWT refresh. + * + * @returns A Promise that resolves to void. + */ +export type SeedlessOnboardingControllerRotateRefreshTokenAction = { + type: `SeedlessOnboardingController:rotateRefreshToken`; + handler: SeedlessOnboardingController['rotateRefreshToken']; +}; + +/** + * Revoke all pending refresh tokens. + * + * This method is to be called after user is authenticated. + * + * @returns A Promise that resolves to void. + */ +export type SeedlessOnboardingControllerRevokePendingRefreshTokensAction = { + type: `SeedlessOnboardingController:revokePendingRefreshTokens`; + handler: SeedlessOnboardingController['revokePendingRefreshTokens']; +}; + +/** + * Get the access token from the state. + * + * If the tokens are expired, the method will refresh them and return the new access token. + * + * @returns The access token. + */ +export type SeedlessOnboardingControllerGetAccessTokenAction = { + type: `SeedlessOnboardingController:getAccessToken`; + handler: SeedlessOnboardingController['getAccessToken']; +}; + +/** + * Check if the current node auth token is expired. + * + * @returns True if the current node auth token is expired, false otherwise. + */ +export type SeedlessOnboardingControllerCheckNodeAuthTokenExpiredAction = { + type: `SeedlessOnboardingController:checkNodeAuthTokenExpired`; + handler: SeedlessOnboardingController['checkNodeAuthTokenExpired']; +}; + +/** + * Check if the current metadata access token should be refreshed. + * Returns true when the token is expired or when less than 10% of its + * lifetime remains (proactive refresh). + * + * @returns True if the metadata access token should be refreshed, false otherwise. + */ +export type SeedlessOnboardingControllerCheckMetadataAccessTokenExpiredAction = + { + type: `SeedlessOnboardingController:checkMetadataAccessTokenExpired`; + handler: SeedlessOnboardingController['checkMetadataAccessTokenExpired']; + }; + +/** + * Check if the current access token should be refreshed. + * Returns true when the token is expired or when less than 10% of its + * lifetime remains (proactive refresh). + * When the vault is locked, the access token is not accessible, so we return false. + * + * @returns True if the access token should be refreshed, false otherwise. + */ +export type SeedlessOnboardingControllerCheckAccessTokenExpiredAction = { + type: `SeedlessOnboardingController:checkAccessTokenExpired`; + handler: SeedlessOnboardingController['checkAccessTokenExpired']; +}; + +/** + * Union of all SeedlessOnboardingController action types. + */ +export type SeedlessOnboardingControllerMethodActions = + | SeedlessOnboardingControllerFetchMetadataAccessCredsAction + | SeedlessOnboardingControllerPreloadToprfNodeDetailsAction + | SeedlessOnboardingControllerAuthenticateAction + | SeedlessOnboardingControllerCreateToprfKeyAndBackupSeedPhraseAction + | SeedlessOnboardingControllerAddNewSecretDataAction + | SeedlessOnboardingControllerFetchAllSecretDataAction + | SeedlessOnboardingControllerChangePasswordAction + | SeedlessOnboardingControllerUpdateBackupMetadataStateAction + | SeedlessOnboardingControllerVerifyVaultPasswordAction + | SeedlessOnboardingControllerGetSecretDataBackupStateAction + | SeedlessOnboardingControllerSubmitPasswordAction + | SeedlessOnboardingControllerSetLockedAction + | SeedlessOnboardingControllerSyncLatestGlobalPasswordAction + | SeedlessOnboardingControllerSubmitGlobalPasswordAction + | SeedlessOnboardingControllerCheckIsPasswordOutdatedAction + | SeedlessOnboardingControllerGetIsUserAuthenticatedAction + | SeedlessOnboardingControllerClearStateAction + | SeedlessOnboardingControllerStoreKeyringEncryptionKeyAction + | SeedlessOnboardingControllerLoadKeyringEncryptionKeyAction + | SeedlessOnboardingControllerRefreshAuthTokensAction + | SeedlessOnboardingControllerRotateRefreshTokenAction + | SeedlessOnboardingControllerRevokePendingRefreshTokensAction + | SeedlessOnboardingControllerGetAccessTokenAction + | SeedlessOnboardingControllerCheckNodeAuthTokenExpiredAction + | SeedlessOnboardingControllerCheckMetadataAccessTokenExpiredAction + | SeedlessOnboardingControllerCheckAccessTokenExpiredAction; diff --git a/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.test.ts b/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.test.ts index 8422ca43aa9..4ade434d22a 100644 --- a/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.test.ts +++ b/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.test.ts @@ -478,6 +478,7 @@ async function mockChangePassword( * * @param toprfClient - The ToprfSecureBackup instance. * @param controller - The SeedlessOnboardingController instance. + * @param baseMessenger - The root messenger to call the method through. * @param password - The mock password. * @param seedPhrase - The mock seed phrase. * @param keyringId - The mock keyring id. @@ -488,6 +489,7 @@ async function mockCreateToprfKeyAndBackupSeedPhrase< >( toprfClient: ToprfSecureBackup, controller: SeedlessOnboardingController, + baseMessenger: RootMessenger, password: string, seedPhrase: Uint8Array, keyringId: string, @@ -502,7 +504,8 @@ async function mockCreateToprfKeyAndBackupSeedPhrase< jest.spyOn(toprfClient, 'persistLocalKey').mockResolvedValueOnce(); // encrypt and store the secret data handleMockSecretDataAdd(); - await controller.createToprfKeyAndBackupSeedPhrase( + await baseMessenger.call( + 'SeedlessOnboardingController:createToprfKeyAndBackupSeedPhrase', password, seedPhrase, keyringId, @@ -766,13 +769,14 @@ describe('SeedlessOnboardingController', () => { withMockAuthenticatedUser: true, }), }, - async ({ controller, toprfClient }) => { + async ({ toprfClient, baseMessenger }) => { // persist the local enc key jest.spyOn(toprfClient, 'persistLocalKey').mockResolvedValueOnce(); // encrypt and store the secret data const mockSecretDataAdd = handleMockSecretDataAdd(); - await controller.createToprfKeyAndBackupSeedPhrase( + await baseMessenger.call( + 'SeedlessOnboardingController:createToprfKeyAndBackupSeedPhrase', MOCK_PASSWORD, MOCK_SEED_PHRASE, MOCK_KEYRING_ID, @@ -847,7 +851,7 @@ describe('SeedlessOnboardingController', () => { describe('fetchNodeDetails', () => { it('should be able to fetch the node details', async () => { - await withController(async ({ controller, toprfClient }) => { + await withController(async ({ toprfClient, baseMessenger }) => { const getNodeDetailsSpy = jest .spyOn(toprfClient, 'getNodeDetails') .mockResolvedValue({ @@ -855,18 +859,22 @@ describe('SeedlessOnboardingController', () => { nodeDetails: [], }); - await controller.preloadToprfNodeDetails(); + await baseMessenger.call( + 'SeedlessOnboardingController:preloadToprfNodeDetails', + ); expect(getNodeDetailsSpy).toHaveBeenCalled(); }); }); it('should not throw an error if the node details fetch fails', async () => { - await withController(async ({ controller, toprfClient }) => { + await withController(async ({ toprfClient, baseMessenger }) => { const getNodeDetailsSpy = jest .spyOn(toprfClient, 'getNodeDetails') .mockRejectedValueOnce(new Error('Failed to fetch node details')); - await controller.preloadToprfNodeDetails(); + await baseMessenger.call( + 'SeedlessOnboardingController:preloadToprfNodeDetails', + ); expect(getNodeDetailsSpy).toHaveBeenCalled(); }); }); @@ -874,117 +882,132 @@ describe('SeedlessOnboardingController', () => { describe('authenticate', () => { it('should be able to register a new user', async () => { - await withController(async ({ controller, toprfClient }) => { - jest.spyOn(toprfClient, 'authenticate').mockResolvedValue({ - nodeAuthTokens: MOCK_NODE_AUTH_TOKENS, - isNewUser: true, - }); + await withController( + async ({ controller, toprfClient, baseMessenger }) => { + jest.spyOn(toprfClient, 'authenticate').mockResolvedValue({ + nodeAuthTokens: MOCK_NODE_AUTH_TOKENS, + isNewUser: true, + }); - const authResult = await controller.authenticate({ - idTokens, - authConnectionId, - userId, - authConnection, - socialLoginEmail, - refreshToken, - revokeToken, - accessToken, - metadataAccessToken, - }); + const authResult = await baseMessenger.call( + 'SeedlessOnboardingController:authenticate', + { + idTokens, + authConnectionId, + userId, + authConnection, + socialLoginEmail, + refreshToken, + revokeToken, + accessToken, + metadataAccessToken, + }, + ); - expect(authResult).toBeDefined(); - expect(authResult.nodeAuthTokens).toBeDefined(); - expect(authResult.isNewUser).toBe(true); + expect(authResult).toBeDefined(); + expect(authResult.nodeAuthTokens).toBeDefined(); + expect(authResult.isNewUser).toBe(true); - expect(controller.state.nodeAuthTokens).toBeDefined(); - expect(controller.state.nodeAuthTokens).toStrictEqual( - MOCK_NODE_AUTH_TOKENS, - ); - expect(controller.state.authConnectionId).toBe(authConnectionId); - expect(controller.state.userId).toBe(userId); - expect(controller.state.authConnection).toBe(authConnection); - expect(controller.state.socialLoginEmail).toBe(socialLoginEmail); - expect(controller.state.isSeedlessOnboardingUserAuthenticated).toBe( - true, - ); - }); + expect(controller.state.nodeAuthTokens).toBeDefined(); + expect(controller.state.nodeAuthTokens).toStrictEqual( + MOCK_NODE_AUTH_TOKENS, + ); + expect(controller.state.authConnectionId).toBe(authConnectionId); + expect(controller.state.userId).toBe(userId); + expect(controller.state.authConnection).toBe(authConnection); + expect(controller.state.socialLoginEmail).toBe(socialLoginEmail); + expect(controller.state.isSeedlessOnboardingUserAuthenticated).toBe( + true, + ); + }, + ); }); it('should be able to authenticate an existing user', async () => { - await withController(async ({ controller, toprfClient }) => { - jest.spyOn(toprfClient, 'authenticate').mockResolvedValue({ - nodeAuthTokens: MOCK_NODE_AUTH_TOKENS, - isNewUser: false, - }); + await withController( + async ({ controller, toprfClient, baseMessenger }) => { + jest.spyOn(toprfClient, 'authenticate').mockResolvedValue({ + nodeAuthTokens: MOCK_NODE_AUTH_TOKENS, + isNewUser: false, + }); - const authResult = await controller.authenticate({ - idTokens, - authConnectionId, - userId, - authConnection, - socialLoginEmail, - refreshToken, - accessToken, - metadataAccessToken, - revokeToken, - }); + const authResult = await baseMessenger.call( + 'SeedlessOnboardingController:authenticate', + { + idTokens, + authConnectionId, + userId, + authConnection, + socialLoginEmail, + refreshToken, + accessToken, + metadataAccessToken, + revokeToken, + }, + ); - expect(authResult).toBeDefined(); - expect(authResult.nodeAuthTokens).toBeDefined(); - expect(authResult.isNewUser).toBe(false); + expect(authResult).toBeDefined(); + expect(authResult.nodeAuthTokens).toBeDefined(); + expect(authResult.isNewUser).toBe(false); - expect(controller.state.nodeAuthTokens).toBeDefined(); - expect(controller.state.nodeAuthTokens).toStrictEqual( - MOCK_NODE_AUTH_TOKENS, - ); - expect(controller.state.authConnectionId).toBe(authConnectionId); - expect(controller.state.userId).toBe(userId); - expect(controller.state.authConnection).toBe(authConnection); - expect(controller.state.socialLoginEmail).toBe(socialLoginEmail); - expect(controller.state.isSeedlessOnboardingUserAuthenticated).toBe( - true, - ); - }); + expect(controller.state.nodeAuthTokens).toBeDefined(); + expect(controller.state.nodeAuthTokens).toStrictEqual( + MOCK_NODE_AUTH_TOKENS, + ); + expect(controller.state.authConnectionId).toBe(authConnectionId); + expect(controller.state.userId).toBe(userId); + expect(controller.state.authConnection).toBe(authConnection); + expect(controller.state.socialLoginEmail).toBe(socialLoginEmail); + expect(controller.state.isSeedlessOnboardingUserAuthenticated).toBe( + true, + ); + }, + ); }); it('should be able to authenticate with groupedAuthConnectionId', async () => { - await withController(async ({ controller, toprfClient }) => { - // mock the authentication method - jest.spyOn(toprfClient, 'authenticate').mockResolvedValue({ - nodeAuthTokens: MOCK_NODE_AUTH_TOKENS, - isNewUser: true, - }); + await withController( + async ({ controller, toprfClient, baseMessenger }) => { + // mock the authentication method + jest.spyOn(toprfClient, 'authenticate').mockResolvedValue({ + nodeAuthTokens: MOCK_NODE_AUTH_TOKENS, + isNewUser: true, + }); - const authResult = await controller.authenticate({ - idTokens, - authConnectionId, - userId, - groupedAuthConnectionId, - authConnection, - socialLoginEmail, - refreshToken, - revokeToken, - accessToken, - metadataAccessToken, - }); + const authResult = await baseMessenger.call( + 'SeedlessOnboardingController:authenticate', + { + idTokens, + authConnectionId, + userId, + groupedAuthConnectionId, + authConnection, + socialLoginEmail, + refreshToken, + revokeToken, + accessToken, + metadataAccessToken, + }, + ); - expect(authResult).toBeDefined(); - expect(authResult.nodeAuthTokens).toBeDefined(); - expect(authResult.isNewUser).toBe(true); + expect(authResult).toBeDefined(); + expect(authResult.nodeAuthTokens).toBeDefined(); + expect(authResult.isNewUser).toBe(true); - expect(controller.state.nodeAuthTokens).toBeDefined(); - expect(controller.state.nodeAuthTokens).toStrictEqual( - MOCK_NODE_AUTH_TOKENS, - ); - expect(controller.state.authConnectionId).toBe(authConnectionId); - expect(controller.state.groupedAuthConnectionId).toBe( - groupedAuthConnectionId, - ); - expect(controller.state.userId).toBe(userId); - expect(controller.state.isSeedlessOnboardingUserAuthenticated).toBe( - true, - ); - }); + expect(controller.state.nodeAuthTokens).toBeDefined(); + expect(controller.state.nodeAuthTokens).toStrictEqual( + MOCK_NODE_AUTH_TOKENS, + ); + expect(controller.state.authConnectionId).toBe(authConnectionId); + expect(controller.state.groupedAuthConnectionId).toBe( + groupedAuthConnectionId, + ); + expect(controller.state.userId).toBe(userId); + expect(controller.state.isSeedlessOnboardingUserAuthenticated).toBe( + true, + ); + }, + ); }); it('should throw an error if the authentication fails', async () => { @@ -996,7 +1019,7 @@ describe('SeedlessOnboardingController', () => { }, }; - await withController(async ({ controller }) => { + await withController(async ({ controller, baseMessenger }) => { const handleCommitment = handleMockCommitment({ status: 200, body: JSONRPC_ERROR, @@ -1006,7 +1029,7 @@ describe('SeedlessOnboardingController', () => { body: JSONRPC_ERROR, }); await expect( - controller.authenticate({ + baseMessenger.call('SeedlessOnboardingController:authenticate', { idTokens, authConnectionId, groupedAuthConnectionId, @@ -1035,30 +1058,35 @@ describe('SeedlessOnboardingController', () => { }); it('should skip the controller lock when skipLock is true', async () => { - await withController(async ({ controller, toprfClient }) => { - jest.spyOn(toprfClient, 'authenticate').mockResolvedValue({ - nodeAuthTokens: MOCK_NODE_AUTH_TOKENS, - isNewUser: false, - }); + await withController( + async ({ controller, toprfClient, baseMessenger }) => { + jest.spyOn(toprfClient, 'authenticate').mockResolvedValue({ + nodeAuthTokens: MOCK_NODE_AUTH_TOKENS, + isNewUser: false, + }); - const authResult = await controller.authenticate({ - idTokens, - authConnectionId, - userId, - authConnection, - socialLoginEmail, - refreshToken, - revokeToken, - accessToken, - metadataAccessToken, - skipLock: true, - }); + const authResult = await baseMessenger.call( + 'SeedlessOnboardingController:authenticate', + { + idTokens, + authConnectionId, + userId, + authConnection, + socialLoginEmail, + refreshToken, + revokeToken, + accessToken, + metadataAccessToken, + skipLock: true, + }, + ); - expect(authResult.isNewUser).toBe(false); - expect(controller.state.isSeedlessOnboardingUserAuthenticated).toBe( - true, - ); - }); + expect(authResult.isNewUser).toBe(false); + expect(controller.state.isSeedlessOnboardingUserAuthenticated).toBe( + true, + ); + }, + ); }); }); @@ -1071,13 +1099,17 @@ describe('SeedlessOnboardingController', () => { withMockAuthPubKey: true, }), }, - async ({ controller, toprfClient }) => { + async ({ toprfClient, baseMessenger }) => { const spy = jest.spyOn(toprfClient, 'fetchAuthPubKey'); mockFetchAuthPubKey(toprfClient, base64ToBytes(MOCK_AUTH_PUB_KEY)); - const result = await controller.checkIsPasswordOutdated(); + const result = await baseMessenger.call( + 'SeedlessOnboardingController:checkIsPasswordOutdated', + ); expect(result).toBe(false); // Call again to test cache - const result2 = await controller.checkIsPasswordOutdated(); + const result2 = await baseMessenger.call( + 'SeedlessOnboardingController:checkIsPasswordOutdated', + ); expect(result2).toBe(false); // Should only call fetchAuthPubKey once due to cache expect(spy).toHaveBeenCalledTimes(1); @@ -1093,13 +1125,17 @@ describe('SeedlessOnboardingController', () => { authPubKey: MOCK_AUTH_PUB_KEY_OUTDATED, }), }, - async ({ controller, toprfClient }) => { + async ({ toprfClient, baseMessenger }) => { const spy = jest.spyOn(toprfClient, 'fetchAuthPubKey'); mockFetchAuthPubKey(toprfClient, base64ToBytes(MOCK_AUTH_PUB_KEY)); - const result = await controller.checkIsPasswordOutdated(); + const result = await baseMessenger.call( + 'SeedlessOnboardingController:checkIsPasswordOutdated', + ); expect(result).toBe(true); // Call again to test cache - const result2 = await controller.checkIsPasswordOutdated(); + const result2 = await baseMessenger.call( + 'SeedlessOnboardingController:checkIsPasswordOutdated', + ); expect(result2).toBe(true); // Should only call fetchAuthPubKey once due to cache expect(spy).toHaveBeenCalledTimes(1); @@ -1115,17 +1151,23 @@ describe('SeedlessOnboardingController', () => { withMockAuthPubKey: true, }), }, - async ({ controller, toprfClient }) => { + async ({ toprfClient, baseMessenger }) => { const spy = jest.spyOn(toprfClient, 'fetchAuthPubKey'); mockFetchAuthPubKey(toprfClient, base64ToBytes(MOCK_AUTH_PUB_KEY)); - const result = await controller.checkIsPasswordOutdated({ - skipCache: true, - }); + const result = await baseMessenger.call( + 'SeedlessOnboardingController:checkIsPasswordOutdated', + { + skipCache: true, + }, + ); expect(result).toBe(false); // Call again with skipCache: true, should call fetchAuthPubKey again - const result2 = await controller.checkIsPasswordOutdated({ - skipCache: true, - }); + const result2 = await baseMessenger.call( + 'SeedlessOnboardingController:checkIsPasswordOutdated', + { + skipCache: true, + }, + ); expect(result2).toBe(false); expect(spy).toHaveBeenCalledTimes(2); }, @@ -1139,8 +1181,12 @@ describe('SeedlessOnboardingController', () => { withMockAuthenticatedUser: true, }), }, - async ({ controller }) => { - await expect(controller.checkIsPasswordOutdated()).rejects.toThrow( + async ({ baseMessenger }) => { + await expect( + baseMessenger.call( + 'SeedlessOnboardingController:checkIsPasswordOutdated', + ), + ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.SRPNotBackedUpError, ); }, @@ -1158,8 +1204,12 @@ describe('SeedlessOnboardingController', () => { nodeAuthTokens: undefined, }, }, - async ({ controller }) => { - await expect(controller.checkIsPasswordOutdated()).rejects.toThrow( + async ({ baseMessenger }) => { + await expect( + baseMessenger.call( + 'SeedlessOnboardingController:checkIsPasswordOutdated', + ), + ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.InsufficientAuthToken, ); }, @@ -1174,13 +1224,17 @@ describe('SeedlessOnboardingController', () => { withMockAuthPubKey: true, }), }, - async ({ controller, toprfClient }) => { + async ({ toprfClient, baseMessenger }) => { // Mock fetchAuthPubKey to reject with an error jest .spyOn(toprfClient, 'fetchAuthPubKey') .mockRejectedValueOnce(new Error('Network error')); - await expect(controller.checkIsPasswordOutdated()).rejects.toThrow( + await expect( + baseMessenger.call( + 'SeedlessOnboardingController:checkIsPasswordOutdated', + ), + ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.FailedToFetchAuthPubKey, ); }, @@ -1196,8 +1250,12 @@ describe('SeedlessOnboardingController', () => { withMockAuthenticatedUser: true, }), }, - async ({ controller }) => { - expect(await controller.getIsUserAuthenticated()).toBe(true); + async ({ baseMessenger }) => { + expect( + await baseMessenger.call( + 'SeedlessOnboardingController:getIsUserAuthenticated', + ), + ).toBe(true); }, ); }); @@ -1210,8 +1268,12 @@ describe('SeedlessOnboardingController', () => { withoutMockAccessToken: true, // missing accessToken }), }, - async ({ controller }) => { - expect(await controller.getIsUserAuthenticated()).toBe(false); + async ({ baseMessenger }) => { + expect( + await baseMessenger.call( + 'SeedlessOnboardingController:getIsUserAuthenticated', + ), + ).toBe(false); }, ); }); @@ -1224,15 +1286,23 @@ describe('SeedlessOnboardingController', () => { withoutMockRevokeToken: true, // missing revokeToken }), }, - async ({ controller }) => { - expect(await controller.getIsUserAuthenticated()).toBe(false); + async ({ baseMessenger }) => { + expect( + await baseMessenger.call( + 'SeedlessOnboardingController:getIsUserAuthenticated', + ), + ).toBe(false); }, ); }); it('should return false if the user is not authenticated (social login details are missing)', async () => { - await withController(async ({ controller }) => { - expect(await controller.getIsUserAuthenticated()).toBe(false); + await withController(async ({ baseMessenger }) => { + expect( + await baseMessenger.call( + 'SeedlessOnboardingController:getIsUserAuthenticated', + ), + ).toBe(false); }); }); }); @@ -1247,7 +1317,13 @@ describe('SeedlessOnboardingController', () => { withMockAuthenticatedUser: true, }), }, - async ({ controller, toprfClient, initialState, encryptor }) => { + async ({ + controller, + toprfClient, + initialState, + encryptor, + baseMessenger, + }) => { const { encKey, pwEncKey, authKeyPair } = mockcreateLocalKey( toprfClient, MOCK_PASSWORD, @@ -1257,7 +1333,8 @@ describe('SeedlessOnboardingController', () => { jest.spyOn(toprfClient, 'persistLocalKey').mockResolvedValueOnce(); // encrypt and store the secret data const mockSecretDataAdd = handleMockSecretDataAdd(); - await controller.createToprfKeyAndBackupSeedPhrase( + await baseMessenger.call( + 'SeedlessOnboardingController:createToprfKeyAndBackupSeedPhrase', MOCK_PASSWORD, MOCK_SEED_PHRASE, MOCK_KEYRING_ID, @@ -1290,7 +1367,10 @@ describe('SeedlessOnboardingController', () => { // should be able to get the hash of the seed phrase backup from the state expect( - controller.getSecretDataBackupState(MOCK_SEED_PHRASE), + baseMessenger.call( + 'SeedlessOnboardingController:getSecretDataBackupState', + MOCK_SEED_PHRASE, + ), ).toBeDefined(); }, ); @@ -1303,11 +1383,12 @@ describe('SeedlessOnboardingController', () => { withMockAuthenticatedUser: true, }), }, - async ({ controller, toprfClient, encryptor }) => { + async ({ controller, toprfClient, encryptor, baseMessenger }) => { // encrypt and store the secret data const mockSecretDataAdd = handleMockSecretDataAdd(); jest.spyOn(toprfClient, 'persistLocalKey').mockResolvedValueOnce(); - await controller.createToprfKeyAndBackupSeedPhrase( + await baseMessenger.call( + 'SeedlessOnboardingController:createToprfKeyAndBackupSeedPhrase', MOCK_PASSWORD, MOCK_SEED_PHRASE, MOCK_KEYRING_ID, @@ -1341,7 +1422,13 @@ describe('SeedlessOnboardingController', () => { withMockAuthenticatedUser: true, }), }, - async ({ controller, toprfClient, initialState, encryptor }) => { + async ({ + controller, + toprfClient, + initialState, + encryptor, + baseMessenger, + }) => { const { encKey, pwEncKey, authKeyPair, createLocalKeySpy } = mockcreateLocalKey(toprfClient, MOCK_PASSWORD); @@ -1365,7 +1452,8 @@ describe('SeedlessOnboardingController', () => { isNewUser: false, }); - await controller.createToprfKeyAndBackupSeedPhrase( + await baseMessenger.call( + 'SeedlessOnboardingController:createToprfKeyAndBackupSeedPhrase', MOCK_PASSWORD, MOCK_SEED_PHRASE, MOCK_KEYRING_ID, @@ -1404,7 +1492,10 @@ describe('SeedlessOnboardingController', () => { // should be able to get the hash of the seed phrase backup from the state expect( - controller.getSecretDataBackupState(MOCK_SEED_PHRASE), + baseMessenger.call( + 'SeedlessOnboardingController:getSecretDataBackupState', + MOCK_SEED_PHRASE, + ), ).toBeDefined(); }, ); @@ -1412,23 +1503,32 @@ describe('SeedlessOnboardingController', () => { it('should be able to create a seed phrase backup without groupedAuthConnectionId', async () => { await withController( - async ({ controller, toprfClient, encryptor, initialState }) => { + async ({ + controller, + toprfClient, + encryptor, + initialState, + baseMessenger, + }) => { jest.spyOn(toprfClient, 'authenticate').mockResolvedValue({ nodeAuthTokens: MOCK_NODE_AUTH_TOKENS, isNewUser: false, }); - await controller.authenticate({ - idTokens, - authConnectionId, - userId, - authConnection, - socialLoginEmail, - refreshToken, - revokeToken, - accessToken, - metadataAccessToken, - }); + await baseMessenger.call( + 'SeedlessOnboardingController:authenticate', + { + idTokens, + authConnectionId, + userId, + authConnection, + socialLoginEmail, + refreshToken, + revokeToken, + accessToken, + metadataAccessToken, + }, + ); const { encKey, pwEncKey, authKeyPair } = mockcreateLocalKey( toprfClient, @@ -1439,7 +1539,8 @@ describe('SeedlessOnboardingController', () => { jest.spyOn(toprfClient, 'persistLocalKey').mockResolvedValueOnce(); // encrypt and store the secret data const mockSecretDataAdd = handleMockSecretDataAdd(); - await controller.createToprfKeyAndBackupSeedPhrase( + await baseMessenger.call( + 'SeedlessOnboardingController:createToprfKeyAndBackupSeedPhrase', MOCK_PASSWORD, MOCK_SEED_PHRASE, MOCK_KEYRING_ID, @@ -1472,7 +1573,10 @@ describe('SeedlessOnboardingController', () => { // should be able to get the hash of the seed phrase backup from the state expect( - controller.getSecretDataBackupState(MOCK_SEED_PHRASE), + baseMessenger.call( + 'SeedlessOnboardingController:getSecretDataBackupState', + MOCK_SEED_PHRASE, + ), ).toBeDefined(); }, ); @@ -1487,7 +1591,7 @@ describe('SeedlessOnboardingController', () => { withoutMockRevokeToken: true, }), }, - async ({ controller, toprfClient }) => { + async ({ toprfClient, baseMessenger }) => { mockcreateLocalKey(toprfClient, MOCK_PASSWORD); // persist the local enc key @@ -1496,7 +1600,8 @@ describe('SeedlessOnboardingController', () => { // encrypt and store the secret data handleMockSecretDataAdd(); await expect( - controller.createToprfKeyAndBackupSeedPhrase( + baseMessenger.call( + 'SeedlessOnboardingController:createToprfKeyAndBackupSeedPhrase', MOCK_PASSWORD, MOCK_SEED_PHRASE, MOCK_KEYRING_ID, @@ -1515,13 +1620,14 @@ describe('SeedlessOnboardingController', () => { withMockAuthenticatedUser: true, }), }, - async ({ controller, toprfClient, initialState }) => { + async ({ controller, toprfClient, initialState, baseMessenger }) => { jest.spyOn(toprfClient, 'createLocalKey').mockImplementation(() => { throw new Error('Failed to create local encryption key'); }); await expect( - controller.createToprfKeyAndBackupSeedPhrase( + baseMessenger.call( + 'SeedlessOnboardingController:createToprfKeyAndBackupSeedPhrase', MOCK_PASSWORD, MOCK_SEED_PHRASE, MOCK_KEYRING_ID, @@ -1535,20 +1641,23 @@ describe('SeedlessOnboardingController', () => { }); it('should throw an error if authenticated user information is not found', async () => { - await withController(async ({ controller, initialState }) => { - await expect( - controller.createToprfKeyAndBackupSeedPhrase( - MOCK_PASSWORD, - MOCK_SEED_PHRASE, - MOCK_KEYRING_ID, - ), - ).rejects.toThrow( - SeedlessOnboardingControllerErrorMessage.MissingAuthUserInfo, - ); + await withController( + async ({ controller, initialState, baseMessenger }) => { + await expect( + baseMessenger.call( + 'SeedlessOnboardingController:createToprfKeyAndBackupSeedPhrase', + MOCK_PASSWORD, + MOCK_SEED_PHRASE, + MOCK_KEYRING_ID, + ), + ).rejects.toThrow( + SeedlessOnboardingControllerErrorMessage.MissingAuthUserInfo, + ); - // verify vault is not created - expect(controller.state.vault).toBe(initialState.vault); - }); + // verify vault is not created + expect(controller.state.vault).toBe(initialState.vault); + }, + ); }); it('should throw error if authenticated user but refreshToken is missing', async () => { @@ -1561,9 +1670,10 @@ describe('SeedlessOnboardingController', () => { refreshToken: undefined, }, }, - async ({ controller }) => { + async ({ baseMessenger }) => { await expect( - controller.createToprfKeyAndBackupSeedPhrase( + baseMessenger.call( + 'SeedlessOnboardingController:createToprfKeyAndBackupSeedPhrase', MOCK_PASSWORD, MOCK_SEED_PHRASE, MOCK_KEYRING_ID, @@ -1585,9 +1695,10 @@ describe('SeedlessOnboardingController', () => { metadataAccessToken: undefined, }, }, - async ({ controller }) => { + async ({ baseMessenger }) => { await expect( - controller.createToprfKeyAndBackupSeedPhrase( + baseMessenger.call( + 'SeedlessOnboardingController:createToprfKeyAndBackupSeedPhrase', MOCK_PASSWORD, MOCK_SEED_PHRASE, MOCK_KEYRING_ID, @@ -1602,9 +1713,10 @@ describe('SeedlessOnboardingController', () => { it('should throw an error if user does not have the AuthToken', async () => { await withController( { state: { userId, authConnectionId, groupedAuthConnectionId } }, - async ({ controller, initialState }) => { + async ({ controller, initialState, baseMessenger }) => { await expect( - controller.createToprfKeyAndBackupSeedPhrase( + baseMessenger.call( + 'SeedlessOnboardingController:createToprfKeyAndBackupSeedPhrase', MOCK_PASSWORD, MOCK_SEED_PHRASE, MOCK_KEYRING_ID, @@ -1626,7 +1738,7 @@ describe('SeedlessOnboardingController', () => { withMockAuthenticatedUser: true, }), }, - async ({ controller, toprfClient }) => { + async ({ toprfClient, baseMessenger }) => { mockcreateLocalKey(toprfClient, MOCK_PASSWORD); jest @@ -1637,7 +1749,8 @@ describe('SeedlessOnboardingController', () => { const mockSecretDataAdd = handleMockSecretDataAdd(); await expect( - controller.createToprfKeyAndBackupSeedPhrase( + baseMessenger.call( + 'SeedlessOnboardingController:createToprfKeyAndBackupSeedPhrase', MOCK_PASSWORD, MOCK_SEED_PHRASE, MOCK_KEYRING_ID, @@ -1658,7 +1771,7 @@ describe('SeedlessOnboardingController', () => { withMockAuthenticatedUser: true, }), }, - async ({ controller, toprfClient }) => { + async ({ toprfClient, baseMessenger }) => { mockcreateLocalKey(toprfClient, MOCK_PASSWORD); jest.spyOn(toprfClient, 'persistLocalKey').mockResolvedValueOnce(); @@ -1668,7 +1781,8 @@ describe('SeedlessOnboardingController', () => { .mockRejectedValueOnce(new Error('Failed to add secret data item')); await expect( - controller.createToprfKeyAndBackupSeedPhrase( + baseMessenger.call( + 'SeedlessOnboardingController:createToprfKeyAndBackupSeedPhrase', MOCK_PASSWORD, MOCK_SEED_PHRASE, MOCK_KEYRING_ID, @@ -1722,9 +1836,10 @@ describe('SeedlessOnboardingController', () => { }); it('should throw an error if the controller is locked', async () => { - await withController(async ({ controller }) => { + await withController(async ({ baseMessenger }) => { await expect( - controller.addNewSecretData( + baseMessenger.call( + 'SeedlessOnboardingController:addNewSecretData', NEW_KEY_RING_1.seedPhrase, SecretType.Mnemonic, { @@ -1748,8 +1863,11 @@ describe('SeedlessOnboardingController', () => { vaultEncryptionSalt: MOCK_VAULT_ENCRYPTION_SALT, }), }, - async ({ controller, toprfClient }) => { - await controller.submitPassword(MOCK_PASSWORD); + async ({ controller, toprfClient, baseMessenger }) => { + await baseMessenger.call( + 'SeedlessOnboardingController:submitPassword', + MOCK_PASSWORD, + ); mockFetchAuthPubKey( toprfClient, @@ -1758,7 +1876,8 @@ describe('SeedlessOnboardingController', () => { // encrypt and store the secret data const mockSecretDataAdd = handleMockSecretDataAdd(); - await controller.addNewSecretData( + await baseMessenger.call( + 'SeedlessOnboardingController:addNewSecretData', NEW_KEY_RING_1.seedPhrase, SecretType.Mnemonic, { @@ -1786,8 +1905,11 @@ describe('SeedlessOnboardingController', () => { vaultEncryptionSalt: MOCK_VAULT_ENCRYPTION_SALT, }), }, - async ({ controller, toprfClient }) => { - await controller.submitPassword(MOCK_PASSWORD); + async ({ controller, toprfClient, baseMessenger }) => { + await baseMessenger.call( + 'SeedlessOnboardingController:submitPassword', + MOCK_PASSWORD, + ); mockFetchAuthPubKey( toprfClient, @@ -1796,7 +1918,8 @@ describe('SeedlessOnboardingController', () => { // encrypt and store the secret data const mockSecretDataAdd = handleMockSecretDataAdd(); - await controller.addNewSecretData( + await baseMessenger.call( + 'SeedlessOnboardingController:addNewSecretData', NEW_KEY_RING_1.seedPhrase, SecretType.Mnemonic, { @@ -1819,7 +1942,8 @@ describe('SeedlessOnboardingController', () => { // add another seed phrase backup const mockSecretDataAdd2 = handleMockSecretDataAdd(); - await controller.addNewSecretData( + await baseMessenger.call( + 'SeedlessOnboardingController:addNewSecretData', NEW_KEY_RING_2.seedPhrase, SecretType.Mnemonic, { @@ -1848,12 +1972,18 @@ describe('SeedlessOnboardingController', () => { ]); // should be able to get the hash of the seed phrase backup from the state expect( - controller.getSecretDataBackupState(NEW_KEY_RING_1.seedPhrase), + baseMessenger.call( + 'SeedlessOnboardingController:getSecretDataBackupState', + NEW_KEY_RING_1.seedPhrase, + ), ).toBeDefined(); // should return undefined if the seed phrase is not backed up expect( - controller.getSecretDataBackupState(NEW_KEY_RING_3.seedPhrase), + baseMessenger.call( + 'SeedlessOnboardingController:getSecretDataBackupState', + NEW_KEY_RING_3.seedPhrase, + ), ).toBeUndefined(); }, ); @@ -1870,8 +2000,11 @@ describe('SeedlessOnboardingController', () => { vaultEncryptionSalt: MOCK_VAULT_ENCRYPTION_SALT, }), }, - async ({ controller, toprfClient }) => { - await controller.submitPassword(MOCK_PASSWORD); + async ({ controller, toprfClient, baseMessenger }) => { + await baseMessenger.call( + 'SeedlessOnboardingController:submitPassword', + MOCK_PASSWORD, + ); mockFetchAuthPubKey( toprfClient, @@ -1880,14 +2013,16 @@ describe('SeedlessOnboardingController', () => { // encrypt and store the secret data const mockSecretDataAdd = handleMockSecretDataAdd(); - await controller.addNewSecretData( + await baseMessenger.call( + 'SeedlessOnboardingController:addNewSecretData', MOCK_PRIVATE_KEY, SecretType.PrivateKey, ); expect(mockSecretDataAdd.isDone()).toBe(true); expect( - controller.getSecretDataBackupState( + baseMessenger.call( + 'SeedlessOnboardingController:getSecretDataBackupState', MOCK_PRIVATE_KEY, SecretType.PrivateKey, ), @@ -1907,11 +2042,15 @@ describe('SeedlessOnboardingController', () => { vaultEncryptionSalt: MOCK_VAULT_ENCRYPTION_SALT, }), }, - async ({ controller, toprfClient }) => { + async ({ toprfClient, baseMessenger }) => { mockFetchAuthPubKey(toprfClient, base64ToBytes(MOCK_AUTH_PUB_KEY)); - await controller.submitPassword(MOCK_PASSWORD); + await baseMessenger.call( + 'SeedlessOnboardingController:submitPassword', + MOCK_PASSWORD, + ); await expect( - controller.addNewSecretData( + baseMessenger.call( + 'SeedlessOnboardingController:addNewSecretData', NEW_KEY_RING_1.seedPhrase, SecretType.Mnemonic, { @@ -1936,8 +2075,11 @@ describe('SeedlessOnboardingController', () => { vaultEncryptionSalt: MOCK_VAULT_ENCRYPTION_SALT, }), }, - async ({ controller, toprfClient }) => { - await controller.submitPassword(MOCK_PASSWORD); + async ({ controller, toprfClient, baseMessenger }) => { + await baseMessenger.call( + 'SeedlessOnboardingController:submitPassword', + MOCK_PASSWORD, + ); mockFetchAuthPubKey( toprfClient, @@ -1945,7 +2087,11 @@ describe('SeedlessOnboardingController', () => { ); await expect( - controller.addNewSecretData(MOCK_SEED_PHRASE, SecretType.Mnemonic), + baseMessenger.call( + 'SeedlessOnboardingController:addNewSecretData', + MOCK_SEED_PHRASE, + SecretType.Mnemonic, + ), ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.MissingKeyringId, ); @@ -1964,7 +2110,13 @@ describe('SeedlessOnboardingController', () => { withMockAuthenticatedUser: true, }), }, - async ({ controller, toprfClient, initialState, encryptor }) => { + async ({ + controller, + toprfClient, + initialState, + encryptor, + baseMessenger, + }) => { // fetch and decrypt the secret data const { encKey, pwEncKey, authKeyPair } = mockRecoverEncKey( toprfClient, @@ -1987,7 +2139,10 @@ describe('SeedlessOnboardingController', () => { MOCK_PASSWORD, ), }); - const secretData = await controller.fetchAllSecretData(MOCK_PASSWORD); + const secretData = await baseMessenger.call( + 'SeedlessOnboardingController:fetchAllSecretData', + MOCK_PASSWORD, + ); expect(mockSecretDataGet.isDone()).toBe(true); expect(secretData).toBeDefined(); @@ -2030,7 +2185,7 @@ describe('SeedlessOnboardingController', () => { withMockAuthenticatedUser: true, }), }, - async ({ controller, toprfClient, encryptor }) => { + async ({ controller, toprfClient, encryptor, baseMessenger }) => { // fetch and decrypt the secret data const { encKey, pwEncKey, authKeyPair } = mockRecoverEncKey( toprfClient, @@ -2044,7 +2199,10 @@ describe('SeedlessOnboardingController', () => { MOCK_PASSWORD, ), }); - const secretData = await controller.fetchAllSecretData(MOCK_PASSWORD); + const secretData = await baseMessenger.call( + 'SeedlessOnboardingController:fetchAllSecretData', + MOCK_PASSWORD, + ); expect(mockSecretDataGet.isDone()).toBe(true); expect(secretData).toBeDefined(); @@ -2101,7 +2259,13 @@ describe('SeedlessOnboardingController', () => { metadataAccessToken, }, }, - async ({ controller, toprfClient, initialState, encryptor }) => { + async ({ + controller, + toprfClient, + initialState, + encryptor, + baseMessenger, + }) => { // fetch and decrypt the secret data const { encKey, pwEncKey, authKeyPair } = mockRecoverEncKey( toprfClient, @@ -2115,7 +2279,10 @@ describe('SeedlessOnboardingController', () => { MOCK_PASSWORD, ), }); - const secretData = await controller.fetchAllSecretData(MOCK_PASSWORD); + const secretData = await baseMessenger.call( + 'SeedlessOnboardingController:fetchAllSecretData', + MOCK_PASSWORD, + ); expect(mockSecretDataGet.isDone()).toBe(true); expect(secretData).toBeDefined(); @@ -2156,7 +2323,13 @@ describe('SeedlessOnboardingController', () => { accessToken, }), }, - async ({ controller, toprfClient, initialState, encryptor }) => { + async ({ + controller, + toprfClient, + initialState, + encryptor, + baseMessenger, + }) => { // fetch and decrypt the secret data const { encKey, pwEncKey, authKeyPair, recoverEncKeySpy } = mockRecoverEncKey(toprfClient, MOCK_PASSWORD, { @@ -2178,7 +2351,10 @@ describe('SeedlessOnboardingController', () => { isNewUser: false, }); - const secretData = await controller.fetchAllSecretData(MOCK_PASSWORD); + const secretData = await baseMessenger.call( + 'SeedlessOnboardingController:fetchAllSecretData', + MOCK_PASSWORD, + ); expect(mockSecretDataGet.isDone()).toBe(true); expect(secretData).toBeDefined(); @@ -2245,8 +2421,11 @@ describe('SeedlessOnboardingController', () => { vaultEncryptionSalt: MOCK_VAULT_ENCRYPTION_SALT, }), }, - async ({ controller }) => { - await controller.submitPassword(MOCK_PASSWORD); + async ({ baseMessenger }) => { + await baseMessenger.call( + 'SeedlessOnboardingController:submitPassword', + MOCK_PASSWORD, + ); const mockSecretDataGet = handleMockSecretDataGet({ status: 200, @@ -2256,7 +2435,9 @@ describe('SeedlessOnboardingController', () => { ), }); - const secretData = await controller.fetchAllSecretData(); + const secretData = await baseMessenger.call( + 'SeedlessOnboardingController:fetchAllSecretData', + ); expect(mockSecretDataGet.isDone()).toBe(true); expect(secretData).toBeDefined(); @@ -2274,7 +2455,7 @@ describe('SeedlessOnboardingController', () => { withMockAuthenticatedUser: true, }), }, - async ({ controller, toprfClient }) => { + async ({ toprfClient, baseMessenger }) => { jest .spyOn(toprfClient, 'recoverEncKey') .mockRejectedValueOnce( @@ -2282,7 +2463,10 @@ describe('SeedlessOnboardingController', () => { ); await expect( - controller.fetchAllSecretData('INCORRECT_PASSWORD'), + baseMessenger.call( + 'SeedlessOnboardingController:fetchAllSecretData', + 'INCORRECT_PASSWORD', + ), ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.LoginFailedError, ); @@ -2297,7 +2481,7 @@ describe('SeedlessOnboardingController', () => { withMockAuthenticatedUser: true, }), }, - async ({ controller, toprfClient }) => { + async ({ toprfClient, baseMessenger }) => { mockRecoverEncKey(toprfClient, MOCK_PASSWORD); jest @@ -2305,7 +2489,10 @@ describe('SeedlessOnboardingController', () => { .mockRejectedValueOnce(new Error('Failed to decrypt data')); await expect( - controller.fetchAllSecretData('INCORRECT_PASSWORD'), + baseMessenger.call( + 'SeedlessOnboardingController:fetchAllSecretData', + 'INCORRECT_PASSWORD', + ), ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.FailedToFetchSecretMetadata, ); @@ -2320,7 +2507,7 @@ describe('SeedlessOnboardingController', () => { withMockAuthenticatedUser: true, }), }, - async ({ controller, toprfClient }) => { + async ({ toprfClient, baseMessenger }) => { mockRecoverEncKey(toprfClient, MOCK_PASSWORD); // mock the incorrect data shape jest @@ -2329,7 +2516,10 @@ describe('SeedlessOnboardingController', () => { stringToBytes(JSON.stringify({ key: 'value' })), ]); await expect( - controller.fetchAllSecretData(MOCK_PASSWORD), + baseMessenger.call( + 'SeedlessOnboardingController:fetchAllSecretData', + MOCK_PASSWORD, + ), ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.InvalidSecretMetadata, ); @@ -2344,7 +2534,7 @@ describe('SeedlessOnboardingController', () => { withMockAuthenticatedUser: true, }), }, - async ({ controller, toprfClient }) => { + async ({ toprfClient, baseMessenger }) => { jest.spyOn(toprfClient, 'recoverEncKey').mockRejectedValueOnce( new TOPRFError(1009, 'Rate limit exceeded', { rateLimitDetails: { @@ -2357,7 +2547,10 @@ describe('SeedlessOnboardingController', () => { ); await expect( - controller.fetchAllSecretData(MOCK_PASSWORD), + baseMessenger.call( + 'SeedlessOnboardingController:fetchAllSecretData', + MOCK_PASSWORD, + ), ).rejects.toStrictEqual( new RecoveryError( SeedlessOnboardingControllerErrorMessage.TooManyLoginAttempts, @@ -2378,7 +2571,7 @@ describe('SeedlessOnboardingController', () => { withMockAuthenticatedUser: true, }), }, - async ({ controller, toprfClient }) => { + async ({ toprfClient, baseMessenger }) => { jest .spyOn(toprfClient, 'recoverEncKey') .mockRejectedValueOnce( @@ -2386,7 +2579,10 @@ describe('SeedlessOnboardingController', () => { ); await expect( - controller.fetchAllSecretData(MOCK_PASSWORD), + baseMessenger.call( + 'SeedlessOnboardingController:fetchAllSecretData', + MOCK_PASSWORD, + ), ).rejects.toStrictEqual( new RecoveryError( SeedlessOnboardingControllerErrorMessage.IncorrectPassword, @@ -2403,7 +2599,7 @@ describe('SeedlessOnboardingController', () => { withMockAuthenticatedUser: true, }), }, - async ({ controller, toprfClient }) => { + async ({ toprfClient, baseMessenger }) => { jest .spyOn(toprfClient, 'recoverEncKey') .mockRejectedValueOnce( @@ -2411,7 +2607,10 @@ describe('SeedlessOnboardingController', () => { ); await expect( - controller.fetchAllSecretData(MOCK_PASSWORD), + baseMessenger.call( + 'SeedlessOnboardingController:fetchAllSecretData', + MOCK_PASSWORD, + ), ).rejects.toStrictEqual( new RecoveryError( SeedlessOnboardingControllerErrorMessage.LoginFailedError, @@ -2428,7 +2627,7 @@ describe('SeedlessOnboardingController', () => { withMockAuthenticatedUser: true, }), }, - async ({ controller, initialState, toprfClient }) => { + async ({ controller, initialState, toprfClient, baseMessenger }) => { expect(initialState.vault).toBeUndefined(); mockRecoverEncKey(toprfClient, MOCK_PASSWORD); @@ -2441,7 +2640,10 @@ describe('SeedlessOnboardingController', () => { }, }); await expect( - controller.fetchAllSecretData(MOCK_PASSWORD), + baseMessenger.call( + 'SeedlessOnboardingController:fetchAllSecretData', + MOCK_PASSWORD, + ), ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.NoSecretDataFound, ); @@ -2460,7 +2662,7 @@ describe('SeedlessOnboardingController', () => { withMockAuthenticatedUser: true, }), }, - async ({ controller, toprfClient }) => { + async ({ toprfClient, baseMessenger }) => { mockRecoverEncKey(toprfClient, MOCK_PASSWORD); const mockSecretDataGet = handleMockSecretDataGet({ @@ -2477,7 +2679,10 @@ describe('SeedlessOnboardingController', () => { }); await expect( - controller.fetchAllSecretData(MOCK_PASSWORD), + baseMessenger.call( + 'SeedlessOnboardingController:fetchAllSecretData', + MOCK_PASSWORD, + ), ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.InvalidPrimarySecretDataType, ); @@ -2515,8 +2720,11 @@ describe('SeedlessOnboardingController', () => { vault: mockVault, }, }, - async ({ controller }) => { - await controller.submitPassword(MOCK_PASSWORD); + async ({ controller, baseMessenger }) => { + await baseMessenger.call( + 'SeedlessOnboardingController:submitPassword', + MOCK_PASSWORD, + ); expect(controller.state.vault).toBe(mockVault); }, @@ -2524,10 +2732,13 @@ describe('SeedlessOnboardingController', () => { }); it('should throw error if the vault is missing', async () => { - await withController(async ({ controller }) => { - await expect(controller.submitPassword(MOCK_PASSWORD)).rejects.toThrow( - SeedlessOnboardingControllerErrorMessage.VaultError, - ); + await withController(async ({ baseMessenger }) => { + await expect( + baseMessenger.call( + 'SeedlessOnboardingController:submitPassword', + MOCK_PASSWORD, + ), + ).rejects.toThrow(SeedlessOnboardingControllerErrorMessage.VaultError); }); }); @@ -2538,9 +2749,14 @@ describe('SeedlessOnboardingController', () => { vault: 'MOCK_VAULT', }, }, - async ({ controller }) => { - // @ts-expect-error intentional test case - await expect(controller.submitPassword(123)).rejects.toThrow( + async ({ baseMessenger }) => { + await expect( + baseMessenger.call( + 'SeedlessOnboardingController:submitPassword', + // @ts-expect-error intentional test case + 123, + ), + ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.WrongPasswordType, ); }, @@ -2557,12 +2773,15 @@ describe('SeedlessOnboardingController', () => { vault: mockVault, }), }, - async ({ controller, encryptor }) => { + async ({ encryptor, baseMessenger }) => { jest .spyOn(encryptor, 'decryptWithKey') .mockResolvedValueOnce(mockVault); await expect( - controller.submitPassword(MOCK_PASSWORD), + baseMessenger.call( + 'SeedlessOnboardingController:submitPassword', + MOCK_PASSWORD, + ), ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.InvalidVaultData, ); @@ -2580,14 +2799,17 @@ describe('SeedlessOnboardingController', () => { vault: mockVault, }), }, - async ({ controller, encryptor }) => { + async ({ encryptor, baseMessenger }) => { jest.spyOn(encryptor, 'decryptWithDetail').mockResolvedValueOnce({ vault: mockVault, exportedKeyString: 'mock-encryption-key', salt: 'mock-salt', }); await expect( - controller.submitPassword(MOCK_PASSWORD), + baseMessenger.call( + 'SeedlessOnboardingController:submitPassword', + MOCK_PASSWORD, + ), ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.VaultDataError, ); @@ -2598,7 +2820,10 @@ describe('SeedlessOnboardingController', () => { salt: 'mock-salt', }); await expect( - controller.submitPassword(MOCK_PASSWORD), + baseMessenger.call( + 'SeedlessOnboardingController:submitPassword', + MOCK_PASSWORD, + ), ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.VaultDataError, ); @@ -2637,9 +2862,12 @@ describe('SeedlessOnboardingController', () => { vaultEncryptionSalt: JSON.parse(vaultWithoutRevokeToken).salt, }), }, - async ({ controller }) => { + async ({ baseMessenger }) => { await expect( - controller.submitPassword(MOCK_PASSWORD), + baseMessenger.call( + 'SeedlessOnboardingController:submitPassword', + MOCK_PASSWORD, + ), ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.InvalidRevokeToken, ); @@ -2659,11 +2887,14 @@ describe('SeedlessOnboardingController', () => { vault: 'MOCK_VAULT', }), }, - async ({ controller, encryptor }) => { + async ({ encryptor, baseMessenger }) => { jest.spyOn(encryptor, 'decrypt').mockResolvedValueOnce('MOCK_VAULT'); expect(async () => { - await controller.verifyVaultPassword(MOCK_PASSWORD); + await baseMessenger.call( + 'SeedlessOnboardingController:verifyVaultPassword', + MOCK_PASSWORD, + ); }).not.toThrow(); }, ); @@ -2677,22 +2908,28 @@ describe('SeedlessOnboardingController', () => { vault: 'MOCK_VAULT', }), }, - async ({ controller, encryptor }) => { + async ({ encryptor, baseMessenger }) => { jest .spyOn(encryptor, 'decrypt') .mockRejectedValueOnce(new Error('Incorrect password')); await expect( - controller.verifyVaultPassword(MOCK_PASSWORD), + baseMessenger.call( + 'SeedlessOnboardingController:verifyVaultPassword', + MOCK_PASSWORD, + ), ).rejects.toThrow('Incorrect password'); }, ); }); it('should throw an error if the vault is missing', async () => { - await withController(async ({ controller }) => { + await withController(async ({ baseMessenger }) => { await expect( - controller.verifyVaultPassword(MOCK_PASSWORD), + baseMessenger.call( + 'SeedlessOnboardingController:verifyVaultPassword', + MOCK_PASSWORD, + ), ).rejects.toThrow(SeedlessOnboardingControllerErrorMessage.VaultError); }); }); @@ -2736,14 +2973,20 @@ describe('SeedlessOnboardingController', () => { vaultEncryptionSalt: MOCK_VAULT_ENCRYPTION_SALT, }), }, - async ({ controller }) => { - await controller.submitPassword(MOCK_PASSWORD); + async ({ controller, baseMessenger }) => { + await baseMessenger.call( + 'SeedlessOnboardingController:submitPassword', + MOCK_PASSWORD, + ); - controller.updateBackupMetadataState({ - keyringId: MOCK_KEYRING_ID, - data: MOCK_SEED_PHRASE, - type: SecretType.Mnemonic, - }); + baseMessenger.call( + 'SeedlessOnboardingController:updateBackupMetadataState', + { + keyringId: MOCK_KEYRING_ID, + data: MOCK_SEED_PHRASE, + type: SecretType.Mnemonic, + }, + ); const MOCK_SEED_PHRASE_HASH = keccak256AndHexify(MOCK_SEED_PHRASE); expect(controller.state.socialBackupsMetadata).toStrictEqual([ { @@ -2766,14 +3009,20 @@ describe('SeedlessOnboardingController', () => { vaultEncryptionSalt: MOCK_VAULT_ENCRYPTION_SALT, }), }, - async ({ controller }) => { - await controller.submitPassword(MOCK_PASSWORD); + async ({ controller, baseMessenger }) => { + await baseMessenger.call( + 'SeedlessOnboardingController:submitPassword', + MOCK_PASSWORD, + ); - controller.updateBackupMetadataState({ - keyringId: MOCK_KEYRING_ID, - data: MOCK_SEED_PHRASE, - type: SecretType.Mnemonic, - }); + baseMessenger.call( + 'SeedlessOnboardingController:updateBackupMetadataState', + { + keyringId: MOCK_KEYRING_ID, + data: MOCK_SEED_PHRASE, + type: SecretType.Mnemonic, + }, + ); const MOCK_SEED_PHRASE_HASH = keccak256AndHexify(MOCK_SEED_PHRASE); expect(controller.state.socialBackupsMetadata).toStrictEqual([ { @@ -2783,11 +3032,14 @@ describe('SeedlessOnboardingController', () => { }, ]); - controller.updateBackupMetadataState({ - keyringId: MOCK_KEYRING_ID, - data: MOCK_SEED_PHRASE, - type: SecretType.Mnemonic, - }); + baseMessenger.call( + 'SeedlessOnboardingController:updateBackupMetadataState', + { + keyringId: MOCK_KEYRING_ID, + data: MOCK_SEED_PHRASE, + type: SecretType.Mnemonic, + }, + ); expect(controller.state.socialBackupsMetadata).toStrictEqual([ { type: SecretType.Mnemonic, @@ -2809,23 +3061,29 @@ describe('SeedlessOnboardingController', () => { vaultEncryptionSalt: MOCK_VAULT_ENCRYPTION_SALT, }), }, - async ({ controller }) => { - await controller.submitPassword(MOCK_PASSWORD); + async ({ controller, baseMessenger }) => { + await baseMessenger.call( + 'SeedlessOnboardingController:submitPassword', + MOCK_PASSWORD, + ); const MOCK_SEED_PHRASE_2 = stringToBytes('mock-seed-phrase-2'); const MOCK_KEYRING_ID_2 = 'mock-keyring-id-2'; - controller.updateBackupMetadataState([ - { - keyringId: MOCK_KEYRING_ID, - data: MOCK_SEED_PHRASE, - type: SecretType.Mnemonic, - }, - { - keyringId: MOCK_KEYRING_ID_2, - data: MOCK_SEED_PHRASE_2, - type: SecretType.Mnemonic, - }, - ]); + baseMessenger.call( + 'SeedlessOnboardingController:updateBackupMetadataState', + [ + { + keyringId: MOCK_KEYRING_ID, + data: MOCK_SEED_PHRASE, + type: SecretType.Mnemonic, + }, + { + keyringId: MOCK_KEYRING_ID_2, + data: MOCK_SEED_PHRASE_2, + type: SecretType.Mnemonic, + }, + ], + ); const MOCK_SEED_PHRASE_HASH = keccak256AndHexify(MOCK_SEED_PHRASE); const MOCK_SEED_PHRASE_2_HASH = keccak256AndHexify(MOCK_SEED_PHRASE_2); @@ -2859,10 +3117,11 @@ describe('SeedlessOnboardingController', () => { withMockAuthPubKey: true, }), }, - async ({ controller, toprfClient }) => { + async ({ controller, toprfClient, baseMessenger }) => { await mockCreateToprfKeyAndBackupSeedPhrase( toprfClient, controller, + baseMessenger, MOCK_PASSWORD, MOCK_SEED_PHRASE, MOCK_KEYRING_ID, @@ -2892,7 +3151,11 @@ describe('SeedlessOnboardingController', () => { const { encKey: newEncKey, authKeyPair: newAuthKeyPair } = mockChangeEncKey(toprfClient, NEW_MOCK_PASSWORD); - await controller.changePassword(NEW_MOCK_PASSWORD, MOCK_PASSWORD); + await baseMessenger.call( + 'SeedlessOnboardingController:changePassword', + NEW_MOCK_PASSWORD, + MOCK_PASSWORD, + ); // verify the vault after update password const vaultAfterUpdatePassword = controller.state.vault; @@ -2935,10 +3198,11 @@ describe('SeedlessOnboardingController', () => { metadataAccessToken, }, }, - async ({ controller, toprfClient }) => { + async ({ controller, toprfClient, baseMessenger }) => { await mockCreateToprfKeyAndBackupSeedPhrase( toprfClient, controller, + baseMessenger, MOCK_PASSWORD, MOCK_SEED_PHRASE, MOCK_KEYRING_ID, @@ -2969,7 +3233,11 @@ describe('SeedlessOnboardingController', () => { const { encKey: newEncKey, authKeyPair: newAuthKeyPair } = mockChangeEncKey(toprfClient, NEW_MOCK_PASSWORD); - await controller.changePassword(NEW_MOCK_PASSWORD, MOCK_PASSWORD); + await baseMessenger.call( + 'SeedlessOnboardingController:changePassword', + NEW_MOCK_PASSWORD, + MOCK_PASSWORD, + ); // verify the vault after update password const vaultAfterUpdatePassword = controller.state.vault; @@ -2999,9 +3267,13 @@ describe('SeedlessOnboardingController', () => { }); it('should throw an error if the controller is locked', async () => { - await withController(async ({ controller }) => { + await withController(async ({ baseMessenger }) => { await expect( - controller.changePassword(NEW_MOCK_PASSWORD, MOCK_PASSWORD), + baseMessenger.call( + 'SeedlessOnboardingController:changePassword', + NEW_MOCK_PASSWORD, + MOCK_PASSWORD, + ), ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.ControllerLocked, ); @@ -3017,10 +3289,11 @@ describe('SeedlessOnboardingController', () => { withMockAuthenticatedUser: true, }), }, - async ({ controller, toprfClient }) => { + async ({ controller, toprfClient, baseMessenger }) => { await mockCreateToprfKeyAndBackupSeedPhrase( toprfClient, controller, + baseMessenger, MOCK_PASSWORD, MOCK_SEED_PHRASE, MOCK_KEYRING_ID, @@ -3031,7 +3304,11 @@ describe('SeedlessOnboardingController', () => { mockRecoverEncKey(toprfClient, MOCK_PASSWORD); await expect( - controller.changePassword(NEW_MOCK_PASSWORD, MOCK_PASSWORD), + baseMessenger.call( + 'SeedlessOnboardingController:changePassword', + NEW_MOCK_PASSWORD, + MOCK_PASSWORD, + ), ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.FailedToChangePassword, ); @@ -3047,10 +3324,11 @@ describe('SeedlessOnboardingController', () => { withMockAuthPubKey: true, }), }, - async ({ controller, toprfClient }) => { + async ({ controller, toprfClient, baseMessenger }) => { await mockCreateToprfKeyAndBackupSeedPhrase( toprfClient, controller, + baseMessenger, MOCK_PASSWORD, MOCK_SEED_PHRASE, MOCK_KEYRING_ID, @@ -3071,7 +3349,11 @@ describe('SeedlessOnboardingController', () => { ); await expect( - controller.changePassword(NEW_MOCK_PASSWORD, MOCK_PASSWORD), + baseMessenger.call( + 'SeedlessOnboardingController:changePassword', + NEW_MOCK_PASSWORD, + MOCK_PASSWORD, + ), ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.FailedToChangePassword, ); @@ -3087,10 +3369,11 @@ describe('SeedlessOnboardingController', () => { withMockAuthPubKey: true, }), }, - async ({ controller, toprfClient }) => { + async ({ controller, toprfClient, baseMessenger }) => { await mockCreateToprfKeyAndBackupSeedPhrase( toprfClient, controller, + baseMessenger, MOCK_PASSWORD, MOCK_SEED_PHRASE, MOCK_KEYRING_ID, @@ -3112,7 +3395,11 @@ describe('SeedlessOnboardingController', () => { const changeEncKeySpy = jest.spyOn(toprfClient, 'changeEncKey'); // Call changePassword (now without keyIndex parameter) - await controller.changePassword(NEW_MOCK_PASSWORD, MOCK_PASSWORD); + await baseMessenger.call( + 'SeedlessOnboardingController:changePassword', + NEW_MOCK_PASSWORD, + MOCK_PASSWORD, + ); // Verify that recoverEncKey was NOT called since vault data is available and key index is provided expect(recoverEncKeySpy).not.toHaveBeenCalled(); @@ -3136,10 +3423,11 @@ describe('SeedlessOnboardingController', () => { withMockAuthPubKey: true, }), }, - async ({ controller, toprfClient }) => { + async ({ controller, toprfClient, baseMessenger }) => { await mockCreateToprfKeyAndBackupSeedPhrase( toprfClient, controller, + baseMessenger, MOCK_PASSWORD, MOCK_SEED_PHRASE, MOCK_KEYRING_ID, @@ -3163,7 +3451,11 @@ describe('SeedlessOnboardingController', () => { const changeEncKeySpy = jest.spyOn(toprfClient, 'changeEncKey'); // Call changePassword - await controller.changePassword(NEW_MOCK_PASSWORD, MOCK_PASSWORD); + await baseMessenger.call( + 'SeedlessOnboardingController:changePassword', + NEW_MOCK_PASSWORD, + MOCK_PASSWORD, + ); // Verify that recoverEncKey was called due to missing keyIndex expect(recoverEncKeySpy).toHaveBeenCalledWith( @@ -3193,10 +3485,11 @@ describe('SeedlessOnboardingController', () => { withMockAuthPubKey: true, }), }, - async ({ controller, toprfClient }) => { + async ({ controller, toprfClient, baseMessenger }) => { await mockCreateToprfKeyAndBackupSeedPhrase( toprfClient, controller, + baseMessenger, MOCK_PASSWORD, MOCK_SEED_PHRASE, MOCK_KEYRING_ID, @@ -3208,7 +3501,11 @@ describe('SeedlessOnboardingController', () => { .mockRejectedValueOnce(new Error('Network error')); await expect( - controller.changePassword(NEW_MOCK_PASSWORD, MOCK_PASSWORD), + baseMessenger.call( + 'SeedlessOnboardingController:changePassword', + NEW_MOCK_PASSWORD, + MOCK_PASSWORD, + ), ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.FailedToChangePassword, ); @@ -3225,14 +3522,14 @@ describe('SeedlessOnboardingController', () => { withMockAuthenticatedUser: true, }), }, - async ({ controller }) => { + async ({ controller, baseMessenger }) => { const { state } = controller; expect(state.nodeAuthTokens).toBeDefined(); expect(state.userId).toBeDefined(); expect(state.authConnectionId).toBeDefined(); - controller.clearState(); + baseMessenger.call('SeedlessOnboardingController:clearState'); expect(controller.state).toStrictEqual( getInitialSeedlessOnboardingControllerStateWithDefaults(), ); @@ -3251,7 +3548,7 @@ describe('SeedlessOnboardingController', () => { withMockAuthenticatedUser: true, }), }, - async ({ controller, toprfClient }) => { + async ({ toprfClient, baseMessenger }) => { // create the local enc key mockcreateLocalKey(toprfClient, MOCK_PASSWORD); // persist the local enc key @@ -3259,7 +3556,8 @@ describe('SeedlessOnboardingController', () => { // mock the secret data add const mockSecretDataAdd = handleMockSecretDataAdd(); await expect( - controller.createToprfKeyAndBackupSeedPhrase( + baseMessenger.call( + 'SeedlessOnboardingController:createToprfKeyAndBackupSeedPhrase', '', MOCK_SEED_PHRASE, MOCK_KEYRING_ID, @@ -3280,7 +3578,7 @@ describe('SeedlessOnboardingController', () => { withMockAuthenticatedUser: true, }), }, - async ({ controller, toprfClient }) => { + async ({ toprfClient, baseMessenger }) => { // create the local enc key mockcreateLocalKey(toprfClient, MOCK_PASSWORD); // persist the local enc key @@ -3288,7 +3586,8 @@ describe('SeedlessOnboardingController', () => { // mock the secret data add const mockSecretDataAdd = handleMockSecretDataAdd(); await expect( - controller.createToprfKeyAndBackupSeedPhrase( + baseMessenger.call( + 'SeedlessOnboardingController:createToprfKeyAndBackupSeedPhrase', // @ts-expect-error Intentionally passing wrong password type 123, MOCK_SEED_PHRASE, @@ -3318,24 +3617,30 @@ describe('SeedlessOnboardingController', () => { withMockAuthenticatedUser: true, }), }, - async ({ controller, toprfClient }) => { + async ({ controller, toprfClient, baseMessenger }) => { await mockCreateToprfKeyAndBackupSeedPhrase( toprfClient, controller, + baseMessenger, MOCK_PASSWORD, MOCK_SEED_PHRASE, MOCK_KEYRING_ID, ); - await controller.setLocked(); + await baseMessenger.call('SeedlessOnboardingController:setLocked'); // verify that the mutex acquire was called expect(mutexAcquireSpy).toHaveBeenCalled(); await expect( - controller.addNewSecretData(MOCK_SEED_PHRASE, SecretType.Mnemonic, { - keyringId: MOCK_KEYRING_ID, - }), + baseMessenger.call( + 'SeedlessOnboardingController:addNewSecretData', + MOCK_SEED_PHRASE, + SecretType.Mnemonic, + { + keyringId: MOCK_KEYRING_ID, + }, + ), ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.ControllerLocked, ); @@ -3528,17 +3833,19 @@ describe('SeedlessOnboardingController', () => { withMockAuthPubKey: true, }), }, - async ({ controller, toprfClient }) => { + async ({ controller, toprfClient, baseMessenger }) => { // Setup and store keyring encryption key. await mockCreateToprfKeyAndBackupSeedPhrase( toprfClient, controller, + baseMessenger, RECOVERED_PASSWORD, MOCK_SEED_PHRASE, MOCK_KEYRING_ID, ); - await controller.storeKeyringEncryptionKey( + await baseMessenger.call( + 'SeedlessOnboardingController:storeKeyringEncryptionKey', MOCK_KEYRING_ENCRYPTION_KEY, ); @@ -3563,14 +3870,18 @@ describe('SeedlessOnboardingController', () => { pwEncKey: recoveredPwEncKey, }); - await controller.setLocked(); + await baseMessenger.call('SeedlessOnboardingController:setLocked'); - await controller.submitGlobalPassword({ - globalPassword: GLOBAL_PASSWORD, - }); + await baseMessenger.call( + 'SeedlessOnboardingController:submitGlobalPassword', + { + globalPassword: GLOBAL_PASSWORD, + }, + ); - const keyringEncryptionKey = - await controller.loadKeyringEncryptionKey(); + const keyringEncryptionKey = await baseMessenger.call( + 'SeedlessOnboardingController:loadKeyringEncryptionKey', + ); expect(keyringEncryptionKey).toStrictEqual( MOCK_KEYRING_ENCRYPTION_KEY, @@ -3590,9 +3901,12 @@ describe('SeedlessOnboardingController', () => { vault: 'mock-vault', }), }, - async ({ controller, toprfClient }) => { + async ({ controller, toprfClient, baseMessenger }) => { await expect( - controller.storeKeyringEncryptionKey(''), + baseMessenger.call( + 'SeedlessOnboardingController:storeKeyringEncryptionKey', + '', + ), ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.WrongPasswordType, ); @@ -3601,12 +3915,17 @@ describe('SeedlessOnboardingController', () => { await mockCreateToprfKeyAndBackupSeedPhrase( toprfClient, controller, + baseMessenger, RECOVERED_PASSWORD, MOCK_SEED_PHRASE, MOCK_KEYRING_ID, ); - await expect(controller.loadKeyringEncryptionKey()).rejects.toThrow( + await expect( + baseMessenger.call( + 'SeedlessOnboardingController:loadKeyringEncryptionKey', + ), + ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.EncryptedKeyringEncryptionKeyNotSet, ); }, @@ -3621,21 +3940,25 @@ describe('SeedlessOnboardingController', () => { withMockAuthPubKey: true, }), }, - async ({ controller, toprfClient }) => { + async ({ controller, toprfClient, baseMessenger }) => { // Setup and store keyring encryption key. await mockCreateToprfKeyAndBackupSeedPhrase( toprfClient, controller, + baseMessenger, RECOVERED_PASSWORD, MOCK_SEED_PHRASE, MOCK_KEYRING_ID, ); - await controller.storeKeyringEncryptionKey( + await baseMessenger.call( + 'SeedlessOnboardingController:storeKeyringEncryptionKey', MOCK_KEYRING_ENCRYPTION_KEY, ); - const result = await controller.loadKeyringEncryptionKey(); + const result = await baseMessenger.call( + 'SeedlessOnboardingController:loadKeyringEncryptionKey', + ); expect(result).toStrictEqual(MOCK_KEYRING_ENCRYPTION_KEY); }, ); @@ -3649,17 +3972,19 @@ describe('SeedlessOnboardingController', () => { withMockAuthPubKey: true, }), }, - async ({ controller, toprfClient }) => { + async ({ controller, toprfClient, baseMessenger }) => { // Setup and store keyring encryption key. await mockCreateToprfKeyAndBackupSeedPhrase( toprfClient, controller, + baseMessenger, RECOVERED_PASSWORD, MOCK_SEED_PHRASE, MOCK_KEYRING_ID, ); - await controller.storeKeyringEncryptionKey( + await baseMessenger.call( + 'SeedlessOnboardingController:storeKeyringEncryptionKey', MOCK_KEYRING_ENCRYPTION_KEY, ); @@ -3670,9 +3995,15 @@ describe('SeedlessOnboardingController', () => { GLOBAL_PASSWORD, ); - await controller.changePassword(GLOBAL_PASSWORD, RECOVERED_PASSWORD); + await baseMessenger.call( + 'SeedlessOnboardingController:changePassword', + GLOBAL_PASSWORD, + RECOVERED_PASSWORD, + ); - const result = await controller.loadKeyringEncryptionKey(); + const result = await baseMessenger.call( + 'SeedlessOnboardingController:loadKeyringEncryptionKey', + ); expect(result).toStrictEqual(MOCK_KEYRING_ENCRYPTION_KEY); }, @@ -3687,17 +4018,19 @@ describe('SeedlessOnboardingController', () => { withMockAuthPubKey: true, }), }, - async ({ controller, toprfClient }) => { + async ({ controller, toprfClient, baseMessenger }) => { // Setup and store keyring encryption key. await mockCreateToprfKeyAndBackupSeedPhrase( toprfClient, controller, + baseMessenger, RECOVERED_PASSWORD, MOCK_SEED_PHRASE, MOCK_KEYRING_ID, ); - await controller.storeKeyringEncryptionKey( + await baseMessenger.call( + 'SeedlessOnboardingController:storeKeyringEncryptionKey', MOCK_KEYRING_ENCRYPTION_KEY, ); @@ -3708,7 +4041,11 @@ describe('SeedlessOnboardingController', () => { GLOBAL_PASSWORD, ); - await controller.changePassword(GLOBAL_PASSWORD, RECOVERED_PASSWORD); + await baseMessenger.call( + 'SeedlessOnboardingController:changePassword', + GLOBAL_PASSWORD, + RECOVERED_PASSWORD, + ); // Mock recoverEncKey for the global password const mockToprfEncryptor = createMockToprfEncryptor(); @@ -3731,14 +4068,18 @@ describe('SeedlessOnboardingController', () => { pwEncKey: recoveredPwEncKey, }); - await controller.setLocked(); + await baseMessenger.call('SeedlessOnboardingController:setLocked'); - await controller.submitGlobalPassword({ - globalPassword: GLOBAL_PASSWORD, - }); + await baseMessenger.call( + 'SeedlessOnboardingController:submitGlobalPassword', + { + globalPassword: GLOBAL_PASSWORD, + }, + ); - const keyringEncryptionKey = - await controller.loadKeyringEncryptionKey(); + const keyringEncryptionKey = await baseMessenger.call( + 'SeedlessOnboardingController:loadKeyringEncryptionKey', + ); expect(keyringEncryptionKey).toStrictEqual( MOCK_KEYRING_ENCRYPTION_KEY, @@ -3755,7 +4096,7 @@ describe('SeedlessOnboardingController', () => { withMockAuthPubKey: true, }), }, - async ({ controller, toprfClient }) => { + async ({ toprfClient, baseMessenger }) => { // Mock recoverEncKey for the global password const mockToprfEncryptor = createMockToprfEncryptor(); const encKey = mockToprfEncryptor.deriveEncKey(GLOBAL_PASSWORD); @@ -3778,9 +4119,12 @@ describe('SeedlessOnboardingController', () => { }); await expect( - controller.submitGlobalPassword({ - globalPassword: GLOBAL_PASSWORD, - }), + baseMessenger.call( + 'SeedlessOnboardingController:submitGlobalPassword', + { + globalPassword: GLOBAL_PASSWORD, + }, + ), ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.CouldNotRecoverPassword, ); @@ -3793,11 +4137,14 @@ describe('SeedlessOnboardingController', () => { { state: getMockInitialControllerState({}), }, - async ({ controller }) => { + async ({ baseMessenger }) => { await expect( - controller.submitGlobalPassword({ - globalPassword: GLOBAL_PASSWORD, - }), + baseMessenger.call( + 'SeedlessOnboardingController:submitGlobalPassword', + { + globalPassword: GLOBAL_PASSWORD, + }, + ), ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.SRPNotBackedUpError, ); @@ -3813,7 +4160,7 @@ describe('SeedlessOnboardingController', () => { withMockAuthPubKey: true, }), }, - async ({ controller, toprfClient }) => { + async ({ toprfClient, baseMessenger }) => { jest .spyOn(toprfClient, 'recoverEncKey') .mockRejectedValueOnce( @@ -3824,9 +4171,12 @@ describe('SeedlessOnboardingController', () => { ); await expect( - controller.submitGlobalPassword({ - globalPassword: GLOBAL_PASSWORD, - }), + baseMessenger.call( + 'SeedlessOnboardingController:submitGlobalPassword', + { + globalPassword: GLOBAL_PASSWORD, + }, + ), ).rejects.toStrictEqual( new RecoveryError( SeedlessOnboardingControllerErrorMessage.IncorrectPassword, @@ -3844,7 +4194,7 @@ describe('SeedlessOnboardingController', () => { withMockAuthPubKey: true, }), }, - async ({ controller, toprfClient }) => { + async ({ toprfClient, baseMessenger }) => { const mockToprfEncryptor = createMockToprfEncryptor(); const encKey = mockToprfEncryptor.deriveEncKey(GLOBAL_PASSWORD); const pwEncKey = mockToprfEncryptor.derivePwEncKey(GLOBAL_PASSWORD); @@ -3868,9 +4218,12 @@ describe('SeedlessOnboardingController', () => { ); await expect( - controller.submitGlobalPassword({ - globalPassword: GLOBAL_PASSWORD, - }), + baseMessenger.call( + 'SeedlessOnboardingController:submitGlobalPassword', + { + globalPassword: GLOBAL_PASSWORD, + }, + ), ).rejects.toStrictEqual( new PasswordSyncError( SeedlessOnboardingControllerErrorMessage.CouldNotRecoverPassword, @@ -3888,7 +4241,7 @@ describe('SeedlessOnboardingController', () => { withMockAuthPubKey: true, }), }, - async ({ controller, toprfClient }) => { + async ({ toprfClient, baseMessenger }) => { const mockToprfEncryptor = createMockToprfEncryptor(); const encKey = mockToprfEncryptor.deriveEncKey(GLOBAL_PASSWORD); const pwEncKey = mockToprfEncryptor.derivePwEncKey(GLOBAL_PASSWORD); @@ -3907,9 +4260,12 @@ describe('SeedlessOnboardingController', () => { .mockRejectedValueOnce(new Error('Unknown error')); await expect( - controller.submitGlobalPassword({ - globalPassword: GLOBAL_PASSWORD, - }), + baseMessenger.call( + 'SeedlessOnboardingController:submitGlobalPassword', + { + globalPassword: GLOBAL_PASSWORD, + }, + ), ).rejects.toStrictEqual( new PasswordSyncError( SeedlessOnboardingControllerErrorMessage.CouldNotRecoverPassword, @@ -3927,7 +4283,7 @@ describe('SeedlessOnboardingController', () => { withMockAuthPubKey: true, }), }, - async ({ controller, toprfClient }) => { + async ({ toprfClient, baseMessenger }) => { const mockToprfEncryptor = createMockToprfEncryptor(); const encKey = mockToprfEncryptor.deriveEncKey(GLOBAL_PASSWORD); const pwEncKey = mockToprfEncryptor.derivePwEncKey(GLOBAL_PASSWORD); @@ -3954,9 +4310,12 @@ describe('SeedlessOnboardingController', () => { ); await expect( - controller.submitGlobalPassword({ - globalPassword: GLOBAL_PASSWORD, - }), + baseMessenger.call( + 'SeedlessOnboardingController:submitGlobalPassword', + { + globalPassword: GLOBAL_PASSWORD, + }, + ), ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.MaxKeyChainLengthExceeded, ); @@ -4029,12 +4388,15 @@ describe('SeedlessOnboardingController', () => { encryptedSeedlessEncryptionKey: b64EncKey, }), }, - async ({ controller, toprfClient, encryptor }) => { + async ({ controller, toprfClient, encryptor, baseMessenger }) => { // Unlock controller first - requires vaultEncryptionKey/Salt or password // Since we provide key/salt in state, submitPassword isn't strictly needed here // but we keep it to match the method's requirement of being unlocked // We'll use the key/salt implicitly by not providing password to unlockVaultAndGetBackupEncKey - await controller.submitPassword(OLD_PASSWORD); // Unlock using the standard method + await baseMessenger.call( + 'SeedlessOnboardingController:submitPassword', + OLD_PASSWORD, + ); // Unlock using the standard method const recoverEncKeySpy = jest.spyOn(toprfClient, 'recoverEncKey'); const encryptorSpy = jest.spyOn(encryptor, 'encryptWithDetail'); @@ -4050,7 +4412,7 @@ describe('SeedlessOnboardingController', () => { // We still need verifyPassword to work conceptually, even if unlock is bypassed // verifyPasswordSpy.mockResolvedValueOnce(); // Don't mock, let the real one run inside syncLatestGlobalPassword - await controller.setLocked(); + await baseMessenger.call('SeedlessOnboardingController:setLocked'); // Mock recoverEncKey for the global password const encKey = mockToprfEncryptor.deriveEncKey(GLOBAL_PASSWORD); @@ -4072,13 +4434,19 @@ describe('SeedlessOnboardingController', () => { pwEncKey: recoveredPwEncKey, }); - await controller.submitGlobalPassword({ - globalPassword: GLOBAL_PASSWORD, - }); + await baseMessenger.call( + 'SeedlessOnboardingController:submitGlobalPassword', + { + globalPassword: GLOBAL_PASSWORD, + }, + ); - await controller.syncLatestGlobalPassword({ - globalPassword: GLOBAL_PASSWORD, - }); + await baseMessenger.call( + 'SeedlessOnboardingController:syncLatestGlobalPassword', + { + globalPassword: GLOBAL_PASSWORD, + }, + ); // Assertions expect(recoverEncKeySpy).toHaveBeenCalledWith( @@ -4128,12 +4496,21 @@ describe('SeedlessOnboardingController', () => { encryptedSeedlessEncryptionKey: b64EncKey, }), }, - async ({ controller, toprfClient, encryptor, mockRefreshJWTToken }) => { + async ({ + controller, + toprfClient, + encryptor, + mockRefreshJWTToken, + baseMessenger, + }) => { // Unlock controller first - requires vaultEncryptionKey/Salt or password // Since we provide key/salt in state, submitPassword isn't strictly needed here // but we keep it to match the method's requirement of being unlocked // We'll use the key/salt implicitly by not providing password to unlockVaultAndGetBackupEncKey - await controller.submitPassword(OLD_PASSWORD); // Unlock using the standard method + await baseMessenger.call( + 'SeedlessOnboardingController:submitPassword', + OLD_PASSWORD, + ); // Unlock using the standard method const recoverEncKeySpy = jest.spyOn(toprfClient, 'recoverEncKey'); const encryptorSpy = jest.spyOn(encryptor, 'encryptWithDetail'); @@ -4146,7 +4523,7 @@ describe('SeedlessOnboardingController', () => { keyShareIndex: 1, }); // Lock the wallet - await controller.setLocked(); + await baseMessenger.call('SeedlessOnboardingController:setLocked'); // The following mocks are to simulate the token expiry and refresh. // mock token expiry @@ -4185,16 +4562,22 @@ describe('SeedlessOnboardingController', () => { pwEncKey: recoveredPwEncKey, }); - await controller.submitGlobalPassword({ - globalPassword: GLOBAL_PASSWORD, - }); + await baseMessenger.call( + 'SeedlessOnboardingController:submitGlobalPassword', + { + globalPassword: GLOBAL_PASSWORD, + }, + ); // assert that the newer access token is set in the state expect(controller.state.accessToken).toBe(newerAccessToken); - await controller.syncLatestGlobalPassword({ - globalPassword: GLOBAL_PASSWORD, - }); + await baseMessenger.call( + 'SeedlessOnboardingController:syncLatestGlobalPassword', + { + globalPassword: GLOBAL_PASSWORD, + }, + ); // Check if vault was re-encrypted with the new password and keys const expectedSerializedVaultData = JSON.stringify({ @@ -4226,9 +4609,12 @@ describe('SeedlessOnboardingController', () => { vaultEncryptionSalt: MOCK_VAULT_ENCRYPTION_SALT, }), }, - async ({ controller, toprfClient }) => { + async ({ toprfClient, baseMessenger }) => { // Unlock controller first - await controller.submitPassword(OLD_PASSWORD); + await baseMessenger.call( + 'SeedlessOnboardingController:submitPassword', + OLD_PASSWORD, + ); const recoverEncKeySpy = jest .spyOn(toprfClient, 'recoverEncKey') @@ -4239,9 +4625,12 @@ describe('SeedlessOnboardingController', () => { ); await expect( - controller.syncLatestGlobalPassword({ - globalPassword: GLOBAL_PASSWORD, - }), + baseMessenger.call( + 'SeedlessOnboardingController:syncLatestGlobalPassword', + { + globalPassword: GLOBAL_PASSWORD, + }, + ), ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.LoginFailedError, ); @@ -4268,9 +4657,12 @@ describe('SeedlessOnboardingController', () => { { state, }, - async ({ controller, toprfClient, encryptor }) => { + async ({ toprfClient, encryptor, baseMessenger }) => { // Unlock controller first - await controller.submitPassword(OLD_PASSWORD); + await baseMessenger.call( + 'SeedlessOnboardingController:submitPassword', + OLD_PASSWORD, + ); const recoverEncKeySpy = jest.spyOn(toprfClient, 'recoverEncKey'); const encryptorSpy = jest.spyOn(encryptor, 'encryptWithDetail'); @@ -4288,9 +4680,12 @@ describe('SeedlessOnboardingController', () => { encryptorSpy.mockRejectedValue(new Error('Vault creation failed')); await expect( - controller.syncLatestGlobalPassword({ - globalPassword: GLOBAL_PASSWORD, - }), + baseMessenger.call( + 'SeedlessOnboardingController:syncLatestGlobalPassword', + { + globalPassword: GLOBAL_PASSWORD, + }, + ), ).rejects.toThrow('Vault creation failed'); expect(recoverEncKeySpy).toHaveBeenCalledWith( @@ -4324,7 +4719,7 @@ describe('SeedlessOnboardingController', () => { encryptedSeedlessEncryptionKey, }), }, - async ({ controller, toprfClient }) => { + async ({ toprfClient, baseMessenger }) => { // Here we are creating mock keys associated with the new global password // and these values are used as mock return values for the recoverEncKey and recoverPwEncKey calls const recoverEncKeySpy = jest @@ -4344,9 +4739,12 @@ describe('SeedlessOnboardingController', () => { }); await expect( - controller.submitGlobalPassword({ - globalPassword: GLOBAL_PASSWORD, - }), + baseMessenger.call( + 'SeedlessOnboardingController:submitGlobalPassword', + { + globalPassword: GLOBAL_PASSWORD, + }, + ), ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.CouldNotRecoverPassword, ); @@ -4370,9 +4768,9 @@ describe('SeedlessOnboardingController', () => { withMockAuthPubKey: true, }), }, - async ({ controller, toprfClient }) => { + async ({ controller, toprfClient, baseMessenger }) => { // Ensure the controller is locked - await controller.setLocked(); + await baseMessenger.call('SeedlessOnboardingController:setLocked'); // Mock fetchAuthPubKey to return a valid response jest.spyOn(toprfClient, 'fetchAuthPubKey').mockResolvedValue({ @@ -4392,7 +4790,9 @@ describe('SeedlessOnboardingController', () => { .mockReturnValue(true); // This should not trigger token refresh since access token check is skipped when locked - await controller.checkIsPasswordOutdated(); + await baseMessenger.call( + 'SeedlessOnboardingController:checkIsPasswordOutdated', + ); // Verify that refreshAuthTokens was not called expect(controller.checkAccessTokenExpired).not.toHaveBeenCalled(); @@ -4408,14 +4808,18 @@ describe('SeedlessOnboardingController', () => { withMockAuthPubKey: true, }), }, - async ({ controller, toprfClient }) => { + async ({ toprfClient, baseMessenger }) => { // Mock fetchAuthPubKey to throw a non-token-related error jest .spyOn(toprfClient, 'fetchAuthPubKey') .mockRejectedValue(new Error('Network error')); // This should throw the wrapped error without retrying - await expect(controller.checkIsPasswordOutdated()).rejects.toThrow( + await expect( + baseMessenger.call( + 'SeedlessOnboardingController:checkIsPasswordOutdated', + ), + ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.FailedToFetchAuthPubKey, ); @@ -4427,6 +4831,9 @@ describe('SeedlessOnboardingController', () => { describe('checkNodeAuthTokenExpired with token refresh', () => { it('should return true if the node auth token is expired', async () => { + const expiredToken = createMockNodeAuthToken({ + exp: Date.now() / 1000 - 1000, + }); await withController( { state: { @@ -4435,23 +4842,36 @@ describe('SeedlessOnboardingController', () => { }), nodeAuthTokens: [ { - authToken: createMockNodeAuthToken({ - exp: Date.now() / 1000 - 1000, - }), + authToken: expiredToken, nodeIndex: 0, nodePubKey: 'mock-node-pub-key', }, + { + authToken: expiredToken, + nodeIndex: 1, + nodePubKey: 'mock-node-pub-key-2', + }, + { + authToken: expiredToken, + nodeIndex: 2, + nodePubKey: 'mock-node-pub-key-3', + }, ], }, }, - async ({ controller }) => { - const isExpired = controller.checkNodeAuthTokenExpired(); - expect(isExpired).toBe(false); + async ({ baseMessenger }) => { + const isExpired = baseMessenger.call( + 'SeedlessOnboardingController:checkNodeAuthTokenExpired', + ); + expect(isExpired).toBe(true); }, ); }); it('should return false if the node auth token is not expired', async () => { + const validToken = createMockNodeAuthToken({ + exp: Date.now() / 1000 + 1000, + }); await withController( { state: { @@ -4460,17 +4880,27 @@ describe('SeedlessOnboardingController', () => { }), nodeAuthTokens: [ { - authToken: createMockNodeAuthToken({ - exp: Date.now() / 1000 + 1000, - }), + authToken: validToken, nodeIndex: 0, nodePubKey: 'mock-node-pub-key', }, + { + authToken: validToken, + nodeIndex: 1, + nodePubKey: 'mock-node-pub-key-2', + }, + { + authToken: validToken, + nodeIndex: 2, + nodePubKey: 'mock-node-pub-key-3', + }, ], }, }, - async ({ controller }) => { - const isExpired = controller.checkNodeAuthTokenExpired(); + async ({ baseMessenger }) => { + const isExpired = baseMessenger.call( + 'SeedlessOnboardingController:checkNodeAuthTokenExpired', + ); expect(isExpired).toBe(false); }, ); @@ -4493,10 +4923,12 @@ describe('SeedlessOnboardingController', () => { })), }, }, - async ({ controller }) => { + async ({ controller, baseMessenger }) => { jest.spyOn(controller, 'checkNodeAuthTokenExpired').mockRestore(); - const isExpired = controller.checkNodeAuthTokenExpired(); + const isExpired = baseMessenger.call( + 'SeedlessOnboardingController:checkNodeAuthTokenExpired', + ); expect(isExpired).toBe(true); }, ); @@ -4519,10 +4951,12 @@ describe('SeedlessOnboardingController', () => { })), }, }, - async ({ controller }) => { + async ({ controller, baseMessenger }) => { jest.spyOn(controller, 'checkNodeAuthTokenExpired').mockRestore(); - const isExpired = controller.checkNodeAuthTokenExpired(); + const isExpired = baseMessenger.call( + 'SeedlessOnboardingController:checkNodeAuthTokenExpired', + ); expect(isExpired).toBe(false); }, ); @@ -4546,7 +4980,12 @@ describe('SeedlessOnboardingController', () => { })), }, }, - async ({ controller, toprfClient, mockRefreshJWTToken }) => { + async ({ + controller, + toprfClient, + mockRefreshJWTToken, + baseMessenger, + }) => { mockFetchAuthPubKey( toprfClient, base64ToBytes(controller.state.authPubKey as string), @@ -4560,7 +4999,9 @@ describe('SeedlessOnboardingController', () => { isNewUser: false, }); - await controller.checkIsPasswordOutdated(); + await baseMessenger.call( + 'SeedlessOnboardingController:checkIsPasswordOutdated', + ); expect(mockRefreshJWTToken).toHaveBeenCalled(); }, @@ -4577,10 +5018,16 @@ describe('SeedlessOnboardingController', () => { withMockAuthPubKey: true, }), }, - async ({ controller, toprfClient, mockRefreshJWTToken }) => { + async ({ + controller, + toprfClient, + mockRefreshJWTToken, + baseMessenger, + }) => { await mockCreateToprfKeyAndBackupSeedPhrase( toprfClient, controller, + baseMessenger, MOCK_PASSWORD, MOCK_SEED_PHRASE, MOCK_KEYRING_ID, @@ -4627,7 +5074,11 @@ describe('SeedlessOnboardingController', () => { isNewUser: false, }); - await controller.changePassword(NEW_MOCK_PASSWORD, MOCK_PASSWORD); + await baseMessenger.call( + 'SeedlessOnboardingController:changePassword', + NEW_MOCK_PASSWORD, + MOCK_PASSWORD, + ); // Verify that getNewRefreshToken was called expect(mockRefreshJWTToken).toHaveBeenCalledWith({ @@ -4652,10 +5103,16 @@ describe('SeedlessOnboardingController', () => { withMockAuthPubKey: true, }), }, - async ({ controller, toprfClient, mockRefreshJWTToken }) => { + async ({ + controller, + toprfClient, + mockRefreshJWTToken, + baseMessenger, + }) => { await mockCreateToprfKeyAndBackupSeedPhrase( toprfClient, controller, + baseMessenger, MOCK_PASSWORD, MOCK_SEED_PHRASE, MOCK_KEYRING_ID, @@ -4685,7 +5142,11 @@ describe('SeedlessOnboardingController', () => { ); await expect( - controller.changePassword(NEW_MOCK_PASSWORD, MOCK_PASSWORD), + baseMessenger.call( + 'SeedlessOnboardingController:changePassword', + NEW_MOCK_PASSWORD, + MOCK_PASSWORD, + ), ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.FailedToChangePassword, ); @@ -4704,10 +5165,16 @@ describe('SeedlessOnboardingController', () => { withMockAuthPubKey: true, }), }, - async ({ controller, toprfClient, mockRefreshJWTToken }) => { + async ({ + controller, + toprfClient, + mockRefreshJWTToken, + baseMessenger, + }) => { await mockCreateToprfKeyAndBackupSeedPhrase( toprfClient, controller, + baseMessenger, MOCK_PASSWORD, MOCK_SEED_PHRASE, MOCK_KEYRING_ID, @@ -4727,7 +5194,11 @@ describe('SeedlessOnboardingController', () => { .mockRejectedValue(new Error('Some other error')); await expect( - controller.changePassword(NEW_MOCK_PASSWORD, MOCK_PASSWORD), + baseMessenger.call( + 'SeedlessOnboardingController:changePassword', + NEW_MOCK_PASSWORD, + MOCK_PASSWORD, + ), ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.FailedToChangePassword, ); @@ -4789,9 +5260,13 @@ describe('SeedlessOnboardingController', () => { toprfClient, encryptor, mockRefreshJWTToken, + baseMessenger, }) => { // Unlock controller first - await controller.submitPassword(OLD_PASSWORD); + await baseMessenger.call( + 'SeedlessOnboardingController:submitPassword', + OLD_PASSWORD, + ); // Capture before the call — proactive renewRefreshToken inside // refreshAuthTokens will rotate state.refreshToken afterwards. @@ -4830,9 +5305,12 @@ describe('SeedlessOnboardingController', () => { isNewUser: false, }); - await controller.syncLatestGlobalPassword({ - globalPassword: GLOBAL_PASSWORD, - }); + await baseMessenger.call( + 'SeedlessOnboardingController:syncLatestGlobalPassword', + { + globalPassword: GLOBAL_PASSWORD, + }, + ); // Verify that getNewRefreshToken was called expect(mockRefreshJWTToken).toHaveBeenCalledWith({ @@ -4881,9 +5359,12 @@ describe('SeedlessOnboardingController', () => { vaultEncryptionSalt: MOCK_VAULT_ENCRYPTION_SALT, }), }, - async ({ controller, toprfClient, mockRefreshJWTToken }) => { + async ({ toprfClient, mockRefreshJWTToken, baseMessenger }) => { // Unlock controller first - await controller.submitPassword(OLD_PASSWORD); + await baseMessenger.call( + 'SeedlessOnboardingController:submitPassword', + OLD_PASSWORD, + ); // Mock recoverEncKey to fail with token expired error jest @@ -4901,9 +5382,12 @@ describe('SeedlessOnboardingController', () => { ); await expect( - controller.syncLatestGlobalPassword({ - globalPassword: GLOBAL_PASSWORD, - }), + baseMessenger.call( + 'SeedlessOnboardingController:syncLatestGlobalPassword', + { + globalPassword: GLOBAL_PASSWORD, + }, + ), ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.FailedToRefreshJWTTokens, ); @@ -4925,9 +5409,12 @@ describe('SeedlessOnboardingController', () => { vaultEncryptionSalt: MOCK_VAULT_ENCRYPTION_SALT, }), }, - async ({ controller, toprfClient, mockRefreshJWTToken }) => { + async ({ toprfClient, mockRefreshJWTToken, baseMessenger }) => { // Unlock controller first - await controller.submitPassword(OLD_PASSWORD); + await baseMessenger.call( + 'SeedlessOnboardingController:submitPassword', + OLD_PASSWORD, + ); // Mock recoverEncKey to fail with a non-token error jest @@ -4935,9 +5422,12 @@ describe('SeedlessOnboardingController', () => { .mockRejectedValue(new Error('Some other error')); await expect( - controller.syncLatestGlobalPassword({ - globalPassword: GLOBAL_PASSWORD, - }), + baseMessenger.call( + 'SeedlessOnboardingController:syncLatestGlobalPassword', + { + globalPassword: GLOBAL_PASSWORD, + }, + ), ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.LoginFailedError, ); @@ -4984,8 +5474,16 @@ describe('SeedlessOnboardingController', () => { vaultEncryptionSalt, }), }, - async ({ controller, toprfClient, mockRefreshJWTToken }) => { - await controller.submitPassword(MOCK_PASSWORD); + async ({ + controller, + toprfClient, + mockRefreshJWTToken, + baseMessenger, + }) => { + await baseMessenger.call( + 'SeedlessOnboardingController:submitPassword', + MOCK_PASSWORD, + ); jest .spyOn(toprfClient, 'addSecretDataItem') @@ -5009,7 +5507,8 @@ describe('SeedlessOnboardingController', () => { base64ToBytes(controller.state.authPubKey as string), ); - await controller.addNewSecretData( + await baseMessenger.call( + 'SeedlessOnboardingController:addNewSecretData', NEW_KEY_RING.seedPhrase, SecretType.Mnemonic, { @@ -5035,10 +5534,16 @@ describe('SeedlessOnboardingController', () => { withMockAuthenticatedUser: true, }), }, - async ({ controller, toprfClient, mockRefreshJWTToken }) => { + async ({ + controller, + toprfClient, + mockRefreshJWTToken, + baseMessenger, + }) => { await mockCreateToprfKeyAndBackupSeedPhrase( toprfClient, controller, + baseMessenger, MOCK_PASSWORD, MOCK_SEED_PHRASE, MOCK_KEYRING_ID, @@ -5066,10 +5571,16 @@ describe('SeedlessOnboardingController', () => { isNewUser: false, }); - await controller.submitPassword(MOCK_PASSWORD); + await baseMessenger.call( + 'SeedlessOnboardingController:submitPassword', + MOCK_PASSWORD, + ); await expect( - controller.fetchAllSecretData(MOCK_PASSWORD), + baseMessenger.call( + 'SeedlessOnboardingController:fetchAllSecretData', + MOCK_PASSWORD, + ), ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.NoSecretDataFound, ); @@ -5091,7 +5602,7 @@ describe('SeedlessOnboardingController', () => { withMockAuthenticatedUser: true, }), }, - async ({ controller, toprfClient, mockRefreshJWTToken }) => { + async ({ toprfClient, mockRefreshJWTToken, baseMessenger }) => { // Mock createLocalKey mockcreateLocalKey(toprfClient, MOCK_PASSWORD); @@ -5120,7 +5631,8 @@ describe('SeedlessOnboardingController', () => { isNewUser: false, }); - await controller.createToprfKeyAndBackupSeedPhrase( + await baseMessenger.call( + 'SeedlessOnboardingController:createToprfKeyAndBackupSeedPhrase', MOCK_PASSWORD, MOCK_SEED_PHRASE, MOCK_KEYRING_ID, @@ -5141,7 +5653,7 @@ describe('SeedlessOnboardingController', () => { withMockAuthenticatedUser: true, }), }, - async ({ controller, toprfClient, mockRefreshJWTToken }) => { + async ({ toprfClient, mockRefreshJWTToken, baseMessenger }) => { // Mock createLocalKey mockcreateLocalKey(toprfClient, MOCK_PASSWORD); @@ -5170,7 +5682,8 @@ describe('SeedlessOnboardingController', () => { isNewUser: false, }); - await controller.createToprfKeyAndBackupSeedPhrase( + await baseMessenger.call( + 'SeedlessOnboardingController:createToprfKeyAndBackupSeedPhrase', MOCK_PASSWORD, MOCK_SEED_PHRASE, MOCK_KEYRING_ID, @@ -5238,8 +5751,11 @@ describe('SeedlessOnboardingController', () => { ), }), }, - async ({ controller, toprfClient, mockRefreshJWTToken }) => { - await controller.submitPassword(MOCK_PASSWORD); + async ({ toprfClient, mockRefreshJWTToken, baseMessenger }) => { + await baseMessenger.call( + 'SeedlessOnboardingController:submitPassword', + MOCK_PASSWORD, + ); // Mock recoverEncKey mockRecoverEncKey(toprfClient, MOCK_PASSWORD); @@ -5266,9 +5782,12 @@ describe('SeedlessOnboardingController', () => { isNewUser: false, }); - await controller.submitGlobalPassword({ - globalPassword: MOCK_PASSWORD, - }); + await baseMessenger.call( + 'SeedlessOnboardingController:submitGlobalPassword', + { + globalPassword: MOCK_PASSWORD, + }, + ); expect(mockRefreshJWTToken).toHaveBeenCalled(); expect(toprfClient.recoverPwEncKey).toHaveBeenCalledTimes(2); @@ -5313,8 +5832,16 @@ describe('SeedlessOnboardingController', () => { vaultEncryptionSalt, }), }, - async ({ controller, toprfClient, mockRefreshJWTToken }) => { - await controller.submitPassword(MOCK_PASSWORD); + async ({ + controller, + toprfClient, + mockRefreshJWTToken, + baseMessenger, + }) => { + await baseMessenger.call( + 'SeedlessOnboardingController:submitPassword', + MOCK_PASSWORD, + ); // Capture before the call — proactive renewRefreshToken inside // refreshAuthTokens will rotate state.refreshToken afterwards. @@ -5326,7 +5853,9 @@ describe('SeedlessOnboardingController', () => { isNewUser: false, }); - await controller.refreshAuthTokens(); + await baseMessenger.call( + 'SeedlessOnboardingController:refreshAuthTokens', + ); expect(mockRefreshJWTToken).toHaveBeenCalledWith({ connection: controller.state.authConnection, @@ -5344,8 +5873,12 @@ describe('SeedlessOnboardingController', () => { }); it('should throw error if controller not authenticated', async () => { - await withController(async ({ controller }) => { - await expect(controller.refreshAuthTokens()).rejects.toThrow( + await withController(async ({ baseMessenger }) => { + await expect( + baseMessenger.call( + 'SeedlessOnboardingController:refreshAuthTokens', + ), + ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.MissingAuthUserInfo, ); }); @@ -5358,14 +5891,18 @@ describe('SeedlessOnboardingController', () => { withMockAuthenticatedUser: true, }), }, - async ({ controller, mockRefreshJWTToken }) => { + async ({ controller, mockRefreshJWTToken, baseMessenger }) => { // Mock token refresh to fail mockRefreshJWTToken.mockRejectedValueOnce( new Error('Refresh failed'), ); // Call refreshAuthTokens and expect it to throw - await expect(controller.refreshAuthTokens()).rejects.toThrow( + await expect( + baseMessenger.call( + 'SeedlessOnboardingController:refreshAuthTokens', + ), + ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.FailedToRefreshJWTTokens, ); @@ -5385,7 +5922,7 @@ describe('SeedlessOnboardingController', () => { withMockAuthenticatedUser: true, }), }, - async ({ controller, mockRefreshJWTToken }) => { + async ({ mockRefreshJWTToken, baseMessenger }) => { // Simulate a RefreshTokenHttpError with statusCode 401 (token revoked). const httpError = Object.assign(new Error('Unauthorized'), { name: 'RefreshTokenHttpError', @@ -5393,7 +5930,11 @@ describe('SeedlessOnboardingController', () => { }); mockRefreshJWTToken.mockRejectedValueOnce(httpError); - await expect(controller.refreshAuthTokens()).rejects.toThrow( + await expect( + baseMessenger.call( + 'SeedlessOnboardingController:refreshAuthTokens', + ), + ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.InvalidRefreshToken, ); }, @@ -5407,7 +5948,7 @@ describe('SeedlessOnboardingController', () => { withMockAuthenticatedUser: true, }), }, - async ({ controller, mockRefreshJWTToken }) => { + async ({ mockRefreshJWTToken, baseMessenger }) => { // Simulate a RefreshTokenHttpError with a transient status code (503). const httpError = Object.assign(new Error('Service Unavailable'), { name: 'RefreshTokenHttpError', @@ -5415,7 +5956,11 @@ describe('SeedlessOnboardingController', () => { }); mockRefreshJWTToken.mockRejectedValueOnce(httpError); - await expect(controller.refreshAuthTokens()).rejects.toThrow( + await expect( + baseMessenger.call( + 'SeedlessOnboardingController:refreshAuthTokens', + ), + ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.FailedToRefreshJWTTokens, ); }, @@ -5429,7 +5974,7 @@ describe('SeedlessOnboardingController', () => { withMockAuthenticatedUser: true, }), }, - async ({ controller, mockRefreshJWTToken, toprfClient }) => { + async ({ mockRefreshJWTToken, toprfClient, baseMessenger }) => { // Mock token refresh to succeed mockRefreshJWTToken.mockResolvedValueOnce({ idTokens: ['new-token'], @@ -5441,7 +5986,11 @@ describe('SeedlessOnboardingController', () => { .mockRejectedValueOnce(new Error('Authentication failed')); // Call refreshAuthTokens and expect it to throw - await expect(controller.refreshAuthTokens()).rejects.toThrow( + await expect( + baseMessenger.call( + 'SeedlessOnboardingController:refreshAuthTokens', + ), + ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.AuthenticationError, ); @@ -5469,9 +6018,13 @@ describe('SeedlessOnboardingController', () => { toprfClient, mockRefreshJWTToken, encryptor, + baseMessenger, }) => { const encryptWithKeySpy = jest.spyOn(encryptor, 'encryptWithKey'); - await controller.submitPassword(MOCK_PASSWORD); + await baseMessenger.call( + 'SeedlessOnboardingController:submitPassword', + MOCK_PASSWORD, + ); // Mock refreshJWTToken to return new tokens mockRefreshJWTToken.mockResolvedValueOnce({ @@ -5486,7 +6039,9 @@ describe('SeedlessOnboardingController', () => { isNewUser: false, }); - await controller.refreshAuthTokens(); + await baseMessenger.call( + 'SeedlessOnboardingController:refreshAuthTokens', + ); // Verify the state is updated with new tokens expect(controller.state.accessToken).toBe(newAccessToken); @@ -5508,7 +6063,12 @@ describe('SeedlessOnboardingController', () => { withMockAuthenticatedUser: true, }), }, - async ({ controller, toprfClient, mockRefreshJWTToken }) => { + async ({ + controller, + toprfClient, + mockRefreshJWTToken, + baseMessenger, + }) => { // Vault is not unlocked (no submitPassword called) // Mock refreshJWTToken to return new tokens @@ -5524,7 +6084,9 @@ describe('SeedlessOnboardingController', () => { isNewUser: false, }); - await controller.refreshAuthTokens(); + await baseMessenger.call( + 'SeedlessOnboardingController:refreshAuthTokens', + ); // The accessToken should be stored in state even when vault is locked expect(controller.state.accessToken).toBe(newAccessToken); @@ -5545,9 +6107,17 @@ describe('SeedlessOnboardingController', () => { vaultEncryptionSalt, }), }, - async ({ controller, toprfClient, mockRefreshJWTToken }) => { + async ({ + controller, + toprfClient, + mockRefreshJWTToken, + baseMessenger, + }) => { // Unlock the vault first - await controller.submitPassword(MOCK_PASSWORD); + await baseMessenger.call( + 'SeedlessOnboardingController:submitPassword', + MOCK_PASSWORD, + ); // Mock refreshJWTToken to return new tokens mockRefreshJWTToken.mockResolvedValueOnce({ @@ -5570,7 +6140,11 @@ describe('SeedlessOnboardingController', () => { }); // Should throw AuthenticationError (which wraps MissingCredentials) - await expect(controller.refreshAuthTokens()).rejects.toThrow( + await expect( + baseMessenger.call( + 'SeedlessOnboardingController:refreshAuthTokens', + ), + ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.AuthenticationError, ); }, @@ -5584,7 +6158,7 @@ describe('SeedlessOnboardingController', () => { withMockAuthenticatedUser: true, }), }, - async ({ controller, mockRefreshJWTToken, toprfClient }) => { + async ({ mockRefreshJWTToken, toprfClient, baseMessenger }) => { // Gate the first HTTP call behind a manually-resolved promise so we // can start a second call while the first is still in-flight. let resolveRefresh!: () => void; @@ -5607,8 +6181,12 @@ describe('SeedlessOnboardingController', () => { }); // Start two concurrent calls before the first HTTP request completes. - const call1 = controller.refreshAuthTokens(); - const call2 = controller.refreshAuthTokens(); + const call1 = baseMessenger.call( + 'SeedlessOnboardingController:refreshAuthTokens', + ); + const call2 = baseMessenger.call( + 'SeedlessOnboardingController:refreshAuthTokens', + ); // Unblock the HTTP call. resolveRefresh(); @@ -5627,13 +6205,17 @@ describe('SeedlessOnboardingController', () => { withMockAuthenticatedUser: true, }), }, - async ({ controller, mockRefreshJWTToken, toprfClient }) => { + async ({ mockRefreshJWTToken, toprfClient, baseMessenger }) => { // First call fails — #pendingRefreshPromise is set then cleared by .finally(). mockRefreshJWTToken.mockRejectedValueOnce( new Error('Network error'), ); - await expect(controller.refreshAuthTokens()).rejects.toThrow( + await expect( + baseMessenger.call( + 'SeedlessOnboardingController:refreshAuthTokens', + ), + ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.FailedToRefreshJWTTokens, ); @@ -5645,7 +6227,11 @@ describe('SeedlessOnboardingController', () => { isNewUser: false, }); - expect(await controller.refreshAuthTokens()).toBeUndefined(); + expect( + await baseMessenger.call( + 'SeedlessOnboardingController:refreshAuthTokens', + ), + ).toBeUndefined(); expect(mockRefreshJWTToken).toHaveBeenCalledTimes(2); }, @@ -5659,7 +6245,7 @@ describe('SeedlessOnboardingController', () => { withMockAuthenticatedUser: true, }), }, - async ({ controller, mockRefreshJWTToken }) => { + async ({ mockRefreshJWTToken, baseMessenger }) => { let rejectRefresh!: (error: Error) => void; const refreshBarrier = new Promise((_resolve, reject) => { rejectRefresh = reject; @@ -5668,8 +6254,12 @@ describe('SeedlessOnboardingController', () => { mockRefreshJWTToken.mockReturnValue(refreshBarrier); // Both callers share the same in-flight promise. - const call1 = controller.refreshAuthTokens(); - const call2 = controller.refreshAuthTokens(); + const call1 = baseMessenger.call( + 'SeedlessOnboardingController:refreshAuthTokens', + ); + const call2 = baseMessenger.call( + 'SeedlessOnboardingController:refreshAuthTokens', + ); // Reject the shared HTTP request. rejectRefresh(new Error('Network failure')); @@ -5695,7 +6285,7 @@ describe('SeedlessOnboardingController', () => { withMockAuthenticatedUser: true, }), }, - async ({ controller, toprfClient, mockRefreshJWTToken }) => { + async ({ toprfClient, mockRefreshJWTToken, baseMessenger }) => { const newIdTokens = ['newIdToken']; mockRefreshJWTToken.mockResolvedValueOnce({ idTokens: newIdTokens, @@ -5710,7 +6300,9 @@ describe('SeedlessOnboardingController', () => { isNewUser: false, }); - await controller.refreshAuthTokens(); + await baseMessenger.call( + 'SeedlessOnboardingController:refreshAuthTokens', + ); // #reAuthenticate passes only idTokens to the TOPRF client — // refreshToken and revokeToken are structurally absent. @@ -5732,12 +6324,15 @@ describe('SeedlessOnboardingController', () => { }), }, async ({ - controller, toprfClient, mockRenewRefreshToken, mockRefreshJWTToken, + baseMessenger, }) => { - await controller.submitPassword(MOCK_PASSWORD); + await baseMessenger.call( + 'SeedlessOnboardingController:submitPassword', + MOCK_PASSWORD, + ); mockRefreshJWTToken.mockResolvedValueOnce({ idTokens: ['newIdToken'], @@ -5750,7 +6345,9 @@ describe('SeedlessOnboardingController', () => { isNewUser: false, }); - await controller.refreshAuthTokens(); + await baseMessenger.call( + 'SeedlessOnboardingController:refreshAuthTokens', + ); expect(mockRenewRefreshToken).toHaveBeenCalled(); }, @@ -5764,13 +6361,15 @@ describe('SeedlessOnboardingController', () => { withMockAuthenticatedUser: true, }), }, - async ({ controller, toprfClient, mockRenewRefreshToken }) => { + async ({ toprfClient, mockRenewRefreshToken, baseMessenger }) => { jest.spyOn(toprfClient, 'authenticate').mockResolvedValue({ nodeAuthTokens: MOCK_NODE_AUTH_TOKENS, isNewUser: false, }); - await controller.refreshAuthTokens(); + await baseMessenger.call( + 'SeedlessOnboardingController:refreshAuthTokens', + ); expect(mockRenewRefreshToken).not.toHaveBeenCalled(); }, @@ -5788,12 +6387,15 @@ describe('SeedlessOnboardingController', () => { }), }, async ({ - controller, toprfClient, mockRenewRefreshToken, mockRefreshJWTToken, + baseMessenger, }) => { - await controller.submitPassword(MOCK_PASSWORD); + await baseMessenger.call( + 'SeedlessOnboardingController:submitPassword', + MOCK_PASSWORD, + ); mockRefreshJWTToken.mockResolvedValueOnce({ idTokens: ['newIdToken'], @@ -5812,7 +6414,11 @@ describe('SeedlessOnboardingController', () => { ); // refreshAuthTokens must still resolve even when renewal fails. - expect(await controller.refreshAuthTokens()).toBeUndefined(); + expect( + await baseMessenger.call( + 'SeedlessOnboardingController:refreshAuthTokens', + ), + ).toBeUndefined(); }, ); }); @@ -5827,8 +6433,16 @@ describe('SeedlessOnboardingController', () => { vaultEncryptionSalt, }), }, - async ({ controller, toprfClient, mockRefreshJWTToken }) => { - await controller.submitPassword(MOCK_PASSWORD); + async ({ + controller, + toprfClient, + mockRefreshJWTToken, + baseMessenger, + }) => { + await baseMessenger.call( + 'SeedlessOnboardingController:submitPassword', + MOCK_PASSWORD, + ); const originalRefreshToken = controller.state.refreshToken; const originalRevokeToken = controller.state.revokeToken; @@ -5844,7 +6458,9 @@ describe('SeedlessOnboardingController', () => { isNewUser: false, }); - await controller.refreshAuthTokens(); + await baseMessenger.call( + 'SeedlessOnboardingController:refreshAuthTokens', + ); // After successful rotation the old token pair should be queued // for background revocation. @@ -5870,8 +6486,16 @@ describe('SeedlessOnboardingController', () => { vaultEncryptionSalt, }), }, - async ({ controller, toprfClient, mockRefreshJWTToken }) => { - await controller.submitPassword(MOCK_PASSWORD); + async ({ + controller, + toprfClient, + mockRefreshJWTToken, + baseMessenger, + }) => { + await baseMessenger.call( + 'SeedlessOnboardingController:submitPassword', + MOCK_PASSWORD, + ); const originalRefreshToken = controller.state.refreshToken; @@ -5886,7 +6510,9 @@ describe('SeedlessOnboardingController', () => { isNewUser: false, }); - await controller.refreshAuthTokens(); + await baseMessenger.call( + 'SeedlessOnboardingController:refreshAuthTokens', + ); // rotateRefreshToken writes the new refresh token to state. expect(controller.state.refreshToken).not.toBe( @@ -5904,7 +6530,7 @@ describe('SeedlessOnboardingController', () => { const futureExp = Math.floor(Date.now() / 1000) + 3600; // 1 hour from now const validToken = createMockJWTToken({ exp: futureExp }); - const { messenger } = mockSeedlessOnboardingMessenger(); + const { messenger, baseMessenger } = mockSeedlessOnboardingMessenger(); const controller = new SeedlessOnboardingController({ messenger, encryptor: createMockVaultEncryptor(), @@ -5916,8 +6542,11 @@ describe('SeedlessOnboardingController', () => { }), renewRefreshToken: jest.fn(), }); + expect(controller).toBeDefined(); - const result = await controller.fetchMetadataAccessCreds(); + const result = await baseMessenger.call( + 'SeedlessOnboardingController:fetchMetadataAccessCreds', + ); expect(result).toStrictEqual({ metadataAccessToken: validToken, @@ -5925,7 +6554,7 @@ describe('SeedlessOnboardingController', () => { }); it('should throw error if metadataAccessToken is missing', async () => { - const { messenger } = mockSeedlessOnboardingMessenger(); + const { messenger, baseMessenger } = mockSeedlessOnboardingMessenger(); const state = getMockInitialControllerState({ withMockAuthenticatedUser: true, }); @@ -5938,8 +6567,13 @@ describe('SeedlessOnboardingController', () => { state, renewRefreshToken: jest.fn(), }); + expect(controller).toBeDefined(); - await expect(controller.fetchMetadataAccessCreds()).rejects.toThrow( + await expect( + baseMessenger.call( + 'SeedlessOnboardingController:fetchMetadataAccessCreds', + ), + ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.InvalidMetadataAccessToken, ); }); @@ -5947,7 +6581,7 @@ describe('SeedlessOnboardingController', () => { it('should call refreshAuthTokens if metadataAccessToken is expired', async () => { const pastExp = Math.floor(Date.now() / 1000) - 3600; // 1 hour ago const expiredToken = createMockJWTToken({ exp: pastExp }); - const { messenger } = mockSeedlessOnboardingMessenger(); + const { messenger, baseMessenger } = mockSeedlessOnboardingMessenger(); const controller = new SeedlessOnboardingController({ messenger, encryptor: createMockVaultEncryptor(), @@ -5963,7 +6597,9 @@ describe('SeedlessOnboardingController', () => { // mock refreshAuthTokens to return a new token jest.spyOn(controller, 'refreshAuthTokens').mockResolvedValue(); - await controller.fetchMetadataAccessCreds(); + await baseMessenger.call( + 'SeedlessOnboardingController:fetchMetadataAccessCreds', + ); expect(controller.refreshAuthTokens).toHaveBeenCalled(); }); @@ -5975,7 +6611,7 @@ describe('SeedlessOnboardingController', () => { iat: now - 9500, exp: now + 500, }); - const { messenger } = mockSeedlessOnboardingMessenger(); + const { messenger, baseMessenger } = mockSeedlessOnboardingMessenger(); const controller = new SeedlessOnboardingController({ messenger, encryptor: createMockVaultEncryptor(), @@ -5990,7 +6626,9 @@ describe('SeedlessOnboardingController', () => { jest.spyOn(controller, 'refreshAuthTokens').mockResolvedValue(); - await controller.fetchMetadataAccessCreds(); + await baseMessenger.call( + 'SeedlessOnboardingController:fetchMetadataAccessCreds', + ); expect(controller.refreshAuthTokens).toHaveBeenCalled(); }); @@ -6002,7 +6640,7 @@ describe('SeedlessOnboardingController', () => { iat: now - 3000, exp: now + 7000, }); - const { messenger } = mockSeedlessOnboardingMessenger(); + const { messenger, baseMessenger } = mockSeedlessOnboardingMessenger(); const controller = new SeedlessOnboardingController({ messenger, encryptor: createMockVaultEncryptor(), @@ -6017,7 +6655,9 @@ describe('SeedlessOnboardingController', () => { jest.spyOn(controller, 'refreshAuthTokens').mockResolvedValue(); - const result = await controller.fetchMetadataAccessCreds(); + const result = await baseMessenger.call( + 'SeedlessOnboardingController:fetchMetadataAccessCreds', + ); expect(controller.refreshAuthTokens).not.toHaveBeenCalled(); expect(result).toStrictEqual({ @@ -6041,13 +6681,15 @@ describe('SeedlessOnboardingController', () => { metadataAccessToken: validToken, }), }, - async ({ controller }) => { + async ({ controller, baseMessenger }) => { // Restore the original implementation to test the real logic jest .spyOn(controller, 'checkMetadataAccessTokenExpired') .mockRestore(); - const result = controller.checkMetadataAccessTokenExpired(); + const result = baseMessenger.call( + 'SeedlessOnboardingController:checkMetadataAccessTokenExpired', + ); expect(result).toBe(false); }, ); @@ -6067,13 +6709,15 @@ describe('SeedlessOnboardingController', () => { metadataAccessToken: expiredToken, }), }, - async ({ controller }) => { + async ({ controller, baseMessenger }) => { // Restore the original implementation to test the real logic jest .spyOn(controller, 'checkMetadataAccessTokenExpired') .mockRestore(); - const result = controller.checkMetadataAccessTokenExpired(); + const result = baseMessenger.call( + 'SeedlessOnboardingController:checkMetadataAccessTokenExpired', + ); expect(result).toBe(true); }, ); @@ -6094,12 +6738,14 @@ describe('SeedlessOnboardingController', () => { metadataAccessToken: tokenAt90Percent, }), }, - async ({ controller }) => { + async ({ controller, baseMessenger }) => { jest .spyOn(controller, 'checkMetadataAccessTokenExpired') .mockRestore(); - const result = controller.checkMetadataAccessTokenExpired(); + const result = baseMessenger.call( + 'SeedlessOnboardingController:checkMetadataAccessTokenExpired', + ); expect(result).toBe(true); }, ); @@ -6120,12 +6766,14 @@ describe('SeedlessOnboardingController', () => { metadataAccessToken: tokenBelow90Percent, }), }, - async ({ controller }) => { + async ({ controller, baseMessenger }) => { jest .spyOn(controller, 'checkMetadataAccessTokenExpired') .mockRestore(); - const result = controller.checkMetadataAccessTokenExpired(); + const result = baseMessenger.call( + 'SeedlessOnboardingController:checkMetadataAccessTokenExpired', + ); expect(result).toBe(false); }, ); @@ -6147,23 +6795,27 @@ describe('SeedlessOnboardingController', () => { metadataAccessToken: malformedToken, }), }, - async ({ controller }) => { + async ({ controller, baseMessenger }) => { jest .spyOn(controller, 'checkMetadataAccessTokenExpired') .mockRestore(); - const result = controller.checkMetadataAccessTokenExpired(); + const result = baseMessenger.call( + 'SeedlessOnboardingController:checkMetadataAccessTokenExpired', + ); expect(result).toBe(true); }, ); }); it('should return true if user is not authenticated', async () => { - await withController(async ({ controller }) => { + await withController(async ({ controller, baseMessenger }) => { // Restore the original implementation to test the real logic jest.spyOn(controller, 'checkMetadataAccessTokenExpired').mockRestore(); - const result = controller.checkMetadataAccessTokenExpired(); + const result = baseMessenger.call( + 'SeedlessOnboardingController:checkMetadataAccessTokenExpired', + ); expect(result).toBe(true); }); }); @@ -6176,13 +6828,15 @@ describe('SeedlessOnboardingController', () => { metadataAccessToken: 'invalid.token.format', }), }, - async ({ controller }) => { + async ({ controller, baseMessenger }) => { // Restore the original implementation to test the real logic jest .spyOn(controller, 'checkMetadataAccessTokenExpired') .mockRestore(); - const result = controller.checkMetadataAccessTokenExpired(); + const result = baseMessenger.call( + 'SeedlessOnboardingController:checkMetadataAccessTokenExpired', + ); expect(result).toBe(true); }, ); @@ -6204,11 +6858,13 @@ describe('SeedlessOnboardingController', () => { accessToken: validToken, }), }, - async ({ controller }) => { + async ({ controller, baseMessenger }) => { // Restore the original implementation to test the real logic jest.spyOn(controller, 'checkAccessTokenExpired').mockRestore(); - const result = controller.checkAccessTokenExpired(); + const result = baseMessenger.call( + 'SeedlessOnboardingController:checkAccessTokenExpired', + ); expect(result).toBe(false); }, ); @@ -6228,11 +6884,13 @@ describe('SeedlessOnboardingController', () => { accessToken: expiredToken, }), }, - async ({ controller }) => { + async ({ controller, baseMessenger }) => { // Restore the original implementation to test the real logic jest.spyOn(controller, 'checkAccessTokenExpired').mockRestore(); - const result = controller.checkAccessTokenExpired(); + const result = baseMessenger.call( + 'SeedlessOnboardingController:checkAccessTokenExpired', + ); expect(result).toBe(true); }, ); @@ -6253,10 +6911,12 @@ describe('SeedlessOnboardingController', () => { accessToken: tokenAt90Percent, }), }, - async ({ controller }) => { + async ({ controller, baseMessenger }) => { jest.spyOn(controller, 'checkAccessTokenExpired').mockRestore(); - const result = controller.checkAccessTokenExpired(); + const result = baseMessenger.call( + 'SeedlessOnboardingController:checkAccessTokenExpired', + ); expect(result).toBe(true); }, ); @@ -6277,21 +6937,25 @@ describe('SeedlessOnboardingController', () => { accessToken: tokenBelow90Percent, }), }, - async ({ controller }) => { + async ({ controller, baseMessenger }) => { jest.spyOn(controller, 'checkAccessTokenExpired').mockRestore(); - const result = controller.checkAccessTokenExpired(); + const result = baseMessenger.call( + 'SeedlessOnboardingController:checkAccessTokenExpired', + ); expect(result).toBe(false); }, ); }); it('should return true if user is not authenticated', async () => { - await withController(async ({ controller }) => { + await withController(async ({ controller, baseMessenger }) => { // Restore the original implementation to test the real logic jest.spyOn(controller, 'checkAccessTokenExpired').mockRestore(); - const result = controller.checkAccessTokenExpired(); + const result = baseMessenger.call( + 'SeedlessOnboardingController:checkAccessTokenExpired', + ); expect(result).toBe(true); }); }); @@ -6304,11 +6968,13 @@ describe('SeedlessOnboardingController', () => { withoutMockAccessToken: true, }), }, - async ({ controller }) => { + async ({ controller, baseMessenger }) => { // Restore the original implementation to test the real logic jest.spyOn(controller, 'checkAccessTokenExpired').mockRestore(); - const result = controller.checkAccessTokenExpired(); + const result = baseMessenger.call( + 'SeedlessOnboardingController:checkAccessTokenExpired', + ); expect(result).toBe(true); }, ); @@ -6322,18 +6988,20 @@ describe('SeedlessOnboardingController', () => { metadataAccessToken: 'invalid.token.format', }), }, - async ({ controller }) => { + async ({ controller, baseMessenger }) => { // Restore the original implementation to test the real logic jest.spyOn(controller, 'checkAccessTokenExpired').mockRestore(); - const result = controller.checkAccessTokenExpired(); + const result = baseMessenger.call( + 'SeedlessOnboardingController:checkAccessTokenExpired', + ); expect(result).toBe(true); }, ); }); }); - describe('getAccessToken', () => { + describe('SeedlessOnboardingController:getAccessToken', () => { const MOCK_ACCESS_TOKEN = 'mock-access-token'; const MOCK_PASSWORD = 'mock-password'; let MOCK_VAULT: string; @@ -6374,10 +7042,15 @@ describe('SeedlessOnboardingController', () => { vaultEncryptionSalt: MOCK_VAULT_ENCRYPTION_SALT, }), }, - async ({ controller }) => { - await controller.submitPassword(MOCK_PASSWORD); + async ({ baseMessenger }) => { + await baseMessenger.call( + 'SeedlessOnboardingController:submitPassword', + MOCK_PASSWORD, + ); - const result = await controller.getAccessToken(); + const result = await baseMessenger.call( + 'SeedlessOnboardingController:getAccessToken', + ); expect(result).toBe(MOCK_ACCESS_TOKEN); }, ); @@ -6396,8 +7069,10 @@ describe('SeedlessOnboardingController', () => { refreshToken, }, }, - async ({ controller }) => { - const result = await controller.getAccessToken(); + async ({ baseMessenger }) => { + const result = await baseMessenger.call( + 'SeedlessOnboardingController:getAccessToken', + ); expect(result).toBeUndefined(); }, ); @@ -6408,8 +7083,10 @@ describe('SeedlessOnboardingController', () => { { state: {}, }, - async ({ controller }) => { - await expect(controller.getAccessToken()).rejects.toThrow( + async ({ baseMessenger }) => { + await expect( + baseMessenger.call('SeedlessOnboardingController:getAccessToken'), + ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.MissingAuthUserInfo, ); }, @@ -6427,7 +7104,7 @@ describe('SeedlessOnboardingController', () => { vaultEncryptionSalt: MOCK_VAULT_ENCRYPTION_SALT, }), }, - async ({ controller }) => { + async ({ controller, baseMessenger }) => { jest .spyOn(controller, 'checkNodeAuthTokenExpired') .mockReturnValueOnce(true); @@ -6439,7 +7116,9 @@ describe('SeedlessOnboardingController', () => { ), ); - await expect(controller.getAccessToken()).rejects.toThrow( + await expect( + baseMessenger.call('SeedlessOnboardingController:getAccessToken'), + ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.AuthenticationError, ); }, @@ -6465,7 +7144,12 @@ describe('SeedlessOnboardingController', () => { vaultEncryptionSalt: MOCK_VAULT_ENCRYPTION_SALT, }), }, - async ({ controller, mockRefreshJWTToken, toprfClient }) => { + async ({ + controller, + baseMessenger, + mockRefreshJWTToken, + toprfClient, + }) => { jest .spyOn(controller, 'checkAccessTokenExpired') .mockReturnValueOnce(true); @@ -6484,9 +7168,14 @@ describe('SeedlessOnboardingController', () => { isNewUser: false, }); - await controller.submitPassword(MOCK_PASSWORD); + await baseMessenger.call( + 'SeedlessOnboardingController:submitPassword', + MOCK_PASSWORD, + ); - const result = await controller.getAccessToken(); + const result = await baseMessenger.call( + 'SeedlessOnboardingController:getAccessToken', + ); expect(result).toBe(NEW_ACCESS_TOKEN); expect(mockRefreshJWTToken).toHaveBeenCalled(); expect(authenticateSpy).toHaveBeenCalled(); @@ -6529,7 +7218,7 @@ describe('SeedlessOnboardingController', () => { vaultEncryptionSalt: MOCK_VAULT_ENCRYPTION_SALT, }), }, - async ({ controller, toprfClient }) => { + async ({ toprfClient, baseMessenger }) => { // fetch and decrypt the secret data mockRecoverEncKey(toprfClient, MOCK_PASSWORD); @@ -6555,7 +7244,10 @@ describe('SeedlessOnboardingController', () => { ), ]); - const secretData = await controller.fetchAllSecretData(MOCK_PASSWORD); + const secretData = await baseMessenger.call( + 'SeedlessOnboardingController:fetchAllSecretData', + MOCK_PASSWORD, + ); expect(secretData).toBeDefined(); expect(secretData).toHaveLength(2); expect(secretData[0].type).toStrictEqual(SecretType.Mnemonic); @@ -6576,7 +7268,7 @@ describe('SeedlessOnboardingController', () => { withoutMockAccessToken: true, }), }, - async ({ controller, toprfClient }) => { + async ({ controller, toprfClient, baseMessenger }) => { // assert that the vault is not available in the state expect(controller.state.vault).toBeUndefined(); @@ -6597,7 +7289,10 @@ describe('SeedlessOnboardingController', () => { ]); await expect( - controller.fetchAllSecretData(MOCK_PASSWORD), + baseMessenger.call( + 'SeedlessOnboardingController:fetchAllSecretData', + MOCK_PASSWORD, + ), ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.InvalidAccessToken, ); @@ -6636,11 +7331,16 @@ describe('SeedlessOnboardingController', () => { vaultEncryptionSalt: mockResult.vaultEncryptionSalt, }), }, - async ({ controller, mockRenewRefreshToken }) => { + async ({ controller, mockRenewRefreshToken, baseMessenger }) => { // Unlock vault so #cachedDecryptedVaultData is populated. - await controller.submitPassword(MOCK_PASSWORD); + await baseMessenger.call( + 'SeedlessOnboardingController:submitPassword', + MOCK_PASSWORD, + ); - await controller.rotateRefreshToken(); + await baseMessenger.call( + 'SeedlessOnboardingController:rotateRefreshToken', + ); expect(mockRenewRefreshToken).toHaveBeenCalledWith({ connection: controller.state.authConnection, @@ -6676,13 +7376,18 @@ describe('SeedlessOnboardingController', () => { vaultEncryptionSalt: mockResult.vaultEncryptionSalt, }), }, - async ({ controller }) => { + async ({ controller, baseMessenger }) => { // Unlock vault so #cachedDecryptedVaultData is populated. - await controller.submitPassword(MOCK_PASSWORD); + await baseMessenger.call( + 'SeedlessOnboardingController:submitPassword', + MOCK_PASSWORD, + ); const originalRefreshToken = controller.state.refreshToken; - await controller.rotateRefreshToken(); + await baseMessenger.call( + 'SeedlessOnboardingController:rotateRefreshToken', + ); // State should be updated with new tokens expect(controller.state.refreshToken).toBe('newRefreshToken'); @@ -6699,8 +7404,10 @@ describe('SeedlessOnboardingController', () => { }); it('should throw when user is not authenticated', async () => { - await withController(async ({ controller }) => { - await expect(controller.rotateRefreshToken()).rejects.toThrow( + await withController(async ({ baseMessenger }) => { + await expect( + baseMessenger.call('SeedlessOnboardingController:rotateRefreshToken'), + ).rejects.toThrow( SeedlessOnboardingControllerErrorMessage.MissingAuthUserInfo, ); }); @@ -6732,9 +7439,12 @@ describe('SeedlessOnboardingController', () => { vaultEncryptionSalt: mockResult.vaultEncryptionSalt, }), }, - async ({ controller, mockRenewRefreshToken }) => { + async ({ controller, mockRenewRefreshToken, baseMessenger }) => { // Unlock vault so #cachedDecryptedVaultData is populated. - await controller.submitPassword(MOCK_PASSWORD); + await baseMessenger.call( + 'SeedlessOnboardingController:submitPassword', + MOCK_PASSWORD, + ); const originalRefreshToken = controller.state.refreshToken; const originalRevokeToken = controller.state.revokeToken; @@ -6745,7 +7455,9 @@ describe('SeedlessOnboardingController', () => { newRefreshToken: null, }); - await controller.rotateRefreshToken(); + await baseMessenger.call( + 'SeedlessOnboardingController:rotateRefreshToken', + ); // State should remain unchanged expect(controller.state.refreshToken).toBe(originalRefreshToken); @@ -6781,9 +7493,12 @@ describe('SeedlessOnboardingController', () => { vaultEncryptionSalt: mockResult.vaultEncryptionSalt, }), }, - async ({ controller, encryptor }) => { + async ({ controller, encryptor, baseMessenger }) => { // Unlock vault so #cachedDecryptedVaultData is populated. - await controller.submitPassword(MOCK_PASSWORD); + await baseMessenger.call( + 'SeedlessOnboardingController:submitPassword', + MOCK_PASSWORD, + ); // Fail the vault update (new revokeToken inside rotateRefreshToken). const originalEncryptWithKey = @@ -6795,9 +7510,11 @@ describe('SeedlessOnboardingController', () => { }); // rotateRefreshToken should throw since the vault update failed. - await expect(controller.rotateRefreshToken()).rejects.toThrow( - 'Storage full', - ); + await expect( + baseMessenger.call( + 'SeedlessOnboardingController:rotateRefreshToken', + ), + ).rejects.toThrow('Storage full'); // Restore encryptor so we can verify state jest @@ -6831,8 +7548,10 @@ describe('SeedlessOnboardingController', () => { ], }), }, - async ({ controller, mockRevokeRefreshToken }) => { - await controller.revokePendingRefreshTokens(); + async ({ controller, mockRevokeRefreshToken, baseMessenger }) => { + await baseMessenger.call( + 'SeedlessOnboardingController:revokePendingRefreshTokens', + ); expect(mockRevokeRefreshToken).toHaveBeenCalledTimes(2); expect(mockRevokeRefreshToken).toHaveBeenCalledWith({ @@ -6854,8 +7573,10 @@ describe('SeedlessOnboardingController', () => { withMockAuthenticatedUser: true, }), }, - async ({ controller, mockRevokeRefreshToken }) => { - await controller.revokePendingRefreshTokens(); + async ({ mockRevokeRefreshToken, baseMessenger }) => { + await baseMessenger.call( + 'SeedlessOnboardingController:revokePendingRefreshTokens', + ); expect(mockRevokeRefreshToken).not.toHaveBeenCalled(); }, @@ -6879,13 +7600,15 @@ describe('SeedlessOnboardingController', () => { ], }), }, - async ({ controller, mockRevokeRefreshToken }) => { + async ({ controller, mockRevokeRefreshToken, baseMessenger }) => { // Mock the revokeRefreshToken to fail for the first token but succeed for the second mockRevokeRefreshToken .mockRejectedValueOnce(new Error('Revoke failed')) .mockResolvedValueOnce(undefined); - await controller.revokePendingRefreshTokens(); + await baseMessenger.call( + 'SeedlessOnboardingController:revokePendingRefreshTokens', + ); expect(mockRevokeRefreshToken).toHaveBeenCalledTimes(2); expect(mockRevokeRefreshToken).toHaveBeenCalledWith({ diff --git a/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.ts b/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.ts index 803fcc7784a..16df39946c1 100644 --- a/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.ts +++ b/packages/seedless-onboarding-controller/src/SeedlessOnboardingController.ts @@ -51,6 +51,7 @@ import { } from './errors'; import { projectLogger, createModuleLogger } from './logger'; import { SecretMetadata } from './SecretMetadata'; +import type { SeedlessOnboardingControllerMethodActions } from './SeedlessOnboardingController-method-action-types'; import type { MutuallyExclusiveCallback, SeedlessOnboardingControllerState, @@ -74,6 +75,35 @@ import { const log = createModuleLogger(projectLogger, controllerName); +const MESSENGER_EXPOSED_METHODS = [ + 'fetchMetadataAccessCreds', + 'preloadToprfNodeDetails', + 'authenticate', + 'createToprfKeyAndBackupSeedPhrase', + 'addNewSecretData', + 'fetchAllSecretData', + 'changePassword', + 'updateBackupMetadataState', + 'verifyVaultPassword', + 'getSecretDataBackupState', + 'submitPassword', + 'setLocked', + 'syncLatestGlobalPassword', + 'submitGlobalPassword', + 'checkIsPasswordOutdated', + 'getIsUserAuthenticated', + 'clearState', + 'storeKeyringEncryptionKey', + 'loadKeyringEncryptionKey', + 'refreshAuthTokens', + 'revokePendingRefreshTokens', + 'rotateRefreshToken', + 'getAccessToken', + 'checkNodeAuthTokenExpired', + 'checkMetadataAccessTokenExpired', + 'checkAccessTokenExpired', +] as const; + // Actions export type SeedlessOnboardingControllerGetStateAction = ControllerGetStateAction< @@ -81,22 +111,9 @@ export type SeedlessOnboardingControllerGetStateAction = SeedlessOnboardingControllerState >; -/** - * Get the access token from the controller. - * If the tokens are expired, the method will refresh them and return the new access token. - * - * @returns The access token. - */ -export type SeedlessOnboardingControllerGetAccessTokenAction = { - type: `${typeof controllerName}:getAccessToken`; - handler: SeedlessOnboardingController< - encryptionUtils.EncryptionKey, - encryptionUtils.KeyDerivationOptions - >['getAccessToken']; -}; export type SeedlessOnboardingControllerActions = | SeedlessOnboardingControllerGetStateAction - | SeedlessOnboardingControllerGetAccessTokenAction; + | SeedlessOnboardingControllerMethodActions; type AllowedActions = never; @@ -126,8 +143,8 @@ export type SeedlessOnboardingControllerMessenger = Messenger< * @param encryptor - The encryptor to use for encrypting and decrypting seedless onboarding vault. */ export type SeedlessOnboardingControllerOptions< - EncryptionKey, - SupportedKeyDerivationParams, + EncryptionKey = encryptionUtils.EncryptionKey, + SupportedKeyDerivationParams = encryptionUtils.KeyDerivationOptions, > = { messenger: SeedlessOnboardingControllerMessenger; @@ -348,7 +365,7 @@ const seedlessOnboardingMetadata: StateMetadata extends BaseController< typeof controllerName, @@ -447,9 +464,9 @@ export class SeedlessOnboardingController< this.#revokeRefreshToken = revokeRefreshToken; this.#renewRefreshToken = renewRefreshToken; - this.messenger.registerActionHandler( - `${controllerName}:getAccessToken`, - this.getAccessToken.bind(this), + this.messenger.registerMethodActionHandlers( + this, + MESSENGER_EXPOSED_METHODS, ); } diff --git a/packages/seedless-onboarding-controller/src/index.ts b/packages/seedless-onboarding-controller/src/index.ts index 09ea04b9e54..6a935294241 100644 --- a/packages/seedless-onboarding-controller/src/index.ts +++ b/packages/seedless-onboarding-controller/src/index.ts @@ -6,11 +6,38 @@ export type { SeedlessOnboardingControllerOptions, SeedlessOnboardingControllerMessenger, SeedlessOnboardingControllerGetStateAction, - SeedlessOnboardingControllerGetAccessTokenAction, SeedlessOnboardingControllerStateChangeEvent, SeedlessOnboardingControllerActions, SeedlessOnboardingControllerEvents, } from './SeedlessOnboardingController'; +export type { + SeedlessOnboardingControllerFetchMetadataAccessCredsAction, + SeedlessOnboardingControllerPreloadToprfNodeDetailsAction, + SeedlessOnboardingControllerAuthenticateAction, + SeedlessOnboardingControllerCreateToprfKeyAndBackupSeedPhraseAction, + SeedlessOnboardingControllerAddNewSecretDataAction, + SeedlessOnboardingControllerFetchAllSecretDataAction, + SeedlessOnboardingControllerChangePasswordAction, + SeedlessOnboardingControllerUpdateBackupMetadataStateAction, + SeedlessOnboardingControllerVerifyVaultPasswordAction, + SeedlessOnboardingControllerGetSecretDataBackupStateAction, + SeedlessOnboardingControllerSubmitPasswordAction, + SeedlessOnboardingControllerSetLockedAction, + SeedlessOnboardingControllerSyncLatestGlobalPasswordAction, + SeedlessOnboardingControllerSubmitGlobalPasswordAction, + SeedlessOnboardingControllerCheckIsPasswordOutdatedAction, + SeedlessOnboardingControllerGetIsUserAuthenticatedAction, + SeedlessOnboardingControllerClearStateAction, + SeedlessOnboardingControllerStoreKeyringEncryptionKeyAction, + SeedlessOnboardingControllerLoadKeyringEncryptionKeyAction, + SeedlessOnboardingControllerRefreshAuthTokensAction, + SeedlessOnboardingControllerRevokePendingRefreshTokensAction, + SeedlessOnboardingControllerRotateRefreshTokenAction, + SeedlessOnboardingControllerGetAccessTokenAction, + SeedlessOnboardingControllerCheckNodeAuthTokenExpiredAction, + SeedlessOnboardingControllerCheckMetadataAccessTokenExpiredAction, + SeedlessOnboardingControllerCheckAccessTokenExpiredAction, +} from './SeedlessOnboardingController-method-action-types'; export type { AuthenticatedUserDetails, SocialBackupsMetadata, diff --git a/packages/shield-controller/CHANGELOG.md b/packages/shield-controller/CHANGELOG.md index cf3fd7a48cb..f8419fc77a8 100644 --- a/packages/shield-controller/CHANGELOG.md +++ b/packages/shield-controller/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Expose all public `ShieldController` methods through its messenger ([#8219](https://github.com/MetaMask/core/pull/8219)) + - The following actions are now available: + - `ShieldController:start` + - `ShieldController:stop` + - `ShieldController:clearState` + - `ShieldController:checkSignatureCoverage` + - Corresponding action types are now exported (e.g. `ShieldControllerCheckCoverageAction`) + ## [5.0.2] ### Changed diff --git a/packages/shield-controller/package.json b/packages/shield-controller/package.json index bda402a5208..f4d16ebe2a9 100644 --- a/packages/shield-controller/package.json +++ b/packages/shield-controller/package.json @@ -40,6 +40,7 @@ "build:docs": "typedoc", "changelog:update": "../../scripts/update-changelog.sh @metamask/shield-controller", "changelog:validate": "../../scripts/validate-changelog.sh @metamask/shield-controller", + "generate-method-action-types": "tsx ../../scripts/generate-method-action-types.ts", "since-latest-release": "../../scripts/since-latest-release.sh", "test": "NODE_OPTIONS=--experimental-vm-modules jest --reporters=jest-silent-reporter", "test:clean": "NODE_OPTIONS=--experimental-vm-modules jest --clearCache", @@ -66,6 +67,7 @@ "jest": "^29.7.0", "lodash": "^4.17.21", "ts-jest": "^29.2.5", + "tsx": "^4.20.5", "typedoc": "^0.25.13", "typedoc-plugin-missing-exports": "^2.0.0", "typescript": "~5.3.3", diff --git a/packages/shield-controller/src/ShieldController-method-action-types.ts b/packages/shield-controller/src/ShieldController-method-action-types.ts new file mode 100644 index 00000000000..8a2d9a7f132 --- /dev/null +++ b/packages/shield-controller/src/ShieldController-method-action-types.ts @@ -0,0 +1,62 @@ +/** + * This file is auto generated by `scripts/generate-method-action-types.ts`. + * Do not edit manually. + */ + +import type { ShieldController } from './ShieldController'; + +/** + * Start the ShieldController and subscribe to the transaction and signature controller state changes. + */ +export type ShieldControllerStartAction = { + type: `ShieldController:start`; + handler: ShieldController['start']; +}; + +/** + * Stop the ShieldController and unsubscribe from the transaction and signature controller state changes. + */ +export type ShieldControllerStopAction = { + type: `ShieldController:stop`; + handler: ShieldController['stop']; +}; + +/** + * Clears the shield state and resets to default values. + */ +export type ShieldControllerClearStateAction = { + type: `ShieldController:clearState`; + handler: ShieldController['clearState']; +}; + +/** + * Checks the coverage of a transaction. + * + * @param txMeta - The transaction to check coverage for. + * @returns The coverage result. + */ +export type ShieldControllerCheckCoverageAction = { + type: `ShieldController:checkCoverage`; + handler: ShieldController['checkCoverage']; +}; + +/** + * Checks the coverage of a signature request. + * + * @param signatureRequest - The signature request to check coverage for. + * @returns The coverage result. + */ +export type ShieldControllerCheckSignatureCoverageAction = { + type: `ShieldController:checkSignatureCoverage`; + handler: ShieldController['checkSignatureCoverage']; +}; + +/** + * Union of all ShieldController action types. + */ +export type ShieldControllerMethodActions = + | ShieldControllerStartAction + | ShieldControllerStopAction + | ShieldControllerClearStateAction + | ShieldControllerCheckCoverageAction + | ShieldControllerCheckSignatureCoverageAction; diff --git a/packages/shield-controller/src/ShieldController.test.ts b/packages/shield-controller/src/ShieldController.test.ts index fd13b0e81ee..00f8ddee92d 100644 --- a/packages/shield-controller/src/ShieldController.test.ts +++ b/packages/shield-controller/src/ShieldController.test.ts @@ -59,7 +59,7 @@ function setup({ normalizeSignatureRequest, state, }); - controller.start(); + rootMessenger.call('ShieldController:start'); return { controller, messenger, @@ -84,11 +84,11 @@ describe('ShieldController', () => { }); it('should tolerate calling start and stop multiple times', async () => { - const { backend, rootMessenger, messenger, controller } = setup(); - controller.stop(); - controller.stop(); - controller.start(); - controller.start(); + const { backend, rootMessenger, messenger } = setup(); + rootMessenger.call('ShieldController:stop'); + rootMessenger.call('ShieldController:stop'); + rootMessenger.call('ShieldController:start'); + rootMessenger.call('ShieldController:start'); const txMeta = generateMockTxMeta(); const coverageResultReceived = setupCoverageResultReceived(messenger); rootMessenger.publish( @@ -101,8 +101,8 @@ describe('ShieldController', () => { }); it('should no longer trigger checkCoverage when controller is stopped', async () => { - const { controller, rootMessenger, backend } = setup(); - controller.stop(); + const { rootMessenger, backend } = setup(); + rootMessenger.call('ShieldController:stop'); const txMeta = generateMockTxMeta(); const coverageResultReceived = new Promise((resolve, reject) => { rootMessenger.subscribe( @@ -126,12 +126,12 @@ describe('ShieldController', () => { }); it('should purge coverage history when the limit is exceeded', async () => { - const { controller } = setup({ + const { controller, rootMessenger } = setup({ coverageHistoryLimit: 1, }); const txMeta = generateMockTxMeta(); - await controller.checkCoverage(txMeta); - await controller.checkCoverage(txMeta); + await rootMessenger.call('ShieldController:checkCoverage', txMeta); + await rootMessenger.call('ShieldController:checkCoverage', txMeta); expect(controller.state.coverageResults).toHaveProperty(txMeta.id); expect(controller.state.coverageResults[txMeta.id].results).toHaveLength( 1, @@ -139,13 +139,13 @@ describe('ShieldController', () => { }); it('should purge transaction history when the limit is exceeded', async () => { - const { controller } = setup({ + const { controller, rootMessenger } = setup({ transactionHistoryLimit: 1, }); const txMeta1 = generateMockTxMeta(); const txMeta2 = generateMockTxMeta(); - await controller.checkCoverage(txMeta1); - await controller.checkCoverage(txMeta2); + await rootMessenger.call('ShieldController:checkCoverage', txMeta1); + await rootMessenger.call('ShieldController:checkCoverage', txMeta2); expect(controller.state.coverageResults).toHaveProperty(txMeta2.id); expect(controller.state.coverageResults[txMeta2.id].results).toHaveLength( 1, @@ -193,7 +193,7 @@ describe('ShieldController', () => { ); it('throws an error when the coverage ID has changed', async () => { - const { controller, backend } = setup(); + const { backend, rootMessenger } = setup(); backend.checkCoverage.mockResolvedValueOnce({ coverageId: '0x00', status: 'covered', @@ -209,10 +209,10 @@ describe('ShieldController', () => { }, }); const txMeta = generateMockTxMeta(); - await controller.checkCoverage(txMeta); - await expect(controller.checkCoverage(txMeta)).rejects.toThrow( - 'Coverage ID has changed', - ); + await rootMessenger.call('ShieldController:checkCoverage', txMeta); + await expect( + rootMessenger.call('ShieldController:checkCoverage', txMeta), + ).rejects.toThrow('Coverage ID has changed'); }); }); @@ -597,7 +597,7 @@ describe('ShieldController', () => { describe('clearState', () => { it('should reset state to default values', () => { const txMeta = generateMockTxMeta(); - const { controller } = setup({ + const { controller, rootMessenger } = setup({ state: { // @ts-expect-error - testing mock coverageResults: { @@ -612,7 +612,7 @@ describe('ShieldController', () => { expect(Object.keys(controller.state.coverageResults)).toHaveLength(1); expect(controller.state.orderedTransactionHistory).toHaveLength(1); - controller.clearState(); + rootMessenger.call('ShieldController:clearState'); expect(controller.state).toStrictEqual(getDefaultShieldControllerState()); expect(Object.keys(controller.state.coverageResults)).toHaveLength(0); diff --git a/packages/shield-controller/src/ShieldController.ts b/packages/shield-controller/src/ShieldController.ts index e6743114ab1..e7109d6e941 100644 --- a/packages/shield-controller/src/ShieldController.ts +++ b/packages/shield-controller/src/ShieldController.ts @@ -18,6 +18,7 @@ import { cloneDeep, isEqual } from 'lodash'; import { controllerName } from './constants'; import { projectLogger, createModuleLogger } from './logger'; +import type { ShieldControllerMethodActions } from './ShieldController-method-action-types'; import type { CoverageResult, NormalizeSignatureRequestFn, @@ -26,6 +27,14 @@ import type { const log = createModuleLogger(projectLogger, 'ShieldController'); +const MESSENGER_EXPOSED_METHODS = [ + 'start', + 'stop', + 'clearState', + 'checkCoverage', + 'checkSignatureCoverage', +] as const; + export type CoverageResultRecordEntry = { /** * History of coverage results, latest first. @@ -64,17 +73,12 @@ export type ShieldControllerGetStateAction = ControllerGetStateAction< ShieldControllerState >; -export type ShieldControllerCheckCoverageAction = { - type: `${typeof controllerName}:checkCoverage`; - handler: ShieldController['checkCoverage']; -}; - /** * The internal actions available to the ShieldController. */ export type ShieldControllerActions = | ShieldControllerGetStateAction - | ShieldControllerCheckCoverageAction; + | ShieldControllerMethodActions; export type ShieldControllerCoverageResultReceivedEvent = { type: `${typeof controllerName}:coverageResultReceived`; @@ -199,6 +203,11 @@ export class ShieldController extends BaseController< this.#handleSignatureControllerStateChange.bind(this); this.#started = false; this.#normalizeSignatureRequest = normalizeSignatureRequest; + + this.messenger.registerMethodActionHandlers( + this, + MESSENGER_EXPOSED_METHODS, + ); } /** diff --git a/packages/shield-controller/src/index.ts b/packages/shield-controller/src/index.ts index a738356262c..7747be50a42 100644 --- a/packages/shield-controller/src/index.ts +++ b/packages/shield-controller/src/index.ts @@ -10,10 +10,16 @@ export type { ShieldControllerMessenger, ShieldControllerState, ShieldControllerGetStateAction, - ShieldControllerCheckCoverageAction, ShieldControllerCoverageResultReceivedEvent, ShieldControllerStateChangeEvent, } from './ShieldController'; +export type { + ShieldControllerStartAction, + ShieldControllerStopAction, + ShieldControllerClearStateAction, + ShieldControllerCheckCoverageAction, + ShieldControllerCheckSignatureCoverageAction, +} from './ShieldController-method-action-types'; export { ShieldController, getDefaultShieldControllerState, diff --git a/packages/subscription-controller/CHANGELOG.md b/packages/subscription-controller/CHANGELOG.md index 51bf51a8ce9..30115440a1f 100644 --- a/packages/subscription-controller/CHANGELOG.md +++ b/packages/subscription-controller/CHANGELOG.md @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Expose all public `SubscriptionController` methods through its messenger ([#8219](https://github.com/MetaMask/core/pull/8219)) + - `SubscriptionController:getSubscriptionsEligibilities` + - `SubscriptionController:unCancelSubscription` + - `SubscriptionController:submitUserEvent` + - `SubscriptionController:assignUserToCohort` + - `SubscriptionController:getTokenApproveAmount` + - `SubscriptionController:getTokenMinimumBalanceAmount` + - `SubscriptionController:clearState` + - `SubscriptionController:triggerAccessTokenRefresh` + - Corresponding action types are now exported (e.g. `SubscriptionControllerGetPricingAction`) + ## [6.0.2] ### Changed diff --git a/packages/subscription-controller/package.json b/packages/subscription-controller/package.json index 9da9aa53950..1ebaf168c7a 100644 --- a/packages/subscription-controller/package.json +++ b/packages/subscription-controller/package.json @@ -40,6 +40,7 @@ "build:docs": "typedoc", "changelog:update": "../../scripts/update-changelog.sh @metamask/subscription-controller", "changelog:validate": "../../scripts/validate-changelog.sh @metamask/subscription-controller", + "generate-method-action-types": "tsx ../../scripts/generate-method-action-types.ts", "since-latest-release": "../../scripts/since-latest-release.sh", "test": "NODE_OPTIONS=--experimental-vm-modules jest --reporters=jest-silent-reporter", "test:clean": "NODE_OPTIONS=--experimental-vm-modules jest --clearCache", @@ -63,6 +64,7 @@ "deepmerge": "^4.2.2", "jest": "^29.7.0", "ts-jest": "^29.2.5", + "tsx": "^4.20.5", "typedoc": "^0.25.13", "typedoc-plugin-missing-exports": "^2.0.0", "typescript": "~5.3.3" diff --git a/packages/subscription-controller/src/SubscriptionController-method-action-types.ts b/packages/subscription-controller/src/SubscriptionController-method-action-types.ts new file mode 100644 index 00000000000..d81456aec19 --- /dev/null +++ b/packages/subscription-controller/src/SubscriptionController-method-action-types.ts @@ -0,0 +1,255 @@ +/** + * This file is auto generated by `scripts/generate-method-action-types.ts`. + * Do not edit manually. + */ + +import type { SubscriptionController } from './SubscriptionController'; + +/** + * Gets the pricing information from the subscription service. + * + * @returns The pricing information. + */ +export type SubscriptionControllerGetPricingAction = { + type: `SubscriptionController:getPricing`; + handler: SubscriptionController['getPricing']; +}; + +export type SubscriptionControllerGetSubscriptionsAction = { + type: `SubscriptionController:getSubscriptions`; + handler: SubscriptionController['getSubscriptions']; +}; + +/** + * Get the subscription by product. + * + * @param productType - The product type. + * @returns The subscription. + */ +export type SubscriptionControllerGetSubscriptionByProductAction = { + type: `SubscriptionController:getSubscriptionByProduct`; + handler: SubscriptionController['getSubscriptionByProduct']; +}; + +/** + * Get the subscriptions eligibilities. + * + * @param request - Optional request object containing user balance to check cohort eligibility. + * @returns The subscriptions eligibilities. + */ +export type SubscriptionControllerGetSubscriptionsEligibilitiesAction = { + type: `SubscriptionController:getSubscriptionsEligibilities`; + handler: SubscriptionController['getSubscriptionsEligibilities']; +}; + +export type SubscriptionControllerCancelSubscriptionAction = { + type: `SubscriptionController:cancelSubscription`; + handler: SubscriptionController['cancelSubscription']; +}; + +export type SubscriptionControllerUnCancelSubscriptionAction = { + type: `SubscriptionController:unCancelSubscription`; + handler: SubscriptionController['unCancelSubscription']; +}; + +export type SubscriptionControllerStartShieldSubscriptionWithCardAction = { + type: `SubscriptionController:startShieldSubscriptionWithCard`; + handler: SubscriptionController['startShieldSubscriptionWithCard']; +}; + +export type SubscriptionControllerStartSubscriptionWithCryptoAction = { + type: `SubscriptionController:startSubscriptionWithCrypto`; + handler: SubscriptionController['startSubscriptionWithCrypto']; +}; + +/** + * Handles shield subscription crypto approval transactions. + * + * @param txMeta - The transaction metadata. + * @param isSponsored - Whether the transaction is sponsored. + * @param rewardAccountId - The account ID of the reward subscription to link to the shield subscription. + * @returns void + */ +export type SubscriptionControllerSubmitShieldSubscriptionCryptoApprovalAction = + { + type: `SubscriptionController:submitShieldSubscriptionCryptoApproval`; + handler: SubscriptionController['submitShieldSubscriptionCryptoApproval']; + }; + +/** + * Get transaction params to create crypto approve transaction for subscription payment + * + * @param request - The request object + * @param request.chainId - The chain ID + * @param request.tokenAddress - The address of the token + * @param request.productType - The product type + * @param request.interval - The interval + * @returns The crypto approve transaction params + */ +export type SubscriptionControllerGetCryptoApproveTransactionParamsAction = { + type: `SubscriptionController:getCryptoApproveTransactionParams`; + handler: SubscriptionController['getCryptoApproveTransactionParams']; +}; + +export type SubscriptionControllerUpdatePaymentMethodAction = { + type: `SubscriptionController:updatePaymentMethod`; + handler: SubscriptionController['updatePaymentMethod']; +}; + +/** + * Gets the billing portal URL. + * + * @returns The billing portal URL + */ +export type SubscriptionControllerGetBillingPortalUrlAction = { + type: `SubscriptionController:getBillingPortalUrl`; + handler: SubscriptionController['getBillingPortalUrl']; +}; + +/** + * Cache the last selected payment method for a specific product. + * + * @param product - The product to cache the payment method for. + * @param paymentMethod - The payment method to cache. + * @param paymentMethod.type - The type of the payment method. + * @param paymentMethod.paymentTokenAddress - The payment token address. + * @param paymentMethod.plan - The plan of the payment method. + * @param paymentMethod.product - The product of the payment method. + */ +export type SubscriptionControllerCacheLastSelectedPaymentMethodAction = { + type: `SubscriptionController:cacheLastSelectedPaymentMethod`; + handler: SubscriptionController['cacheLastSelectedPaymentMethod']; +}; + +/** + * Clear the last selected payment method for a specific product. + * + * @param product - The product to clear the payment method for. + */ +export type SubscriptionControllerClearLastSelectedPaymentMethodAction = { + type: `SubscriptionController:clearLastSelectedPaymentMethod`; + handler: SubscriptionController['clearLastSelectedPaymentMethod']; +}; + +/** + * Submit sponsorship intents to the Subscription Service backend. + * + * This is intended to be used together with the crypto subscription flow. + * When the user has enabled the smart transaction feature, we will sponsor the gas fees for the subscription approval transaction. + * + * @param request - Request object containing the address and products. + * @example { + * address: '0x1234567890123456789012345678901234567890', + * products: [ProductType.Shield], + * recurringInterval: RecurringInterval.Month, + * billingCycles: 1, + * } + * @returns resolves to true if the sponsorship is supported and intents were submitted successfully, false otherwise + */ +export type SubscriptionControllerSubmitSponsorshipIntentsAction = { + type: `SubscriptionController:submitSponsorshipIntents`; + handler: SubscriptionController['submitSponsorshipIntents']; +}; + +/** + * Submit a user event from the UI. (e.g. shield modal viewed) + * + * @param request - Request object containing the event to submit. + * @example { event: SubscriptionUserEvent.ShieldEntryModalViewed, cohort: 'post_tx' } + */ +export type SubscriptionControllerSubmitUserEventAction = { + type: `SubscriptionController:submitUserEvent`; + handler: SubscriptionController['submitUserEvent']; +}; + +/** + * Assign user to a cohort. + * + * @param request - Request object containing the cohort to assign the user to. + * @example { cohort: 'post_tx' } + */ +export type SubscriptionControllerAssignUserToCohortAction = { + type: `SubscriptionController:assignUserToCohort`; + handler: SubscriptionController['assignUserToCohort']; +}; + +/** + * Link rewards to a subscription. + * + * @param request - Request object containing the reward subscription ID. + * @param request.subscriptionId - The ID of the subscription to link rewards to. + * @param request.rewardAccountId - The account ID of the reward subscription to link to the subscription. + * @example { subscriptionId: '1234567890', rewardAccountId: 'eip155:1:0x1234567890123456789012345678901234567890' } + * @returns Resolves when the rewards are linked successfully. + */ +export type SubscriptionControllerLinkRewardsAction = { + type: `SubscriptionController:linkRewards`; + handler: SubscriptionController['linkRewards']; +}; + +/** + * Calculate token approve amount from price info + * + * @param price - The price info + * @param tokenPaymentInfo - The token price info + * @returns The token approve amount + */ +export type SubscriptionControllerGetTokenApproveAmountAction = { + type: `SubscriptionController:getTokenApproveAmount`; + handler: SubscriptionController['getTokenApproveAmount']; +}; + +/** + * Calculate token minimum balance amount from price info + * + * @param price - The price info + * @param tokenPaymentInfo - The token price info + * @returns The token balance amount + */ +export type SubscriptionControllerGetTokenMinimumBalanceAmountAction = { + type: `SubscriptionController:getTokenMinimumBalanceAmount`; + handler: SubscriptionController['getTokenMinimumBalanceAmount']; +}; + +/** + * Clears the subscription state and resets to default values. + */ +export type SubscriptionControllerClearStateAction = { + type: `SubscriptionController:clearState`; + handler: SubscriptionController['clearState']; +}; + +/** + * Triggers an access token refresh. + */ +export type SubscriptionControllerTriggerAccessTokenRefreshAction = { + type: `SubscriptionController:triggerAccessTokenRefresh`; + handler: SubscriptionController['triggerAccessTokenRefresh']; +}; + +/** + * Union of all SubscriptionController action types. + */ +export type SubscriptionControllerMethodActions = + | SubscriptionControllerGetPricingAction + | SubscriptionControllerGetSubscriptionsAction + | SubscriptionControllerGetSubscriptionByProductAction + | SubscriptionControllerGetSubscriptionsEligibilitiesAction + | SubscriptionControllerCancelSubscriptionAction + | SubscriptionControllerUnCancelSubscriptionAction + | SubscriptionControllerStartShieldSubscriptionWithCardAction + | SubscriptionControllerStartSubscriptionWithCryptoAction + | SubscriptionControllerSubmitShieldSubscriptionCryptoApprovalAction + | SubscriptionControllerGetCryptoApproveTransactionParamsAction + | SubscriptionControllerUpdatePaymentMethodAction + | SubscriptionControllerGetBillingPortalUrlAction + | SubscriptionControllerCacheLastSelectedPaymentMethodAction + | SubscriptionControllerClearLastSelectedPaymentMethodAction + | SubscriptionControllerSubmitSponsorshipIntentsAction + | SubscriptionControllerSubmitUserEventAction + | SubscriptionControllerAssignUserToCohortAction + | SubscriptionControllerLinkRewardsAction + | SubscriptionControllerGetTokenApproveAmountAction + | SubscriptionControllerGetTokenMinimumBalanceAmountAction + | SubscriptionControllerClearStateAction + | SubscriptionControllerTriggerAccessTokenRefreshAction; diff --git a/packages/subscription-controller/src/SubscriptionController.test.ts b/packages/subscription-controller/src/SubscriptionController.test.ts index 8d527264619..4c9431f1bb6 100644 --- a/packages/subscription-controller/src/SubscriptionController.test.ts +++ b/packages/subscription-controller/src/SubscriptionController.test.ts @@ -388,56 +388,66 @@ describe('SubscriptionController', () => { describe('getSubscriptions', () => { it('should fetch and store subscription successfully', async () => { - await withController(async ({ controller, mockService }) => { - mockService.getSubscriptions.mockResolvedValue( - MOCK_GET_SUBSCRIPTIONS_RESPONSE, - ); + await withController( + async ({ controller, rootMessenger, mockService }) => { + mockService.getSubscriptions.mockResolvedValue( + MOCK_GET_SUBSCRIPTIONS_RESPONSE, + ); - const result = await controller.getSubscriptions(); + const result = await rootMessenger.call( + 'SubscriptionController:getSubscriptions', + ); - expect(result).toStrictEqual([MOCK_SUBSCRIPTION]); - expect(controller.state.subscriptions).toStrictEqual([ - MOCK_SUBSCRIPTION, - ]); - expect(mockService.getSubscriptions).toHaveBeenCalledTimes(1); - }); + expect(result).toStrictEqual([MOCK_SUBSCRIPTION]); + expect(controller.state.subscriptions).toStrictEqual([ + MOCK_SUBSCRIPTION, + ]); + expect(mockService.getSubscriptions).toHaveBeenCalledTimes(1); + }, + ); }); it('should handle null subscription response', async () => { - await withController(async ({ controller, mockService }) => { - mockService.getSubscriptions.mockResolvedValue({ - customerId: 'cus_1', - subscriptions: [], - trialedProducts: [], - }); + await withController( + async ({ controller, rootMessenger, mockService }) => { + mockService.getSubscriptions.mockResolvedValue({ + customerId: 'cus_1', + subscriptions: [], + trialedProducts: [], + }); - const result = await controller.getSubscriptions(); + const result = await rootMessenger.call( + 'SubscriptionController:getSubscriptions', + ); - expect(result).toHaveLength(0); - expect(controller.state.subscriptions).toStrictEqual([]); - expect(mockService.getSubscriptions).toHaveBeenCalledTimes(1); - }); + expect(result).toHaveLength(0); + expect(controller.state.subscriptions).toStrictEqual([]); + expect(mockService.getSubscriptions).toHaveBeenCalledTimes(1); + }, + ); }); it('should handle subscription service errors', async () => { - await withController(async ({ controller, mockService }) => { - const errorMessage = 'Failed to fetch subscription'; - mockService.getSubscriptions.mockRejectedValue( - new SubscriptionServiceError(errorMessage), - ); + await withController( + async ({ controller, rootMessenger, mockService }) => { + const errorMessage = 'Failed to fetch subscription'; + mockService.getSubscriptions.mockRejectedValue( + new SubscriptionServiceError(errorMessage), + ); - await expect(controller.getSubscriptions()).rejects.toThrow( - SubscriptionServiceError, - ); + await expect( + rootMessenger.call('SubscriptionController:getSubscriptions'), + ).rejects.toThrow(SubscriptionServiceError); - expect(controller.state.subscriptions).toStrictEqual([]); - expect(mockService.getSubscriptions).toHaveBeenCalledTimes(1); - }); + expect(controller.state.subscriptions).toStrictEqual([]); + expect(mockService.getSubscriptions).toHaveBeenCalledTimes(1); + }, + ); }); it('should surface triggerAccessTokenRefresh errors', async () => { await withController( - async ({ controller, mockService, mockPerformSignOut }) => { + async ({ rootMessenger, mockService, mockPerformSignOut }) => { mockService.getSubscriptions.mockResolvedValue( MOCK_GET_SUBSCRIPTIONS_RESPONSE, ); @@ -445,9 +455,9 @@ describe('SubscriptionController', () => { throw new Error('Wallet is locked'); }); - await expect(controller.getSubscriptions()).rejects.toThrow( - 'Wallet is locked', - ); + await expect( + rootMessenger.call('SubscriptionController:getSubscriptions'), + ).rejects.toThrow('Wallet is locked'); }, ); }); @@ -462,7 +472,7 @@ describe('SubscriptionController', () => { subscriptions: [initialSubscription], }, }, - async ({ controller, mockService }) => { + async ({ controller, rootMessenger, mockService }) => { expect(controller.state.subscriptions).toStrictEqual([ initialSubscription, ]); @@ -473,7 +483,9 @@ describe('SubscriptionController', () => { subscriptions: [newSubscription], trialedProducts: [], }); - const result = await controller.getSubscriptions(); + const result = await rootMessenger.call( + 'SubscriptionController:getSubscriptions', + ); expect(result).toStrictEqual([newSubscription]); expect(controller.state.subscriptions).toStrictEqual([ @@ -500,7 +512,7 @@ describe('SubscriptionController', () => { ], }, }, - async ({ controller, mockService }) => { + async ({ controller, rootMessenger, mockService }) => { // Return the same subscriptions but in different order mockService.getSubscriptions.mockResolvedValue({ customerId: 'cus_1', @@ -513,7 +525,7 @@ describe('SubscriptionController', () => { }); const initialState = [...controller.state.subscriptions]; - await controller.getSubscriptions(); + await rootMessenger.call('SubscriptionController:getSubscriptions'); // Should not update state since subscriptions are the same (just different order) expect(controller.state.subscriptions).toStrictEqual(initialState); @@ -548,7 +560,7 @@ describe('SubscriptionController', () => { trialedProducts: [PRODUCT_TYPES.SHIELD], }, }, - async ({ controller, mockService }) => { + async ({ controller, rootMessenger, mockService }) => { mockService.getSubscriptions.mockResolvedValue({ ...MOCK_SUBSCRIPTION, subscriptions: [ @@ -556,7 +568,7 @@ describe('SubscriptionController', () => { ], trialedProducts: [PRODUCT_TYPES.SHIELD], }); - await controller.getSubscriptions(); + await rootMessenger.call('SubscriptionController:getSubscriptions'); expect(controller.state.subscriptions).toStrictEqual([ mockSubscription, ]); @@ -590,7 +602,7 @@ describe('SubscriptionController', () => { subscriptions: [mockSubscription], }, }, - async ({ controller, mockService }) => { + async ({ controller, rootMessenger, mockService }) => { mockService.getSubscriptions.mockResolvedValue({ ...MOCK_SUBSCRIPTION, subscriptions: [ @@ -598,7 +610,7 @@ describe('SubscriptionController', () => { ], trialedProducts: [PRODUCT_TYPES.SHIELD], }); - await controller.getSubscriptions(); + await rootMessenger.call('SubscriptionController:getSubscriptions'); expect(controller.state.subscriptions).toStrictEqual([ mockSubscription, ]); @@ -616,7 +628,7 @@ describe('SubscriptionController', () => { lastSubscription: undefined, }, }, - async ({ controller, mockService }) => { + async ({ controller, rootMessenger, mockService }) => { mockService.getSubscriptions.mockResolvedValue({ customerId: 'cus_1', subscriptions: [], @@ -624,7 +636,7 @@ describe('SubscriptionController', () => { lastSubscription: MOCK_SUBSCRIPTION, }); - await controller.getSubscriptions(); + await rootMessenger.call('SubscriptionController:getSubscriptions'); expect(controller.state.lastSubscription).toStrictEqual( MOCK_SUBSCRIPTION, @@ -640,7 +652,7 @@ describe('SubscriptionController', () => { rewardAccountId: undefined, }, }, - async ({ controller, mockService }) => { + async ({ controller, rootMessenger, mockService }) => { mockService.getSubscriptions.mockResolvedValue({ customerId: 'cus_1', subscriptions: [], @@ -649,7 +661,7 @@ describe('SubscriptionController', () => { 'eip155:1:0x1234567890123456789012345678901234567890', }); - await controller.getSubscriptions(); + await rootMessenger.call('SubscriptionController:getSubscriptions'); expect(controller.state.rewardAccountId).toBe( 'eip155:1:0x1234567890123456789012345678901234567890', @@ -671,7 +683,7 @@ describe('SubscriptionController', () => { rewardAccountId: mockRewardAccountId, }, }, - async ({ controller, mockService, rootMessenger }) => { + async ({ mockService, rootMessenger }) => { mockService.getSubscriptions.mockResolvedValue({ customerId: 'cus_1', subscriptions: [], @@ -685,7 +697,7 @@ describe('SubscriptionController', () => { stateChangeListener, ); - await controller.getSubscriptions(); + await rootMessenger.call('SubscriptionController:getSubscriptions'); // State should not have changed since rewardAccountId is the same expect(stateChangeListener).not.toHaveBeenCalled(); @@ -702,18 +714,24 @@ describe('SubscriptionController', () => { subscriptions: [MOCK_SUBSCRIPTION], }, }, - async ({ controller }) => { + async ({ rootMessenger }) => { expect( - controller.getSubscriptionByProduct(PRODUCT_TYPES.SHIELD), + rootMessenger.call( + 'SubscriptionController:getSubscriptionByProduct', + PRODUCT_TYPES.SHIELD, + ), ).toStrictEqual(MOCK_SUBSCRIPTION); }, ); }); it('should return undefined if no subscription is found', async () => { - await withController(async ({ controller }) => { + await withController(async ({ rootMessenger }) => { expect( - controller.getSubscriptionByProduct(PRODUCT_TYPES.SHIELD), + rootMessenger.call( + 'SubscriptionController:getSubscriptionByProduct', + PRODUCT_TYPES.SHIELD, + ), ).toBeUndefined(); }); }); @@ -728,15 +746,18 @@ describe('SubscriptionController', () => { subscriptions: [MOCK_SUBSCRIPTION, mockSubscription2], }, }, - async ({ controller, mockService }) => { + async ({ controller, rootMessenger, mockService }) => { mockService.cancelSubscription.mockResolvedValue({ ...MOCK_SUBSCRIPTION, status: SUBSCRIPTION_STATUSES.canceled, }); expect( - await controller.cancelSubscription({ - subscriptionId: MOCK_SUBSCRIPTION.id, - }), + await rootMessenger.call( + 'SubscriptionController:cancelSubscription', + { + subscriptionId: MOCK_SUBSCRIPTION.id, + }, + ), ).toBeUndefined(); expect(controller.state.subscriptions).toStrictEqual([ { ...MOCK_SUBSCRIPTION, status: SUBSCRIPTION_STATUSES.canceled }, @@ -757,9 +778,9 @@ describe('SubscriptionController', () => { subscriptions: [], }, }, - async ({ controller }) => { + async ({ rootMessenger }) => { await expect( - controller.cancelSubscription({ + rootMessenger.call('SubscriptionController:cancelSubscription', { subscriptionId: 'sub_123456789', }), ).rejects.toThrow( @@ -776,9 +797,9 @@ describe('SubscriptionController', () => { subscriptions: [], }, }, - async ({ controller, mockService }) => { + async ({ rootMessenger, mockService }) => { await expect( - controller.cancelSubscription({ + rootMessenger.call('SubscriptionController:cancelSubscription', { subscriptionId: 'sub_123456789', }), ).rejects.toThrow( @@ -798,14 +819,14 @@ describe('SubscriptionController', () => { subscriptions: [MOCK_SUBSCRIPTION], }, }, - async ({ controller, mockService }) => { + async ({ rootMessenger, mockService }) => { const errorMessage = 'Failed to cancel subscription'; mockService.cancelSubscription.mockRejectedValue( new SubscriptionServiceError(errorMessage), ); await expect( - controller.cancelSubscription({ + rootMessenger.call('SubscriptionController:cancelSubscription', { subscriptionId: 'sub_123456789', }), ).rejects.toThrow(SubscriptionServiceError); @@ -828,15 +849,18 @@ describe('SubscriptionController', () => { subscriptions: [MOCK_SUBSCRIPTION, mockSubscription2], }, }, - async ({ controller, mockService }) => { + async ({ controller, rootMessenger, mockService }) => { mockService.unCancelSubscription.mockResolvedValue({ ...MOCK_SUBSCRIPTION, status: SUBSCRIPTION_STATUSES.active, }); expect( - await controller.unCancelSubscription({ - subscriptionId: MOCK_SUBSCRIPTION.id, - }), + await rootMessenger.call( + 'SubscriptionController:unCancelSubscription', + { + subscriptionId: MOCK_SUBSCRIPTION.id, + }, + ), ).toBeUndefined(); expect(controller.state.subscriptions).toStrictEqual([ { ...MOCK_SUBSCRIPTION, status: SUBSCRIPTION_STATUSES.active }, @@ -857,9 +881,9 @@ describe('SubscriptionController', () => { subscriptions: [], }, }, - async ({ controller }) => { + async ({ rootMessenger }) => { await expect( - controller.unCancelSubscription({ + rootMessenger.call('SubscriptionController:unCancelSubscription', { subscriptionId: 'sub_123456789', }), ).rejects.toThrow( @@ -876,9 +900,9 @@ describe('SubscriptionController', () => { subscriptions: [], }, }, - async ({ controller, mockService }) => { + async ({ rootMessenger, mockService }) => { await expect( - controller.unCancelSubscription({ + rootMessenger.call('SubscriptionController:unCancelSubscription', { subscriptionId: 'sub_123456789', }), ).rejects.toThrow( @@ -898,14 +922,14 @@ describe('SubscriptionController', () => { subscriptions: [MOCK_SUBSCRIPTION], }, }, - async ({ controller, mockService }) => { + async ({ rootMessenger, mockService }) => { const errorMessage = 'Failed to unCancel subscription'; mockService.unCancelSubscription.mockRejectedValue( new SubscriptionServiceError(errorMessage), ); await expect( - controller.unCancelSubscription({ + rootMessenger.call('SubscriptionController:unCancelSubscription', { subscriptionId: 'sub_123456789', }), ).rejects.toThrow(SubscriptionServiceError); @@ -931,16 +955,19 @@ describe('SubscriptionController', () => { subscriptions: [], }, }, - async ({ controller, mockService }) => { + async ({ rootMessenger, mockService }) => { mockService.startSubscriptionWithCard.mockResolvedValue( MOCK_START_SUBSCRIPTION_RESPONSE, ); - const result = await controller.startShieldSubscriptionWithCard({ - products: [PRODUCT_TYPES.SHIELD], - isTrialRequested: true, - recurringInterval: RECURRING_INTERVALS.month, - }); + const result = await rootMessenger.call( + 'SubscriptionController:startShieldSubscriptionWithCard', + { + products: [PRODUCT_TYPES.SHIELD], + isTrialRequested: true, + recurringInterval: RECURRING_INTERVALS.month, + }, + ); expect(result).toStrictEqual(MOCK_START_SUBSCRIPTION_RESPONSE); expect(mockService.startSubscriptionWithCard).toHaveBeenCalledWith({ @@ -959,13 +986,16 @@ describe('SubscriptionController', () => { subscriptions: [MOCK_SUBSCRIPTION], }, }, - async ({ controller, mockService }) => { + async ({ rootMessenger, mockService }) => { await expect( - controller.startShieldSubscriptionWithCard({ - products: [PRODUCT_TYPES.SHIELD], - isTrialRequested: true, - recurringInterval: RECURRING_INTERVALS.month, - }), + rootMessenger.call( + 'SubscriptionController:startShieldSubscriptionWithCard', + { + products: [PRODUCT_TYPES.SHIELD], + isTrialRequested: true, + recurringInterval: RECURRING_INTERVALS.month, + }, + ), ).rejects.toThrow( SubscriptionControllerErrorMessage.UserAlreadySubscribed, ); @@ -983,18 +1013,21 @@ describe('SubscriptionController', () => { subscriptions: [], }, }, - async ({ controller, mockService }) => { + async ({ rootMessenger, mockService }) => { const errorMessage = 'Failed to start subscription'; mockService.startSubscriptionWithCard.mockRejectedValue( new SubscriptionServiceError(errorMessage), ); await expect( - controller.startShieldSubscriptionWithCard({ - products: [PRODUCT_TYPES.SHIELD], - isTrialRequested: true, - recurringInterval: RECURRING_INTERVALS.month, - }), + rootMessenger.call( + 'SubscriptionController:startShieldSubscriptionWithCard', + { + products: [PRODUCT_TYPES.SHIELD], + isTrialRequested: true, + recurringInterval: RECURRING_INTERVALS.month, + }, + ), ).rejects.toThrow(SubscriptionServiceError); expect(mockService.startSubscriptionWithCard).toHaveBeenCalledWith({ @@ -1015,7 +1048,7 @@ describe('SubscriptionController', () => { subscriptions: [], }, }, - async ({ controller, mockService }) => { + async ({ rootMessenger, mockService }) => { const request: StartCryptoSubscriptionRequest = { products: [PRODUCT_TYPES.SHIELD], isTrialRequested: false, @@ -1034,7 +1067,10 @@ describe('SubscriptionController', () => { mockService.startSubscriptionWithCrypto.mockResolvedValue(response); - const result = await controller.startSubscriptionWithCrypto(request); + const result = await rootMessenger.call( + 'SubscriptionController:startSubscriptionWithCrypto', + request, + ); expect(result).toStrictEqual(response); expect(mockService.startSubscriptionWithCrypto).toHaveBeenCalledWith( @@ -1081,42 +1117,51 @@ describe('SubscriptionController', () => { describe('integration scenarios', () => { it('should handle complete subscription lifecycle with updated logic', async () => { - await withController(async ({ controller, mockService }) => { - // 1. Initially no subscription - expect(controller.state.subscriptions).toStrictEqual([]); + await withController( + async ({ controller, rootMessenger, mockService }) => { + // 1. Initially no subscription + expect(controller.state.subscriptions).toStrictEqual([]); - // 2. Try to cancel subscription (should fail - user not subscribed) - await expect( - controller.cancelSubscription({ - subscriptionId: 'sub_123456789', - }), - ).rejects.toThrow(SubscriptionControllerErrorMessage.UserNotSubscribed); + // 2. Try to cancel subscription (should fail - user not subscribed) + await expect( + rootMessenger.call('SubscriptionController:cancelSubscription', { + subscriptionId: 'sub_123456789', + }), + ).rejects.toThrow( + SubscriptionControllerErrorMessage.UserNotSubscribed, + ); - // 3. Fetch subscription - mockService.getSubscriptions.mockResolvedValue({ - customerId: 'cus_1', - subscriptions: [MOCK_SUBSCRIPTION], - trialedProducts: [], - }); - const subscriptions = await controller.getSubscriptions(); + // 3. Fetch subscription + mockService.getSubscriptions.mockResolvedValue({ + customerId: 'cus_1', + subscriptions: [MOCK_SUBSCRIPTION], + trialedProducts: [], + }); + const subscriptions = await rootMessenger.call( + 'SubscriptionController:getSubscriptions', + ); - expect(subscriptions).toStrictEqual([MOCK_SUBSCRIPTION]); - expect(controller.state.subscriptions).toStrictEqual([ - MOCK_SUBSCRIPTION, - ]); + expect(subscriptions).toStrictEqual([MOCK_SUBSCRIPTION]); + expect(controller.state.subscriptions).toStrictEqual([ + MOCK_SUBSCRIPTION, + ]); - // 4. Now cancel should work (user is subscribed) - mockService.cancelSubscription.mockResolvedValue(MOCK_SUBSCRIPTION); - expect( - await controller.cancelSubscription({ - subscriptionId: 'sub_123456789', - }), - ).toBeUndefined(); + // 4. Now cancel should work (user is subscribed) + mockService.cancelSubscription.mockResolvedValue(MOCK_SUBSCRIPTION); + expect( + await rootMessenger.call( + 'SubscriptionController:cancelSubscription', + { + subscriptionId: 'sub_123456789', + }, + ), + ).toBeUndefined(); - expect(mockService.cancelSubscription).toHaveBeenCalledWith({ - subscriptionId: 'sub_123456789', - }); - }); + expect(mockService.cancelSubscription).toHaveBeenCalledWith({ + subscriptionId: 'sub_123456789', + }); + }, + ); }); }); @@ -1127,10 +1172,12 @@ describe('SubscriptionController', () => { }; it('should return pricing response', async () => { - await withController(async ({ controller, mockService }) => { + await withController(async ({ rootMessenger, mockService }) => { mockService.getPricing.mockResolvedValue(mockPricingResponse); - const result = await controller.getPricing(); + const result = await rootMessenger.call( + 'SubscriptionController:getPricing', + ); expect(result).toStrictEqual(mockPricingResponse); }); @@ -1145,13 +1192,16 @@ describe('SubscriptionController', () => { pricing: MOCK_PRICE_INFO_RESPONSE, }, }, - async ({ controller }) => { - const result = controller.getCryptoApproveTransactionParams({ - chainId: '0x1', - paymentTokenAddress: '0xtoken', - productType: PRODUCT_TYPES.SHIELD, - interval: RECURRING_INTERVALS.month, - }); + async ({ rootMessenger }) => { + const result = rootMessenger.call( + 'SubscriptionController:getCryptoApproveTransactionParams', + { + chainId: '0x1', + paymentTokenAddress: '0xtoken', + productType: PRODUCT_TYPES.SHIELD, + interval: RECURRING_INTERVALS.month, + }, + ); expect(result).toStrictEqual({ approveAmount: '108000000000000000000', @@ -1164,14 +1214,17 @@ describe('SubscriptionController', () => { }); it('throws when pricing not found', async () => { - await withController(async ({ controller }) => { + await withController(async ({ rootMessenger }) => { expect(() => - controller.getCryptoApproveTransactionParams({ - chainId: '0x1', - paymentTokenAddress: '0xtoken', - productType: PRODUCT_TYPES.SHIELD, - interval: RECURRING_INTERVALS.month, - }), + rootMessenger.call( + 'SubscriptionController:getCryptoApproveTransactionParams', + { + chainId: '0x1', + paymentTokenAddress: '0xtoken', + productType: PRODUCT_TYPES.SHIELD, + interval: RECURRING_INTERVALS.month, + }, + ), ).toThrow('Subscription pricing not found'); }); }); @@ -1186,14 +1239,17 @@ describe('SubscriptionController', () => { }, }, }, - async ({ controller }) => { + async ({ rootMessenger }) => { expect(() => - controller.getCryptoApproveTransactionParams({ - chainId: '0x1', - paymentTokenAddress: '0xtoken', - productType: PRODUCT_TYPES.SHIELD, - interval: RECURRING_INTERVALS.month, - }), + rootMessenger.call( + 'SubscriptionController:getCryptoApproveTransactionParams', + { + chainId: '0x1', + paymentTokenAddress: '0xtoken', + productType: PRODUCT_TYPES.SHIELD, + interval: RECURRING_INTERVALS.month, + }, + ), ).toThrow('Product price not found'); }, ); @@ -1224,14 +1280,17 @@ describe('SubscriptionController', () => { }, }, }, - async ({ controller }) => { + async ({ rootMessenger }) => { expect(() => - controller.getCryptoApproveTransactionParams({ - chainId: '0x1', - paymentTokenAddress: '0xtoken', - productType: PRODUCT_TYPES.SHIELD, - interval: RECURRING_INTERVALS.month, - }), + rootMessenger.call( + 'SubscriptionController:getCryptoApproveTransactionParams', + { + chainId: '0x1', + paymentTokenAddress: '0xtoken', + productType: PRODUCT_TYPES.SHIELD, + interval: RECURRING_INTERVALS.month, + }, + ), ).toThrow('Price not found'); }, ); @@ -1251,14 +1310,17 @@ describe('SubscriptionController', () => { }, }, }, - async ({ controller }) => { + async ({ rootMessenger }) => { expect(() => - controller.getCryptoApproveTransactionParams({ - chainId: '0x1', - paymentTokenAddress: '0xtoken', - productType: PRODUCT_TYPES.SHIELD, - interval: RECURRING_INTERVALS.month, - }), + rootMessenger.call( + 'SubscriptionController:getCryptoApproveTransactionParams', + { + chainId: '0x1', + paymentTokenAddress: '0xtoken', + productType: PRODUCT_TYPES.SHIELD, + interval: RECURRING_INTERVALS.month, + }, + ), ).toThrow('Chains payment info not found'); }, ); @@ -1285,14 +1347,17 @@ describe('SubscriptionController', () => { }, }, }, - async ({ controller }) => { + async ({ rootMessenger }) => { expect(() => - controller.getCryptoApproveTransactionParams({ - chainId: '0x1', - paymentTokenAddress: '0xtoken', - productType: PRODUCT_TYPES.SHIELD, - interval: RECURRING_INTERVALS.month, - }), + rootMessenger.call( + 'SubscriptionController:getCryptoApproveTransactionParams', + { + chainId: '0x1', + paymentTokenAddress: '0xtoken', + productType: PRODUCT_TYPES.SHIELD, + interval: RECURRING_INTERVALS.month, + }, + ), ).toThrow('Invalid chain id'); }, ); @@ -1305,14 +1370,17 @@ describe('SubscriptionController', () => { pricing: MOCK_PRICE_INFO_RESPONSE, }, }, - async ({ controller }) => { + async ({ rootMessenger }) => { expect(() => - controller.getCryptoApproveTransactionParams({ - chainId: '0x1', - paymentTokenAddress: '0xtoken-invalid', - productType: PRODUCT_TYPES.SHIELD, - interval: RECURRING_INTERVALS.month, - }), + rootMessenger.call( + 'SubscriptionController:getCryptoApproveTransactionParams', + { + chainId: '0x1', + paymentTokenAddress: '0xtoken-invalid', + productType: PRODUCT_TYPES.SHIELD, + interval: RECURRING_INTERVALS.month, + }, + ), ).toThrow('Invalid token address'); }, ); @@ -1346,14 +1414,17 @@ describe('SubscriptionController', () => { }, }, }, - async ({ controller }) => { + async ({ rootMessenger }) => { expect(() => - controller.getCryptoApproveTransactionParams({ - chainId: '0x1', - paymentTokenAddress: '0xtoken', - productType: PRODUCT_TYPES.SHIELD, - interval: RECURRING_INTERVALS.month, - }), + rootMessenger.call( + 'SubscriptionController:getCryptoApproveTransactionParams', + { + chainId: '0x1', + paymentTokenAddress: '0xtoken', + productType: PRODUCT_TYPES.SHIELD, + interval: RECURRING_INTERVALS.month, + }, + ), ).toThrow('Conversion rate not found'); }, ); @@ -1362,7 +1433,7 @@ describe('SubscriptionController', () => { describe('getTokenMinimumBalanceAmount', () => { it('returns correct minimum balance amount for token', async () => { - await withController(async ({ controller }) => { + await withController(async ({ rootMessenger }) => { const [price] = MOCK_PRODUCT_PRICE.prices; const { chains } = MOCK_PRICING_PAYMENT_METHOD; if (!chains || chains.length === 0) { @@ -1370,7 +1441,8 @@ describe('SubscriptionController', () => { } const [tokenPaymentInfo] = chains[0].tokens; - const result = controller.getTokenMinimumBalanceAmount( + const result = rootMessenger.call( + 'SubscriptionController:getTokenMinimumBalanceAmount', price, tokenPaymentInfo, ); @@ -1380,7 +1452,7 @@ describe('SubscriptionController', () => { }); it('throws when conversion rate not found', async () => { - await withController(async ({ controller }) => { + await withController(async ({ rootMessenger }) => { const price = MOCK_PRODUCT_PRICE.prices[0]; const tokenPaymentInfoWithoutRate = { address: '0xtoken' as const, @@ -1390,7 +1462,8 @@ describe('SubscriptionController', () => { }; expect(() => - controller.getTokenMinimumBalanceAmount( + rootMessenger.call( + 'SubscriptionController:getTokenMinimumBalanceAmount', price, tokenPaymentInfoWithoutRate, ), @@ -1401,8 +1474,8 @@ describe('SubscriptionController', () => { describe('triggerAuthTokenRefresh', () => { it('should trigger auth token refresh', async () => { - await withController(async ({ controller, mockPerformSignOut }) => { - controller.triggerAccessTokenRefresh(); + await withController(async ({ rootMessenger, mockPerformSignOut }) => { + rootMessenger.call('SubscriptionController:triggerAccessTokenRefresh'); expect(mockPerformSignOut).toHaveBeenCalledWith(); }); @@ -1479,7 +1552,7 @@ describe('SubscriptionController', () => { describe('updatePaymentMethod', () => { it('should update card payment method successfully', async () => { - await withController(async ({ controller, mockService }) => { + await withController(async ({ rootMessenger, mockService }) => { const redirectUrl = 'https://redirect.com'; mockService.updatePaymentMethodCard.mockResolvedValue({ redirectUrl, @@ -1488,11 +1561,14 @@ describe('SubscriptionController', () => { MOCK_GET_SUBSCRIPTIONS_RESPONSE, ); - const result = await controller.updatePaymentMethod({ - subscriptionId: 'sub_123456789', - paymentType: PAYMENT_TYPES.byCard, - recurringInterval: RECURRING_INTERVALS.month, - }); + const result = await rootMessenger.call( + 'SubscriptionController:updatePaymentMethod', + { + subscriptionId: 'sub_123456789', + paymentType: PAYMENT_TYPES.byCard, + recurringInterval: RECURRING_INTERVALS.month, + }, + ); expect(mockService.updatePaymentMethodCard).toHaveBeenCalledWith({ subscriptionId: 'sub_123456789', @@ -1503,60 +1579,72 @@ describe('SubscriptionController', () => { }); it('should update crypto payment method successfully', async () => { - await withController(async ({ controller, mockService }) => { - mockService.updatePaymentMethodCrypto.mockResolvedValue(undefined); - mockService.getSubscriptions.mockResolvedValue( - MOCK_GET_SUBSCRIPTIONS_RESPONSE, - ); + await withController( + async ({ controller, rootMessenger, mockService }) => { + mockService.updatePaymentMethodCrypto.mockResolvedValue(undefined); + mockService.getSubscriptions.mockResolvedValue( + MOCK_GET_SUBSCRIPTIONS_RESPONSE, + ); - const opts: UpdatePaymentMethodOpts = { - paymentType: PAYMENT_TYPES.byCrypto, - subscriptionId: 'sub_123456789', - chainId: '0x1', - payerAddress: '0x0000000000000000000000000000000000000001', - tokenSymbol: 'USDC', - rawTransaction: '0xdeadbeef', - recurringInterval: RECURRING_INTERVALS.month, - billingCycles: 3, - }; + const opts: UpdatePaymentMethodOpts = { + paymentType: PAYMENT_TYPES.byCrypto, + subscriptionId: 'sub_123456789', + chainId: '0x1', + payerAddress: '0x0000000000000000000000000000000000000001', + tokenSymbol: 'USDC', + rawTransaction: '0xdeadbeef', + recurringInterval: RECURRING_INTERVALS.month, + billingCycles: 3, + }; - await controller.updatePaymentMethod(opts); + await rootMessenger.call( + 'SubscriptionController:updatePaymentMethod', + opts, + ); - const req = { - ...opts, - paymentType: undefined, - }; - expect(mockService.updatePaymentMethodCrypto).toHaveBeenCalledWith(req); + const req = { + ...opts, + paymentType: undefined, + }; + expect(mockService.updatePaymentMethodCrypto).toHaveBeenCalledWith( + req, + ); - expect(controller.state.subscriptions).toStrictEqual([ - MOCK_SUBSCRIPTION, - ]); - }); + expect(controller.state.subscriptions).toStrictEqual([ + MOCK_SUBSCRIPTION, + ]); + }, + ); }); it('throws when invalid payment type', async () => { - await withController(async ({ controller }) => { + await withController(async ({ rootMessenger }) => { const opts = { subscriptionId: 'sub_123456789', paymentType: 'invalid', recurringInterval: RECURRING_INTERVALS.month, }; - // @ts-expect-error Intentionally testing with invalid payment type. - await expect(controller.updatePaymentMethod(opts)).rejects.toThrow( - 'Invalid payment type', - ); + await expect( + rootMessenger.call( + 'SubscriptionController:updatePaymentMethod', + // @ts-expect-error Intentionally testing with invalid payment type. + opts, + ), + ).rejects.toThrow('Invalid payment type'); }); }); }); describe('getBillingPortalUrl', () => { it('should get the billing portal URL', async () => { - await withController(async ({ controller, mockService }) => { + await withController(async ({ rootMessenger, mockService }) => { mockService.getBillingPortalUrl.mockResolvedValue({ url: 'https://billing-portal.com', }); - const result = await controller.getBillingPortalUrl(); + const result = await rootMessenger.call( + 'SubscriptionController:getBillingPortalUrl', + ); expect(result).toStrictEqual({ url: 'https://billing-portal.com' }); }); }); @@ -1574,18 +1662,20 @@ describe('SubscriptionController', () => { }; it('should get the subscriptions eligibilities', async () => { - await withController(async ({ controller, mockService }) => { + await withController(async ({ rootMessenger, mockService }) => { mockService.getSubscriptionsEligibilities.mockResolvedValue([ MOCK_SUBSCRIPTION_ELIGIBILITY, ]); - const result = await controller.getSubscriptionsEligibilities(); + const result = await rootMessenger.call( + 'SubscriptionController:getSubscriptionsEligibilities', + ); expect(result).toStrictEqual([MOCK_SUBSCRIPTION_ELIGIBILITY]); }); }); it('should get the subscriptions eligibilities with balanceCategory parameter', async () => { - await withController(async ({ controller, mockService }) => { + await withController(async ({ rootMessenger, mockService }) => { const mockEligibilityWithCohorts: SubscriptionEligibility = { ...MOCK_SUBSCRIPTION_ELIGIBILITY, cohorts: MOCK_COHORTS, @@ -1597,9 +1687,12 @@ describe('SubscriptionController', () => { ]); const balanceCategory = '1k-9.9k'; - const result = await controller.getSubscriptionsEligibilities({ - balanceCategory, - }); + const result = await rootMessenger.call( + 'SubscriptionController:getSubscriptionsEligibilities', + { + balanceCategory, + }, + ); expect(result).toStrictEqual([mockEligibilityWithCohorts]); expect(mockService.getSubscriptionsEligibilities).toHaveBeenCalledWith({ balanceCategory, @@ -1608,14 +1701,16 @@ describe('SubscriptionController', () => { }); it('should handle subscription service errors', async () => { - await withController(async ({ controller, mockService }) => { + await withController(async ({ rootMessenger, mockService }) => { const errorMessage = 'Failed to get subscriptions eligibilities'; mockService.getSubscriptionsEligibilities.mockRejectedValue( new SubscriptionServiceError(errorMessage), ); await expect( - controller.getSubscriptionsEligibilities(), + rootMessenger.call( + 'SubscriptionController:getSubscriptionsEligibilities', + ), ).rejects.toThrow(SubscriptionServiceError); }); }); @@ -1623,14 +1718,17 @@ describe('SubscriptionController', () => { describe('submitUserEvent', () => { it('should submit user event successfully', async () => { - await withController(async ({ controller, mockService }) => { + await withController(async ({ rootMessenger, mockService }) => { const submitUserEventSpy = jest .spyOn(mockService, 'submitUserEvent') .mockResolvedValue(undefined); - const result = await controller.submitUserEvent({ - event: SubscriptionUserEvent.ShieldEntryModalViewed, - }); + const result = await rootMessenger.call( + 'SubscriptionController:submitUserEvent', + { + event: SubscriptionUserEvent.ShieldEntryModalViewed, + }, + ); expect(result).toBeUndefined(); expect(submitUserEventSpy).toHaveBeenCalledWith({ event: SubscriptionUserEvent.ShieldEntryModalViewed, @@ -1640,15 +1738,18 @@ describe('SubscriptionController', () => { }); it('should submit user event with cohort successfully', async () => { - await withController(async ({ controller, mockService }) => { + await withController(async ({ rootMessenger, mockService }) => { const submitUserEventSpy = jest .spyOn(mockService, 'submitUserEvent') .mockResolvedValue(undefined); - const result = await controller.submitUserEvent({ - event: SubscriptionUserEvent.ShieldCohortAssigned, - cohort: 'post_tx', - }); + const result = await rootMessenger.call( + 'SubscriptionController:submitUserEvent', + { + event: SubscriptionUserEvent.ShieldCohortAssigned, + cohort: 'post_tx', + }, + ); expect(result).toBeUndefined(); expect(submitUserEventSpy).toHaveBeenCalledWith({ event: SubscriptionUserEvent.ShieldCohortAssigned, @@ -1659,14 +1760,14 @@ describe('SubscriptionController', () => { }); it('should handle subscription service errors', async () => { - await withController(async ({ controller, mockService }) => { + await withController(async ({ rootMessenger, mockService }) => { const errorMessage = 'Failed to submit user event'; mockService.submitUserEvent.mockRejectedValue( new SubscriptionServiceError(errorMessage), ); await expect( - controller.submitUserEvent({ + rootMessenger.call('SubscriptionController:submitUserEvent', { event: SubscriptionUserEvent.ShieldEntryModalViewed, }), ).rejects.toThrow(SubscriptionServiceError); @@ -1676,14 +1777,17 @@ describe('SubscriptionController', () => { describe('assignUserToCohort', () => { it('should assign user to cohort successfully', async () => { - await withController(async ({ controller, mockService }) => { + await withController(async ({ rootMessenger, mockService }) => { const assignUserToCohortSpy = jest .spyOn(mockService, 'assignUserToCohort') .mockResolvedValue(undefined); - const result = await controller.assignUserToCohort({ - cohort: 'post_tx', - }); + const result = await rootMessenger.call( + 'SubscriptionController:assignUserToCohort', + { + cohort: 'post_tx', + }, + ); expect(result).toBeUndefined(); expect(assignUserToCohortSpy).toHaveBeenCalledWith({ cohort: 'post_tx', @@ -1693,14 +1797,16 @@ describe('SubscriptionController', () => { }); it('should handle subscription service errors', async () => { - await withController(async ({ controller, mockService }) => { + await withController(async ({ rootMessenger, mockService }) => { const errorMessage = 'Failed to assign user to cohort'; mockService.assignUserToCohort.mockRejectedValue( new SubscriptionServiceError(errorMessage), ); await expect( - controller.assignUserToCohort({ cohort: 'post_tx' }), + rootMessenger.call('SubscriptionController:assignUserToCohort', { + cohort: 'post_tx', + }), ).rejects.toThrow(SubscriptionServiceError); }); }); @@ -1715,11 +1821,15 @@ describe('SubscriptionController', () => { }; it('should cache last selected payment method successfully', async () => { - await withController(async ({ controller }) => { - controller.cacheLastSelectedPaymentMethod(PRODUCT_TYPES.SHIELD, { - type: PAYMENT_TYPES.byCard, - plan: RECURRING_INTERVALS.month, - }); + await withController(async ({ controller, rootMessenger }) => { + rootMessenger.call( + 'SubscriptionController:cacheLastSelectedPaymentMethod', + PRODUCT_TYPES.SHIELD, + { + type: PAYMENT_TYPES.byCard, + plan: RECURRING_INTERVALS.month, + }, + ); expect(controller.state.lastSelectedPaymentMethod).toStrictEqual({ [PRODUCT_TYPES.SHIELD]: { @@ -1742,7 +1852,7 @@ describe('SubscriptionController', () => { }, }, }, - async ({ controller }) => { + async ({ controller, rootMessenger }) => { expect(controller.state.lastSelectedPaymentMethod).toStrictEqual({ [PRODUCT_TYPES.SHIELD]: { type: PAYMENT_TYPES.byCard, @@ -1750,7 +1860,8 @@ describe('SubscriptionController', () => { }, }); - controller.cacheLastSelectedPaymentMethod( + rootMessenger.call( + 'SubscriptionController:cacheLastSelectedPaymentMethod', PRODUCT_TYPES.SHIELD, MOCK_CACHED_PAYMENT_METHOD, ); @@ -1763,12 +1874,16 @@ describe('SubscriptionController', () => { }); it('should throw error when payment token address is not provided for crypto payment', async () => { - await withController(({ controller }) => { + await withController(({ rootMessenger }) => { expect(() => - controller.cacheLastSelectedPaymentMethod(PRODUCT_TYPES.SHIELD, { - type: PAYMENT_TYPES.byCrypto, - plan: RECURRING_INTERVALS.month, - } as CachedLastSelectedPaymentMethod), + rootMessenger.call( + 'SubscriptionController:cacheLastSelectedPaymentMethod', + PRODUCT_TYPES.SHIELD, + { + type: PAYMENT_TYPES.byCrypto, + plan: RECURRING_INTERVALS.month, + } as CachedLastSelectedPaymentMethod, + ), ).toThrow( SubscriptionControllerErrorMessage.PaymentTokenAddressAndSymbolRequiredForCrypto, ); @@ -1789,7 +1904,7 @@ describe('SubscriptionController', () => { }, }, }, - async ({ controller }) => { + async ({ controller, rootMessenger }) => { expect(controller.state.lastSelectedPaymentMethod).toStrictEqual({ [PRODUCT_TYPES.SHIELD]: { type: PAYMENT_TYPES.byCard, @@ -1797,7 +1912,10 @@ describe('SubscriptionController', () => { }, }); - controller.clearLastSelectedPaymentMethod(PRODUCT_TYPES.SHIELD); + rootMessenger.call( + 'SubscriptionController:clearLastSelectedPaymentMethod', + PRODUCT_TYPES.SHIELD, + ); expect(controller.state.lastSelectedPaymentMethod).toStrictEqual({}); }, @@ -1805,10 +1923,13 @@ describe('SubscriptionController', () => { }); it('should do nothing when lastSelectedPaymentMethod is undefined', async () => { - await withController(async ({ controller }) => { + await withController(async ({ controller, rootMessenger }) => { expect(controller.state.lastSelectedPaymentMethod).toBeUndefined(); - controller.clearLastSelectedPaymentMethod(PRODUCT_TYPES.SHIELD); + rootMessenger.call( + 'SubscriptionController:clearLastSelectedPaymentMethod', + PRODUCT_TYPES.SHIELD, + ); expect(controller.state.lastSelectedPaymentMethod).toBeUndefined(); }); @@ -1831,12 +1952,15 @@ describe('SubscriptionController', () => { } as Record, }, }, - async ({ controller }) => { + async ({ controller, rootMessenger }) => { expect( controller.state.lastSelectedPaymentMethod?.[PRODUCT_TYPES.SHIELD], ).toBeDefined(); - controller.clearLastSelectedPaymentMethod(PRODUCT_TYPES.SHIELD); + rootMessenger.call( + 'SubscriptionController:clearLastSelectedPaymentMethod', + PRODUCT_TYPES.SHIELD, + ); expect( controller.state.lastSelectedPaymentMethod?.[ @@ -1868,7 +1992,7 @@ describe('SubscriptionController', () => { }, }, }, - async ({ controller }) => { + async ({ controller, rootMessenger }) => { expect(controller.state.subscriptions).toStrictEqual([ MOCK_SUBSCRIPTION, ]); @@ -1876,7 +2000,7 @@ describe('SubscriptionController', () => { MOCK_PRICE_INFO_RESPONSE, ); - controller.clearState(); + rootMessenger.call('SubscriptionController:clearState'); expect(controller.state).toStrictEqual( getDefaultSubscriptionControllerState(), @@ -1916,12 +2040,13 @@ describe('SubscriptionController', () => { pricing: MOCK_PRICE_INFO_RESPONSE, }, }, - async ({ controller, mockService }) => { + async ({ rootMessenger, mockService }) => { const submitSponsorshipIntentsSpy = jest .spyOn(mockService, 'submitSponsorshipIntents') .mockResolvedValue(undefined); - await controller.submitSponsorshipIntents( + await rootMessenger.call( + 'SubscriptionController:submitSponsorshipIntents', MOCK_SUBMISSION_INTENTS_REQUEST, ); expect(submitSponsorshipIntentsSpy).toHaveBeenCalledWith({ @@ -1935,12 +2060,15 @@ describe('SubscriptionController', () => { }); it('should throw error when products array is empty', async () => { - await withController(async ({ controller }) => { + await withController(async ({ rootMessenger }) => { await expect( - controller.submitSponsorshipIntents({ - ...MOCK_SUBMISSION_INTENTS_REQUEST, - products: [], - }), + rootMessenger.call( + 'SubscriptionController:submitSponsorshipIntents', + { + ...MOCK_SUBMISSION_INTENTS_REQUEST, + products: [], + }, + ), ).rejects.toThrow( SubscriptionControllerErrorMessage.SubscriptionProductsEmpty, ); @@ -1954,9 +2082,10 @@ describe('SubscriptionController', () => { subscriptions: [MOCK_SUBSCRIPTION], }, }, - async ({ controller, mockService }) => { + async ({ rootMessenger, mockService }) => { await expect( - controller.submitSponsorshipIntents( + rootMessenger.call( + 'SubscriptionController:submitSponsorshipIntents', MOCK_SUBMISSION_INTENTS_REQUEST, ), ).rejects.toThrow( @@ -1984,10 +2113,11 @@ describe('SubscriptionController', () => { trialedProducts: [PRODUCT_TYPES.SHIELD], }, }, - async ({ controller, mockService }) => { + async ({ rootMessenger, mockService }) => { mockService.submitSponsorshipIntents.mockResolvedValue(undefined); - const isSponsored = await controller.submitSponsorshipIntents( + const isSponsored = await rootMessenger.call( + 'SubscriptionController:submitSponsorshipIntents', MOCK_SUBMISSION_INTENTS_REQUEST, ); expect(isSponsored).toBe(false); @@ -2017,8 +2147,9 @@ describe('SubscriptionController', () => { }, }, }, - async ({ controller, mockService }) => { - const isSponsored = await controller.submitSponsorshipIntents( + async ({ rootMessenger, mockService }) => { + const isSponsored = await rootMessenger.call( + 'SubscriptionController:submitSponsorshipIntents', MOCK_SUBMISSION_INTENTS_REQUEST, ); expect(isSponsored).toBe(false); @@ -2028,9 +2159,12 @@ describe('SubscriptionController', () => { }); it('should throw error when no cached payment method is found', async () => { - await withController(async ({ controller }) => { + await withController(async ({ rootMessenger }) => { await expect( - controller.submitSponsorshipIntents(MOCK_SUBMISSION_INTENTS_REQUEST), + rootMessenger.call( + 'SubscriptionController:submitSponsorshipIntents', + MOCK_SUBMISSION_INTENTS_REQUEST, + ), ).rejects.toThrow( SubscriptionControllerErrorMessage.PaymentMethodNotCrypto, ); @@ -2049,9 +2183,10 @@ describe('SubscriptionController', () => { }, }, }, - async ({ controller }) => { + async ({ rootMessenger }) => { await expect( - controller.submitSponsorshipIntents( + rootMessenger.call( + 'SubscriptionController:submitSponsorshipIntents', MOCK_SUBMISSION_INTENTS_REQUEST, ), ).rejects.toThrow( @@ -2072,9 +2207,10 @@ describe('SubscriptionController', () => { }, }, }, - async ({ controller }) => { + async ({ rootMessenger }) => { await expect( - controller.submitSponsorshipIntents( + rootMessenger.call( + 'SubscriptionController:submitSponsorshipIntents', MOCK_SUBMISSION_INTENTS_REQUEST, ), ).rejects.toThrow( @@ -2097,7 +2233,7 @@ describe('SubscriptionController', () => { pricing: MOCK_PRICE_INFO_RESPONSE, }, }, - async ({ controller, mockService }) => { + async ({ rootMessenger, mockService }) => { mockService.submitSponsorshipIntents.mockRejectedValue( new SubscriptionServiceError( 'Failed to submit sponsorship intents', @@ -2105,7 +2241,8 @@ describe('SubscriptionController', () => { ); await expect( - controller.submitSponsorshipIntents( + rootMessenger.call( + 'SubscriptionController:submitSponsorshipIntents', MOCK_SUBMISSION_INTENTS_REQUEST, ), ).rejects.toThrow(SubscriptionServiceError); @@ -2138,7 +2275,7 @@ describe('SubscriptionController', () => { }, }, }, - async ({ controller, mockService }) => { + async ({ rootMessenger, mockService }) => { mockService.startSubscriptionWithCrypto.mockResolvedValue({ subscriptionId: 'sub_123', status: SUBSCRIPTION_STATUSES.trialing, @@ -2165,7 +2302,10 @@ describe('SubscriptionController', () => { status: TransactionStatus.submitted, }; - await controller.submitShieldSubscriptionCryptoApproval(txMeta); + await rootMessenger.call( + 'SubscriptionController:submitShieldSubscriptionCryptoApproval', + txMeta, + ); expect(mockService.startSubscriptionWithCrypto).toHaveBeenCalledTimes( 1, @@ -2191,7 +2331,7 @@ describe('SubscriptionController', () => { }, }, }, - async ({ controller, mockService }) => { + async ({ rootMessenger, mockService }) => { mockService.startSubscriptionWithCrypto.mockResolvedValue({ subscriptionId: 'sub_123', status: SUBSCRIPTION_STATUSES.trialing, @@ -2218,7 +2358,8 @@ describe('SubscriptionController', () => { status: TransactionStatus.submitted, }; - await controller.submitShieldSubscriptionCryptoApproval( + await rootMessenger.call( + 'SubscriptionController:submitShieldSubscriptionCryptoApproval', txMeta, false, // isSponsored 'eip155:1:0x1234567890123456789012345678901234567890', @@ -2251,7 +2392,7 @@ describe('SubscriptionController', () => { subscriptions: [], }, }, - async ({ controller, mockService }) => { + async ({ rootMessenger, mockService }) => { // Create a non-shield subscription transaction const txMeta = { ...generateMockTxMeta(), @@ -2262,7 +2403,10 @@ describe('SubscriptionController', () => { }; await expect( - controller.submitShieldSubscriptionCryptoApproval(txMeta), + rootMessenger.call( + 'SubscriptionController:submitShieldSubscriptionCryptoApproval', + txMeta, + ), ).rejects.toThrow('Subscription pricing not found'); // Verify that startSubscriptionWithCrypto was not called @@ -2282,7 +2426,7 @@ describe('SubscriptionController', () => { subscriptions: [], }, }, - async ({ controller, mockService }) => { + async ({ rootMessenger, mockService }) => { // Create a non-shield subscription transaction const txMeta = { ...generateMockTxMeta(), @@ -2291,7 +2435,10 @@ describe('SubscriptionController', () => { hash: '0x123', }; - await controller.submitShieldSubscriptionCryptoApproval(txMeta); + await rootMessenger.call( + 'SubscriptionController:submitShieldSubscriptionCryptoApproval', + txMeta, + ); // Verify that decodeTransactionDataHandler was not called expect( @@ -2310,7 +2457,7 @@ describe('SubscriptionController', () => { subscriptions: [], }, }, - async ({ controller, mockService }) => { + async ({ rootMessenger, mockService }) => { // Create a transaction without chainId const txMeta = { ...generateMockTxMeta(), @@ -2327,7 +2474,10 @@ describe('SubscriptionController', () => { }; await expect( - controller.submitShieldSubscriptionCryptoApproval(txMeta), + rootMessenger.call( + 'SubscriptionController:submitShieldSubscriptionCryptoApproval', + txMeta, + ), ).rejects.toThrow('Chain ID or raw transaction not found'); // Verify that decodeTransactionDataHandler was not called due to early error @@ -2347,7 +2497,7 @@ describe('SubscriptionController', () => { subscriptions: [], }, }, - async ({ controller, mockService }) => { + async ({ rootMessenger, mockService }) => { // Create a shield subscription approval transaction with token address that doesn't exist const txMeta = { ...generateMockTxMeta(), @@ -2364,7 +2514,10 @@ describe('SubscriptionController', () => { }; await expect( - controller.submitShieldSubscriptionCryptoApproval(txMeta), + rootMessenger.call( + 'SubscriptionController:submitShieldSubscriptionCryptoApproval', + txMeta, + ), ).rejects.toThrow('Last selected payment method not found'); expect( @@ -2391,7 +2544,7 @@ describe('SubscriptionController', () => { }, }, }, - async ({ controller, mockService }) => { + async ({ rootMessenger, mockService }) => { // Create a shield subscription approval transaction const txMeta = { ...generateMockTxMeta(), @@ -2408,7 +2561,10 @@ describe('SubscriptionController', () => { }; await expect( - controller.submitShieldSubscriptionCryptoApproval(txMeta), + rootMessenger.call( + 'SubscriptionController:submitShieldSubscriptionCryptoApproval', + txMeta, + ), ).rejects.toThrow( SubscriptionControllerErrorMessage.ProductPriceNotFound, ); @@ -2437,7 +2593,7 @@ describe('SubscriptionController', () => { }, }, }, - async ({ controller, mockService }) => { + async ({ rootMessenger, mockService }) => { mockService.updatePaymentMethodCrypto.mockResolvedValue(undefined); mockService.getSubscriptions.mockResolvedValue( MOCK_GET_SUBSCRIPTIONS_RESPONSE, @@ -2456,7 +2612,10 @@ describe('SubscriptionController', () => { status: TransactionStatus.submitted, }; - await controller.submitShieldSubscriptionCryptoApproval(txMeta); + await rootMessenger.call( + 'SubscriptionController:submitShieldSubscriptionCryptoApproval', + txMeta, + ); expect(mockService.updatePaymentMethodCrypto).toHaveBeenCalledTimes( 1, @@ -2485,7 +2644,7 @@ describe('SubscriptionController', () => { }, }, }, - async ({ controller, mockService }) => { + async ({ rootMessenger, mockService }) => { mockService.getSubscriptions.mockResolvedValue({ subscriptions: [ { @@ -2510,7 +2669,10 @@ describe('SubscriptionController', () => { }; await expect( - controller.submitShieldSubscriptionCryptoApproval(txMeta), + rootMessenger.call( + 'SubscriptionController:submitShieldSubscriptionCryptoApproval', + txMeta, + ), ).rejects.toThrow( SubscriptionControllerErrorMessage.SubscriptionNotValidForCryptoApproval, ); @@ -2527,13 +2689,13 @@ describe('SubscriptionController', () => { subscriptions: [MOCK_SUBSCRIPTION], }, }, - async ({ controller, mockService }) => { + async ({ rootMessenger, mockService }) => { const linkRewardsSpy = jest .spyOn(mockService, 'linkRewards') .mockResolvedValue({ success: true, }); - await controller.linkRewards({ + await rootMessenger.call('SubscriptionController:linkRewards', { subscriptionId: 'sub_123456789', rewardAccountId: 'eip155:1:0x1234567890123456789012345678901234567890', @@ -2548,9 +2710,9 @@ describe('SubscriptionController', () => { }); it('should throw error when user is not subscribed', async () => { - await withController(async ({ controller }) => { + await withController(async ({ rootMessenger }) => { await expect( - controller.linkRewards({ + rootMessenger.call('SubscriptionController:linkRewards', { subscriptionId: 'sub_123456789', rewardAccountId: 'eip155:1:0x1234567890123456789012345678901234567890', @@ -2566,12 +2728,12 @@ describe('SubscriptionController', () => { subscriptions: [MOCK_SUBSCRIPTION], }, }, - async ({ controller, mockService }) => { + async ({ rootMessenger, mockService }) => { mockService.linkRewards.mockResolvedValue({ success: false, }); await expect( - controller.linkRewards({ + rootMessenger.call('SubscriptionController:linkRewards', { subscriptionId: 'sub_123456789', rewardAccountId: 'eip155:1:0x1234567890123456789012345678901234567890', diff --git a/packages/subscription-controller/src/SubscriptionController.ts b/packages/subscription-controller/src/SubscriptionController.ts index e640a5005dc..2b01d0bd523 100644 --- a/packages/subscription-controller/src/SubscriptionController.ts +++ b/packages/subscription-controller/src/SubscriptionController.ts @@ -17,6 +17,7 @@ import { DEFAULT_POLLING_INTERVAL, SubscriptionControllerErrorMessage, } from './constants'; +import type { SubscriptionControllerMethodActions } from './SubscriptionController-method-action-types'; import { PAYMENT_TYPES, PRODUCT_TYPES, SUBSCRIPTION_STATUSES } from './types'; import type { AssignCohortRequest, @@ -68,90 +69,13 @@ export type SubscriptionControllerState = { >; }; -// Messenger Actions -export type SubscriptionControllerGetSubscriptionsAction = { - type: `${typeof controllerName}:getSubscriptions`; - handler: SubscriptionController['getSubscriptions']; -}; -export type SubscriptionControllerGetSubscriptionByProductAction = { - type: `${typeof controllerName}:getSubscriptionByProduct`; - handler: SubscriptionController['getSubscriptionByProduct']; -}; -export type SubscriptionControllerCancelSubscriptionAction = { - type: `${typeof controllerName}:cancelSubscription`; - handler: SubscriptionController['cancelSubscription']; -}; -export type SubscriptionControllerStartShieldSubscriptionWithCardAction = { - type: `${typeof controllerName}:startShieldSubscriptionWithCard`; - handler: SubscriptionController['startShieldSubscriptionWithCard']; -}; -export type SubscriptionControllerGetPricingAction = { - type: `${typeof controllerName}:getPricing`; - handler: SubscriptionController['getPricing']; -}; -export type SubscriptionControllerGetCryptoApproveTransactionParamsAction = { - type: `${typeof controllerName}:getCryptoApproveTransactionParams`; - handler: SubscriptionController['getCryptoApproveTransactionParams']; -}; -export type SubscriptionControllerStartSubscriptionWithCryptoAction = { - type: `${typeof controllerName}:startSubscriptionWithCrypto`; - handler: SubscriptionController['startSubscriptionWithCrypto']; -}; -export type SubscriptionControllerUpdatePaymentMethodAction = { - type: `${typeof controllerName}:updatePaymentMethod`; - handler: SubscriptionController['updatePaymentMethod']; -}; -export type SubscriptionControllerGetBillingPortalUrlAction = { - type: `${typeof controllerName}:getBillingPortalUrl`; - handler: SubscriptionController['getBillingPortalUrl']; -}; - -export type SubscriptionControllerSubmitSponsorshipIntentsAction = { - type: `${typeof controllerName}:submitSponsorshipIntents`; - handler: SubscriptionController['submitSponsorshipIntents']; -}; - -export type SubscriptionControllerCacheLastSelectedPaymentMethodAction = { - type: `${typeof controllerName}:cacheLastSelectedPaymentMethod`; - handler: SubscriptionController['cacheLastSelectedPaymentMethod']; -}; - -export type SubscriptionControllerClearLastSelectedPaymentMethodAction = { - type: `${typeof controllerName}:clearLastSelectedPaymentMethod`; - handler: SubscriptionController['clearLastSelectedPaymentMethod']; -}; - -export type SubscriptionControllerLinkRewardsAction = { - type: `${typeof controllerName}:linkRewards`; - handler: SubscriptionController['linkRewards']; -}; - -export type SubscriptionControllerSubmitShieldSubscriptionCryptoApprovalAction = - { - type: `${typeof controllerName}:submitShieldSubscriptionCryptoApproval`; - handler: SubscriptionController['submitShieldSubscriptionCryptoApproval']; - }; - export type SubscriptionControllerGetStateAction = ControllerGetStateAction< typeof controllerName, SubscriptionControllerState >; export type SubscriptionControllerActions = - | SubscriptionControllerGetSubscriptionsAction - | SubscriptionControllerGetSubscriptionByProductAction - | SubscriptionControllerCancelSubscriptionAction - | SubscriptionControllerStartShieldSubscriptionWithCardAction - | SubscriptionControllerGetPricingAction | SubscriptionControllerGetStateAction - | SubscriptionControllerGetCryptoApproveTransactionParamsAction - | SubscriptionControllerStartSubscriptionWithCryptoAction - | SubscriptionControllerUpdatePaymentMethodAction - | SubscriptionControllerGetBillingPortalUrlAction - | SubscriptionControllerSubmitSponsorshipIntentsAction - | SubscriptionControllerSubmitShieldSubscriptionCryptoApprovalAction - | SubscriptionControllerLinkRewardsAction - | SubscriptionControllerCacheLastSelectedPaymentMethodAction - | SubscriptionControllerClearLastSelectedPaymentMethodAction; + | SubscriptionControllerMethodActions; export type AllowedActions = | AuthenticationController.AuthenticationControllerGetBearerTokenAction @@ -264,6 +188,31 @@ const subscriptionControllerMetadata: StateMetadata }, }; +const MESSENGER_EXPOSED_METHODS = [ + 'getPricing', + 'getSubscriptions', + 'getSubscriptionByProduct', + 'getSubscriptionsEligibilities', + 'cancelSubscription', + 'unCancelSubscription', + 'startShieldSubscriptionWithCard', + 'startSubscriptionWithCrypto', + 'submitShieldSubscriptionCryptoApproval', + 'getCryptoApproveTransactionParams', + 'updatePaymentMethod', + 'getBillingPortalUrl', + 'cacheLastSelectedPaymentMethod', + 'clearLastSelectedPaymentMethod', + 'submitSponsorshipIntents', + 'submitUserEvent', + 'assignUserToCohort', + 'linkRewards', + 'getTokenApproveAmount', + 'getTokenMinimumBalanceAmount', + 'clearState', + 'triggerAccessTokenRefresh', +] as const; + export class SubscriptionController extends StaticIntervalPollingController()< typeof controllerName, SubscriptionControllerState, @@ -298,82 +247,9 @@ export class SubscriptionController extends StaticIntervalPollingController()< this.setIntervalLength(pollingInterval); this.#subscriptionService = subscriptionService; - this.#registerMessageHandlers(); - } - - /** - * Constructor helper for registering this controller's messaging system - * actions. - */ - #registerMessageHandlers(): void { - this.messenger.registerActionHandler( - 'SubscriptionController:getSubscriptions', - this.getSubscriptions.bind(this), - ); - - this.messenger.registerActionHandler( - 'SubscriptionController:getSubscriptionByProduct', - this.getSubscriptionByProduct.bind(this), - ); - - this.messenger.registerActionHandler( - 'SubscriptionController:cancelSubscription', - this.cancelSubscription.bind(this), - ); - - this.messenger.registerActionHandler( - 'SubscriptionController:startShieldSubscriptionWithCard', - this.startShieldSubscriptionWithCard.bind(this), - ); - - this.messenger.registerActionHandler( - 'SubscriptionController:getPricing', - this.getPricing.bind(this), - ); - - this.messenger.registerActionHandler( - 'SubscriptionController:getCryptoApproveTransactionParams', - this.getCryptoApproveTransactionParams.bind(this), - ); - - this.messenger.registerActionHandler( - 'SubscriptionController:startSubscriptionWithCrypto', - this.startSubscriptionWithCrypto.bind(this), - ); - - this.messenger.registerActionHandler( - 'SubscriptionController:updatePaymentMethod', - this.updatePaymentMethod.bind(this), - ); - - this.messenger.registerActionHandler( - 'SubscriptionController:getBillingPortalUrl', - this.getBillingPortalUrl.bind(this), - ); - - this.messenger.registerActionHandler( - `${controllerName}:submitSponsorshipIntents`, - this.submitSponsorshipIntents.bind(this), - ); - - this.messenger.registerActionHandler( - `${controllerName}:submitShieldSubscriptionCryptoApproval`, - this.submitShieldSubscriptionCryptoApproval.bind(this), - ); - - this.messenger.registerActionHandler( - `${controllerName}:linkRewards`, - this.linkRewards.bind(this), - ); - - this.messenger.registerActionHandler( - `${controllerName}:cacheLastSelectedPaymentMethod`, - this.cacheLastSelectedPaymentMethod.bind(this), - ); - - this.messenger.registerActionHandler( - `${controllerName}:clearLastSelectedPaymentMethod`, - this.clearLastSelectedPaymentMethod.bind(this), + this.messenger.registerMethodActionHandlers( + this, + MESSENGER_EXPOSED_METHODS, ); } diff --git a/packages/subscription-controller/src/index.ts b/packages/subscription-controller/src/index.ts index faa87dbca01..1eaa2d930a9 100644 --- a/packages/subscription-controller/src/index.ts +++ b/packages/subscription-controller/src/index.ts @@ -2,27 +2,37 @@ export type { SubscriptionControllerActions, SubscriptionControllerState, SubscriptionControllerEvents, + SubscriptionControllerGetStateAction, + SubscriptionControllerMessenger, + SubscriptionControllerOptions, + SubscriptionControllerStateChangeEvent, + AllowedActions, + AllowedEvents, +} from './SubscriptionController'; +export type { + SubscriptionControllerGetPricingAction, SubscriptionControllerGetSubscriptionsAction, SubscriptionControllerGetSubscriptionByProductAction, + SubscriptionControllerGetSubscriptionsEligibilitiesAction, SubscriptionControllerCancelSubscriptionAction, + SubscriptionControllerUnCancelSubscriptionAction, SubscriptionControllerStartShieldSubscriptionWithCardAction, - SubscriptionControllerGetPricingAction, - SubscriptionControllerGetCryptoApproveTransactionParamsAction, SubscriptionControllerStartSubscriptionWithCryptoAction, - SubscriptionControllerGetBillingPortalUrlAction, + SubscriptionControllerSubmitShieldSubscriptionCryptoApprovalAction, + SubscriptionControllerGetCryptoApproveTransactionParamsAction, SubscriptionControllerUpdatePaymentMethodAction, - SubscriptionControllerGetStateAction, - SubscriptionControllerMessenger, - SubscriptionControllerOptions, - SubscriptionControllerStateChangeEvent, - SubscriptionControllerSubmitSponsorshipIntentsAction, + SubscriptionControllerGetBillingPortalUrlAction, SubscriptionControllerCacheLastSelectedPaymentMethodAction, SubscriptionControllerClearLastSelectedPaymentMethodAction, + SubscriptionControllerSubmitSponsorshipIntentsAction, + SubscriptionControllerSubmitUserEventAction, + SubscriptionControllerAssignUserToCohortAction, SubscriptionControllerLinkRewardsAction, - SubscriptionControllerSubmitShieldSubscriptionCryptoApprovalAction, - AllowedActions, - AllowedEvents, -} from './SubscriptionController'; + SubscriptionControllerGetTokenApproveAmountAction, + SubscriptionControllerGetTokenMinimumBalanceAmountAction, + SubscriptionControllerClearStateAction, + SubscriptionControllerTriggerAccessTokenRefreshAction, +} from './SubscriptionController-method-action-types'; export { SubscriptionController, getDefaultSubscriptionControllerState, diff --git a/yarn.lock b/yarn.lock index a4d8ecb8c9f..dd65161780d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3143,6 +3143,7 @@ __metadata: deepmerge: "npm:^4.2.2" jest: "npm:^29.7.0" ts-jest: "npm:^29.2.5" + tsx: "npm:^4.20.5" typedoc: "npm:^0.25.13" typedoc-plugin-missing-exports: "npm:^2.0.0" typescript: "npm:~5.3.3" @@ -5084,6 +5085,7 @@ __metadata: jest-environment-node: "npm:^29.7.0" nock: "npm:^13.3.1" ts-jest: "npm:^29.2.5" + tsx: "npm:^4.20.5" typedoc: "npm:^0.25.13" typedoc-plugin-missing-exports: "npm:^2.0.0" typescript: "npm:~5.3.3" @@ -5139,6 +5141,7 @@ __metadata: jest: "npm:^29.7.0" lodash: "npm:^4.17.21" ts-jest: "npm:^29.2.5" + tsx: "npm:^4.20.5" typedoc: "npm:^0.25.13" typedoc-plugin-missing-exports: "npm:^2.0.0" typescript: "npm:~5.3.3" @@ -5376,6 +5379,7 @@ __metadata: deepmerge: "npm:^4.2.2" jest: "npm:^29.7.0" ts-jest: "npm:^29.2.5" + tsx: "npm:^4.20.5" typedoc: "npm:^0.25.13" typedoc-plugin-missing-exports: "npm:^2.0.0" typescript: "npm:~5.3.3"