diff --git a/packages/perps-controller/CHANGELOG.md b/packages/perps-controller/CHANGELOG.md index a8cfc9878c5..37b2a916fa5 100644 --- a/packages/perps-controller/CHANGELOG.md +++ b/packages/perps-controller/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `MarketCategory.Memecoin` (`'memecoin'`) as a new UI-only filter category and expose it via `MARKET_CATEGORIES` and `MarketTypeFilter` ([#10168](https://github.com/MetaMask/core/pull/10168)). + - Derived in `matchesCategory` from a non-HIP-3 crypto market carrying the `'memecoin'` tag. + - Overlaps with the `'crypto'` filter by design — memecoin markets appear under both pills. + ### Changed - Bump `uuid` from `^8.3.2` to `^9.0.1` ([#10117](https://github.com/MetaMask/core/pull/10117)) diff --git a/packages/perps-controller/src/types/index.ts b/packages/perps-controller/src/types/index.ts index 6b6be5d701b..70efbd4380e 100644 --- a/packages/perps-controller/src/types/index.ts +++ b/packages/perps-controller/src/types/index.ts @@ -92,6 +92,7 @@ export enum MarketCategory { Etf = 'etf', Commodity = 'commodity', Forex = 'forex', + Memecoin = 'memecoin', } export type MarketType = `${MarketCategory}`; @@ -118,6 +119,7 @@ export type TerminalAssetMetadata = { export type MarketTypeFilter = | 'all' | 'crypto' + | 'memecoin' | 'stock' | 'pre-ipo' | 'index' @@ -127,13 +129,19 @@ export type MarketTypeFilter = | 'new'; /** - * Ordered list of the 7 data-model market categories for UI pills. + * Ordered list of data-model market categories for UI pills. * Does not include the 'all' or 'new' sentinel values — those are applied * via dedicated UI controls, not the category pills. + * + * Note: 'memecoin' is a derived category (marketType === 'crypto' && + * tags.includes('memecoin')). It overlaps with 'crypto' by design — + * memecoins appear under both pills. + * * Kept in sync with {@link MarketTypeFilter} via `satisfies`. */ export const MARKET_CATEGORIES = [ 'crypto', + 'memecoin', 'stock', 'pre-ipo', 'index', diff --git a/packages/perps-controller/src/utils/marketUtils.ts b/packages/perps-controller/src/utils/marketUtils.ts index 5dfd7f7b140..3f2f48310be 100644 --- a/packages/perps-controller/src/utils/marketUtils.ts +++ b/packages/perps-controller/src/utils/marketUtils.ts @@ -67,6 +67,16 @@ export function matchesCategory( case 'crypto': // Main-DEX markets, plus HIP-3 assets explicitly typed as CryptoCurrency. return !isHip3Market(market) || market.marketType === 'crypto'; + case 'memecoin': + // Derived category: a crypto market that carries the 'memecoin' tag. + // Aligned with the 'crypto' case above so tagged main-DEX markets + // still match when their marketType is unset (the common + // provider-sourced shape prior to Terminal enrichment). Overlaps + // with 'crypto' by design — memecoins appear under both pills. + return ( + (!isHip3Market(market) || market.marketType === 'crypto') && + (market.tags?.includes('memecoin') ?? false) + ); default: // Every other filter is a 1:1 data-model category match. return market.marketType !== undefined && market.marketType === category; diff --git a/packages/perps-controller/tests/src/PerpsController.market-filtering.test.ts b/packages/perps-controller/tests/src/PerpsController.market-filtering.test.ts index 464a64f5d57..fb0345c2f2f 100644 --- a/packages/perps-controller/tests/src/PerpsController.market-filtering.test.ts +++ b/packages/perps-controller/tests/src/PerpsController.market-filtering.test.ts @@ -117,9 +117,10 @@ describe('PerpsController — market categories & filtering', () => { expect(categories).not.toContain('new'); }); - it('includes all 7 data categories', () => { + it('includes all 8 data categories', () => { const categories = controller.getMarketCategories(); expect(categories).toContain('crypto'); + expect(categories).toContain('memecoin'); expect(categories).toContain('stock'); expect(categories).toContain('pre-ipo'); expect(categories).toContain('index'); diff --git a/packages/perps-controller/tests/src/constants/hyperLiquidConfig.test.ts b/packages/perps-controller/tests/src/constants/hyperLiquidConfig.test.ts index 468821780fc..098fbdff349 100644 --- a/packages/perps-controller/tests/src/constants/hyperLiquidConfig.test.ts +++ b/packages/perps-controller/tests/src/constants/hyperLiquidConfig.test.ts @@ -93,7 +93,7 @@ describe('HIP3_ASSET_MARKET_TYPES', () => { }); describe('MarketCategory', () => { - it('has string values for all 7 data-model categories', () => { + it('has string values for all 8 data-model categories', () => { expect(MarketCategory.CryptoCurrency).toBe('crypto'); expect(MarketCategory.Stock).toBe('stock'); expect(MarketCategory.PreIpo).toBe('pre-ipo'); @@ -101,17 +101,18 @@ describe('MarketCategory', () => { expect(MarketCategory.Etf).toBe('etf'); expect(MarketCategory.Commodity).toBe('commodity'); expect(MarketCategory.Forex).toBe('forex'); + expect(MarketCategory.Memecoin).toBe('memecoin'); }); - it('has exactly 7 members', () => { + it('has exactly 8 members', () => { const values = Object.values(MarketCategory); - expect(values).toHaveLength(7); + expect(values).toHaveLength(8); }); }); describe('MARKET_CATEGORIES', () => { - it('has exactly 7 entries (one per data-model category)', () => { - expect(MARKET_CATEGORIES).toHaveLength(7); + it('has exactly 8 entries (one per data-model category)', () => { + expect(MARKET_CATEGORIES).toHaveLength(8); }); it('does not include the all or new sentinel values', () => { @@ -119,9 +120,10 @@ describe('MARKET_CATEGORIES', () => { expect(MARKET_CATEGORIES).not.toContain('new'); }); - it('includes all 7 MarketTypeFilter data categories', () => { + it('includes all 8 MarketTypeFilter data categories', () => { const dataCategories: MarketTypeFilter[] = [ 'crypto', + 'memecoin', 'stock', 'pre-ipo', 'index', @@ -139,6 +141,7 @@ describe('MARKET_CATEGORIES', () => { // The runtime check here mirrors that constraint. const validValues: readonly string[] = [ 'crypto', + 'memecoin', 'stock', 'pre-ipo', 'index', diff --git a/packages/perps-controller/tests/src/utils/marketUtils.test.ts b/packages/perps-controller/tests/src/utils/marketUtils.test.ts index a881c05c4c0..97d90685a9c 100644 --- a/packages/perps-controller/tests/src/utils/marketUtils.test.ts +++ b/packages/perps-controller/tests/src/utils/marketUtils.test.ts @@ -91,6 +91,87 @@ describe('marketUtils category classification', () => { expect(matchesCategory(market({ marketType }), filter)).toBe(true); }, ); + + describe("'memecoin' filter", () => { + it('matches a crypto market carrying the memecoin tag', () => { + expect( + matchesCategory( + market({ marketType: 'crypto', tags: ['memecoin'] }), + 'memecoin', + ), + ).toBe(true); + }); + + it('matches a main-DEX market with unset marketType and the memecoin tag', () => { + // Common provider-sourced shape: main-DEX markets often have + // marketType undefined until Terminal enrichment applies it. The + // memecoin filter must still surface them, mirroring the 'crypto' + // case above. + expect( + matchesCategory( + market({ + isHip3: false, + marketType: undefined, + tags: ['memecoin'], + }), + 'memecoin', + ), + ).toBe(true); + }); + + it('still matches crypto for a memecoin (overlapping by design)', () => { + expect( + matchesCategory( + market({ marketType: 'crypto', tags: ['memecoin'] }), + 'crypto', + ), + ).toBe(true); + }); + + it('does not match crypto without the memecoin tag', () => { + expect( + matchesCategory( + market({ marketType: 'crypto', tags: ['top-100'] }), + 'memecoin', + ), + ).toBe(false); + }); + + it('does not match a crypto market with no tags', () => { + expect( + matchesCategory(market({ marketType: 'crypto' }), 'memecoin'), + ).toBe(false); + }); + + it('does not match non-crypto HIP-3 markets even with the memecoin tag', () => { + expect( + matchesCategory( + market({ + isHip3: true, + marketType: 'stock', + tags: ['memecoin'], + }), + 'memecoin', + ), + ).toBe(false); + }); + + it('does not match a marketSource-only HIP-3 market with the memecoin tag', () => { + // isHip3Market also treats markets with only a marketSource DEX id + // as HIP-3, so those shouldn't leak into the memecoin filter + // either. + expect( + matchesCategory( + market({ + isHip3: undefined, + marketSource: 'xyz', + tags: ['memecoin'], + }), + 'memecoin', + ), + ).toBe(false); + }); + }); }); describe('getMarketTypeFilter', () => {