diff --git a/packages/ai-controllers/CHANGELOG.md b/packages/ai-controllers/CHANGELOG.md index d09b75317e..7596a29633 100644 --- a/packages/ai-controllers/CHANGELOG.md +++ b/packages/ai-controllers/CHANGELOG.md @@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bump `@metamask/messenger` from `^1.0.0` to `^1.2.0` ([#8364](https://github.com/MetaMask/core/pull/8364), [#8373](https://github.com/MetaMask/core/pull/8373), [#8632](https://github.com/MetaMask/core/pull/8632)) - Bump `@metamask/base-controller` from `^9.0.1` to `^9.1.0` ([#8457](https://github.com/MetaMask/core/pull/8457)) +### Fixed + +- `RelatedAsset.name` and `RelatedAsset.sourceAssetId` are now optional in both the superstruct schema and TypeScript type, so a `relatedAsset` missing either field no longer causes `fetchMarketOverview` to throw `API_INVALID_RESPONSE` ([#8920](https://github.com/MetaMask/core/pull/8920)). + ## [0.6.3] ### Fixed diff --git a/packages/ai-controllers/src/AiDigestService.test.ts b/packages/ai-controllers/src/AiDigestService.test.ts index 780036ef67..7640b57abe 100644 --- a/packages/ai-controllers/src/AiDigestService.test.ts +++ b/packages/ai-controllers/src/AiDigestService.test.ts @@ -784,7 +784,7 @@ describe('AiDigestService', () => { trends: [ { ...mockMarketOverview.trends[0], - relatedAssets: [{ name: 'Bitcoin' }], // missing required fields + relatedAssets: [{ name: 'Bitcoin' }], // missing required symbol }, ], }), @@ -799,6 +799,76 @@ describe('AiDigestService', () => { ); }); + it('returns overview when an asset is missing sourceAssetId', async () => { + const assetWithoutSourceId = { + name: 'Bitcoin', + symbol: 'BTC', + caip19: ['bip122:000000000019d6689c085ae165831e93/slip44:0'], + hlPerpsMarket: ['BTC'], + // sourceAssetId intentionally absent + }; + + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + json: () => + Promise.resolve({ + ...mockMarketOverview, + trends: [ + { + ...mockMarketOverview.trends[0], + relatedAssets: [assetWithoutSourceId], + }, + ], + }), + }); + + const service = new AiDigestService({ + baseUrl: 'http://test.com/api/v1', + }); + const result = await service.fetchMarketOverview(); + + expect(result?.trends).toHaveLength(1); + expect(result?.trends[0].relatedAssets).toHaveLength(1); + expect(result?.trends[0].relatedAssets[0].symbol).toBe('BTC'); + expect(result?.trends[0].relatedAssets[0].sourceAssetId).toBeUndefined(); + }); + + it('returns overview when an asset is missing name', async () => { + const assetWithoutName = { + symbol: 'ETH', + sourceAssetId: 'ethereum', + caip19: ['eip155:1/slip44:60'], + hlPerpsMarket: ['ETH'], + // name intentionally absent + }; + + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + json: () => + Promise.resolve({ + ...mockMarketOverview, + trends: [ + { + ...mockMarketOverview.trends[0], + relatedAssets: [assetWithoutName], + }, + ], + }), + }); + + const service = new AiDigestService({ + baseUrl: 'http://test.com/api/v1', + }); + const result = await service.fetchMarketOverview(); + + expect(result?.trends).toHaveLength(1); + expect(result?.trends[0].relatedAssets).toHaveLength(1); + expect(result?.trends[0].relatedAssets[0].symbol).toBe('ETH'); + expect(result?.trends[0].relatedAssets[0].name).toBeUndefined(); + }); + it('accepts additional unknown fields in payload', async () => { const withExtras = { ...mockMarketOverview, diff --git a/packages/ai-controllers/src/AiDigestService.ts b/packages/ai-controllers/src/AiDigestService.ts index 022ef0fb63..23240521c3 100644 --- a/packages/ai-controllers/src/AiDigestService.ts +++ b/packages/ai-controllers/src/AiDigestService.ts @@ -86,10 +86,10 @@ const MarketInsightsDigestEnvelopeStruct = structType({ // Market Overview structs const RelatedAssetStruct = structType({ - name: string(), + name: optional(string()), symbol: string(), caip19: optional(array(string())), - sourceAssetId: string(), + sourceAssetId: optional(string()), hlPerpsMarket: optional(array(string())), }); diff --git a/packages/ai-controllers/src/ai-digest-types.ts b/packages/ai-controllers/src/ai-digest-types.ts index f68d2c9098..8f3f14b59b 100644 --- a/packages/ai-controllers/src/ai-digest-types.ts +++ b/packages/ai-controllers/src/ai-digest-types.ts @@ -132,9 +132,12 @@ export type MarketOverviewEntry = { * Returned by the `/market-overview` API as a rich object. */ export type RelatedAsset = { - /** Human-readable asset name (e.g. "Bitcoin") */ - name: string; - /** Ticker symbol (e.g. "BTC") */ + /** + * Human-readable asset name (e.g. "Bitcoin"). Optional — clients must fall + * back to `symbol` when absent. + */ + name?: string; + /** Ticker symbol (e.g. "BTC"). The only field guaranteed to be present. */ symbol: string; /** * CAIP-19 identifiers for this asset across chains. May be absent for @@ -142,8 +145,11 @@ export type RelatedAsset = { * normalises missing values to `[]`. */ caip19?: string[]; - /** Canonical source asset identifier (e.g. "bitcoin") */ - sourceAssetId: string; + /** + * Canonical source asset identifier (e.g. "bitcoin"). Optional — may be + * absent when the API pipeline cannot enrich the asset record. + */ + sourceAssetId?: string; /** * Optional HyperLiquid market identifiers for this asset (e.g. `BTC`, `ETH`, * `xyz:TSLA`). Covers both regular crypto tokens that trade on HyperLiquid