diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 360764abba6..e86feeb7514 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -149,6 +149,7 @@ /packages/wallet/src/initialization/instances/accounts-controller/ @MetaMask/accounts-engineers /packages/wallet/src/initialization/instances/address-book-controller/ @MetaMask/confirmations /packages/wallet/src/initialization/instances/approval-controller/ @MetaMask/confirmations +/packages/wallet/src/initialization/instances/claims-controller/ @MetaMask/web3auth /packages/wallet/src/initialization/instances/connectivity-controller/ @MetaMask/core-platform /packages/wallet/src/initialization/instances/gas-fee-controller/ @MetaMask/confirmations /packages/wallet/src/initialization/instances/keyring-controller/ @MetaMask/accounts-engineers @MetaMask/core-platform diff --git a/README.md b/README.md index e03df83f857..b3c7b4e8f59 100644 --- a/README.md +++ b/README.md @@ -647,6 +647,7 @@ linkStyle default opacity:0.5 wallet --> address_book_controller; wallet --> approval_controller; wallet --> base_controller; + wallet --> claims_controller; wallet --> connectivity_controller; wallet --> controller_utils; wallet --> gas_fee_controller; diff --git a/codeowners.ts b/codeowners.ts index 2a864d499ba..06e24da31f6 100644 --- a/codeowners.ts +++ b/codeowners.ts @@ -98,6 +98,7 @@ const PACKAGES: Record = { }, 'claims-controller': { teams: ['@MetaMask/web3auth'], + initializationPath: 'claims-controller', }, 'client-controller': { teams: [ diff --git a/packages/wallet-cli/CHANGELOG.md b/packages/wallet-cli/CHANGELOG.md index e70a1ee28fa..dea09199568 100644 --- a/packages/wallet-cli/CHANGELOG.md +++ b/packages/wallet-cli/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Wire the `claimsService` slot in the daemon wallet's instance options (`ClaimsEnv.PRD` and `globalThis.fetch`), so the daemon initializes `ClaimsService` with the wallet's required configuration ([#9588](https://github.com/MetaMask/core/pull/9588)) - Wire the `transactionController` slot in the daemon wallet's instance options, so the daemon runs the `TransactionController` with an explicit CLI-appropriate configuration (swaps processing disabled, no client hooks) rather than relying on the controller's implicit defaults ([#9509](https://github.com/MetaMask/core/pull/9509)) - Wire the `gasFeeController` slot in the daemon wallet's instance options, passing `clientId: 'cli'` so the CLI identifies itself to the gas estimation API, now that `@metamask/wallet` requires this option ([#9527](https://github.com/MetaMask/core/pull/9527)) - Add the `mm wallet unlock` command, which dispatches `KeyringController:submitPassword` over the daemon socket, allowing the keyring to be unlocked after a daemon start with no password or after a `mm daemon call KeyringController:setLocked` ([#8821](https://github.com/MetaMask/core/pull/8821)) diff --git a/packages/wallet-cli/src/daemon/wallet-factory.test.ts b/packages/wallet-cli/src/daemon/wallet-factory.test.ts index d50a2b4fd78..327d425cb56 100644 --- a/packages/wallet-cli/src/daemon/wallet-factory.test.ts +++ b/packages/wallet-cli/src/daemon/wallet-factory.test.ts @@ -2,6 +2,7 @@ import { ClientConfigApiService } from '@metamask/remote-feature-flag-controller import { InMemoryStorageAdapter } from '@metamask/storage-service'; import { AlwaysOnlineAdapter, + ClaimsEnv, importSecretRecoveryPhrase, Wallet, } from '@metamask/wallet'; @@ -121,6 +122,10 @@ describe('createWallet', () => { ); expect(instanceOptions.transactionController?.disableSwaps).toBe(true); expect(instanceOptions.transactionController?.hooks).toStrictEqual({}); + expect(instanceOptions.claimsService).toStrictEqual({ + env: ClaimsEnv.PRD, + fetchFunction: globalThis.fetch, + }); expect(ClientConfigApiService).toHaveBeenCalled(); await dispose(); diff --git a/packages/wallet-cli/src/daemon/wallet-factory.ts b/packages/wallet-cli/src/daemon/wallet-factory.ts index 832c16a155a..b56b61cafc0 100644 --- a/packages/wallet-cli/src/daemon/wallet-factory.ts +++ b/packages/wallet-cli/src/daemon/wallet-factory.ts @@ -8,6 +8,7 @@ import { InMemoryStorageAdapter } from '@metamask/storage-service'; import type { Json } from '@metamask/utils'; import { AlwaysOnlineAdapter, + ClaimsEnv, importSecretRecoveryPhrase, Wallet, } from '@metamask/wallet'; @@ -65,6 +66,8 @@ export type CreateWalletResult = { * - `transactionController` — swaps processing disabled and no client hooks; * see the slot's inline comment for why the daemon relies on the * controller's defaults for everything else. + * - `claimsService` — production Claims API environment and the platform + * `fetch` implementation. * * The optional `keyringController` slot is intentionally omitted so the * controller's built-in defaults (e.g. the PBKDF2 encryptor) apply. @@ -121,6 +124,10 @@ function buildInstanceOptions( // the controller's default. hooks: {}, }, + claimsService: { + env: ClaimsEnv.PRD, + fetchFunction: globalThis.fetch, + }, }; } diff --git a/packages/wallet/CHANGELOG.md b/packages/wallet/CHANGELOG.md index 9eaa012500c..6d638d3dfb4 100644 --- a/packages/wallet/CHANGELOG.md +++ b/packages/wallet/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- **BREAKING:** Wire `ClaimsService` and `ClaimsController` into the default wallet initialization ([#9588](https://github.com/MetaMask/core/pull/9588)) + - Passing `instanceOptions.claimsService.env` is now required. + - Passing `instanceOptions.claimsService.fetchFunction` is now required. + - `ClaimsService` delegates `AuthenticationController:getBearerToken`; hosts must register `AuthenticationController` on the supplied root messenger before authenticated Claims API calls succeed. + - Re-exports `Env` from `@metamask/claims-controller` as `ClaimsEnv` for `instanceOptions.claimsService.env`. + ## [8.0.0] ### Added diff --git a/packages/wallet/package.json b/packages/wallet/package.json index e9ebc5e43ae..e1d946f5823 100644 --- a/packages/wallet/package.json +++ b/packages/wallet/package.json @@ -60,6 +60,7 @@ "@metamask/approval-controller": "^9.0.2", "@metamask/base-controller": "^9.1.0", "@metamask/browser-passworder": "^6.0.0", + "@metamask/claims-controller": "^0.5.3", "@metamask/connectivity-controller": "^0.3.0", "@metamask/controller-utils": "^12.3.0", "@metamask/gas-fee-controller": "^26.2.4", diff --git a/packages/wallet/src/Wallet.test.ts b/packages/wallet/src/Wallet.test.ts index e370c699aa0..2e0d93af926 100644 --- a/packages/wallet/src/Wallet.test.ts +++ b/packages/wallet/src/Wallet.test.ts @@ -1,4 +1,6 @@ import { getDefaultAddressBookControllerState } from '@metamask/address-book-controller'; +import { Env } from '@metamask/claims-controller'; +import { getDefaultClaimsControllerState } from '@metamask/claims-controller'; import { CONNECTIVITY_STATUSES } from '@metamask/connectivity-controller'; import { Messenger } from '@metamask/messenger'; import { InMemoryStorageAdapter } from '@metamask/storage-service'; @@ -8,6 +10,7 @@ import { webcrypto } from 'crypto'; import MockEncryptor from '../../keyring-controller/tests/mocks/mockEncryptor.js'; import * as initializationModule from './initialization/initialization.js'; import { AlwaysOnlineAdapter } from './initialization/instances/connectivity-controller/always-online-adapter.js'; +import type { WalletOptions } from './types.js'; import { importSecretRecoveryPhrase } from './utilities.js'; import { Wallet } from './Wallet.js'; @@ -23,23 +26,31 @@ const REMOTE_FEATURE_FLAG_OPTIONS = { }, }; +function getInstanceOptions(): WalletOptions['instanceOptions'] { + return { + connectivityController: { + connectivityAdapter: new AlwaysOnlineAdapter(), + }, + gasFeeController: { + clientId: 'test', + }, + networkController: { + infuraProjectId: 'fake-infura-project-id', + }, + storageService: { + storage: new InMemoryStorageAdapter(), + }, + remoteFeatureFlagController: REMOTE_FEATURE_FLAG_OPTIONS, + claimsService: { + env: Env.DEV, + fetchFunction: globalThis.fetch, + }, + }; +} + async function setupWallet(): Promise { const wallet = new Wallet({ - instanceOptions: { - connectivityController: { - connectivityAdapter: new AlwaysOnlineAdapter(), - }, - gasFeeController: { - clientId: 'test', - }, - networkController: { - infuraProjectId: 'fake-infura-project-id', - }, - storageService: { - storage: new InMemoryStorageAdapter(), - }, - remoteFeatureFlagController: REMOTE_FEATURE_FLAG_OPTIONS, - }, + instanceOptions: getInstanceOptions(), }); await importSecretRecoveryPhrase(wallet, TEST_PASSWORD, TEST_SRP); @@ -89,22 +100,10 @@ describe('Wallet', () => { it('supports passing instance options', async () => { const wallet = new Wallet({ instanceOptions: { - connectivityController: { - connectivityAdapter: new AlwaysOnlineAdapter(), - }, - gasFeeController: { - clientId: 'test', - }, + ...getInstanceOptions(), keyringController: { encryptor: new MockEncryptor(), }, - networkController: { - infuraProjectId: 'fake-infura-project-id', - }, - storageService: { - storage: new InMemoryStorageAdapter(), - }, - remoteFeatureFlagController: REMOTE_FEATURE_FLAG_OPTIONS, }, }); @@ -143,21 +142,7 @@ describe('Wallet', () => { init: (): DummyService => new DummyService(), }, ], - instanceOptions: { - connectivityController: { - connectivityAdapter: new AlwaysOnlineAdapter(), - }, - gasFeeController: { - clientId: 'test', - }, - networkController: { - infuraProjectId: 'fake-infura-project-id', - }, - storageService: { - storage: new InMemoryStorageAdapter(), - }, - remoteFeatureFlagController: REMOTE_FEATURE_FLAG_OPTIONS, - }, + instanceOptions: getInstanceOptions(), }); const { state } = wallet; @@ -189,21 +174,7 @@ describe('Wallet', () => { }); const wallet = new Wallet({ - instanceOptions: { - connectivityController: { - connectivityAdapter: new AlwaysOnlineAdapter(), - }, - gasFeeController: { - clientId: 'test', - }, - networkController: { - infuraProjectId: 'fake-infura-project-id', - }, - storageService: { - storage: new InMemoryStorageAdapter(), - }, - remoteFeatureFlagController: REMOTE_FEATURE_FLAG_OPTIONS, - }, + instanceOptions: getInstanceOptions(), }); expect(wallet.controllerMetadata).toStrictEqual({ @@ -302,21 +273,7 @@ describe('Wallet', () => { addressBook: { '0x1': { [ADDRESS]: entry } }, }, }, - instanceOptions: { - connectivityController: { - connectivityAdapter: new AlwaysOnlineAdapter(), - }, - gasFeeController: { - clientId: 'test', - }, - networkController: { - infuraProjectId: 'fake-infura-project-id', - }, - storageService: { - storage: new InMemoryStorageAdapter(), - }, - remoteFeatureFlagController: REMOTE_FEATURE_FLAG_OPTIONS, - }, + instanceOptions: getInstanceOptions(), }); expect( @@ -337,21 +294,7 @@ describe('Wallet', () => { describe('ConnectivityController', () => { it('reports online connectivity status', () => { const wallet = new Wallet({ - instanceOptions: { - connectivityController: { - connectivityAdapter: new AlwaysOnlineAdapter(), - }, - gasFeeController: { - clientId: 'test', - }, - networkController: { - infuraProjectId: 'fake-infura-project-id', - }, - storageService: { - storage: new InMemoryStorageAdapter(), - }, - remoteFeatureFlagController: REMOTE_FEATURE_FLAG_OPTIONS, - }, + instanceOptions: getInstanceOptions(), }); expect(wallet.state.ConnectivityController.connectivityStatus).toBe( @@ -380,21 +323,7 @@ describe('Wallet', () => { vault, }, }, - instanceOptions: { - connectivityController: { - connectivityAdapter: new AlwaysOnlineAdapter(), - }, - gasFeeController: { - clientId: 'test', - }, - networkController: { - infuraProjectId: 'fake-infura-project-id', - }, - storageService: { - storage: new InMemoryStorageAdapter(), - }, - remoteFeatureFlagController: REMOTE_FEATURE_FLAG_OPTIONS, - }, + instanceOptions: getInstanceOptions(), }); await wallet.messenger.call( @@ -472,17 +401,8 @@ describe('Wallet', () => { it('routes injected instanceOptions through to the controller', async () => { const wallet = new Wallet({ instanceOptions: { - connectivityController: { - connectivityAdapter: new AlwaysOnlineAdapter(), - }, - gasFeeController: { - clientId: 'test', - }, - networkController: { - infuraProjectId: 'fake-infura-project-id', - }, + ...getInstanceOptions(), keyringController: { encryptor: new MockEncryptor() }, - storageService: { storage: new InMemoryStorageAdapter() }, remoteFeatureFlagController: { clientConfigApiService: { fetchRemoteFeatureFlags: async (): Promise<{ @@ -508,4 +428,15 @@ describe('Wallet', () => { ).toStrictEqual({ testFlag: true }); }); }); + + describe('ClaimsController', () => { + it('is wired and exposes its state on the wallet messenger', async () => { + const wallet = await setupWallet(); + const { messenger } = wallet; + + expect(messenger.call('ClaimsController:getState')).toStrictEqual( + getDefaultClaimsControllerState(), + ); + }); + }); }); diff --git a/packages/wallet/src/index.ts b/packages/wallet/src/index.ts index 50e9b004160..c804ba31bb9 100644 --- a/packages/wallet/src/index.ts +++ b/packages/wallet/src/index.ts @@ -1,6 +1,7 @@ export { Wallet } from './Wallet.js'; export { AlwaysOnlineAdapter } from './initialization/instances/connectivity-controller/always-online-adapter.js'; export { importSecretRecoveryPhrase } from './utilities.js'; +export { Env as ClaimsEnv } from '@metamask/claims-controller'; export type { WalletOptions } from './types.js'; export type { DefaultActions, diff --git a/packages/wallet/src/initialization/instances/claims-controller/claims-controller.test.ts b/packages/wallet/src/initialization/instances/claims-controller/claims-controller.test.ts new file mode 100644 index 00000000000..e39404adf5e --- /dev/null +++ b/packages/wallet/src/initialization/instances/claims-controller/claims-controller.test.ts @@ -0,0 +1,171 @@ +import { + ClaimsController, + getDefaultClaimsControllerState, +} from '@metamask/claims-controller'; +import { Env } from '@metamask/claims-controller'; +import { Messenger } from '@metamask/messenger'; + +import { defaultConfigurations } from '../../defaults.js'; +import type { + DefaultActions, + DefaultEvents, + RootMessenger, +} from '../../defaults.js'; +import { claimsController } from './claims-controller.js'; +import { claimsService } from './claims-service.js'; +import type { ClaimsServiceInstanceOptions } from './types.js'; + +const CLAIMS_SERVICE_OPTIONS: ClaimsServiceInstanceOptions = { + env: Env.DEV, + fetchFunction: globalThis.fetch, +}; + +type ActionHandler = (...args: unknown[]) => unknown; + +type AnyMessenger = Messenger; + +/** + * Creates a root messenger for use in tests. + * + * @returns A root messenger. + */ +function getRootMessenger(): RootMessenger { + return new Messenger({ namespace: 'Root' }); +} + +function registerActionHandler( + parent: RootMessenger, + namespace: string, + actionType: string, + handler: ActionHandler, +): void { + const messenger = new Messenger({ + namespace, + parent: parent as unknown as AnyMessenger, + }); + + ( + messenger as unknown as { + registerActionHandler(type: string, handler: ActionHandler): void; + } + ).registerActionHandler(actionType, handler); +} + +describe('claimsController', () => { + it('is registered as a default initialization configuration', () => { + expect(Object.values(defaultConfigurations)).toContain(claimsController); + }); + + it('initializes a ClaimsController with default state', () => { + const messenger = claimsController.getMessenger(getRootMessenger()); + + const instance = claimsController.init({ + state: undefined, + messenger, + options: {}, + }); + + expect(instance).toBeInstanceOf(ClaimsController); + expect(instance.state).toStrictEqual(getDefaultClaimsControllerState()); + }); + + it('merges provided state over the defaults', () => { + const messenger = claimsController.getMessenger(getRootMessenger()); + + const instance = claimsController.init({ + state: { drafts: [{ id: 'draft-1' }] }, + messenger, + options: {}, + }); + + expect(instance.state.drafts).toStrictEqual([{ id: 'draft-1' }]); + }); + + it('delegates only the ClaimsService and Keyring actions it uses', () => { + const rootMessenger = getRootMessenger(); + const delegateSpy = jest.spyOn(rootMessenger, 'delegate'); + + claimsController.getMessenger(rootMessenger); + + expect(delegateSpy).toHaveBeenCalledWith({ + messenger: expect.any(Messenger), + actions: [ + 'ClaimsService:fetchClaimsConfigurations', + 'ClaimsService:getRequestHeaders', + 'ClaimsService:getClaimsApiUrl', + 'ClaimsService:generateMessageForClaimSignature', + 'ClaimsService:getClaims', + 'KeyringController:signPersonalMessage', + ], + }); + }); + + it('exposes its state through the root messenger', () => { + const rootMessenger = getRootMessenger(); + const messenger = claimsController.getMessenger(rootMessenger); + + claimsController.init({ + state: undefined, + messenger, + options: {}, + }); + + expect(rootMessenger.call('ClaimsController:getState')).toStrictEqual( + getDefaultClaimsControllerState(), + ); + }); + + it('routes ClaimsService actions when both units are initialized', async () => { + const rootMessenger = getRootMessenger(); + registerActionHandler( + rootMessenger, + 'AuthenticationController', + 'AuthenticationController:getBearerToken', + jest.fn().mockResolvedValue('test-token'), + ); + + const serviceMessenger = claimsService.getMessenger(rootMessenger); + claimsService.init({ + state: undefined, + messenger: serviceMessenger, + options: CLAIMS_SERVICE_OPTIONS, + }); + + const controllerMessenger = claimsController.getMessenger(rootMessenger); + claimsController.init({ + state: undefined, + messenger: controllerMessenger, + options: {}, + }); + + const config = await rootMessenger.call( + 'ClaimsController:getSubmitClaimConfig', + { + chainId: '0x1', + email: 'test@test.com', + impactedWalletAddress: '0x123', + impactedTxHash: '0x123', + reimbursementWalletAddress: '0x456', + description: 'test description', + signature: + '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12', + }, + ); + + expect(config).toStrictEqual({ + data: { + chainId: '0x1', + email: 'test@test.com', + impactedWalletAddress: '0x123', + impactedTxHash: '0x123', + reimbursementWalletAddress: '0x456', + description: 'test description', + signature: + '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12', + }, + headers: { Authorization: 'Bearer test-token' }, + method: 'POST', + url: 'https://claims.dev-api.cx.metamask.io/claims', + }); + }); +}); diff --git a/packages/wallet/src/initialization/instances/claims-controller/claims-controller.ts b/packages/wallet/src/initialization/instances/claims-controller/claims-controller.ts new file mode 100644 index 00000000000..8ebbf66a2fc --- /dev/null +++ b/packages/wallet/src/initialization/instances/claims-controller/claims-controller.ts @@ -0,0 +1,37 @@ +import { ClaimsController } from '@metamask/claims-controller'; +import type { ClaimsControllerMessenger } from '@metamask/claims-controller'; +import { Messenger } from '@metamask/messenger'; + +import type { InitializationConfiguration } from '../../types.js'; + +export const claimsController: InitializationConfiguration< + ClaimsController, + ClaimsControllerMessenger +> = { + name: 'ClaimsController', + init: ({ state, messenger }) => + new ClaimsController({ + messenger, + state, + }), + getMessenger: (parent) => { + const messenger: ClaimsControllerMessenger = new Messenger({ + namespace: 'ClaimsController', + parent, + }); + + parent.delegate({ + messenger, + actions: [ + 'ClaimsService:fetchClaimsConfigurations', + 'ClaimsService:getRequestHeaders', + 'ClaimsService:getClaimsApiUrl', + 'ClaimsService:generateMessageForClaimSignature', + 'ClaimsService:getClaims', + 'KeyringController:signPersonalMessage', + ], + }); + + return messenger; + }, +}; diff --git a/packages/wallet/src/initialization/instances/claims-controller/claims-service.test.ts b/packages/wallet/src/initialization/instances/claims-controller/claims-service.test.ts new file mode 100644 index 00000000000..90de61a19f8 --- /dev/null +++ b/packages/wallet/src/initialization/instances/claims-controller/claims-service.test.ts @@ -0,0 +1,122 @@ +import { ClaimsService } from '@metamask/claims-controller'; +import { Env } from '@metamask/claims-controller'; +import { Messenger } from '@metamask/messenger'; + +import { defaultConfigurations } from '../../defaults.js'; +import type { + DefaultActions, + DefaultEvents, + RootMessenger, +} from '../../defaults.js'; +import { claimsService } from './claims-service.js'; +import type { ClaimsServiceInstanceOptions } from './types.js'; + +const REQUIRED_OPTIONS: ClaimsServiceInstanceOptions = { + env: Env.DEV, + fetchFunction: globalThis.fetch, +}; + +type ActionHandler = (...args: unknown[]) => unknown; + +type AnyMessenger = Messenger; + +/** + * Creates a root messenger for use in tests. + * + * @returns A root messenger. + */ +function getRootMessenger(): RootMessenger { + return new Messenger({ namespace: 'Root' }); +} + +function registerActionHandler( + parent: RootMessenger, + namespace: string, + actionType: string, + handler: ActionHandler, +): void { + const messenger = new Messenger({ + namespace, + parent: parent as unknown as AnyMessenger, + }); + + ( + messenger as unknown as { + registerActionHandler(type: string, handler: ActionHandler): void; + } + ).registerActionHandler(actionType, handler); +} + +describe('claimsService', () => { + it('is registered as a default initialization configuration', () => { + expect(Object.values(defaultConfigurations)).toContain(claimsService); + }); + + it('initializes a ClaimsService', () => { + const messenger = claimsService.getMessenger(getRootMessenger()); + + const instance = claimsService.init({ + state: undefined, + messenger, + options: REQUIRED_OPTIONS, + }); + + expect(instance).toBeInstanceOf(ClaimsService); + }); + + it('forwards env and fetchFunction to the service', async () => { + const rootMessenger = getRootMessenger(); + const mockGetBearerToken = jest.fn().mockResolvedValue('test-token'); + registerActionHandler( + rootMessenger, + 'AuthenticationController', + 'AuthenticationController:getBearerToken', + mockGetBearerToken, + ); + + const messenger = claimsService.getMessenger(rootMessenger); + claimsService.init({ + state: undefined, + messenger, + options: REQUIRED_OPTIONS, + }); + + const headers = await rootMessenger.call('ClaimsService:getRequestHeaders'); + + expect(headers).toStrictEqual({ Authorization: 'Bearer test-token' }); + expect(mockGetBearerToken).toHaveBeenCalledTimes(1); + }); + + it('delegates only AuthenticationController:getBearerToken', () => { + const rootMessenger = getRootMessenger(); + const delegateSpy = jest.spyOn(rootMessenger, 'delegate'); + + claimsService.getMessenger(rootMessenger); + + expect(delegateSpy).toHaveBeenCalledWith({ + messenger: expect.any(Messenger), + actions: ['AuthenticationController:getBearerToken'], + }); + }); + + it('exposes service actions through the root messenger', async () => { + const rootMessenger = getRootMessenger(); + registerActionHandler( + rootMessenger, + 'AuthenticationController', + 'AuthenticationController:getBearerToken', + jest.fn().mockResolvedValue('test-token'), + ); + + const messenger = claimsService.getMessenger(rootMessenger); + claimsService.init({ + state: undefined, + messenger, + options: REQUIRED_OPTIONS, + }); + + expect(rootMessenger.call('ClaimsService:getClaimsApiUrl')).toBe( + 'https://claims.dev-api.cx.metamask.io', + ); + }); +}); diff --git a/packages/wallet/src/initialization/instances/claims-controller/claims-service.ts b/packages/wallet/src/initialization/instances/claims-controller/claims-service.ts new file mode 100644 index 00000000000..52109154099 --- /dev/null +++ b/packages/wallet/src/initialization/instances/claims-controller/claims-service.ts @@ -0,0 +1,33 @@ +import { ClaimsService } from '@metamask/claims-controller'; +import type { ClaimsServiceMessenger } from '@metamask/claims-controller'; +import { Messenger } from '@metamask/messenger'; + +import type { InitializationConfiguration } from '../../types.js'; + +export type { ClaimsServiceInstanceOptions } from './types.js'; + +export const claimsService: InitializationConfiguration< + ClaimsService, + ClaimsServiceMessenger +> = { + name: 'ClaimsService', + init: ({ messenger, options }) => + new ClaimsService({ + messenger, + env: options.env, + fetchFunction: options.fetchFunction, + }), + getMessenger: (parent) => { + const messenger: ClaimsServiceMessenger = new Messenger({ + namespace: 'ClaimsService', + parent, + }); + + parent.delegate({ + messenger, + actions: ['AuthenticationController:getBearerToken'], + }); + + return messenger; + }, +}; diff --git a/packages/wallet/src/initialization/instances/claims-controller/types.ts b/packages/wallet/src/initialization/instances/claims-controller/types.ts new file mode 100644 index 00000000000..8a1e5f43590 --- /dev/null +++ b/packages/wallet/src/initialization/instances/claims-controller/types.ts @@ -0,0 +1,16 @@ +import type { Env } from '@metamask/claims-controller'; + +export type ClaimsControllerInstanceOptions = Record; + +export type ClaimsServiceInstanceOptions = { + /** + * Claims API environment. Supplied by the consumer per build flavor + * (dev, uat, production). + */ + env: Env; + + /** + * Platform fetch implementation used for Claims API requests. + */ + fetchFunction: typeof fetch; +}; diff --git a/packages/wallet/src/initialization/instances/index.ts b/packages/wallet/src/initialization/instances/index.ts index d45bb0917b2..232f3648bbf 100644 --- a/packages/wallet/src/initialization/instances/index.ts +++ b/packages/wallet/src/initialization/instances/index.ts @@ -1,6 +1,8 @@ export { accountsController } from './accounts-controller/accounts-controller.js'; export { addressBookController } from './address-book-controller/address-book-controller.js'; export { approvalController } from './approval-controller/approval-controller.js'; +export { claimsController } from './claims-controller/claims-controller.js'; +export { claimsService } from './claims-controller/claims-service.js'; export { connectivityController } from './connectivity-controller/connectivity-controller.js'; export { gasFeeController } from './gas-fee-controller/gas-fee-controller.js'; export { keyringController } from './keyring-controller/keyring-controller.js'; diff --git a/packages/wallet/src/initialization/instances/transaction-controller/transaction-controller.test.ts b/packages/wallet/src/initialization/instances/transaction-controller/transaction-controller.test.ts index a3831f1b973..21039b08a71 100644 --- a/packages/wallet/src/initialization/instances/transaction-controller/transaction-controller.test.ts +++ b/packages/wallet/src/initialization/instances/transaction-controller/transaction-controller.test.ts @@ -1,3 +1,4 @@ +import { Env } from '@metamask/claims-controller'; import { Messenger } from '@metamask/messenger'; import { InMemoryStorageAdapter } from '@metamask/storage-service'; import { TransactionController } from '@metamask/transaction-controller'; @@ -125,6 +126,10 @@ function getInstanceOptions(): WalletOptions['instanceOptions'] { storage: new InMemoryStorageAdapter(), }, remoteFeatureFlagController: REMOTE_FEATURE_FLAG_OPTIONS, + claimsService: { + env: Env.DEV, + fetchFunction: globalThis.fetch, + }, }; } diff --git a/packages/wallet/src/types.ts b/packages/wallet/src/types.ts index 978bf1b1afd..c27918cad00 100644 --- a/packages/wallet/src/types.ts +++ b/packages/wallet/src/types.ts @@ -6,6 +6,10 @@ import type { RootMessenger, } from './initialization/defaults.js'; import type { ApprovalControllerInstanceOptions } from './initialization/instances/approval-controller/types.js'; +import type { + ClaimsControllerInstanceOptions, + ClaimsServiceInstanceOptions, +} from './initialization/instances/claims-controller/types.js'; import type { ConnectivityControllerInstanceOptions } from './initialization/instances/connectivity-controller/types.js'; import type { GasFeeControllerInstanceOptions } from './initialization/instances/gas-fee-controller/types.js'; import type { KeyringControllerInstanceOptions } from './initialization/instances/keyring-controller/types.js'; @@ -29,6 +33,8 @@ export type WalletOptions = { export type InstanceSpecificOptions = { approvalController?: ApprovalControllerInstanceOptions; + claimsService: ClaimsServiceInstanceOptions; + claimsController?: ClaimsControllerInstanceOptions; connectivityController: ConnectivityControllerInstanceOptions; gasFeeController: GasFeeControllerInstanceOptions; keyringController?: KeyringControllerInstanceOptions; diff --git a/packages/wallet/tsconfig.build.json b/packages/wallet/tsconfig.build.json index dae6d88285a..3fbf5fe6617 100644 --- a/packages/wallet/tsconfig.build.json +++ b/packages/wallet/tsconfig.build.json @@ -9,6 +9,7 @@ { "path": "../accounts-controller/tsconfig.build.json" }, { "path": "../address-book-controller/tsconfig.build.json" }, { "path": "../approval-controller/tsconfig.build.json" }, + { "path": "../claims-controller/tsconfig.build.json" }, { "path": "../base-controller/tsconfig.build.json" }, { "path": "../connectivity-controller/tsconfig.build.json" }, { "path": "../controller-utils/tsconfig.build.json" }, diff --git a/packages/wallet/tsconfig.json b/packages/wallet/tsconfig.json index 10221177468..aac9c953bac 100644 --- a/packages/wallet/tsconfig.json +++ b/packages/wallet/tsconfig.json @@ -13,6 +13,9 @@ { "path": "../approval-controller" }, + { + "path": "../claims-controller" + }, { "path": "../base-controller" }, diff --git a/yarn.lock b/yarn.lock index 97d8dc6c6d9..e7a5e69f23b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6183,7 +6183,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/claims-controller@workspace:packages/claims-controller": +"@metamask/claims-controller@npm:^0.5.3, @metamask/claims-controller@workspace:packages/claims-controller": version: 0.0.0-use.local resolution: "@metamask/claims-controller@workspace:packages/claims-controller" dependencies: @@ -9260,6 +9260,7 @@ __metadata: "@metamask/auto-changelog": "npm:^6.1.0" "@metamask/base-controller": "npm:^9.1.0" "@metamask/browser-passworder": "npm:^6.0.0" + "@metamask/claims-controller": "npm:^0.5.3" "@metamask/connectivity-controller": "npm:^0.3.0" "@metamask/controller-utils": "npm:^12.3.0" "@metamask/gas-fee-controller": "npm:^26.2.4"