From 558776142ccda35624d98283f9396c20b683e40b Mon Sep 17 00:00:00 2001 From: Tuna Date: Wed, 22 Jul 2026 15:09:48 +0700 Subject: [PATCH 01/18] feat(wallet): wire ClaimsService and ClaimsController initialization Add default wallet initialization units for ClaimsService and ClaimsController with tight messenger delegation, required claimsService instance options, wallet-cli defaults, ownership, and dependency graph updates. Co-authored-by: Cursor --- .github/CODEOWNERS | 2 + README.md | 2 + codeowners.ts | 37 ++-- packages/wallet-cli/package.json | 1 + .../src/daemon/wallet-factory.test.ts | 5 + .../wallet-cli/src/daemon/wallet-factory.ts | 7 + packages/wallet-cli/tsconfig.build.json | 1 + packages/wallet-cli/tsconfig.json | 3 + packages/wallet/package.json | 1 + packages/wallet/src/Wallet.test.ts | 130 +++++-------- .../claims-controller.test.ts | 171 ++++++++++++++++++ .../claims-controller/claims-controller.ts | 39 ++++ .../instances/claims-controller/types.ts | 1 + .../claims-service/claims-service.test.ts | 122 +++++++++++++ .../claims-service/claims-service.ts | 33 ++++ .../instances/claims-service/types.ts | 14 ++ .../src/initialization/instances/index.ts | 2 + .../transaction-controller.test.ts | 5 + packages/wallet/src/types.ts | 4 + packages/wallet/tsconfig.build.json | 1 + packages/wallet/tsconfig.json | 3 + yarn.lock | 4 +- 22 files changed, 480 insertions(+), 108 deletions(-) create mode 100644 packages/wallet/src/initialization/instances/claims-controller/claims-controller.test.ts create mode 100644 packages/wallet/src/initialization/instances/claims-controller/claims-controller.ts create mode 100644 packages/wallet/src/initialization/instances/claims-controller/types.ts create mode 100644 packages/wallet/src/initialization/instances/claims-service/claims-service.test.ts create mode 100644 packages/wallet/src/initialization/instances/claims-service/claims-service.ts create mode 100644 packages/wallet/src/initialization/instances/claims-service/types.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f7d8e29ec70..02a7c744ed9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -149,6 +149,8 @@ /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/claims-service/ @MetaMask/web3auth /packages/wallet/src/initialization/instances/connectivity-controller/ @MetaMask/core-platform /packages/wallet/src/initialization/instances/keyring-controller/ @MetaMask/accounts-engineers @MetaMask/core-platform /packages/wallet/src/initialization/instances/passkey-controller/ @MetaMask/web3auth diff --git a/README.md b/README.md index ec3d61dc7b9..c03de120027 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 --> keyring_controller; @@ -658,6 +659,7 @@ linkStyle default opacity:0.5 wallet --> storage_service; wallet --> transaction_controller; wallet_cli --> base_controller; + wallet_cli --> claims_controller; wallet_cli --> remote_feature_flag_controller; wallet_cli --> storage_service; wallet_cli --> wallet; diff --git a/codeowners.ts b/codeowners.ts index ce77d6b6877..16ec97facd4 100644 --- a/codeowners.ts +++ b/codeowners.ts @@ -14,12 +14,12 @@ type PackageInfo = { teams: string[]; /** - * The package's directory name under - * `/packages/wallet/src/initialization/instances`, used to generate its rule + * The package's directory name(s) under + * `/packages/wallet/src/initialization/instances`, used to generate its rule(s) * in the "Initialization" section. Omit this if the package has not been * added to the Wallet Library yet. */ - initializationPath?: string; + initializationPaths?: string[]; }; /** @@ -34,11 +34,11 @@ const PACKAGES: Record = { }, 'accounts-controller': { teams: ['@MetaMask/accounts-engineers'], - initializationPath: 'accounts-controller', + initializationPaths: ['accounts-controller'], }, 'address-book-controller': { teams: ['@MetaMask/confirmations'], - initializationPath: 'address-book-controller', + initializationPaths: ['address-book-controller'], }, 'ai-controllers': { teams: ['@MetaMask/social-ai'], @@ -57,7 +57,7 @@ const PACKAGES: Record = { }, 'approval-controller': { teams: ['@MetaMask/confirmations'], - initializationPath: 'approval-controller', + initializationPaths: ['approval-controller'], }, 'assets-controller': { teams: ['@MetaMask/metamask-assets'], @@ -98,6 +98,7 @@ const PACKAGES: Record = { }, 'claims-controller': { teams: ['@MetaMask/web3auth'], + initializationPaths: ['claims-controller', 'claims-service'], }, 'client-controller': { teams: [ @@ -120,7 +121,7 @@ const PACKAGES: Record = { }, 'connectivity-controller': { teams: ['@MetaMask/core-platform'], - initializationPath: 'connectivity-controller', + initializationPaths: ['connectivity-controller'], }, 'controller-utils': { teams: ['@MetaMask/core-platform'], @@ -182,7 +183,7 @@ const PACKAGES: Record = { }, 'keyring-controller': { teams: ['@MetaMask/accounts-engineers', '@MetaMask/core-platform'], - initializationPath: 'keyring-controller', + initializationPaths: ['keyring-controller'], }, 'local-node-utils': { teams: [ @@ -248,7 +249,7 @@ const PACKAGES: Record = { }, 'passkey-controller': { teams: ['@MetaMask/web3auth'], - initializationPath: 'passkey-controller', + initializationPaths: ['passkey-controller'], }, 'permission-controller': { teams: ['@MetaMask/core-platform'], @@ -292,14 +293,14 @@ const PACKAGES: Record = { '@MetaMask/mobile-platform', '@MetaMask/core-platform', ], - initializationPath: 'remote-feature-flag-controller', + initializationPaths: ['remote-feature-flag-controller'], }, 'sample-controllers': { teams: ['@MetaMask/core-platform'], }, 'seedless-onboarding-controller': { teams: ['@MetaMask/web3auth'], - initializationPath: 'seedless-onboarding-controller', + initializationPaths: ['seedless-onboarding-controller'], }, 'selected-network-controller': { teams: ['@MetaMask/core-platform'], @@ -342,14 +343,14 @@ const PACKAGES: Record = { '@MetaMask/mobile-platform', '@MetaMask/core-platform', ], - initializationPath: 'storage-service', + initializationPaths: ['storage-service'], }, 'subscription-controller': { teams: ['@MetaMask/web3auth'], }, 'transaction-controller': { teams: ['@MetaMask/confirmations'], - initializationPath: 'transaction-controller', + initializationPaths: ['transaction-controller'], }, 'transaction-pay-controller': { teams: ['@MetaMask/confirmations'], @@ -603,14 +604,14 @@ function buildInitializationSection(): CodeownersSection { return { title: 'Initialization', rules: Object.keys(PACKAGES) - .filter((name) => PACKAGES[name].initializationPath !== undefined) + .filter((name) => PACKAGES[name].initializationPaths !== undefined) .sort() - .map((name) => { - const { teams, initializationPath } = PACKAGES[name]; - return { + .flatMap((name) => { + const { teams, initializationPaths } = PACKAGES[name]; + return (initializationPaths ?? []).map((initializationPath) => ({ pattern: `/packages/wallet/src/initialization/instances/${initializationPath}/`, owners: teams, - }; + })); }), }; } diff --git a/packages/wallet-cli/package.json b/packages/wallet-cli/package.json index 2023edb1647..1cbadf61908 100644 --- a/packages/wallet-cli/package.json +++ b/packages/wallet-cli/package.json @@ -49,6 +49,7 @@ "@inquirer/confirm": "^6.0.11", "@inquirer/password": "^5.1.1", "@metamask/base-controller": "^9.1.0", + "@metamask/claims-controller": "workspace:^", "@metamask/remote-feature-flag-controller": "^4.2.2", "@metamask/rpc-errors": "^7.0.2", "@metamask/scure-bip39": "^2.1.1", diff --git a/packages/wallet-cli/src/daemon/wallet-factory.test.ts b/packages/wallet-cli/src/daemon/wallet-factory.test.ts index de29cebdd7c..f51b0e626f0 100644 --- a/packages/wallet-cli/src/daemon/wallet-factory.test.ts +++ b/packages/wallet-cli/src/daemon/wallet-factory.test.ts @@ -1,3 +1,4 @@ +import { Env } from '@metamask/claims-controller'; import { ClientConfigApiService } from '@metamask/remote-feature-flag-controller'; import { InMemoryStorageAdapter } from '@metamask/storage-service'; import { @@ -120,6 +121,10 @@ describe('createWallet', () => { ); expect(instanceOptions.transactionController?.disableSwaps).toBe(true); expect(instanceOptions.transactionController?.hooks).toStrictEqual({}); + expect(instanceOptions.claimsService).toStrictEqual({ + env: Env.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 f281c74f230..98845c7ffb9 100644 --- a/packages/wallet-cli/src/daemon/wallet-factory.ts +++ b/packages/wallet-cli/src/daemon/wallet-factory.ts @@ -1,3 +1,4 @@ +import { Env } from '@metamask/claims-controller'; import { ClientConfigApiService, ClientType, @@ -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. @@ -116,6 +119,10 @@ function buildInstanceOptions( // the controller's default. hooks: {}, }, + claimsService: { + env: Env.PRD, + fetchFunction: globalThis.fetch, + }, }; } diff --git a/packages/wallet-cli/tsconfig.build.json b/packages/wallet-cli/tsconfig.build.json index 934a11754a4..c2d3d2accf6 100644 --- a/packages/wallet-cli/tsconfig.build.json +++ b/packages/wallet-cli/tsconfig.build.json @@ -7,6 +7,7 @@ }, "references": [ { "path": "../base-controller/tsconfig.build.json" }, + { "path": "../claims-controller/tsconfig.build.json" }, { "path": "../remote-feature-flag-controller/tsconfig.build.json" }, { "path": "../storage-service/tsconfig.build.json" }, { "path": "../wallet/tsconfig.build.json" } diff --git a/packages/wallet-cli/tsconfig.json b/packages/wallet-cli/tsconfig.json index d0fd77d3457..a9fb1ecd172 100644 --- a/packages/wallet-cli/tsconfig.json +++ b/packages/wallet-cli/tsconfig.json @@ -7,6 +7,9 @@ { "path": "../base-controller" }, + { + "path": "../claims-controller" + }, { "path": "../remote-feature-flag-controller" }, diff --git a/packages/wallet/package.json b/packages/wallet/package.json index b56f1b253f5..039cbcf1a9e 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/keyring-controller": "^27.1.0", diff --git a/packages/wallet/src/Wallet.test.ts b/packages/wallet/src/Wallet.test.ts index 75dac034e92..992709efe91 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'; @@ -10,6 +12,7 @@ import * as initializationModule from './initialization/initialization.js'; import { AlwaysOnlineAdapter } from './initialization/instances/connectivity-controller/always-online-adapter.js'; import { importSecretRecoveryPhrase } from './utilities.js'; import { Wallet } from './Wallet.js'; +import type { WalletOptions } from './types.js'; const TEST_SRP = 'test test test test test test test test test test test ball'; const TEST_PASSWORD = 'testpass'; @@ -23,20 +26,28 @@ const REMOTE_FEATURE_FLAG_OPTIONS = { }, }; +function getInstanceOptions(): WalletOptions['instanceOptions'] { + return { + connectivityController: { + connectivityAdapter: new AlwaysOnlineAdapter(), + }, + 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(), - }, - networkController: { - infuraProjectId: 'fake-infura-project-id', - }, - storageService: { - storage: new InMemoryStorageAdapter(), - }, - remoteFeatureFlagController: REMOTE_FEATURE_FLAG_OPTIONS, - }, + instanceOptions: getInstanceOptions(), }); await importSecretRecoveryPhrase(wallet, TEST_PASSWORD, TEST_SRP); @@ -86,19 +97,10 @@ describe('Wallet', () => { it('supports passing instance options', async () => { const wallet = new Wallet({ instanceOptions: { - connectivityController: { - connectivityAdapter: new AlwaysOnlineAdapter(), - }, + ...getInstanceOptions(), keyringController: { encryptor: new MockEncryptor(), }, - networkController: { - infuraProjectId: 'fake-infura-project-id', - }, - storageService: { - storage: new InMemoryStorageAdapter(), - }, - remoteFeatureFlagController: REMOTE_FEATURE_FLAG_OPTIONS, }, }); @@ -137,18 +139,7 @@ describe('Wallet', () => { init: (): DummyService => new DummyService(), }, ], - instanceOptions: { - connectivityController: { - connectivityAdapter: new AlwaysOnlineAdapter(), - }, - networkController: { - infuraProjectId: 'fake-infura-project-id', - }, - storageService: { - storage: new InMemoryStorageAdapter(), - }, - remoteFeatureFlagController: REMOTE_FEATURE_FLAG_OPTIONS, - }, + instanceOptions: getInstanceOptions(), }); const { state } = wallet; @@ -180,18 +171,7 @@ describe('Wallet', () => { }); const wallet = new Wallet({ - instanceOptions: { - connectivityController: { - connectivityAdapter: new AlwaysOnlineAdapter(), - }, - networkController: { - infuraProjectId: 'fake-infura-project-id', - }, - storageService: { - storage: new InMemoryStorageAdapter(), - }, - remoteFeatureFlagController: REMOTE_FEATURE_FLAG_OPTIONS, - }, + instanceOptions: getInstanceOptions(), }); expect(wallet.controllerMetadata).toStrictEqual({ @@ -290,18 +270,7 @@ describe('Wallet', () => { addressBook: { '0x1': { [ADDRESS]: entry } }, }, }, - instanceOptions: { - connectivityController: { - connectivityAdapter: new AlwaysOnlineAdapter(), - }, - networkController: { - infuraProjectId: 'fake-infura-project-id', - }, - storageService: { - storage: new InMemoryStorageAdapter(), - }, - remoteFeatureFlagController: REMOTE_FEATURE_FLAG_OPTIONS, - }, + instanceOptions: getInstanceOptions(), }); expect( @@ -322,18 +291,7 @@ describe('Wallet', () => { describe('ConnectivityController', () => { it('reports online connectivity status', () => { const wallet = new Wallet({ - instanceOptions: { - connectivityController: { - connectivityAdapter: new AlwaysOnlineAdapter(), - }, - networkController: { - infuraProjectId: 'fake-infura-project-id', - }, - storageService: { - storage: new InMemoryStorageAdapter(), - }, - remoteFeatureFlagController: REMOTE_FEATURE_FLAG_OPTIONS, - }, + instanceOptions: getInstanceOptions(), }); expect(wallet.state.ConnectivityController.connectivityStatus).toBe( @@ -362,18 +320,7 @@ describe('Wallet', () => { vault, }, }, - instanceOptions: { - connectivityController: { - connectivityAdapter: new AlwaysOnlineAdapter(), - }, - networkController: { - infuraProjectId: 'fake-infura-project-id', - }, - storageService: { - storage: new InMemoryStorageAdapter(), - }, - remoteFeatureFlagController: REMOTE_FEATURE_FLAG_OPTIONS, - }, + instanceOptions: getInstanceOptions(), }); await wallet.messenger.call( @@ -451,14 +398,8 @@ describe('Wallet', () => { it('routes injected instanceOptions through to the controller', async () => { const wallet = new Wallet({ instanceOptions: { - connectivityController: { - connectivityAdapter: new AlwaysOnlineAdapter(), - }, - networkController: { - infuraProjectId: 'fake-infura-project-id', - }, + ...getInstanceOptions(), keyringController: { encryptor: new MockEncryptor() }, - storageService: { storage: new InMemoryStorageAdapter() }, remoteFeatureFlagController: { clientConfigApiService: { fetchRemoteFeatureFlags: async (): Promise<{ @@ -484,4 +425,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/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..023043c70e1 --- /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 { 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/claims-service.js'; +import type { ClaimsServiceInstanceOptions } from '../claims-service/types.js'; +import { Env } from '@metamask/claims-controller'; + +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..f4d30796ffc --- /dev/null +++ b/packages/wallet/src/initialization/instances/claims-controller/claims-controller.ts @@ -0,0 +1,39 @@ +import { + ClaimsController, + 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/types.ts b/packages/wallet/src/initialization/instances/claims-controller/types.ts new file mode 100644 index 00000000000..1f02d73880d --- /dev/null +++ b/packages/wallet/src/initialization/instances/claims-controller/types.ts @@ -0,0 +1 @@ +export type ClaimsControllerInstanceOptions = Record; diff --git a/packages/wallet/src/initialization/instances/claims-service/claims-service.test.ts b/packages/wallet/src/initialization/instances/claims-service/claims-service.test.ts new file mode 100644 index 00000000000..90de61a19f8 --- /dev/null +++ b/packages/wallet/src/initialization/instances/claims-service/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-service/claims-service.ts b/packages/wallet/src/initialization/instances/claims-service/claims-service.ts new file mode 100644 index 00000000000..52109154099 --- /dev/null +++ b/packages/wallet/src/initialization/instances/claims-service/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-service/types.ts b/packages/wallet/src/initialization/instances/claims-service/types.ts new file mode 100644 index 00000000000..bd49871ffa3 --- /dev/null +++ b/packages/wallet/src/initialization/instances/claims-service/types.ts @@ -0,0 +1,14 @@ +import type { Env } from '@metamask/claims-controller'; + +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 858e36e9769..33f6774ef1a 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-service/claims-service.js'; export { connectivityController } from './connectivity-controller/connectivity-controller.js'; export { keyringController } from './keyring-controller/keyring-controller.js'; export { networkController } from './network-controller/network-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 4870eca7c18..c42fe2c47e5 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,4 +1,5 @@ import { Messenger } from '@metamask/messenger'; +import { Env } from '@metamask/claims-controller'; import { InMemoryStorageAdapter } from '@metamask/storage-service'; import { TransactionController } from '@metamask/transaction-controller'; @@ -122,6 +123,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 46bb9c5993c..310d8404544 100644 --- a/packages/wallet/src/types.ts +++ b/packages/wallet/src/types.ts @@ -6,6 +6,8 @@ import type { RootMessenger, } from './initialization/defaults.js'; import type { ApprovalControllerInstanceOptions } from './initialization/instances/approval-controller/types.js'; +import type { ClaimsControllerInstanceOptions } from './initialization/instances/claims-controller/types.js'; +import type { ClaimsServiceInstanceOptions } from './initialization/instances/claims-service/types.js'; import type { ConnectivityControllerInstanceOptions } from './initialization/instances/connectivity-controller/types.js'; import type { KeyringControllerInstanceOptions } from './initialization/instances/keyring-controller/types.js'; import type { NetworkControllerInstanceOptions } from './initialization/instances/network-controller/types.js'; @@ -28,6 +30,8 @@ export type WalletOptions = { export type InstanceSpecificOptions = { approvalController?: ApprovalControllerInstanceOptions; + claimsService: ClaimsServiceInstanceOptions; + claimsController?: ClaimsControllerInstanceOptions; connectivityController: ConnectivityControllerInstanceOptions; keyringController?: KeyringControllerInstanceOptions; networkController: NetworkControllerInstanceOptions; diff --git a/packages/wallet/tsconfig.build.json b/packages/wallet/tsconfig.build.json index d4caa50ec1c..7fcfeb7219d 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 62f5137fc5b..3eeaa124483 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 8fc98d15567..a63362bd8eb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6091,7 +6091,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/claims-controller@workspace:packages/claims-controller": +"@metamask/claims-controller@npm:^0.5.3, @metamask/claims-controller@workspace:^, @metamask/claims-controller@workspace:packages/claims-controller": version: 0.0.0-use.local resolution: "@metamask/claims-controller@workspace:packages/claims-controller" dependencies: @@ -9101,6 +9101,7 @@ __metadata: "@inquirer/password": "npm:^5.1.1" "@metamask/auto-changelog": "npm:^6.1.0" "@metamask/base-controller": "npm:^9.1.0" + "@metamask/claims-controller": "workspace:^" "@metamask/remote-feature-flag-controller": "npm:^4.2.2" "@metamask/rpc-errors": "npm:^7.0.2" "@metamask/scure-bip39": "npm:^2.1.1" @@ -9168,6 +9169,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/keyring-controller": "npm:^27.1.0" From 4285b444eb2c866c88fcc62b9590d5edc5f32a3f Mon Sep 17 00:00:00 2001 From: Tuna Date: Wed, 22 Jul 2026 15:10:33 +0700 Subject: [PATCH 02/18] docs(wallet): add Claims initialization changelog entry Co-authored-by: Cursor --- packages/wallet/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/wallet/CHANGELOG.md b/packages/wallet/CHANGELOG.md index b39b7418e51..f1efa08c41d 100644 --- a/packages/wallet/CHANGELOG.md +++ b/packages/wallet/CHANGELOG.md @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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. - **BREAKING** Wire `SeedlessOnboardingController` and `PasskeyController` into the default wallet initialization ([#9533](https://github.com/MetaMask/core/pull/9533)) ### Changed From be1ebc75a1f1ff643cb52087a51ea13880eeb703 Mon Sep 17 00:00:00 2001 From: Tuna Date: Wed, 22 Jul 2026 16:25:58 +0700 Subject: [PATCH 03/18] refactor: claim controller export env --- README.md | 1 - packages/wallet-cli/package.json | 1 - packages/wallet-cli/src/daemon/wallet-factory.test.ts | 2 +- packages/wallet-cli/src/daemon/wallet-factory.ts | 2 +- packages/wallet-cli/tsconfig.build.json | 1 - packages/wallet-cli/tsconfig.json | 3 --- packages/wallet/CHANGELOG.md | 1 + packages/wallet/src/index.ts | 1 + yarn.lock | 3 +-- 9 files changed, 5 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index c03de120027..dedbab16583 100644 --- a/README.md +++ b/README.md @@ -659,7 +659,6 @@ linkStyle default opacity:0.5 wallet --> storage_service; wallet --> transaction_controller; wallet_cli --> base_controller; - wallet_cli --> claims_controller; wallet_cli --> remote_feature_flag_controller; wallet_cli --> storage_service; wallet_cli --> wallet; diff --git a/packages/wallet-cli/package.json b/packages/wallet-cli/package.json index 1cbadf61908..2023edb1647 100644 --- a/packages/wallet-cli/package.json +++ b/packages/wallet-cli/package.json @@ -49,7 +49,6 @@ "@inquirer/confirm": "^6.0.11", "@inquirer/password": "^5.1.1", "@metamask/base-controller": "^9.1.0", - "@metamask/claims-controller": "workspace:^", "@metamask/remote-feature-flag-controller": "^4.2.2", "@metamask/rpc-errors": "^7.0.2", "@metamask/scure-bip39": "^2.1.1", diff --git a/packages/wallet-cli/src/daemon/wallet-factory.test.ts b/packages/wallet-cli/src/daemon/wallet-factory.test.ts index f51b0e626f0..7f9fb949724 100644 --- a/packages/wallet-cli/src/daemon/wallet-factory.test.ts +++ b/packages/wallet-cli/src/daemon/wallet-factory.test.ts @@ -1,8 +1,8 @@ -import { Env } from '@metamask/claims-controller'; import { ClientConfigApiService } from '@metamask/remote-feature-flag-controller'; import { InMemoryStorageAdapter } from '@metamask/storage-service'; import { AlwaysOnlineAdapter, + Env, importSecretRecoveryPhrase, Wallet, } from '@metamask/wallet'; diff --git a/packages/wallet-cli/src/daemon/wallet-factory.ts b/packages/wallet-cli/src/daemon/wallet-factory.ts index 98845c7ffb9..c3c54518fa2 100644 --- a/packages/wallet-cli/src/daemon/wallet-factory.ts +++ b/packages/wallet-cli/src/daemon/wallet-factory.ts @@ -1,4 +1,3 @@ -import { Env } from '@metamask/claims-controller'; import { ClientConfigApiService, ClientType, @@ -9,6 +8,7 @@ import { InMemoryStorageAdapter } from '@metamask/storage-service'; import type { Json } from '@metamask/utils'; import { AlwaysOnlineAdapter, + Env, importSecretRecoveryPhrase, Wallet, } from '@metamask/wallet'; diff --git a/packages/wallet-cli/tsconfig.build.json b/packages/wallet-cli/tsconfig.build.json index c2d3d2accf6..934a11754a4 100644 --- a/packages/wallet-cli/tsconfig.build.json +++ b/packages/wallet-cli/tsconfig.build.json @@ -7,7 +7,6 @@ }, "references": [ { "path": "../base-controller/tsconfig.build.json" }, - { "path": "../claims-controller/tsconfig.build.json" }, { "path": "../remote-feature-flag-controller/tsconfig.build.json" }, { "path": "../storage-service/tsconfig.build.json" }, { "path": "../wallet/tsconfig.build.json" } diff --git a/packages/wallet-cli/tsconfig.json b/packages/wallet-cli/tsconfig.json index a9fb1ecd172..d0fd77d3457 100644 --- a/packages/wallet-cli/tsconfig.json +++ b/packages/wallet-cli/tsconfig.json @@ -7,9 +7,6 @@ { "path": "../base-controller" }, - { - "path": "../claims-controller" - }, { "path": "../remote-feature-flag-controller" }, diff --git a/packages/wallet/CHANGELOG.md b/packages/wallet/CHANGELOG.md index f1efa08c41d..54b8808c411 100644 --- a/packages/wallet/CHANGELOG.md +++ b/packages/wallet/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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` for consumers setting `claimsService.env`. - **BREAKING** Wire `SeedlessOnboardingController` and `PasskeyController` into the default wallet initialization ([#9533](https://github.com/MetaMask/core/pull/9533)) ### Changed diff --git a/packages/wallet/src/index.ts b/packages/wallet/src/index.ts index 50e9b004160..b958d047d5f 100644 --- a/packages/wallet/src/index.ts +++ b/packages/wallet/src/index.ts @@ -1,3 +1,4 @@ +export { Env } from '@metamask/claims-controller'; export { Wallet } from './Wallet.js'; export { AlwaysOnlineAdapter } from './initialization/instances/connectivity-controller/always-online-adapter.js'; export { importSecretRecoveryPhrase } from './utilities.js'; diff --git a/yarn.lock b/yarn.lock index a63362bd8eb..8c394dcab88 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6091,7 +6091,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/claims-controller@npm:^0.5.3, @metamask/claims-controller@workspace:^, @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: @@ -9101,7 +9101,6 @@ __metadata: "@inquirer/password": "npm:^5.1.1" "@metamask/auto-changelog": "npm:^6.1.0" "@metamask/base-controller": "npm:^9.1.0" - "@metamask/claims-controller": "workspace:^" "@metamask/remote-feature-flag-controller": "npm:^4.2.2" "@metamask/rpc-errors": "npm:^7.0.2" "@metamask/scure-bip39": "npm:^2.1.1" From 84750e09ba2564d20909013af957760866751997 Mon Sep 17 00:00:00 2001 From: Tuna Date: Wed, 22 Jul 2026 20:37:25 +0700 Subject: [PATCH 04/18] refactor(wallet): nest ClaimsService under claims-controller init Colocate ClaimsService with ClaimsController so a single CODEOWNERS initializationPath covers both units, and revert multi-path ownership. Co-authored-by: Cursor --- .github/CODEOWNERS | 1 - codeowners.ts | 38 +++++++++---------- .../claims-controller.test.ts | 7 ++-- .../claims-service.test.ts | 0 .../claims-service.ts | 0 .../instances/claims-controller/types.ts | 15 ++++++++ .../instances/claims-service/types.ts | 14 ------- .../src/initialization/instances/index.ts | 2 +- packages/wallet/src/types.ts | 6 ++- 9 files changed, 43 insertions(+), 40 deletions(-) rename packages/wallet/src/initialization/instances/{claims-service => claims-controller}/claims-service.test.ts (100%) rename packages/wallet/src/initialization/instances/{claims-service => claims-controller}/claims-service.ts (100%) delete mode 100644 packages/wallet/src/initialization/instances/claims-service/types.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 02a7c744ed9..f7735e1d3c9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -150,7 +150,6 @@ /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/claims-service/ @MetaMask/web3auth /packages/wallet/src/initialization/instances/connectivity-controller/ @MetaMask/core-platform /packages/wallet/src/initialization/instances/keyring-controller/ @MetaMask/accounts-engineers @MetaMask/core-platform /packages/wallet/src/initialization/instances/passkey-controller/ @MetaMask/web3auth diff --git a/codeowners.ts b/codeowners.ts index 16ec97facd4..d9cbc200f35 100644 --- a/codeowners.ts +++ b/codeowners.ts @@ -14,12 +14,12 @@ type PackageInfo = { teams: string[]; /** - * The package's directory name(s) under - * `/packages/wallet/src/initialization/instances`, used to generate its rule(s) + * The package's directory name under + * `/packages/wallet/src/initialization/instances`, used to generate its rule * in the "Initialization" section. Omit this if the package has not been * added to the Wallet Library yet. */ - initializationPaths?: string[]; + initializationPath?: string; }; /** @@ -34,11 +34,11 @@ const PACKAGES: Record = { }, 'accounts-controller': { teams: ['@MetaMask/accounts-engineers'], - initializationPaths: ['accounts-controller'], + initializationPath: 'accounts-controller', }, 'address-book-controller': { teams: ['@MetaMask/confirmations'], - initializationPaths: ['address-book-controller'], + initializationPath: 'address-book-controller', }, 'ai-controllers': { teams: ['@MetaMask/social-ai'], @@ -57,7 +57,7 @@ const PACKAGES: Record = { }, 'approval-controller': { teams: ['@MetaMask/confirmations'], - initializationPaths: ['approval-controller'], + initializationPath: 'approval-controller', }, 'assets-controller': { teams: ['@MetaMask/metamask-assets'], @@ -98,7 +98,7 @@ const PACKAGES: Record = { }, 'claims-controller': { teams: ['@MetaMask/web3auth'], - initializationPaths: ['claims-controller', 'claims-service'], + initializationPath: 'claims-controller', }, 'client-controller': { teams: [ @@ -121,7 +121,7 @@ const PACKAGES: Record = { }, 'connectivity-controller': { teams: ['@MetaMask/core-platform'], - initializationPaths: ['connectivity-controller'], + initializationPath: 'connectivity-controller', }, 'controller-utils': { teams: ['@MetaMask/core-platform'], @@ -183,7 +183,7 @@ const PACKAGES: Record = { }, 'keyring-controller': { teams: ['@MetaMask/accounts-engineers', '@MetaMask/core-platform'], - initializationPaths: ['keyring-controller'], + initializationPath: 'keyring-controller', }, 'local-node-utils': { teams: [ @@ -249,7 +249,7 @@ const PACKAGES: Record = { }, 'passkey-controller': { teams: ['@MetaMask/web3auth'], - initializationPaths: ['passkey-controller'], + initializationPath: 'passkey-controller', }, 'permission-controller': { teams: ['@MetaMask/core-platform'], @@ -293,14 +293,14 @@ const PACKAGES: Record = { '@MetaMask/mobile-platform', '@MetaMask/core-platform', ], - initializationPaths: ['remote-feature-flag-controller'], + initializationPath: 'remote-feature-flag-controller', }, 'sample-controllers': { teams: ['@MetaMask/core-platform'], }, 'seedless-onboarding-controller': { teams: ['@MetaMask/web3auth'], - initializationPaths: ['seedless-onboarding-controller'], + initializationPath: 'seedless-onboarding-controller', }, 'selected-network-controller': { teams: ['@MetaMask/core-platform'], @@ -343,14 +343,14 @@ const PACKAGES: Record = { '@MetaMask/mobile-platform', '@MetaMask/core-platform', ], - initializationPaths: ['storage-service'], + initializationPath: 'storage-service', }, 'subscription-controller': { teams: ['@MetaMask/web3auth'], }, 'transaction-controller': { teams: ['@MetaMask/confirmations'], - initializationPaths: ['transaction-controller'], + initializationPath: 'transaction-controller', }, 'transaction-pay-controller': { teams: ['@MetaMask/confirmations'], @@ -604,14 +604,14 @@ function buildInitializationSection(): CodeownersSection { return { title: 'Initialization', rules: Object.keys(PACKAGES) - .filter((name) => PACKAGES[name].initializationPaths !== undefined) + .filter((name) => PACKAGES[name].initializationPath !== undefined) .sort() - .flatMap((name) => { - const { teams, initializationPaths } = PACKAGES[name]; - return (initializationPaths ?? []).map((initializationPath) => ({ + .map((name) => { + const { teams, initializationPath } = PACKAGES[name]; + return { pattern: `/packages/wallet/src/initialization/instances/${initializationPath}/`, owners: teams, - })); + }; }), }; } 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 index 023043c70e1..f6bada1e21e 100644 --- a/packages/wallet/src/initialization/instances/claims-controller/claims-controller.test.ts +++ b/packages/wallet/src/initialization/instances/claims-controller/claims-controller.test.ts @@ -10,11 +10,12 @@ import type { DefaultEvents, RootMessenger, } from '../../defaults.js'; -import { claimsController } from './claims-controller.js'; -import { claimsService } from '../claims-service/claims-service.js'; -import type { ClaimsServiceInstanceOptions } from '../claims-service/types.js'; import { Env } from '@metamask/claims-controller'; +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, diff --git a/packages/wallet/src/initialization/instances/claims-service/claims-service.test.ts b/packages/wallet/src/initialization/instances/claims-controller/claims-service.test.ts similarity index 100% rename from packages/wallet/src/initialization/instances/claims-service/claims-service.test.ts rename to packages/wallet/src/initialization/instances/claims-controller/claims-service.test.ts diff --git a/packages/wallet/src/initialization/instances/claims-service/claims-service.ts b/packages/wallet/src/initialization/instances/claims-controller/claims-service.ts similarity index 100% rename from packages/wallet/src/initialization/instances/claims-service/claims-service.ts rename to packages/wallet/src/initialization/instances/claims-controller/claims-service.ts diff --git a/packages/wallet/src/initialization/instances/claims-controller/types.ts b/packages/wallet/src/initialization/instances/claims-controller/types.ts index 1f02d73880d..8a1e5f43590 100644 --- a/packages/wallet/src/initialization/instances/claims-controller/types.ts +++ b/packages/wallet/src/initialization/instances/claims-controller/types.ts @@ -1 +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/claims-service/types.ts b/packages/wallet/src/initialization/instances/claims-service/types.ts deleted file mode 100644 index bd49871ffa3..00000000000 --- a/packages/wallet/src/initialization/instances/claims-service/types.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { Env } from '@metamask/claims-controller'; - -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 33f6774ef1a..7be0b37a6b7 100644 --- a/packages/wallet/src/initialization/instances/index.ts +++ b/packages/wallet/src/initialization/instances/index.ts @@ -2,7 +2,7 @@ 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-service/claims-service.js'; +export { claimsService } from './claims-controller/claims-service.js'; export { connectivityController } from './connectivity-controller/connectivity-controller.js'; export { keyringController } from './keyring-controller/keyring-controller.js'; export { networkController } from './network-controller/network-controller.js'; diff --git a/packages/wallet/src/types.ts b/packages/wallet/src/types.ts index 310d8404544..49f49ff0ba6 100644 --- a/packages/wallet/src/types.ts +++ b/packages/wallet/src/types.ts @@ -6,8 +6,10 @@ import type { RootMessenger, } from './initialization/defaults.js'; import type { ApprovalControllerInstanceOptions } from './initialization/instances/approval-controller/types.js'; -import type { ClaimsControllerInstanceOptions } from './initialization/instances/claims-controller/types.js'; -import type { ClaimsServiceInstanceOptions } from './initialization/instances/claims-service/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 { KeyringControllerInstanceOptions } from './initialization/instances/keyring-controller/types.js'; import type { NetworkControllerInstanceOptions } from './initialization/instances/network-controller/types.js'; From 7dc5df25f432440bf01a4c1d9a546b81dd671728 Mon Sep 17 00:00:00 2001 From: Tuna Date: Wed, 22 Jul 2026 20:38:37 +0700 Subject: [PATCH 05/18] refactor(wallet): re-export claims Env as ClaimsEnv Match the SubscriptionEnv naming pattern so wallet consumers can set claimsService.env without colliding with other Env enums. Co-authored-by: Cursor --- packages/wallet-cli/src/daemon/wallet-factory.test.ts | 4 ++-- packages/wallet-cli/src/daemon/wallet-factory.ts | 4 ++-- packages/wallet/CHANGELOG.md | 2 +- packages/wallet/src/index.ts | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/wallet-cli/src/daemon/wallet-factory.test.ts b/packages/wallet-cli/src/daemon/wallet-factory.test.ts index 7f9fb949724..000c62c0708 100644 --- a/packages/wallet-cli/src/daemon/wallet-factory.test.ts +++ b/packages/wallet-cli/src/daemon/wallet-factory.test.ts @@ -2,7 +2,7 @@ import { ClientConfigApiService } from '@metamask/remote-feature-flag-controller import { InMemoryStorageAdapter } from '@metamask/storage-service'; import { AlwaysOnlineAdapter, - Env, + ClaimsEnv, importSecretRecoveryPhrase, Wallet, } from '@metamask/wallet'; @@ -122,7 +122,7 @@ describe('createWallet', () => { expect(instanceOptions.transactionController?.disableSwaps).toBe(true); expect(instanceOptions.transactionController?.hooks).toStrictEqual({}); expect(instanceOptions.claimsService).toStrictEqual({ - env: Env.PRD, + env: ClaimsEnv.PRD, fetchFunction: globalThis.fetch, }); expect(ClientConfigApiService).toHaveBeenCalled(); diff --git a/packages/wallet-cli/src/daemon/wallet-factory.ts b/packages/wallet-cli/src/daemon/wallet-factory.ts index c3c54518fa2..8835aee5f2b 100644 --- a/packages/wallet-cli/src/daemon/wallet-factory.ts +++ b/packages/wallet-cli/src/daemon/wallet-factory.ts @@ -8,7 +8,7 @@ import { InMemoryStorageAdapter } from '@metamask/storage-service'; import type { Json } from '@metamask/utils'; import { AlwaysOnlineAdapter, - Env, + ClaimsEnv, importSecretRecoveryPhrase, Wallet, } from '@metamask/wallet'; @@ -120,7 +120,7 @@ function buildInstanceOptions( hooks: {}, }, claimsService: { - env: Env.PRD, + env: ClaimsEnv.PRD, fetchFunction: globalThis.fetch, }, }; diff --git a/packages/wallet/CHANGELOG.md b/packages/wallet/CHANGELOG.md index abff7a5c657..cc0d6c4c96f 100644 --- a/packages/wallet/CHANGELOG.md +++ b/packages/wallet/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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` for consumers setting `claimsService.env`. + - Re-exports `Env` from `@metamask/claims-controller` as `ClaimsEnv` for `instanceOptions.claimsService.env`. - **BREAKING** Wire `SeedlessOnboardingController` and `PasskeyController` into the default wallet initialization ([#9533](https://github.com/MetaMask/core/pull/9533)) ### Changed diff --git a/packages/wallet/src/index.ts b/packages/wallet/src/index.ts index b958d047d5f..c804ba31bb9 100644 --- a/packages/wallet/src/index.ts +++ b/packages/wallet/src/index.ts @@ -1,7 +1,7 @@ -export { Env } from '@metamask/claims-controller'; 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, From 19de465d25e95a0d7fd593d456404c6578eedb12 Mon Sep 17 00:00:00 2001 From: Tuna Date: Wed, 22 Jul 2026 20:40:23 +0700 Subject: [PATCH 06/18] docs(wallet-cli): note claimsService instance options wiring Co-authored-by: Cursor --- packages/wallet-cli/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/wallet-cli/CHANGELOG.md b/packages/wallet-cli/CHANGELOG.md index 9e83315b448..cc128379245 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)) - 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)) - Add the `mm daemon list` command, which prints the messenger actions the running daemon can dispatch via `daemon call`, enumerated from the live messenger so the list cannot drift from what `call` accepts ([#9339](https://github.com/MetaMask/core/pull/9339)) From 8a31a85b66f89f4e673e80eb11a8f5b3b6fa22f5 Mon Sep 17 00:00:00 2001 From: Tuna Date: Thu, 23 Jul 2026 07:47:57 +0700 Subject: [PATCH 07/18] fix: lint --- packages/wallet/src/Wallet.test.ts | 2 +- .../instances/claims-controller/claims-controller.test.ts | 3 +-- .../transaction-controller/transaction-controller.test.ts | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/wallet/src/Wallet.test.ts b/packages/wallet/src/Wallet.test.ts index 992709efe91..a5f13d99bf1 100644 --- a/packages/wallet/src/Wallet.test.ts +++ b/packages/wallet/src/Wallet.test.ts @@ -10,9 +10,9 @@ 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'; -import type { WalletOptions } from './types.js'; const TEST_SRP = 'test test test test test test test test test test test ball'; const TEST_PASSWORD = 'testpass'; 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 index f6bada1e21e..e39404adf5e 100644 --- a/packages/wallet/src/initialization/instances/claims-controller/claims-controller.test.ts +++ b/packages/wallet/src/initialization/instances/claims-controller/claims-controller.test.ts @@ -2,6 +2,7 @@ import { ClaimsController, getDefaultClaimsControllerState, } from '@metamask/claims-controller'; +import { Env } from '@metamask/claims-controller'; import { Messenger } from '@metamask/messenger'; import { defaultConfigurations } from '../../defaults.js'; @@ -10,8 +11,6 @@ import type { DefaultEvents, RootMessenger, } from '../../defaults.js'; -import { Env } from '@metamask/claims-controller'; - import { claimsController } from './claims-controller.js'; import { claimsService } from './claims-service.js'; import type { ClaimsServiceInstanceOptions } from './types.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 c42fe2c47e5..e08fd7c50cc 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,5 +1,5 @@ -import { Messenger } from '@metamask/messenger'; import { Env } from '@metamask/claims-controller'; +import { Messenger } from '@metamask/messenger'; import { InMemoryStorageAdapter } from '@metamask/storage-service'; import { TransactionController } from '@metamask/transaction-controller'; From 4446ad54684863c83e580fef6769262b347b97a2 Mon Sep 17 00:00:00 2001 From: Tuna Date: Thu, 23 Jul 2026 07:58:13 +0700 Subject: [PATCH 08/18] fix: changelog --- packages/wallet/CHANGELOG.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/wallet/CHANGELOG.md b/packages/wallet/CHANGELOG.md index 35b438de622..6d638d3dfb4 100644 --- a/packages/wallet/CHANGELOG.md +++ b/packages/wallet/CHANGELOG.md @@ -7,8 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -## [8.0.0] - ### Added - **BREAKING:** Wire `ClaimsService` and `ClaimsController` into the default wallet initialization ([#9588](https://github.com/MetaMask/core/pull/9588)) @@ -16,6 +14,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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 + - **BREAKING:** Wire `GasFeeController` into the default wallet initialization ([#9527](https://github.com/MetaMask/core/pull/9527)) - Adds a required `instanceOptions.gasFeeController` option whose `clientId` (sent as `X-Client-Id` to the gas API) is required, so every client identifies itself; all other fields are optional and fall back to platform-agnostic defaults. - **BREAKING** Wire `SeedlessOnboardingController` and `PasskeyController` into the default wallet initialization ([#9533](https://github.com/MetaMask/core/pull/9533)) From 745fd3786290a390f8bcef34c9f5e7ed402f021e Mon Sep 17 00:00:00 2001 From: Tuna Date: Thu, 23 Jul 2026 08:19:17 +0700 Subject: [PATCH 09/18] fix: lint --- .../instances/claims-controller/claims-controller.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/wallet/src/initialization/instances/claims-controller/claims-controller.ts b/packages/wallet/src/initialization/instances/claims-controller/claims-controller.ts index f4d30796ffc..8ebbf66a2fc 100644 --- a/packages/wallet/src/initialization/instances/claims-controller/claims-controller.ts +++ b/packages/wallet/src/initialization/instances/claims-controller/claims-controller.ts @@ -1,7 +1,5 @@ -import { - ClaimsController, - type ClaimsControllerMessenger, -} from '@metamask/claims-controller'; +import { ClaimsController } from '@metamask/claims-controller'; +import type { ClaimsControllerMessenger } from '@metamask/claims-controller'; import { Messenger } from '@metamask/messenger'; import type { InitializationConfiguration } from '../../types.js'; From 458c94b4efb250cb6e4c93b5add09686a9328fa0 Mon Sep 17 00:00:00 2001 From: Tuna Date: Thu, 30 Jul 2026 18:13:50 +0700 Subject: [PATCH 10/18] refactor(claims-controller): update ClaimsService environment handling --- packages/wallet-cli/src/daemon/wallet-factory.test.ts | 2 -- packages/wallet-cli/src/daemon/wallet-factory.ts | 1 - packages/wallet/src/index.ts | 1 - .../instances/claims-controller/claims-service.ts | 4 ++-- .../src/initialization/instances/claims-controller/types.ts | 2 +- 5 files changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/wallet-cli/src/daemon/wallet-factory.test.ts b/packages/wallet-cli/src/daemon/wallet-factory.test.ts index 4a0d1392dcc..0c3ea325907 100644 --- a/packages/wallet-cli/src/daemon/wallet-factory.test.ts +++ b/packages/wallet-cli/src/daemon/wallet-factory.test.ts @@ -2,7 +2,6 @@ import { ClientConfigApiService } from '@metamask/remote-feature-flag-controller import { InMemoryStorageAdapter } from '@metamask/storage-service'; import { AlwaysOnlineAdapter, - ClaimsEnv, importSecretRecoveryPhrase, Wallet, } from '@metamask/wallet'; @@ -126,7 +125,6 @@ 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(); diff --git a/packages/wallet-cli/src/daemon/wallet-factory.ts b/packages/wallet-cli/src/daemon/wallet-factory.ts index cba75b27048..9d6793eb235 100644 --- a/packages/wallet-cli/src/daemon/wallet-factory.ts +++ b/packages/wallet-cli/src/daemon/wallet-factory.ts @@ -146,7 +146,6 @@ function buildInstanceOptions( hooks: {}, }, claimsService: { - env: ClaimsEnv.PRD, fetchFunction: globalThis.fetch, }, }; diff --git a/packages/wallet/src/index.ts b/packages/wallet/src/index.ts index c804ba31bb9..50e9b004160 100644 --- a/packages/wallet/src/index.ts +++ b/packages/wallet/src/index.ts @@ -1,7 +1,6 @@ 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-service.ts b/packages/wallet/src/initialization/instances/claims-controller/claims-service.ts index 52109154099..37dd2f70e04 100644 --- a/packages/wallet/src/initialization/instances/claims-controller/claims-service.ts +++ b/packages/wallet/src/initialization/instances/claims-controller/claims-service.ts @@ -1,4 +1,4 @@ -import { ClaimsService } from '@metamask/claims-controller'; +import { ClaimsService, Env } from '@metamask/claims-controller'; import type { ClaimsServiceMessenger } from '@metamask/claims-controller'; import { Messenger } from '@metamask/messenger'; @@ -14,7 +14,7 @@ export const claimsService: InitializationConfiguration< init: ({ messenger, options }) => new ClaimsService({ messenger, - env: options.env, + env: options.env ?? Env.PRD, fetchFunction: options.fetchFunction, }), getMessenger: (parent) => { diff --git a/packages/wallet/src/initialization/instances/claims-controller/types.ts b/packages/wallet/src/initialization/instances/claims-controller/types.ts index 8a1e5f43590..518456ca74d 100644 --- a/packages/wallet/src/initialization/instances/claims-controller/types.ts +++ b/packages/wallet/src/initialization/instances/claims-controller/types.ts @@ -7,7 +7,7 @@ export type ClaimsServiceInstanceOptions = { * Claims API environment. Supplied by the consumer per build flavor * (dev, uat, production). */ - env: Env; + env?: Env; /** * Platform fetch implementation used for Claims API requests. From a93ecdbcafa59846dcc283b2c6e83b1b2eb49ae5 Mon Sep 17 00:00:00 2001 From: Tuna Date: Thu, 30 Jul 2026 18:29:54 +0700 Subject: [PATCH 11/18] chore: update changelog --- packages/wallet-cli/CHANGELOG.md | 2 +- packages/wallet/CHANGELOG.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/wallet-cli/CHANGELOG.md b/packages/wallet-cli/CHANGELOG.md index 91b41d4c4a9..36d2c2c2793 100644 --- a/packages/wallet-cli/CHANGELOG.md +++ b/packages/wallet-cli/CHANGELOG.md @@ -9,7 +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 `claimsService` slot in the daemon wallet's instance options (`globalThis.fetch`), so the daemon initializes `ClaimsService` with the wallet's required configuration ([#9588](https://github.com/MetaMask/core/pull/9588)) - Add the `mm wallet send` command and a dedicated daemon `sendTransaction` RPC handler for sending a transaction through the daemon-hosted `TransactionController` ([#9636](https://github.com/MetaMask/core/pull/9636)) - The command converts the ether `--value` to wei, resolves the network client (`--network-client-id` or `--chain-id`) and sender (defaulting to the selected account), previews the resolved plan, and broadcasts after confirmation, printing the resulting transaction hash. `--yes` skips the prompt; `--dry-run` resolves and validates without broadcasting. - The `sendTransaction` handler awaits the broadcast server-side and returns a serializable `{ transactionHash, transactionId, status }`, because `addTransaction`'s `result` promise cannot travel back over the generic `call` dispatch. Transactions are submitted as internal (auto-approved by the daemon). diff --git a/packages/wallet/CHANGELOG.md b/packages/wallet/CHANGELOG.md index e83a960b696..675d03028eb 100644 --- a/packages/wallet/CHANGELOG.md +++ b/packages/wallet/CHANGELOG.md @@ -10,8 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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. + - `instanceOptions.claimsService.env` is optional and defaults to production (`Env.PRD`); pass a different value per build flavor when not targeting production. - `ClaimsService` delegates `AuthenticationController:getBearerToken`; hosts must register `AuthenticationController` on the supplied root messenger before authenticated Claims API calls succeed. - **BREAKING:** Wire analytics into the default `NetworkController` initialization ([#9270](https://github.com/MetaMask/core/pull/9270)) - Adds an optional `instanceOptions.networkController.analyticsOptions` option (`isRpcEndpointUrlPublic` and `rpcServiceEventsSampleRate`, both optional) that the controller uses to emit `RPC Service Unavailable` and `RPC Service Degraded` events. From 022dd84d6d2378317a8e0cc12aae37d323214c0a Mon Sep 17 00:00:00 2001 From: Tuna Date: Thu, 30 Jul 2026 18:35:29 +0700 Subject: [PATCH 12/18] fix: remove unneeded import --- packages/wallet-cli/src/daemon/wallet-factory.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/wallet-cli/src/daemon/wallet-factory.ts b/packages/wallet-cli/src/daemon/wallet-factory.ts index 9d6793eb235..8a6541829bb 100644 --- a/packages/wallet-cli/src/daemon/wallet-factory.ts +++ b/packages/wallet-cli/src/daemon/wallet-factory.ts @@ -14,7 +14,6 @@ import { InMemoryStorageAdapter } from '@metamask/storage-service'; import type { Json } from '@metamask/utils'; import { AlwaysOnlineAdapter, - ClaimsEnv, importSecretRecoveryPhrase, Wallet, } from '@metamask/wallet'; From e799716a3100b9c93e3e12ce36998d9effdba99f Mon Sep 17 00:00:00 2001 From: Tuna Date: Thu, 30 Jul 2026 20:06:33 +0700 Subject: [PATCH 13/18] feat(claims-controller): add ClaimsControllerOptions and ClaimsServiceConfig types --- packages/claims-controller/src/index.ts | 2 ++ .../instances/claims-controller/types.ts | 27 +++++++++---------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/packages/claims-controller/src/index.ts b/packages/claims-controller/src/index.ts index 5c33db2987b..2ad03a54b38 100644 --- a/packages/claims-controller/src/index.ts +++ b/packages/claims-controller/src/index.ts @@ -8,6 +8,7 @@ export type { ClaimsControllerActions, ClaimsControllerStateChangeEvent, ClaimsControllerMessenger, + ClaimsControllerOptions, } from './ClaimsController.js'; export type { @@ -42,6 +43,7 @@ export type { ClaimsServiceGetClaimByIdAction, ClaimsServiceGenerateMessageForClaimSignatureAction, ClaimsServiceActions, + ClaimsServiceConfig, ClaimsServiceMessenger, } from './ClaimsService.js'; diff --git a/packages/wallet/src/initialization/instances/claims-controller/types.ts b/packages/wallet/src/initialization/instances/claims-controller/types.ts index 518456ca74d..03a7ba370bc 100644 --- a/packages/wallet/src/initialization/instances/claims-controller/types.ts +++ b/packages/wallet/src/initialization/instances/claims-controller/types.ts @@ -1,16 +1,15 @@ -import type { Env } from '@metamask/claims-controller'; +import type { + ClaimsControllerOptions, + ClaimsServiceConfig, +} from '@metamask/claims-controller'; -export type ClaimsControllerInstanceOptions = Record; +export type ClaimsControllerInstanceOptions = Omit< + ClaimsControllerOptions, + 'messenger' | 'state' +>; -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; -}; +export type ClaimsServiceInstanceOptions = Omit< + ClaimsServiceConfig, + 'messenger' | 'env' +> & + Partial>; From 6af6ff48502fafe3605b35fece63bc568ddca11c Mon Sep 17 00:00:00 2001 From: Tuna Date: Thu, 30 Jul 2026 20:17:17 +0700 Subject: [PATCH 14/18] chore: update changelog --- packages/claims-controller/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/claims-controller/CHANGELOG.md b/packages/claims-controller/CHANGELOG.md index 67805b8b250..84d365242ad 100644 --- a/packages/claims-controller/CHANGELOG.md +++ b/packages/claims-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Export `ClaimsControllerOptions` and `ClaimsServiceConfig` types ([#9588](https://github.com/MetaMask/core/pull/9588)) + ### Changed - Bump `@metamask/utils` from `^11.9.0` to `^11.11.0` ([#9074](https://github.com/MetaMask/core/pull/9074)) From 063e0972ff8588bb02f1eaaef216b95441279cbe Mon Sep 17 00:00:00 2001 From: Tuna Date: Sun, 2 Aug 2026 14:40:23 +0700 Subject: [PATCH 15/18] feat: enhance Claims Service Integration --- .github/CODEOWNERS | 1 + README.md | 1 + codeowners.ts | 4 + packages/claims-controller/CHANGELOG.md | 4 + packages/claims-controller/package.json | 5 +- .../claims-controller/src/ClaimsController.ts | 2 +- .../src/ClaimsService-method-action-types.ts | 80 ++++ .../src/ClaimsService-structs.ts | 45 +++ .../src/ClaimsService.test.ts | 218 +++++++++++ .../claims-controller/src/ClaimsService.ts | 347 +++++++++++------- packages/claims-controller/src/index.ts | 8 + .../tests/mocks/messenger.ts | 7 +- .../claims-controller/tsconfig.build.json | 3 + packages/claims-controller/tsconfig.json | 3 + packages/wallet/CHANGELOG.md | 5 + packages/wallet/src/Wallet.test.ts | 2 +- .../claims-controller.test.ts | 4 +- .../instances/claims-controller/types.ts | 11 +- .../claims-service.test.ts | 10 +- .../claims-service.ts | 4 + .../instances/claims-service/types.ts | 16 + .../src/initialization/instances/index.ts | 2 +- packages/wallet/src/types.ts | 6 +- yarn.lock | 3 + 24 files changed, 637 insertions(+), 154 deletions(-) create mode 100644 packages/claims-controller/src/ClaimsService-method-action-types.ts create mode 100644 packages/claims-controller/src/ClaimsService-structs.ts rename packages/wallet/src/initialization/instances/{claims-controller => claims-service}/claims-service.test.ts (92%) rename packages/wallet/src/initialization/instances/{claims-controller => claims-service}/claims-service.ts (84%) create mode 100644 packages/wallet/src/initialization/instances/claims-service/types.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index aac9edc744d..ab7761a6ff7 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -151,6 +151,7 @@ /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/claims-service/ @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 e306ef5d080..8121b15d392 100644 --- a/README.md +++ b/README.md @@ -345,6 +345,7 @@ linkStyle default opacity:0.5 chomp_api_service --> controller_utils; chomp_api_service --> messenger; claims_controller --> base_controller; + claims_controller --> base_data_service; claims_controller --> controller_utils; claims_controller --> keyring_controller; claims_controller --> messenger; diff --git a/codeowners.ts b/codeowners.ts index 7ff1f0361ca..7af191a851e 100644 --- a/codeowners.ts +++ b/codeowners.ts @@ -100,6 +100,10 @@ const PACKAGES: Record = { teams: ['@MetaMask/web3auth'], initializationPath: 'claims-controller', }, + 'claims-service': { + teams: ['@MetaMask/web3auth'], + initializationPath: 'claims-service', + }, 'client-controller': { teams: [ '@MetaMask/core-platform', diff --git a/packages/claims-controller/CHANGELOG.md b/packages/claims-controller/CHANGELOG.md index 84d365242ad..43ad3f87df4 100644 --- a/packages/claims-controller/CHANGELOG.md +++ b/packages/claims-controller/CHANGELOG.md @@ -10,9 +10,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Export `ClaimsControllerOptions` and `ClaimsServiceConfig` types ([#9588](https://github.com/MetaMask/core/pull/9588)) +- Add `ClaimsService-method-action-types.ts` with generated method action types for `ClaimsService` ([#9588](https://github.com/MetaMask/core/pull/9588)) +- Add `ClaimsService:invalidateQueries` action and `ClaimsService:cacheUpdated` events via `BaseDataService` ([#9588](https://github.com/MetaMask/core/pull/9588)) ### Changed +- Migrate `ClaimsService` to `BaseDataService` with TanStack Query caching, circuit-breaker policy support, and response validation ([#9588](https://github.com/MetaMask/core/pull/9588)) +- Add `@metamask/base-data-service` `^0.1.3`, `@metamask/superstruct` `^3.1.0`, and `@tanstack/query-core` `^4.43.0` as dependencies ([#9588](https://github.com/MetaMask/core/pull/9588)) - Bump `@metamask/utils` from `^11.9.0` to `^11.11.0` ([#9074](https://github.com/MetaMask/core/pull/9074)) - Bump `@metamask/controller-utils` from `^12.1.1` to `^12.3.0` ([#9083](https://github.com/MetaMask/core/pull/9083), [#9218](https://github.com/MetaMask/core/pull/9218)) - Bump `@metamask/profile-sync-controller` from `^28.1.1` to `^28.3.0` ([#9119](https://github.com/MetaMask/core/pull/9119), [#9463](https://github.com/MetaMask/core/pull/9463)) diff --git a/packages/claims-controller/package.json b/packages/claims-controller/package.json index 06291d96d33..efc0eee7802 100644 --- a/packages/claims-controller/package.json +++ b/packages/claims-controller/package.json @@ -56,11 +56,14 @@ }, "dependencies": { "@metamask/base-controller": "^9.1.0", + "@metamask/base-data-service": "^0.1.3", "@metamask/controller-utils": "^12.3.0", "@metamask/keyring-controller": "^27.1.0", "@metamask/messenger": "^2.0.0", "@metamask/profile-sync-controller": "^28.3.0", - "@metamask/utils": "^11.11.0" + "@metamask/superstruct": "^3.1.0", + "@metamask/utils": "^11.11.0", + "@tanstack/query-core": "^4.43.0" }, "devDependencies": { "@metamask/auto-changelog": "^6.1.0", diff --git a/packages/claims-controller/src/ClaimsController.ts b/packages/claims-controller/src/ClaimsController.ts index 473fe3531ef..faea44d8279 100644 --- a/packages/claims-controller/src/ClaimsController.ts +++ b/packages/claims-controller/src/ClaimsController.ts @@ -17,7 +17,7 @@ import type { ClaimsServiceGetClaimsAction, ClaimsServiceGetClaimsApiUrlAction, ClaimsServiceGetRequestHeadersAction, -} from './ClaimsService.js'; +} from './ClaimsService-method-action-types.js'; import { ClaimsControllerErrorMessages, CONTROLLER_NAME, diff --git a/packages/claims-controller/src/ClaimsService-method-action-types.ts b/packages/claims-controller/src/ClaimsService-method-action-types.ts new file mode 100644 index 00000000000..1fe4b2b9ce3 --- /dev/null +++ b/packages/claims-controller/src/ClaimsService-method-action-types.ts @@ -0,0 +1,80 @@ +/** + * This file is auto generated. + * Do not edit manually. + */ + +import type { ClaimsService } from './ClaimsService.js'; + +/** + * Fetch required configurations for the claims service. + * + * @returns The required configurations for the claims service. + */ +export type ClaimsServiceFetchClaimsConfigurationsAction = { + type: `ClaimsService:fetchClaimsConfigurations`; + handler: ClaimsService['fetchClaimsConfigurations']; +}; + +/** + * Get the claims for the current user. + * + * @returns The claims for the current user. + */ +export type ClaimsServiceGetClaimsAction = { + type: `ClaimsService:getClaims`; + handler: ClaimsService['getClaims']; +}; + +/** + * Get the claim by id. + * + * @param id - The id of the claim to get. + * @returns The claim by id. + */ +export type ClaimsServiceGetClaimByIdAction = { + type: `ClaimsService:getClaimById`; + handler: ClaimsService['getClaimById']; +}; + +/** + * Generate a message to be signed by the user for the claim request. + * + * @param chainId - The chain id of the claim. + * @param walletAddress - The impacted wallet address of the claim. + * @returns The message for the claim signature. + */ +export type ClaimsServiceGenerateMessageForClaimSignatureAction = { + type: `ClaimsService:generateMessageForClaimSignature`; + handler: ClaimsService['generateMessageForClaimSignature']; +}; + +/** + * Create the headers for the current request. + * + * @returns The headers for the current request. + */ +export type ClaimsServiceGetRequestHeadersAction = { + type: `ClaimsService:getRequestHeaders`; + handler: ClaimsService['getRequestHeaders']; +}; + +/** + * Get the URL for the claims API for the current environment. + * + * @returns The URL for the claims API for the current environment. + */ +export type ClaimsServiceGetClaimsApiUrlAction = { + type: `ClaimsService:getClaimsApiUrl`; + handler: ClaimsService['getClaimsApiUrl']; +}; + +/** + * Union of all ClaimsService action types. + */ +export type ClaimsServiceMethodActions = + | ClaimsServiceFetchClaimsConfigurationsAction + | ClaimsServiceGetClaimsAction + | ClaimsServiceGetClaimByIdAction + | ClaimsServiceGenerateMessageForClaimSignatureAction + | ClaimsServiceGetRequestHeadersAction + | ClaimsServiceGetClaimsApiUrlAction; diff --git a/packages/claims-controller/src/ClaimsService-structs.ts b/packages/claims-controller/src/ClaimsService-structs.ts new file mode 100644 index 00000000000..c61055d7ec5 --- /dev/null +++ b/packages/claims-controller/src/ClaimsService-structs.ts @@ -0,0 +1,45 @@ +import { + array, + enums, + integer, + object, + optional, + string, +} from '@metamask/superstruct'; + +import { ClaimStatusEnum } from './constants.js'; + +const HexStringStruct = string(); + +const AttachmentStruct = object({ + publicUrl: string(), + contentType: string(), + originalname: string(), +}); + +export const ClaimStruct = object({ + id: string(), + shortId: string(), + chainId: string(), + email: string(), + impactedWalletAddress: HexStringStruct, + impactedTxHash: HexStringStruct, + reimbursementWalletAddress: HexStringStruct, + description: string(), + signature: HexStringStruct, + attachments: optional(array(AttachmentStruct)), + status: enums(Object.values(ClaimStatusEnum)), + createdAt: string(), + updatedAt: string(), + intercomId: optional(string()), +}); + +export const ClaimsConfigurationsResponseStruct = object({ + validSubmissionWindowDays: integer(), + networks: array(integer()), +}); + +export const GenerateSignatureMessageResponseStruct = object({ + message: string(), + nonce: string(), +}); diff --git a/packages/claims-controller/src/ClaimsService.test.ts b/packages/claims-controller/src/ClaimsService.test.ts index e760a0a635b..d2fcd7ae49f 100644 --- a/packages/claims-controller/src/ClaimsService.test.ts +++ b/packages/claims-controller/src/ClaimsService.test.ts @@ -31,6 +31,7 @@ function createMockClaimsService(env: Env = Env.DEV): ClaimsService { env, messenger, fetchFunction: mockFetchFunction, + captureException: mockCaptureException, }); } @@ -333,4 +334,221 @@ describe('ClaimsService', () => { ); }); }); + + describe('caching', () => { + const MOCK_CONFIGURATIONS: ClaimsConfigurationsResponse = { + validSubmissionWindowDays: 21, + networks: [1, 5, 11155111], + }; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('deduplicates cached GET requests', async () => { + mockAuthenticationControllerGetBearerToken.mockResolvedValue( + 'test-token', + ); + mockFetchFunction.mockResolvedValue({ + ok: true, + json: jest.fn().mockResolvedValue(MOCK_CONFIGURATIONS), + }); + + const service = createMockClaimsService(); + + await service.fetchClaimsConfigurations(); + await service.fetchClaimsConfigurations(); + + expect(mockFetchFunction).toHaveBeenCalledTimes(1); + }); + + it('does not cache signature message POST requests', async () => { + mockAuthenticationControllerGetBearerToken.mockResolvedValue( + 'test-token', + ); + mockFetchFunction.mockResolvedValue({ + ok: true, + json: jest.fn().mockResolvedValue({ + message: 'test message', + nonce: 'test nonce', + }), + }); + + const service = createMockClaimsService(); + + await service.generateMessageForClaimSignature(1, '0x123'); + await service.generateMessageForClaimSignature(1, '0x123'); + + expect(mockFetchFunction).toHaveBeenCalledTimes(2); + }); + + it('does not deduplicate concurrent signature message POST requests', async () => { + mockAuthenticationControllerGetBearerToken.mockResolvedValue( + 'test-token', + ); + + mockFetchFunction.mockImplementation(async () => { + await new Promise((resolve) => { + setTimeout(resolve, 50); + }); + return { + ok: true, + json: jest.fn().mockResolvedValue({ + message: `test message ${mockFetchFunction.mock.calls.length}`, + nonce: `test nonce ${mockFetchFunction.mock.calls.length}`, + }), + }; + }); + + const service = createMockClaimsService(); + + await Promise.all([ + service.generateMessageForClaimSignature(1, '0x123'), + service.generateMessageForClaimSignature(1, '0x123'), + ]); + + expect(mockFetchFunction).toHaveBeenCalledTimes(2); + }); + + it('refetches GET requests after invalidation', async () => { + mockAuthenticationControllerGetBearerToken.mockResolvedValue( + 'test-token', + ); + mockFetchFunction.mockResolvedValue({ + ok: true, + json: jest.fn().mockResolvedValue([MOCK_CLAIM_1]), + }); + + const service = createMockClaimsService(); + + await service.getClaims(); + await service.invalidateQueries({ + queryKey: [`ClaimsService:getClaims`], + }); + await service.getClaims(); + + expect(mockFetchFunction).toHaveBeenCalledTimes(2); + }); + + it('publishes cacheUpdated events for cached GET requests', async () => { + mockAuthenticationControllerGetBearerToken.mockResolvedValue( + 'test-token', + ); + mockFetchFunction.mockResolvedValue({ + ok: true, + json: jest.fn().mockResolvedValue(MOCK_CONFIGURATIONS), + }); + + const { messenger } = createMockClaimsServiceMessenger( + mockAuthenticationControllerGetBearerToken, + mockCaptureException, + ); + const publishSpy = jest.spyOn(messenger, 'publish'); + const service = new ClaimsService({ + env: Env.DEV, + messenger, + fetchFunction: mockFetchFunction, + }); + + await service.fetchClaimsConfigurations(); + + expect(publishSpy).toHaveBeenCalledWith( + 'ClaimsService:cacheUpdated', + expect.objectContaining({ + type: 'updated', + }), + ); + }); + }); + + describe('response validation', () => { + beforeEach(() => { + jest.resetAllMocks(); + mockAuthenticationControllerGetBearerToken.mockResolvedValue( + 'test-token', + ); + }); + + it('throws when configurations response is malformed', async () => { + mockFetchFunction.mockResolvedValue({ + ok: true, + json: jest.fn().mockResolvedValue({ invalid: true }), + }); + + const service = createMockClaimsService(); + + await expect(service.fetchClaimsConfigurations()).rejects.toThrow( + ClaimsServiceErrorMessages.FAILED_TO_FETCH_CONFIGURATIONS, + ); + }); + + it('throws when claims response is malformed', async () => { + mockFetchFunction.mockResolvedValue({ + ok: true, + json: jest.fn().mockResolvedValue([{ invalid: true }]), + }); + + const service = createMockClaimsService(); + + await expect(service.getClaims()).rejects.toThrow( + ClaimsServiceErrorMessages.FAILED_TO_GET_CLAIMS, + ); + }); + }); + + describe('captureException', () => { + it('falls back to messenger.captureException when config captureException is omitted', async () => { + jest.resetAllMocks(); + mockAuthenticationControllerGetBearerToken.mockResolvedValue( + 'test-token', + ); + mockFetchFunction.mockRejectedValue(new Error('Fetch error')); + + const { messenger } = createMockClaimsServiceMessenger( + mockAuthenticationControllerGetBearerToken, + mockCaptureException, + ); + const service = new ClaimsService({ + env: Env.DEV, + messenger, + fetchFunction: mockFetchFunction, + }); + + await expect(service.getClaimById('1')).rejects.toThrow( + ClaimsServiceErrorMessages.FAILED_TO_GET_CLAIM_BY_ID, + ); + + expect(mockCaptureException).toHaveBeenCalledWith( + createSentryError( + ClaimsServiceErrorMessages.FAILED_TO_GET_CLAIM_BY_ID, + new Error('Fetch error'), + ), + ); + }); + + it('ignores errors thrown by captureException', async () => { + jest.resetAllMocks(); + mockAuthenticationControllerGetBearerToken.mockResolvedValue( + 'test-token', + ); + mockFetchFunction.mockRejectedValue(new Error('Fetch error')); + + const { messenger } = createMockClaimsServiceMessenger( + mockAuthenticationControllerGetBearerToken, + jest.fn(), + ); + const service = new ClaimsService({ + env: Env.DEV, + messenger, + fetchFunction: mockFetchFunction, + captureException: () => { + throw new Error('capture failed'); + }, + }); + + await expect(service.getClaimById('1')).rejects.toThrow( + ClaimsServiceErrorMessages.FAILED_TO_GET_CLAIM_BY_ID, + ); + }); + }); }); diff --git a/packages/claims-controller/src/ClaimsService.ts b/packages/claims-controller/src/ClaimsService.ts index 209cbd92417..eb447c6054a 100644 --- a/packages/claims-controller/src/ClaimsService.ts +++ b/packages/claims-controller/src/ClaimsService.ts @@ -1,7 +1,23 @@ +import { BaseDataService } from '@metamask/base-data-service'; +import type { + DataServiceCacheUpdatedEvent, + DataServiceGranularCacheUpdatedEvent, + DataServiceInvalidateQueriesAction, +} from '@metamask/base-data-service'; +import type { CreateServicePolicyOptions } from '@metamask/controller-utils'; import type { Messenger } from '@metamask/messenger'; import type { AuthenticationController } from '@metamask/profile-sync-controller'; +import { array, validate } from '@metamask/superstruct'; +import type { Struct } from '@metamask/superstruct'; import type { Hex } from '@metamask/utils'; +import type { QueryClientConfig } from '@tanstack/query-core'; +import type { ClaimsServiceMethodActions } from './ClaimsService-method-action-types.js'; +import { + ClaimStruct, + ClaimsConfigurationsResponseStruct, + GenerateSignatureMessageResponseStruct, +} from './ClaimsService-structs.js'; import { CLAIMS_API_URL_MAP, ClaimsServiceErrorMessages, @@ -16,99 +32,128 @@ import type { } from './types.js'; import { createSentryError, getErrorFromResponse } from './utils.js'; -export type ClaimsServiceFetchClaimsConfigurationsAction = { - type: `${typeof SERVICE_NAME}:fetchClaimsConfigurations`; - handler: ClaimsService['fetchClaimsConfigurations']; -}; - -export type ClaimsServiceGetClaimsAction = { - type: `${typeof SERVICE_NAME}:getClaims`; - handler: ClaimsService['getClaims']; +const MESSENGER_EXPOSED_METHODS = [ + 'fetchClaimsConfigurations', + 'getClaims', + 'getClaimById', + 'getRequestHeaders', + 'getClaimsApiUrl', + 'generateMessageForClaimSignature', +] as const; + +const DEFAULT_POLICY_OPTIONS: CreateServicePolicyOptions = { + maxRetries: 0, }; -export type ClaimsServiceGetClaimByIdAction = { - type: `${typeof SERVICE_NAME}:getClaimById`; - handler: ClaimsService['getClaimById']; -}; - -export type ClaimsServiceGetRequestHeadersAction = { - type: `${typeof SERVICE_NAME}:getRequestHeaders`; - handler: ClaimsService['getRequestHeaders']; -}; - -export type ClaimsServiceGetClaimsApiUrlAction = { - type: `${typeof SERVICE_NAME}:getClaimsApiUrl`; - handler: ClaimsService['getClaimsApiUrl']; -}; - -export type ClaimsServiceGenerateMessageForClaimSignatureAction = { - type: `${typeof SERVICE_NAME}:generateMessageForClaimSignature`; - handler: ClaimsService['generateMessageForClaimSignature']; -}; +/** + * Invalidates cached queries for {@link ClaimsService}. + */ +export type ClaimsServiceInvalidateQueriesAction = + DataServiceInvalidateQueriesAction; +/** + * Actions that {@link ClaimsService} exposes to other consumers. + */ export type ClaimsServiceActions = - | ClaimsServiceFetchClaimsConfigurationsAction - | ClaimsServiceGetClaimsAction - | ClaimsServiceGetClaimByIdAction - | ClaimsServiceGetRequestHeadersAction - | ClaimsServiceGetClaimsApiUrlAction - | ClaimsServiceGenerateMessageForClaimSignatureAction; + | ClaimsServiceMethodActions + | ClaimsServiceInvalidateQueriesAction; +/** + * Actions from other messengers that {@link ClaimsService} calls. + */ export type AllowedActions = AuthenticationController.AuthenticationControllerGetBearerTokenAction; -export type ClaimsServiceEvents = never; +/** + * Published when {@link ClaimsService}'s cache is updated. + */ +export type ClaimsServiceCacheUpdatedEvent = DataServiceCacheUpdatedEvent< + typeof SERVICE_NAME +>; +/** + * Published when a key within {@link ClaimsService}'s cache is updated. + */ +export type ClaimsServiceGranularCacheUpdatedEvent = + DataServiceGranularCacheUpdatedEvent; + +/** + * Events that {@link ClaimsService} exposes to other consumers. + */ +export type ClaimsServiceEvents = + | ClaimsServiceCacheUpdatedEvent + | ClaimsServiceGranularCacheUpdatedEvent; + +/** + * Events from other messengers that {@link ClaimsService} subscribes to. + */ +type AllowedEvents = never; + +/** + * The messenger which is restricted to actions and events accessed by + * {@link ClaimsService}. + */ export type ClaimsServiceMessenger = Messenger< typeof SERVICE_NAME, - ClaimsServiceActions | AllowedActions + ClaimsServiceActions | AllowedActions, + ClaimsServiceEvents | AllowedEvents >; export type ClaimsServiceConfig = { env: Env; messenger: ClaimsServiceMessenger; fetchFunction: typeof fetch; + captureException?: (error: Error) => void; + queryClientConfig?: QueryClientConfig; + policyOptions?: CreateServicePolicyOptions; }; const log = createModuleLogger(projectLogger, 'ClaimsService'); -export class ClaimsService { - readonly name = SERVICE_NAME; // required for Modular Initialization - +/** + * This service is responsible for communicating with the Claims API. + * + * All requests are authenticated via JWT Bearer tokens obtained from the + * `AuthenticationController:getBearerToken` messenger action. + */ +export class ClaimsService extends BaseDataService< + typeof SERVICE_NAME, + ClaimsServiceMessenger +> { readonly #env: Env; readonly #fetch: typeof fetch; - readonly #messenger: ClaimsServiceMessenger; + readonly #captureException?: (error: Error) => void; + + constructor({ + env, + messenger, + fetchFunction, + captureException: captureExceptionFn, + queryClientConfig = {}, + policyOptions = {}, + }: ClaimsServiceConfig) { + super({ + name: SERVICE_NAME, + messenger, + queryClientConfig, + policyOptions: { ...DEFAULT_POLICY_OPTIONS, ...policyOptions }, + }); - constructor({ env, messenger, fetchFunction }: ClaimsServiceConfig) { this.#env = env; - this.#messenger = messenger; this.#fetch = fetchFunction; + this.#captureException = (error: Error): void => { + try { + (captureExceptionFn ?? messenger.captureException)?.(error); + } catch { + // ignore error thrown when calling captureException + } + }; - this.#messenger.registerActionHandler( - `${SERVICE_NAME}:fetchClaimsConfigurations`, - this.fetchClaimsConfigurations.bind(this), - ); - this.#messenger.registerActionHandler( - `${SERVICE_NAME}:getClaims`, - this.getClaims.bind(this), - ); - this.#messenger.registerActionHandler( - `${SERVICE_NAME}:getClaimById`, - this.getClaimById.bind(this), - ); - this.#messenger.registerActionHandler( - `${SERVICE_NAME}:getRequestHeaders`, - this.getRequestHeaders.bind(this), - ); - this.#messenger.registerActionHandler( - `${SERVICE_NAME}:getClaimsApiUrl`, - this.getClaimsApiUrl.bind(this), - ); - this.#messenger.registerActionHandler( - `${SERVICE_NAME}:generateMessageForClaimSignature`, - this.generateMessageForClaimSignature.bind(this), + this.messenger.registerMethodActionHandlers( + this, + MESSENGER_EXPOSED_METHODS, ); } @@ -119,29 +164,33 @@ export class ClaimsService { */ async fetchClaimsConfigurations(): Promise { try { - const headers = await this.getRequestHeaders(); - const url = `${this.getClaimsApiUrl()}/configurations`; - const response = await this.#fetch(url, { - headers, + const configurations = await this.fetchQuery({ + queryKey: [`${this.name}:fetchClaimsConfigurations`], + queryFn: async () => { + const headers = await this.getRequestHeaders(); + const url = `${this.getClaimsApiUrl()}/configurations`; + const response = await this.#fetch(url, { + headers, + }); + + if (!response.ok) { + throw await getErrorFromResponse(response); + } + + return response.json(); + }, }); - if (!response.ok) { - const error = await getErrorFromResponse(response); - throw error; - } - - const configurations = await response.json(); - return configurations; - } catch (error) { - log('fetchClaimsConfigurations', error); - this.#messenger.captureException?.( - createSentryError( - ClaimsServiceErrorMessages.FAILED_TO_FETCH_CONFIGURATIONS, - error as Error, - ), + return this.#validateResponse( + configurations, + ClaimsConfigurationsResponseStruct, + ClaimsServiceErrorMessages.FAILED_TO_FETCH_CONFIGURATIONS, ); - throw new Error( + } catch (error) { + return this.#handleError( + 'fetchClaimsConfigurations', ClaimsServiceErrorMessages.FAILED_TO_FETCH_CONFIGURATIONS, + error, ); } } @@ -153,28 +202,40 @@ export class ClaimsService { */ async getClaims(): Promise { try { - const headers = await this.getRequestHeaders(); - const url = `${this.getClaimsApiUrl()}/claims`; - const response = await this.#fetch(url, { - headers, + const claims = await this.fetchQuery({ + queryKey: [`${this.name}:getClaims`], + queryFn: async () => { + const headers = await this.getRequestHeaders(); + const url = `${this.getClaimsApiUrl()}/claims`; + const response = await this.#fetch(url, { + headers, + }); + + if (!response.ok) { + throw await getErrorFromResponse(response); + } + + return response.json(); + }, }); - if (!response.ok) { - const error = await getErrorFromResponse(response); - throw error; + const [validationError, validatedClaims] = validate( + claims, + array(ClaimStruct), + ); + if (validationError) { + throw new Error( + `${ClaimsServiceErrorMessages.FAILED_TO_GET_CLAIMS}: ${validationError.message}`, + ); } - const claims = await response.json(); - return claims; + return validatedClaims as Claim[]; } catch (error) { - log('getClaims', error); - this.#messenger.captureException?.( - createSentryError( - ClaimsServiceErrorMessages.FAILED_TO_GET_CLAIMS, - error as Error, - ), + return this.#handleError( + 'getClaims', + ClaimsServiceErrorMessages.FAILED_TO_GET_CLAIMS, + error, ); - throw new Error(ClaimsServiceErrorMessages.FAILED_TO_GET_CLAIMS); } } @@ -186,28 +247,34 @@ export class ClaimsService { */ async getClaimById(id: string): Promise { try { - const headers = await this.getRequestHeaders(); - const url = `${this.getClaimsApiUrl()}/claims/byId/${id}`; - const response = await this.#fetch(url, { - headers, + const claim = await this.fetchQuery({ + queryKey: [`${this.name}:getClaimById`, id], + queryFn: async () => { + const headers = await this.getRequestHeaders(); + const url = `${this.getClaimsApiUrl()}/claims/byId/${id}`; + const response = await this.#fetch(url, { + headers, + }); + + if (!response.ok) { + throw await getErrorFromResponse(response); + } + + return response.json(); + }, }); - if (!response.ok) { - const error = await getErrorFromResponse(response); - throw error; - } - - const claim = await response.json(); - return claim; + return this.#validateResponse( + claim, + ClaimStruct, + ClaimsServiceErrorMessages.FAILED_TO_GET_CLAIM_BY_ID, + ) as Claim; } catch (error) { - log('getClaimById', error); - this.#messenger.captureException?.( - createSentryError( - ClaimsServiceErrorMessages.FAILED_TO_GET_CLAIM_BY_ID, - error as Error, - ), + return this.#handleError( + 'getClaimById', + ClaimsServiceErrorMessages.FAILED_TO_GET_CLAIM_BY_ID, + error, ); - throw new Error(ClaimsServiceErrorMessages.FAILED_TO_GET_CLAIM_BY_ID); } } @@ -238,22 +305,21 @@ export class ClaimsService { }); if (!response.ok) { - const error = await getErrorFromResponse(response); - throw error; + throw await getErrorFromResponse(response); } const message = await response.json(); - return message; - } catch (error) { - log('generateMessageForClaimSignature', error); - this.#messenger.captureException?.( - createSentryError( - ClaimsServiceErrorMessages.SIGNATURE_MESSAGE_GENERATION_FAILED, - error as Error, - ), + + return this.#validateResponse( + message, + GenerateSignatureMessageResponseStruct, + ClaimsServiceErrorMessages.SIGNATURE_MESSAGE_GENERATION_FAILED, ); - throw new Error( + } catch (error) { + return this.#handleError( + 'generateMessageForClaimSignature', ClaimsServiceErrorMessages.SIGNATURE_MESSAGE_GENERATION_FAILED, + error, ); } } @@ -264,7 +330,7 @@ export class ClaimsService { * @returns The headers for the current request. */ async getRequestHeaders(): Promise> { - const bearerToken = await this.#messenger.call( + const bearerToken = await this.messenger.call( 'AuthenticationController:getBearerToken', ); return { @@ -280,4 +346,27 @@ export class ClaimsService { getClaimsApiUrl(): string { return `${CLAIMS_API_URL_MAP[this.#env]}`; } + + #validateResponse( + responseData: unknown, + struct: Struct, + errorMessage: string, + ): TValidated { + const [error, validatedResponseData] = validate(responseData, struct); + if (error) { + throw new Error(`${errorMessage}: ${error.message}`); + } + + return validatedResponseData; + } + + #handleError( + methodName: string, + errorMessage: string, + error: unknown, + ): never { + log(methodName, error); + this.#captureException?.(createSentryError(errorMessage, error as Error)); + throw new Error(errorMessage); + } } diff --git a/packages/claims-controller/src/index.ts b/packages/claims-controller/src/index.ts index 2ad03a54b38..07c71d7bb95 100644 --- a/packages/claims-controller/src/index.ts +++ b/packages/claims-controller/src/index.ts @@ -42,9 +42,17 @@ export type { ClaimsServiceGetClaimsApiUrlAction, ClaimsServiceGetClaimByIdAction, ClaimsServiceGenerateMessageForClaimSignatureAction, + ClaimsServiceMethodActions, +} from './ClaimsService-method-action-types.js'; + +export type { ClaimsServiceActions, ClaimsServiceConfig, ClaimsServiceMessenger, + ClaimsServiceInvalidateQueriesAction, + ClaimsServiceCacheUpdatedEvent, + ClaimsServiceGranularCacheUpdatedEvent, + ClaimsServiceEvents, } from './ClaimsService.js'; export { diff --git a/packages/claims-controller/tests/mocks/messenger.ts b/packages/claims-controller/tests/mocks/messenger.ts index 8a903b2e34a..2961d4f783a 100644 --- a/packages/claims-controller/tests/mocks/messenger.ts +++ b/packages/claims-controller/tests/mocks/messenger.ts @@ -113,11 +113,12 @@ export function createMockClaimsControllerMessenger({ } type AllServiceActions = MessengerActions; +type AllServiceEvents = MessengerEvents; export type RootServiceMessenger = Messenger< MockAnyNamespace, - AllServiceActions - // since there's no events for the service, we don't need to specify them + AllServiceActions, + AllServiceEvents >; /** @@ -146,7 +147,7 @@ export function createMockClaimsServiceMessenger( const messenger = new Messenger< typeof SERVICE_NAME, AllServiceActions, - never, // No events for the service + AllServiceEvents, RootServiceMessenger >({ namespace: SERVICE_NAME, diff --git a/packages/claims-controller/tsconfig.build.json b/packages/claims-controller/tsconfig.build.json index 4a9745aeb55..c10496fd666 100644 --- a/packages/claims-controller/tsconfig.build.json +++ b/packages/claims-controller/tsconfig.build.json @@ -9,6 +9,9 @@ { "path": "../base-controller/tsconfig.build.json" }, + { + "path": "../base-data-service/tsconfig.build.json" + }, { "path": "../messenger/tsconfig.build.json" }, diff --git a/packages/claims-controller/tsconfig.json b/packages/claims-controller/tsconfig.json index ee69ba51f8c..275aa9962a7 100644 --- a/packages/claims-controller/tsconfig.json +++ b/packages/claims-controller/tsconfig.json @@ -7,6 +7,9 @@ { "path": "../base-controller" }, + { + "path": "../base-data-service" + }, { "path": "../messenger" }, diff --git a/packages/wallet/CHANGELOG.md b/packages/wallet/CHANGELOG.md index 1bbc4d4c5db..f90f1b0cb0e 100644 --- a/packages/wallet/CHANGELOG.md +++ b/packages/wallet/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Move `ClaimsService` wallet initialization into `packages/wallet/src/initialization/instances/claims-service/` ([#9588](https://github.com/MetaMask/core/pull/9588)) +- `instanceOptions.claimsService` now accepts optional `queryClientConfig`, `policyOptions`, and `captureException` forwarded to `ClaimsService` ([#9588](https://github.com/MetaMask/core/pull/9588)) + ## [9.0.0] ### Added diff --git a/packages/wallet/src/Wallet.test.ts b/packages/wallet/src/Wallet.test.ts index 2e0d93af926..ed32d17e841 100644 --- a/packages/wallet/src/Wallet.test.ts +++ b/packages/wallet/src/Wallet.test.ts @@ -188,7 +188,7 @@ describe('Wallet', () => { const results = await wallet.init(); - expect(results).toHaveLength(2); + expect(results).toHaveLength(3); }); it('disallows modifying the messenger', async () => { 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 index e39404adf5e..16a6747ef4d 100644 --- a/packages/wallet/src/initialization/instances/claims-controller/claims-controller.test.ts +++ b/packages/wallet/src/initialization/instances/claims-controller/claims-controller.test.ts @@ -11,9 +11,9 @@ import type { DefaultEvents, RootMessenger, } from '../../defaults.js'; +import { claimsService } from '../claims-service/claims-service.js'; +import type { ClaimsServiceInstanceOptions } from '../claims-service/types.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, diff --git a/packages/wallet/src/initialization/instances/claims-controller/types.ts b/packages/wallet/src/initialization/instances/claims-controller/types.ts index 03a7ba370bc..9b6d9c52878 100644 --- a/packages/wallet/src/initialization/instances/claims-controller/types.ts +++ b/packages/wallet/src/initialization/instances/claims-controller/types.ts @@ -1,15 +1,6 @@ -import type { - ClaimsControllerOptions, - ClaimsServiceConfig, -} from '@metamask/claims-controller'; +import type { ClaimsControllerOptions } from '@metamask/claims-controller'; export type ClaimsControllerInstanceOptions = Omit< ClaimsControllerOptions, 'messenger' | 'state' >; - -export type ClaimsServiceInstanceOptions = Omit< - ClaimsServiceConfig, - 'messenger' | 'env' -> & - Partial>; diff --git a/packages/wallet/src/initialization/instances/claims-controller/claims-service.test.ts b/packages/wallet/src/initialization/instances/claims-service/claims-service.test.ts similarity index 92% rename from packages/wallet/src/initialization/instances/claims-controller/claims-service.test.ts rename to packages/wallet/src/initialization/instances/claims-service/claims-service.test.ts index 90de61a19f8..1cc221c857f 100644 --- a/packages/wallet/src/initialization/instances/claims-controller/claims-service.test.ts +++ b/packages/wallet/src/initialization/instances/claims-service/claims-service.test.ts @@ -64,7 +64,7 @@ describe('claimsService', () => { expect(instance).toBeInstanceOf(ClaimsService); }); - it('forwards env and fetchFunction to the service', async () => { + it('forwards env, fetchFunction, and policy options to the service', async () => { const rootMessenger = getRootMessenger(); const mockGetBearerToken = jest.fn().mockResolvedValue('test-token'); registerActionHandler( @@ -74,11 +74,16 @@ describe('claimsService', () => { mockGetBearerToken, ); + const mockFetch = jest.fn(); const messenger = claimsService.getMessenger(rootMessenger); claimsService.init({ state: undefined, messenger, - options: REQUIRED_OPTIONS, + options: { + ...REQUIRED_OPTIONS, + fetchFunction: mockFetch, + policyOptions: { maxRetries: 0 }, + }, }); const headers = await rootMessenger.call('ClaimsService:getRequestHeaders'); @@ -96,6 +101,7 @@ describe('claimsService', () => { expect(delegateSpy).toHaveBeenCalledWith({ messenger: expect.any(Messenger), actions: ['AuthenticationController:getBearerToken'], + events: [], }); }); diff --git a/packages/wallet/src/initialization/instances/claims-controller/claims-service.ts b/packages/wallet/src/initialization/instances/claims-service/claims-service.ts similarity index 84% rename from packages/wallet/src/initialization/instances/claims-controller/claims-service.ts rename to packages/wallet/src/initialization/instances/claims-service/claims-service.ts index 37dd2f70e04..0df23d5c779 100644 --- a/packages/wallet/src/initialization/instances/claims-controller/claims-service.ts +++ b/packages/wallet/src/initialization/instances/claims-service/claims-service.ts @@ -16,6 +16,9 @@ export const claimsService: InitializationConfiguration< messenger, env: options.env ?? Env.PRD, fetchFunction: options.fetchFunction, + captureException: options.captureException, + queryClientConfig: options.queryClientConfig, + policyOptions: options.policyOptions, }), getMessenger: (parent) => { const messenger: ClaimsServiceMessenger = new Messenger({ @@ -26,6 +29,7 @@ export const claimsService: InitializationConfiguration< parent.delegate({ messenger, actions: ['AuthenticationController:getBearerToken'], + events: [], }); return messenger; diff --git a/packages/wallet/src/initialization/instances/claims-service/types.ts b/packages/wallet/src/initialization/instances/claims-service/types.ts new file mode 100644 index 00000000000..97910fd32cb --- /dev/null +++ b/packages/wallet/src/initialization/instances/claims-service/types.ts @@ -0,0 +1,16 @@ +import type { Env, ClaimsService } from '@metamask/claims-controller'; + +type ClaimsServiceOptions = ConstructorParameters[0]; + +type ClaimsServiceCommonOptions = Pick< + ClaimsServiceOptions, + 'captureException' | 'queryClientConfig' | 'policyOptions' +>; + +/** + * Per-instance options for the wallet's `ClaimsService`. + */ +export type ClaimsServiceInstanceOptions = ClaimsServiceCommonOptions & { + fetchFunction: ClaimsServiceOptions['fetchFunction']; + env?: Env; +}; diff --git a/packages/wallet/src/initialization/instances/index.ts b/packages/wallet/src/initialization/instances/index.ts index 232f3648bbf..abe2d34dddb 100644 --- a/packages/wallet/src/initialization/instances/index.ts +++ b/packages/wallet/src/initialization/instances/index.ts @@ -2,7 +2,7 @@ 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 { claimsService } from './claims-service/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/types.ts b/packages/wallet/src/types.ts index c27918cad00..32f4364373b 100644 --- a/packages/wallet/src/types.ts +++ b/packages/wallet/src/types.ts @@ -6,10 +6,8 @@ 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 { ClaimsControllerInstanceOptions } from './initialization/instances/claims-controller/types.js'; +import type { ClaimsServiceInstanceOptions } from './initialization/instances/claims-service/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'; diff --git a/yarn.lock b/yarn.lock index 9970de57e0c..a36ed0d11be 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6415,11 +6415,14 @@ __metadata: dependencies: "@metamask/auto-changelog": "npm:^6.1.0" "@metamask/base-controller": "npm:^9.1.0" + "@metamask/base-data-service": "npm:^0.1.3" "@metamask/controller-utils": "npm:^12.3.0" "@metamask/keyring-controller": "npm:^27.1.0" "@metamask/messenger": "npm:^2.0.0" "@metamask/profile-sync-controller": "npm:^28.3.0" + "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^11.11.0" + "@tanstack/query-core": "npm:^4.43.0" "@ts-bridge/cli": "npm:^0.6.4" "@types/jest": "npm:^30.0.0" deepmerge: "npm:^4.2.2" From 99dba2765f3932213476b098c9564a9fca00b926 Mon Sep 17 00:00:00 2001 From: Tuna Date: Sun, 2 Aug 2026 14:57:11 +0700 Subject: [PATCH 16/18] fix: lint --- packages/claims-controller/src/ClaimsService.test.ts | 2 +- packages/claims-controller/src/index.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/claims-controller/src/ClaimsService.test.ts b/packages/claims-controller/src/ClaimsService.test.ts index d2fcd7ae49f..9c645b65fdd 100644 --- a/packages/claims-controller/src/ClaimsService.test.ts +++ b/packages/claims-controller/src/ClaimsService.test.ts @@ -541,7 +541,7 @@ describe('ClaimsService', () => { env: Env.DEV, messenger, fetchFunction: mockFetchFunction, - captureException: () => { + captureException: (_error: Error): void => { throw new Error('capture failed'); }, }); diff --git a/packages/claims-controller/src/index.ts b/packages/claims-controller/src/index.ts index 07c71d7bb95..622e8b3d7bc 100644 --- a/packages/claims-controller/src/index.ts +++ b/packages/claims-controller/src/index.ts @@ -42,7 +42,6 @@ export type { ClaimsServiceGetClaimsApiUrlAction, ClaimsServiceGetClaimByIdAction, ClaimsServiceGenerateMessageForClaimSignatureAction, - ClaimsServiceMethodActions, } from './ClaimsService-method-action-types.js'; export type { From 5f5394dc5488d1aa5c81fe28d0ab82ef025cdc2c Mon Sep 17 00:00:00 2001 From: Tuna Date: Sun, 2 Aug 2026 15:03:08 +0700 Subject: [PATCH 17/18] feat: enhance ClaimsService with bearer token handling and caching logic --- .../src/ClaimsService.test.ts | 88 +++++++++++++++++++ .../claims-controller/src/ClaimsService.ts | 37 +++++--- 2 files changed, 115 insertions(+), 10 deletions(-) diff --git a/packages/claims-controller/src/ClaimsService.test.ts b/packages/claims-controller/src/ClaimsService.test.ts index 9c645b65fdd..2d5dd770ffb 100644 --- a/packages/claims-controller/src/ClaimsService.test.ts +++ b/packages/claims-controller/src/ClaimsService.test.ts @@ -410,6 +410,94 @@ describe('ClaimsService', () => { expect(mockFetchFunction).toHaveBeenCalledTimes(2); }); + it('does not serve cached claims across different bearer tokens', async () => { + const tokens = ['profile-a-token', 'profile-b-token']; + mockAuthenticationControllerGetBearerToken.mockImplementation( + async () => tokens.shift() ?? 'exhausted', + ); + mockFetchFunction + .mockResolvedValueOnce({ + ok: true, + json: jest.fn().mockResolvedValue([MOCK_CLAIM_1]), + }) + .mockResolvedValueOnce({ + ok: true, + json: jest.fn().mockResolvedValue([MOCK_CLAIM_2]), + }); + + const service = createMockClaimsService(); + + const first = await service.getClaims(); + const second = await service.getClaims(); + + expect(first).toStrictEqual([MOCK_CLAIM_1]); + expect(second).toStrictEqual([MOCK_CLAIM_2]); + expect(mockFetchFunction).toHaveBeenCalledTimes(2); + }); + + it('does not share an in-flight getClaims request across different bearer tokens', async () => { + const tokens = ['profile-a-token', 'profile-b-token']; + mockAuthenticationControllerGetBearerToken.mockImplementation( + async () => tokens.shift() ?? 'exhausted', + ); + mockFetchFunction.mockImplementation(async (_url, options) => { + const authHeader = (options as { headers: Record }) + .headers.Authorization; + await new Promise((resolve) => { + setTimeout(resolve, 50); + }); + return { + ok: true, + json: jest + .fn() + .mockResolvedValue( + authHeader === 'Bearer profile-a-token' + ? [MOCK_CLAIM_1] + : [MOCK_CLAIM_2], + ), + }; + }); + + const service = createMockClaimsService(); + + const [first, second] = await Promise.all([ + service.getClaims(), + service.getClaims(), + ]); + + expect(first).toStrictEqual([MOCK_CLAIM_1]); + expect(second).toStrictEqual([MOCK_CLAIM_2]); + expect(mockFetchFunction).toHaveBeenCalledTimes(2); + }); + + it('does not leak the bearer token through cache update events', async () => { + mockAuthenticationControllerGetBearerToken.mockResolvedValue( + 'sensitive-token', + ); + mockFetchFunction.mockResolvedValue({ + ok: true, + json: jest.fn().mockResolvedValue([MOCK_CLAIM_1]), + }); + + const { messenger } = createMockClaimsServiceMessenger( + mockAuthenticationControllerGetBearerToken, + mockCaptureException, + ); + const publishSpy = jest.spyOn(messenger, 'publish'); + const service = new ClaimsService({ + env: Env.DEV, + messenger, + fetchFunction: mockFetchFunction, + }); + + await service.getClaims(); + + expect(publishSpy).toHaveBeenCalled(); + expect(JSON.stringify(publishSpy.mock.calls)).not.toContain( + 'sensitive-token', + ); + }); + it('refetches GET requests after invalidation', async () => { mockAuthenticationControllerGetBearerToken.mockResolvedValue( 'test-token', diff --git a/packages/claims-controller/src/ClaimsService.ts b/packages/claims-controller/src/ClaimsService.ts index eb447c6054a..cf2e34bb4ef 100644 --- a/packages/claims-controller/src/ClaimsService.ts +++ b/packages/claims-controller/src/ClaimsService.ts @@ -10,6 +10,7 @@ import type { AuthenticationController } from '@metamask/profile-sync-controller import { array, validate } from '@metamask/superstruct'; import type { Struct } from '@metamask/superstruct'; import type { Hex } from '@metamask/utils'; +import { bytesToHex, sha256, stringToBytes } from '@metamask/utils'; import type { QueryClientConfig } from '@tanstack/query-core'; import type { ClaimsServiceMethodActions } from './ClaimsService-method-action-types.js'; @@ -164,13 +165,17 @@ export class ClaimsService extends BaseDataService< */ async fetchClaimsConfigurations(): Promise { try { + const bearerToken = await this.messenger.call( + 'AuthenticationController:getBearerToken', + ); + const profileKey = bytesToHex(await sha256(stringToBytes(bearerToken))); + const configurations = await this.fetchQuery({ - queryKey: [`${this.name}:fetchClaimsConfigurations`], + queryKey: [`${this.name}:fetchClaimsConfigurations`, profileKey], queryFn: async () => { - const headers = await this.getRequestHeaders(); const url = `${this.getClaimsApiUrl()}/configurations`; const response = await this.#fetch(url, { - headers, + headers: this.#headersForToken(bearerToken), }); if (!response.ok) { @@ -202,13 +207,17 @@ export class ClaimsService extends BaseDataService< */ async getClaims(): Promise { try { + const bearerToken = await this.messenger.call( + 'AuthenticationController:getBearerToken', + ); + const profileKey = bytesToHex(await sha256(stringToBytes(bearerToken))); + const claims = await this.fetchQuery({ - queryKey: [`${this.name}:getClaims`], + queryKey: [`${this.name}:getClaims`, profileKey], queryFn: async () => { - const headers = await this.getRequestHeaders(); const url = `${this.getClaimsApiUrl()}/claims`; const response = await this.#fetch(url, { - headers, + headers: this.#headersForToken(bearerToken), }); if (!response.ok) { @@ -247,13 +256,17 @@ export class ClaimsService extends BaseDataService< */ async getClaimById(id: string): Promise { try { + const bearerToken = await this.messenger.call( + 'AuthenticationController:getBearerToken', + ); + const profileKey = bytesToHex(await sha256(stringToBytes(bearerToken))); + const claim = await this.fetchQuery({ - queryKey: [`${this.name}:getClaimById`, id], + queryKey: [`${this.name}:getClaimById`, id, profileKey], queryFn: async () => { - const headers = await this.getRequestHeaders(); const url = `${this.getClaimsApiUrl()}/claims/byId/${id}`; const response = await this.#fetch(url, { - headers, + headers: this.#headersForToken(bearerToken), }); if (!response.ok) { @@ -333,8 +346,12 @@ export class ClaimsService extends BaseDataService< const bearerToken = await this.messenger.call( 'AuthenticationController:getBearerToken', ); + return this.#headersForToken(bearerToken); + } + + #headersForToken(token: string): Record { return { - Authorization: `Bearer ${bearerToken}`, + Authorization: `Bearer ${token}`, }; } From c0be7170456b4a69eb1ef9cd2e059315902b539e Mon Sep 17 00:00:00 2001 From: Tuna Date: Sun, 2 Aug 2026 15:05:34 +0700 Subject: [PATCH 18/18] test: add handling for malformed GET responses in ClaimsService tests --- .../src/ClaimsService.test.ts | 26 +++++++++ .../claims-controller/src/ClaimsService.ts | 54 +++++++++---------- 2 files changed, 50 insertions(+), 30 deletions(-) diff --git a/packages/claims-controller/src/ClaimsService.test.ts b/packages/claims-controller/src/ClaimsService.test.ts index 2d5dd770ffb..9b49a1fdb96 100644 --- a/packages/claims-controller/src/ClaimsService.test.ts +++ b/packages/claims-controller/src/ClaimsService.test.ts @@ -518,6 +518,32 @@ describe('ClaimsService', () => { expect(mockFetchFunction).toHaveBeenCalledTimes(2); }); + it('does not cache malformed GET responses', async () => { + mockAuthenticationControllerGetBearerToken.mockResolvedValue( + 'test-token', + ); + mockFetchFunction + .mockResolvedValueOnce({ + ok: true, + json: jest.fn().mockResolvedValue({ invalid: true }), + }) + .mockResolvedValueOnce({ + ok: true, + json: jest.fn().mockResolvedValue(MOCK_CONFIGURATIONS), + }); + + const service = createMockClaimsService(); + + await expect(service.fetchClaimsConfigurations()).rejects.toThrow( + ClaimsServiceErrorMessages.FAILED_TO_FETCH_CONFIGURATIONS, + ); + + const configurations = await service.fetchClaimsConfigurations(); + + expect(configurations).toStrictEqual(MOCK_CONFIGURATIONS); + expect(mockFetchFunction).toHaveBeenCalledTimes(2); + }); + it('publishes cacheUpdated events for cached GET requests', async () => { mockAuthenticationControllerGetBearerToken.mockResolvedValue( 'test-token', diff --git a/packages/claims-controller/src/ClaimsService.ts b/packages/claims-controller/src/ClaimsService.ts index cf2e34bb4ef..c501ef29d72 100644 --- a/packages/claims-controller/src/ClaimsService.ts +++ b/packages/claims-controller/src/ClaimsService.ts @@ -170,7 +170,7 @@ export class ClaimsService extends BaseDataService< ); const profileKey = bytesToHex(await sha256(stringToBytes(bearerToken))); - const configurations = await this.fetchQuery({ + return await this.fetchQuery({ queryKey: [`${this.name}:fetchClaimsConfigurations`, profileKey], queryFn: async () => { const url = `${this.getClaimsApiUrl()}/configurations`; @@ -182,15 +182,15 @@ export class ClaimsService extends BaseDataService< throw await getErrorFromResponse(response); } - return response.json(); + const configurations = await response.json(); + + return this.#validateResponse( + configurations, + ClaimsConfigurationsResponseStruct, + ClaimsServiceErrorMessages.FAILED_TO_FETCH_CONFIGURATIONS, + ); }, }); - - return this.#validateResponse( - configurations, - ClaimsConfigurationsResponseStruct, - ClaimsServiceErrorMessages.FAILED_TO_FETCH_CONFIGURATIONS, - ); } catch (error) { return this.#handleError( 'fetchClaimsConfigurations', @@ -212,7 +212,7 @@ export class ClaimsService extends BaseDataService< ); const profileKey = bytesToHex(await sha256(stringToBytes(bearerToken))); - const claims = await this.fetchQuery({ + return await this.fetchQuery({ queryKey: [`${this.name}:getClaims`, profileKey], queryFn: async () => { const url = `${this.getClaimsApiUrl()}/claims`; @@ -224,21 +224,15 @@ export class ClaimsService extends BaseDataService< throw await getErrorFromResponse(response); } - return response.json(); + const claims = await response.json(); + + return this.#validateResponse( + claims, + array(ClaimStruct), + ClaimsServiceErrorMessages.FAILED_TO_GET_CLAIMS, + ); }, }); - - const [validationError, validatedClaims] = validate( - claims, - array(ClaimStruct), - ); - if (validationError) { - throw new Error( - `${ClaimsServiceErrorMessages.FAILED_TO_GET_CLAIMS}: ${validationError.message}`, - ); - } - - return validatedClaims as Claim[]; } catch (error) { return this.#handleError( 'getClaims', @@ -261,7 +255,7 @@ export class ClaimsService extends BaseDataService< ); const profileKey = bytesToHex(await sha256(stringToBytes(bearerToken))); - const claim = await this.fetchQuery({ + return await this.fetchQuery({ queryKey: [`${this.name}:getClaimById`, id, profileKey], queryFn: async () => { const url = `${this.getClaimsApiUrl()}/claims/byId/${id}`; @@ -273,15 +267,15 @@ export class ClaimsService extends BaseDataService< throw await getErrorFromResponse(response); } - return response.json(); + const claim = await response.json(); + + return this.#validateResponse( + claim, + ClaimStruct, + ClaimsServiceErrorMessages.FAILED_TO_GET_CLAIM_BY_ID, + ); }, }); - - return this.#validateResponse( - claim, - ClaimStruct, - ClaimsServiceErrorMessages.FAILED_TO_GET_CLAIM_BY_ID, - ) as Claim; } catch (error) { return this.#handleError( 'getClaimById',