diff --git a/README.md b/README.md index 27f8d326fe2..8cbcdf0a7c8 100644 --- a/README.md +++ b/README.md @@ -259,8 +259,6 @@ linkStyle default opacity:0.5 address_book_controller --> base_controller; address_book_controller --> controller_utils; address_book_controller --> messenger; - ai_controllers --> base_controller; - ai_controllers --> messenger; analytics_controller --> base_controller; analytics_controller --> geolocation_controller; analytics_controller --> messenger; diff --git a/packages/ai-controllers/CHANGELOG.md b/packages/ai-controllers/CHANGELOG.md index ffaa06c3bef..291564d05c3 100644 --- a/packages/ai-controllers/CHANGELOG.md +++ b/packages/ai-controllers/CHANGELOG.md @@ -11,6 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bump `@metamask/utils` from `^11.11.0` to `^11.12.0` ([#10076](https://github.com/MetaMask/core/pull/10076)) +### Removed + +- **BREAKING:** Remove `AiDigestController` and related messenger/state exports. Digest fetch/validate lives on `AiDigestService`; clients construct the service and own freshness (e.g. React Query). ([#10089](https://github.com/MetaMask/core/pull/10089)) + - Removed `AiDigestController`, `getDefaultAiDigestControllerState`, `aiDigestControllerName`, `AiDigestControllerState`, and controller action/event/messenger types. + - `AiDigestService.searchDigest` and `fetchFrontPageItem` now throw on an empty identifier (previously the controller). + ## [1.0.0] ### Changed diff --git a/packages/ai-controllers/package.json b/packages/ai-controllers/package.json index f177ab0ef8f..cd12f6e5e4d 100644 --- a/packages/ai-controllers/package.json +++ b/packages/ai-controllers/package.json @@ -46,8 +46,6 @@ "changelog:validate": "../../scripts/validate-changelog.sh @metamask/ai-controllers", "lint:tsconfigs": "tsx ../../scripts/lint-tsconfigs/lint-tsconfigs.mts", "lint:tsconfigs:fix": "tsx ../../scripts/lint-tsconfigs/lint-tsconfigs.mts --fix", - "messenger-action-types:check": "tsx ../../packages/messenger-cli/src/cli.ts --formatter oxfmt --esm --check", - "messenger-action-types:generate": "tsx ../../packages/messenger-cli/src/cli.ts --formatter oxfmt --esm --generate", "since-latest-release": "../../scripts/since-latest-release.sh", "test": "NODE_OPTIONS=--experimental-vm-modules jest --reporters=jest-silent-reporter", "test:clean": "NODE_OPTIONS=--experimental-vm-modules jest --clearCache", @@ -55,8 +53,6 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/base-controller": "^9.1.0", - "@metamask/messenger": "^2.0.0", "@metamask/superstruct": "^3.4.1", "@metamask/utils": "^11.12.0" }, diff --git a/packages/ai-controllers/src/AiDigestController-method-action-types.ts b/packages/ai-controllers/src/AiDigestController-method-action-types.ts deleted file mode 100644 index db550f14638..00000000000 --- a/packages/ai-controllers/src/AiDigestController-method-action-types.ts +++ /dev/null @@ -1,56 +0,0 @@ -/** - * This file is auto generated. - * Do not edit manually. - */ - -import type { AiDigestController } from './AiDigestController.js'; - -/** - * Fetches market insights for a given asset identifier. - * - * Accepts either a CAIP-19 asset type (e.g. `eip155:1/slip44:60`) or a perps - * market symbol (e.g. `ETH`). The service handles choosing the correct API - * query parameter automatically. Clients own freshness/caching. - * - * @param assetIdentifier - The asset identifier (CAIP-19 ID or perps market symbol). - * @returns The market insights report, or `null` if none exists. - */ -export type AiDigestControllerFetchMarketInsightsAction = { - type: `AiDigestController:fetchMarketInsights`; - handler: AiDigestController['fetchMarketInsights']; -}; - -/** - * Fetches the market overview report. - * - * Clients own freshness/caching. - * - * @returns The market overview report, or `null` if none exists. - */ -export type AiDigestControllerFetchMarketOverviewAction = { - type: `AiDigestController:fetchMarketOverview`; - handler: AiDigestController['fetchMarketOverview']; -}; - -/** - * Fetches a single market overview front page by id. - * - * Unlike the market overview report (which only returns the latest items), - * this resolves an older item that has since dropped out of the report, so - * clients can render it directly (e.g. from a deep link). - * - * @param id - The front-page identifier (UUID). - * @returns The market overview front page, or `null` if none exists. - */ -export type AiDigestControllerFetchFrontPageItemAction = { - type: `AiDigestController:fetchFrontPageItem`; - handler: AiDigestController['fetchFrontPageItem']; -}; - -/** - * Union of all AiDigestController action types. - */ -export type AiDigestControllerMethodActions = - | AiDigestControllerFetchMarketInsightsAction - | AiDigestControllerFetchMarketOverviewAction - | AiDigestControllerFetchFrontPageItemAction; diff --git a/packages/ai-controllers/src/AiDigestController.test.ts b/packages/ai-controllers/src/AiDigestController.test.ts deleted file mode 100644 index 6350ac75bbc..00000000000 --- a/packages/ai-controllers/src/AiDigestController.test.ts +++ /dev/null @@ -1,249 +0,0 @@ -import { Messenger } from '@metamask/messenger'; - -import { - AiDigestController, - getDefaultAiDigestControllerState, - AiDigestControllerErrorMessage, -} from './index.js'; -import type { - AiDigestControllerMessenger, - DigestService, - MarketInsightsReport, - MarketOverview, - MarketOverviewFrontPage, -} from './index.js'; - -const mockReport: MarketInsightsReport = { - digestId: 'digest-1', - version: '1.0', - asset: 'btc', - generatedAt: '2026-02-11T10:32:52.403Z', - headline: 'BTC update', - summary: 'Momentum remains positive.', - trends: [], - sources: [], -}; - -const mockOverview: MarketOverview = { - version: '1.0', - generatedAt: '2026-02-11T10:32:52.403Z', - trends: [], -}; - -const mockFrontPage: MarketOverviewFrontPage = { - id: 'a3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d', - item: { - title: 'Institutional adoption', - description: 'Institutional players continue accumulating.', - category: 'macro', - impact: 'positive', - articles: [], - relatedAssets: [], - }, - ctaTitle: 'Majors steady as volatility cools', - ctaDescription: 'Bitcoin and Ethereum held firm as funding rates normalized.', - createdAt: '2026-02-11T10:32:52.403Z', -}; - -const createMessenger = (): AiDigestControllerMessenger => - new Messenger({ - namespace: 'AiDigestController', - }) as AiDigestControllerMessenger; - -const createService = (overrides?: Partial): DigestService => ({ - searchDigest: jest.fn().mockResolvedValue(mockReport), - fetchMarketOverview: jest.fn().mockResolvedValue(mockOverview), - fetchFrontPageItem: jest.fn().mockResolvedValue(mockFrontPage), - ...overrides, -}); - -describe('AiDigestController (market insights)', () => { - it('returns default empty state', () => { - expect(getDefaultAiDigestControllerState()).toStrictEqual({}); - }); - - it('registers fetch action on messenger', async () => { - const digestService = createService(); - const messenger = createMessenger(); - const controller = new AiDigestController({ messenger, digestService }); - - const result = await messenger.call( - 'AiDigestController:fetchMarketInsights', - 'eip155:1/slip44:0', - ); - - expect(result).toStrictEqual(mockReport); - expect(controller.state).toStrictEqual({}); - }); - - it('does not cache; each call hits the service', async () => { - const digestService = createService(); - const controller = new AiDigestController({ - messenger: createMessenger(), - digestService, - }); - - await controller.fetchMarketInsights('eip155:1/slip44:0'); - await controller.fetchMarketInsights('eip155:1/slip44:0'); - - expect(digestService.searchDigest).toHaveBeenCalledTimes(2); - }); - - it('throws for empty asset identifier', async () => { - const digestService = createService(); - const controller = new AiDigestController({ - messenger: createMessenger(), - digestService, - }); - - await expect(controller.fetchMarketInsights('')).rejects.toThrow( - AiDigestControllerErrorMessage.INVALID_ASSET_IDENTIFIER, - ); - expect(digestService.searchDigest).not.toHaveBeenCalled(); - }); - - it('accepts a perps market symbol as asset identifier', async () => { - const digestService = createService(); - const controller = new AiDigestController({ - messenger: createMessenger(), - digestService, - }); - const perpsSymbol = 'ETH'; - - const result = await controller.fetchMarketInsights(perpsSymbol); - - expect(result).toStrictEqual(mockReport); - expect(digestService.searchDigest).toHaveBeenCalledWith(perpsSymbol); - }); - - it('treats perps and CAIP-19 identifiers as independent service calls', async () => { - const digestService = createService(); - const controller = new AiDigestController({ - messenger: createMessenger(), - digestService, - }); - const perpsSymbol = 'ETH'; - const caip19Id = 'eip155:1/slip44:60'; - - await controller.fetchMarketInsights(perpsSymbol); - await controller.fetchMarketInsights(caip19Id); - - expect(digestService.searchDigest).toHaveBeenCalledTimes(2); - expect(digestService.searchDigest).toHaveBeenNthCalledWith(1, perpsSymbol); - expect(digestService.searchDigest).toHaveBeenNthCalledWith(2, caip19Id); - }); - - it('returns null when the service returns null', async () => { - const digestService = createService({ - searchDigest: jest.fn().mockResolvedValue(null), - }); - const controller = new AiDigestController({ - messenger: createMessenger(), - digestService, - }); - - const result = await controller.fetchMarketInsights('eip155:1/slip44:0'); - - expect(result).toBeNull(); - }); -}); - -describe('AiDigestController (market overview)', () => { - it('registers fetchMarketOverview action on messenger', async () => { - const digestService = createService(); - const messenger = createMessenger(); - const controller = new AiDigestController({ messenger, digestService }); - - const result = await messenger.call( - 'AiDigestController:fetchMarketOverview', - ); - - expect(result).toStrictEqual(mockOverview); - expect(controller.state).toStrictEqual({}); - }); - - it('does not cache; each call hits the service', async () => { - const digestService = createService(); - const controller = new AiDigestController({ - messenger: createMessenger(), - digestService, - }); - - await controller.fetchMarketOverview(); - await controller.fetchMarketOverview(); - - expect(digestService.fetchMarketOverview).toHaveBeenCalledTimes(2); - }); - - it('returns null when the service returns null', async () => { - const digestService = createService({ - fetchMarketOverview: jest.fn().mockResolvedValue(null), - }); - const controller = new AiDigestController({ - messenger: createMessenger(), - digestService, - }); - - const result = await controller.fetchMarketOverview(); - - expect(result).toBeNull(); - }); -}); - -describe('AiDigestController (front page)', () => { - it('registers fetchFrontPageItem action on messenger', async () => { - const digestService = createService(); - const messenger = createMessenger(); - const controller = new AiDigestController({ messenger, digestService }); - - const result = await messenger.call( - 'AiDigestController:fetchFrontPageItem', - mockFrontPage.id, - ); - - expect(result).toStrictEqual(mockFrontPage); - expect(digestService.fetchFrontPageItem).toHaveBeenCalledWith( - mockFrontPage.id, - ); - expect(controller.state).toStrictEqual({}); - }); - - it('delegates to the service and returns the front page', async () => { - const digestService = createService(); - const controller = new AiDigestController({ - messenger: createMessenger(), - digestService, - }); - - const result = await controller.fetchFrontPageItem(mockFrontPage.id); - - expect(result).toStrictEqual(mockFrontPage); - }); - - it('returns null when the service returns null', async () => { - const digestService = createService({ - fetchFrontPageItem: jest.fn().mockResolvedValue(null), - }); - const controller = new AiDigestController({ - messenger: createMessenger(), - digestService, - }); - - const result = await controller.fetchFrontPageItem(mockFrontPage.id); - - expect(result).toBeNull(); - }); - - it('throws for an empty id without calling the service', async () => { - const digestService = createService(); - const controller = new AiDigestController({ - messenger: createMessenger(), - digestService, - }); - - await expect(controller.fetchFrontPageItem('')).rejects.toThrow( - AiDigestControllerErrorMessage.INVALID_FRONT_PAGE_ID, - ); - expect(digestService.fetchFrontPageItem).not.toHaveBeenCalled(); - }); -}); diff --git a/packages/ai-controllers/src/AiDigestController.ts b/packages/ai-controllers/src/AiDigestController.ts deleted file mode 100644 index 7f27cc5c08d..00000000000 --- a/packages/ai-controllers/src/AiDigestController.ts +++ /dev/null @@ -1,133 +0,0 @@ -import type { - StateMetadata, - ControllerStateChangeEvent, - ControllerGetStateAction, -} from '@metamask/base-controller'; -import { BaseController } from '@metamask/base-controller'; -import type { Messenger } from '@metamask/messenger'; - -import { - AiDigestControllerErrorMessage, - controllerName, -} from './ai-digest-constants.js'; -import type { - AiDigestControllerState, - DigestService, - MarketInsightsReport, - MarketOverview, - MarketOverviewFrontPage, -} from './ai-digest-types.js'; -import type { AiDigestControllerMethodActions } from './AiDigestController-method-action-types.js'; - -export type AiDigestControllerGetStateAction = ControllerGetStateAction< - typeof controllerName, - AiDigestControllerState ->; - -export type AiDigestControllerActions = - | AiDigestControllerGetStateAction - | AiDigestControllerMethodActions; - -export type AiDigestControllerStateChangeEvent = ControllerStateChangeEvent< - typeof controllerName, - AiDigestControllerState ->; - -export type AiDigestControllerEvents = AiDigestControllerStateChangeEvent; - -export type AiDigestControllerMessenger = Messenger< - typeof controllerName, - AiDigestControllerActions, - AiDigestControllerEvents ->; - -export type AiDigestControllerOptions = { - messenger: AiDigestControllerMessenger; - digestService: DigestService; -}; - -export function getDefaultAiDigestControllerState(): AiDigestControllerState { - return {}; -} - -const aiDigestControllerMetadata: StateMetadata = {}; - -const MESSENGER_EXPOSED_METHODS = [ - 'fetchMarketInsights', - 'fetchMarketOverview', - 'fetchFrontPageItem', -] as const; - -export class AiDigestController extends BaseController< - typeof controllerName, - AiDigestControllerState, - AiDigestControllerMessenger -> { - readonly #digestService: DigestService; - - constructor({ messenger, digestService }: AiDigestControllerOptions) { - super({ - name: controllerName, - metadata: aiDigestControllerMetadata, - state: getDefaultAiDigestControllerState(), - messenger, - }); - - this.#digestService = digestService; - this.messenger.registerMethodActionHandlers( - this, - MESSENGER_EXPOSED_METHODS, - ); - } - - /** - * Fetches market insights for a given asset identifier. - * - * Accepts either a CAIP-19 asset type (e.g. `eip155:1/slip44:60`) or a perps - * market symbol (e.g. `ETH`). The service handles choosing the correct API - * query parameter automatically. Clients own freshness/caching. - * - * @param assetIdentifier - The asset identifier (CAIP-19 ID or perps market symbol). - * @returns The market insights report, or `null` if none exists. - */ - async fetchMarketInsights( - assetIdentifier: string, - ): Promise { - if (!assetIdentifier) { - throw new Error(AiDigestControllerErrorMessage.INVALID_ASSET_IDENTIFIER); - } - - return this.#digestService.searchDigest(assetIdentifier); - } - - /** - * Fetches the market overview report. - * - * Clients own freshness/caching. - * - * @returns The market overview report, or `null` if none exists. - */ - async fetchMarketOverview(): Promise { - return this.#digestService.fetchMarketOverview(); - } - - /** - * Fetches a single market overview front page by id. - * - * Unlike the market overview report (which only returns the latest items), - * this resolves an older item that has since dropped out of the report, so - * clients can render it directly (e.g. from a deep link). - * - * @param id - The front-page identifier (UUID). - * @returns The market overview front page, or `null` if none exists. - */ - async fetchFrontPageItem( - id: string, - ): Promise { - if (!id) { - throw new Error(AiDigestControllerErrorMessage.INVALID_FRONT_PAGE_ID); - } - - return this.#digestService.fetchFrontPageItem(id); - } -} diff --git a/packages/ai-controllers/src/AiDigestService.test.ts b/packages/ai-controllers/src/AiDigestService.test.ts index 6c682427de6..5dfaa939b0a 100644 --- a/packages/ai-controllers/src/AiDigestService.test.ts +++ b/packages/ai-controllers/src/AiDigestService.test.ts @@ -58,6 +58,17 @@ describe('AiDigestService', () => { digest: mockMarketInsightsReport, }; + it('throws for an empty asset identifier without calling the API', async () => { + const service = new AiDigestService({ + baseUrl: 'http://test.com/api/v1', + }); + + await expect(service.searchDigest('')).rejects.toThrow( + AiDigestControllerErrorMessage.INVALID_ASSET_IDENTIFIER, + ); + expect(mockFetch).not.toHaveBeenCalled(); + }); + it('fetches market insights using universal asset= param for CAIP-19 identifiers', async () => { mockFetch.mockResolvedValue({ ok: true, @@ -928,6 +939,17 @@ describe('AiDigestService', () => { createdAt: '2026-02-16T10:00:00.000Z', }; + it('throws for an empty id without calling the API', async () => { + const service = new AiDigestService({ + baseUrl: 'http://test.com/api/v1', + }); + + await expect(service.fetchFrontPageItem('')).rejects.toThrow( + AiDigestControllerErrorMessage.INVALID_FRONT_PAGE_ID, + ); + expect(mockFetch).not.toHaveBeenCalled(); + }); + it('fetches a front page item from the correct endpoint', async () => { mockFetch.mockResolvedValue({ ok: true, diff --git a/packages/ai-controllers/src/AiDigestService.ts b/packages/ai-controllers/src/AiDigestService.ts index cc46185f68a..399541e01e4 100644 --- a/packages/ai-controllers/src/AiDigestService.ts +++ b/packages/ai-controllers/src/AiDigestService.ts @@ -220,6 +220,10 @@ export class AiDigestService implements DigestService { async fetchFrontPageItem( id: string, ): Promise { + if (!id) { + throw new Error(AiDigestControllerErrorMessage.INVALID_FRONT_PAGE_ID); + } + const response = await fetch( `${this.#baseUrl}/market-overview/front-page/${encodeURIComponent(id)}`, ); @@ -259,6 +263,10 @@ export class AiDigestService implements DigestService { async searchDigest( assetIdentifier: string, ): Promise { + if (!assetIdentifier) { + throw new Error(AiDigestControllerErrorMessage.INVALID_ASSET_IDENTIFIER); + } + const response = await fetch( `${this.#baseUrl}/asset-summary?asset=${encodeURIComponent(assetIdentifier)}`, ); diff --git a/packages/ai-controllers/src/ai-digest-constants.ts b/packages/ai-controllers/src/ai-digest-constants.ts index e33b7e8d15d..536809cba2a 100644 --- a/packages/ai-controllers/src/ai-digest-constants.ts +++ b/packages/ai-controllers/src/ai-digest-constants.ts @@ -1,5 +1,3 @@ -export const controllerName = 'AiDigestController'; - export const AiDigestControllerErrorMessage = { API_REQUEST_FAILED: 'API request failed', API_INVALID_RESPONSE: 'API returned invalid response', diff --git a/packages/ai-controllers/src/ai-digest-types.ts b/packages/ai-controllers/src/ai-digest-types.ts index a856f40b180..977d29ed944 100644 --- a/packages/ai-controllers/src/ai-digest-types.ts +++ b/packages/ai-controllers/src/ai-digest-types.ts @@ -193,16 +193,6 @@ export type MarketOverviewFrontPage = { createdAt: string; }; -// --------------------------------------------------------------------------- -// Controller state -// --------------------------------------------------------------------------- - -/** - * AiDigestController has no persisted cache. Digest freshness is owned by - * clients (e.g. React Query). - */ -export type AiDigestControllerState = Record; - // --------------------------------------------------------------------------- // Service interface // --------------------------------------------------------------------------- diff --git a/packages/ai-controllers/src/index.ts b/packages/ai-controllers/src/index.ts index 89d33a22e20..def255d5a26 100644 --- a/packages/ai-controllers/src/index.ts +++ b/packages/ai-controllers/src/index.ts @@ -1,26 +1,7 @@ -export type { - AiDigestControllerActions, - AiDigestControllerEvents, - AiDigestControllerGetStateAction, - AiDigestControllerMessenger, - AiDigestControllerOptions, - AiDigestControllerStateChangeEvent, -} from './AiDigestController.js'; -export { - AiDigestController, - getDefaultAiDigestControllerState, -} from './AiDigestController.js'; -export type { - AiDigestControllerFetchMarketInsightsAction, - AiDigestControllerFetchMarketOverviewAction, - AiDigestControllerFetchFrontPageItemAction, -} from './AiDigestController-method-action-types.js'; - export type { AiDigestServiceConfig } from './AiDigestService.js'; export { AiDigestService } from './AiDigestService.js'; export type { - AiDigestControllerState, AIResponseMetadata, Article, DigestService, @@ -38,7 +19,4 @@ export type { Tweet, } from './ai-digest-types.js'; -export { - controllerName as aiDigestControllerName, - AiDigestControllerErrorMessage, -} from './ai-digest-constants.js'; +export { AiDigestControllerErrorMessage } from './ai-digest-constants.js'; diff --git a/packages/ai-controllers/tsconfig.build.json b/packages/ai-controllers/tsconfig.build.json index 931c4d6594b..0df910b2151 100644 --- a/packages/ai-controllers/tsconfig.build.json +++ b/packages/ai-controllers/tsconfig.build.json @@ -5,9 +5,5 @@ "outDir": "./dist", "rootDir": "./src" }, - "references": [ - { "path": "../base-controller/tsconfig.build.json" }, - { "path": "../messenger/tsconfig.build.json" } - ], "include": ["../../types", "./src"] } diff --git a/packages/ai-controllers/tsconfig.json b/packages/ai-controllers/tsconfig.json index 68c3ddfc2cd..ee9de925a21 100644 --- a/packages/ai-controllers/tsconfig.json +++ b/packages/ai-controllers/tsconfig.json @@ -3,6 +3,5 @@ "compilerOptions": { "baseUrl": "./" }, - "references": [{ "path": "../base-controller" }, { "path": "../messenger" }], "include": ["../../types", "./src"] } diff --git a/yarn.lock b/yarn.lock index d19b8a720b0..3d241e5341e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5686,8 +5686,6 @@ __metadata: resolution: "@metamask/ai-controllers@workspace:packages/ai-controllers" dependencies: "@metamask/auto-changelog": "npm:^6.1.0" - "@metamask/base-controller": "npm:^9.1.0" - "@metamask/messenger": "npm:^2.0.0" "@metamask/superstruct": "npm:^3.4.1" "@metamask/utils": "npm:^11.12.0" "@ts-bridge/cli": "npm:^0.6.4"