Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/perps-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
10 changes: 9 additions & 1 deletion packages/perps-controller/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export enum MarketCategory {
Etf = 'etf',
Commodity = 'commodity',
Forex = 'forex',
Memecoin = 'memecoin',
}

export type MarketType = `${MarketCategory}`;
Expand All @@ -118,6 +119,7 @@ export type TerminalAssetMetadata = {
export type MarketTypeFilter =
| 'all'
| 'crypto'
| 'memecoin'
| 'stock'
| 'pre-ipo'
| 'index'
Expand All @@ -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',
Expand Down
10 changes: 10 additions & 0 deletions packages/perps-controller/src/utils/marketUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);
Comment thread
cursor[bot] marked this conversation as resolved.
default:
// Every other filter is a 1:1 data-model category match.
return market.marketType !== undefined && market.marketType === category;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,35 +93,37 @@ 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');
expect(MarketCategory.Index).toBe('index');
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', () => {
expect(MARKET_CATEGORIES).not.toContain('all');
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',
Expand All @@ -139,6 +141,7 @@ describe('MARKET_CATEGORIES', () => {
// The runtime check here mirrors that constraint.
const validValues: readonly string[] = [
'crypto',
'memecoin',
'stock',
'pre-ipo',
'index',
Expand Down
81 changes: 81 additions & 0 deletions packages/perps-controller/tests/src/utils/marketUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down