Skip to content
Open
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
7 changes: 7 additions & 0 deletions packages/perps-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Centralise market category classification so consumers share one model instead of re-deriving it per client ([#9009](https://github.com/MetaMask/core/pull/9009))
- Export `getMarketTypeFilter` (resolves a market to its UI category filter, collapsing stock-like categories into `stocks`), `isEquityAsset`, `isHip3Market`, and `STOCK_LIKE_MARKET_TYPES`. `getMarketTypeFilter` and `matchesCategory` treat a `marketSource` DEX id as a HIP-3 signal consistently, so partial (route-param) markets classify the same way in both.
- Export the pure `matchesCategory` and `applyMarketFilters` helpers (moved from `MarketDataService`).
- Add `MARKET_TYPE_FILTER` named constants for the `MarketTypeFilter` values.

## [7.0.0]

### Added
Expand Down
7 changes: 7 additions & 0 deletions packages/perps-controller/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ export {
WebSocketConnectionState,
PerpsAnalyticsEvent,
MARKET_CATEGORIES,
MARKET_TYPE_FILTER,
MarketCategory,
} from './types';
export type {
Expand Down Expand Up @@ -491,6 +492,12 @@ export {
calculateFundingCountdown,
calculate24hHighLow,
filterMarketsByQuery,
matchesCategory,
getMarketTypeFilter,
applyMarketFilters,
isEquityAsset,
isHip3Market,
STOCK_LIKE_MARKET_TYPES,
} from './utils';
export type { MarketPatternMatcher, CompiledMarketPattern } from './utils';
export type {
Expand Down
94 changes: 2 additions & 92 deletions packages/perps-controller/src/services/MarketDataService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@ import type { CandlePeriod } from '../constants/chartConfig';
import { PerpsMeasurementName } from '../constants/performanceMetrics';
import { PERPS_CONSTANTS } from '../constants/perpsConfig';
import { PERPS_ERROR_CODES } from '../perpsErrorCodes';
import {
MarketCategory,
PerpsTraceNames,
PerpsTraceOperations,
} from '../types';
import { PerpsTraceNames, PerpsTraceOperations } from '../types';
import type {
PerpsProvider,
Position,
Expand Down Expand Up @@ -36,12 +32,11 @@ import type {
AssetRoute,
PerpsPlatformDependencies,
PerpsMarketData,
MarketTypeFilter,
} from '../types';
import type { CandleData } from '../types/perps-types';
import { coalescePerpsRestRequest } from '../utils/coalescePerpsRestRequest';
import { ensureError, isAbortError } from '../utils/errorUtils';
import { sortMarkets } from '../utils/sortMarkets';
import { applyMarketFilters } from '../utils/marketUtils';
import type { ServiceContext } from './ServiceContext';

/**
Expand Down Expand Up @@ -1258,88 +1253,3 @@ export class MarketDataService {
return provider.getBlockExplorerUrl(address);
}
}

// ============================================================================
// Market filtering helpers (module-level pure functions)
// These live outside the class because they have no service dependencies —
// they are pure data transformations that can be tested and reused independently.
// ============================================================================

/**
* Returns true when a market matches the given UI filter category.
*
* @param market - The market data to test.
* @param category - The filter category to test against.
* @returns Whether the market matches the category.
*/
export function matchesCategory(
market: PerpsMarketData,
category: MarketTypeFilter,
): boolean {
switch (category) {
case 'all':
return true;
case 'new':
return market.isNewMarket === true;
case 'crypto':
// Includes non-HIP3 markets AND HIP-3 assets explicitly typed as CryptoCurrency.
return (
!market.isHip3 || market.marketType === MarketCategory.CryptoCurrency
);
case 'stocks':
return market.marketType === MarketCategory.Stock;
case 'pre-ipo':
return market.marketType === MarketCategory.PreIpo;
case 'indices':
return market.marketType === MarketCategory.Index;
case 'etfs':
return market.marketType === MarketCategory.Etf;
case 'commodities':
return market.marketType === MarketCategory.Commodity;
case 'forex':
return market.marketType === MarketCategory.Forex;
default:
return true;
}
}

/**
* Applies optional category filtering, sorting, and limit to a list of markets.
*
* @param markets - Source market array.
* @param params - Optional filter/sort/limit params.
* @returns Filtered, sorted, and/or sliced market array.
*/
export function applyMarketFilters(
markets: PerpsMarketData[],
params?: GetMarketDataWithPricesParams,
): PerpsMarketData[] {
let result = markets;

if (params?.categories?.length) {
const { categories } = params;
result = result.filter((market) =>
// A market is included if it matches ANY of the requested categories.
categories.some((category) => matchesCategory(market, category)),
);
}

if (params?.excludeSymbols?.length) {
const excluded = new Set(params.excludeSymbols);
result = result.filter((market) => !excluded.has(market.symbol));
}

if (params?.sortBy) {
result = sortMarkets({
markets: result,
sortBy: params.sortBy,
direction: params.direction,
});
}

if (params?.limit !== undefined) {
result = result.slice(0, params.limit);
}

return result;
}
16 changes: 16 additions & 0 deletions packages/perps-controller/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,22 @@ export type MarketTypeFilter =
| 'forex'
| 'new';

/**
* Named constants for the {@link MarketTypeFilter} values, so consumers
* reference `MARKET_TYPE_FILTER.Stocks` instead of bare string literals.
*/
export const MARKET_TYPE_FILTER = {
All: 'all',
Crypto: 'crypto',
Stocks: 'stocks',
PreIpo: 'pre-ipo',
Indices: 'indices',
Etfs: 'etfs',
Commodities: 'commodities',
Forex: 'forex',
New: 'new',
} as const satisfies Record<string, MarketTypeFilter>;

/**
* Ordered list of the 7 data-model market categories for UI pills.
* Does not include the 'all' or 'new' sentinel values — those are applied
Expand Down
176 changes: 175 additions & 1 deletion packages/perps-controller/src/utils/marketUtils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,179 @@
import type { PerpsMarketData } from '../types';
import { MARKET_TYPE_FILTER, MarketCategory } from '../types';
import type {
GetMarketDataWithPricesParams,
MarketType,
MarketTypeFilter,
PerpsMarketData,
} from '../types';
import type { CandleData, CandleStick } from '../types/perps-types';
import { sortMarkets } from './sortMarkets';

// ============================================================================
// Market category classification (pure functions)
// No service dependencies — pure data transformations that can be tested and
// reused independently. `matchesCategory` is the single source of truth for the
// UI category model; `getMarketTypeFilter` is its inverse.
// ============================================================================

/**
* Maps each data-model {@link MarketCategory} to its UI {@link MarketTypeFilter}
* (e.g. `stock` → `stocks`). Exhaustive: adding a `MarketCategory` is a compile
* error here until it is mapped, so the model can't silently drift.
*/
const MARKET_CATEGORY_TO_FILTER: Record<MarketCategory, MarketTypeFilter> = {
[MarketCategory.CryptoCurrency]: MARKET_TYPE_FILTER.Crypto,
[MarketCategory.Stock]: MARKET_TYPE_FILTER.Stocks,
[MarketCategory.PreIpo]: MARKET_TYPE_FILTER.PreIpo,
[MarketCategory.Index]: MARKET_TYPE_FILTER.Indices,
[MarketCategory.Etf]: MARKET_TYPE_FILTER.Etfs,
[MarketCategory.Commodity]: MARKET_TYPE_FILTER.Commodities,
[MarketCategory.Forex]: MARKET_TYPE_FILTER.Forex,
};
Comment on lines +23 to +31
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should align market category with filter category to be the same all singular. The current approach is a bit confusing


/**
* Whether a market is a HIP-3 (non-main-DEX) market. A `marketSource` DEX id
* marks a HIP-3 market even when the `isHip3` flag is unset (e.g. partial route
* params), so both signals are checked. Used as the single HIP-3 signal so the
* classifiers stay consistent.
*
* @param market - The market data to test.
* @returns True if the market is HIP-3.
*/
export const isHip3Market = (
market: Pick<PerpsMarketData, 'isHip3' | 'marketSource'>,
): boolean => Boolean(market.isHip3) || Boolean(market.marketSource);

/**
* Returns true when a market matches the given UI filter category.
*
* @param market - The market data to test.
* @param category - The filter category to test against.
* @returns Whether the market matches the category.
*/
export function matchesCategory(
market: PerpsMarketData,
category: MarketTypeFilter,
): boolean {
switch (category) {
case MARKET_TYPE_FILTER.All:
return true;
case MARKET_TYPE_FILTER.New:
// Explicitly flagged, or an uncategorized HIP-3 market (kept in sync with
// getMarketTypeFilter's 'new' bucket).
return (
market.isNewMarket === true ||
(isHip3Market(market) && market.marketType === undefined)
);
case MARKET_TYPE_FILTER.Crypto:
// Main-DEX markets, plus HIP-3 assets explicitly typed as CryptoCurrency.
return (
!isHip3Market(market) ||
market.marketType === MarketCategory.CryptoCurrency
);
default:
// Every other filter is a 1:1 data-model category match.
return (
market.marketType !== undefined &&
MARKET_CATEGORY_TO_FILTER[market.marketType] === category
);
}
}

/**
* Stock-like market categories (stock, pre-ipo, index, etf). Clients collapse
* these into the single 'stocks' filter, and they share traditional market
* hours. Single source of truth for the equity bucket.
*/
export const STOCK_LIKE_MARKET_TYPES: ReadonlySet<MarketType> = new Set([
MarketCategory.Stock,
MarketCategory.PreIpo,
MarketCategory.Index,
MarketCategory.Etf,
]);
Comment on lines +87 to +92
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we shouln't have have this anymore, we are showing all categories now


/**
* Check whether a market type is stock-like (stock, pre-ipo, index, etf).
*
* @param marketType - The market type from {@link PerpsMarketData}.
* @returns True if the asset is stock-like.
*/
export const isEquityAsset = (marketType?: string): boolean =>
marketType !== undefined &&
STOCK_LIKE_MARKET_TYPES.has(marketType as MarketType);
Comment on lines +100 to +102
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same we this, we shouldn't need this


/**
* Resolve the user-facing category bucket for a market — one of `crypto`,
* `stocks`, `commodities`, `forex`, or `new`. Stock-like categories (stock,
* pre-ipo, index, etf) collapse into the single `stocks` bucket (see
* {@link isEquityAsset}); the remaining categories map 1:1. A market with no
* data-model category is `crypto` when it is main-DEX, or `new` when it is an
* uncategorized HIP-3 market (`isHip3`, or a `marketSource` DEX id when `isHip3`
* is unset, e.g. minimal route params). Never returns the `all` sentinel.
*
* Centralised as the single source of truth so consumers (e.g. category
* shortcuts, related markets) share one classification instead of re-deriving
* it per client and drifting as new categories are added.
*
* @param market - The market data to classify.
* @returns The market type filter bucket.
*/
export function getMarketTypeFilter(market: PerpsMarketData): MarketTypeFilter {
const { marketType } = market;
// Stock-like categories (stock, pre-ipo, index, etf) collapse into one pill.
if (isEquityAsset(marketType)) {
return MARKET_TYPE_FILTER.Stocks;
}
// Every other data-model category maps 1:1 (commodity, forex, crypto).
if (marketType) {
return MARKET_CATEGORY_TO_FILTER[marketType];
}
// No data-model category: an uncategorized HIP-3 market is the 'new' bucket;
// otherwise it's a main-DEX crypto market.
return isHip3Market(market)
? MARKET_TYPE_FILTER.New
: MARKET_TYPE_FILTER.Crypto;
Comment thread
abretonc7s marked this conversation as resolved.
}

/**
* Applies optional category filtering, sorting, and limit to a list of markets.
*
* @param markets - Source market array.
* @param params - Optional filter/sort/limit params.
* @returns Filtered, sorted, and/or sliced market array.
*/
export function applyMarketFilters(
markets: PerpsMarketData[],
params?: GetMarketDataWithPricesParams,
): PerpsMarketData[] {
let result = markets;

if (params?.categories?.length) {
const { categories } = params;
result = result.filter((market) =>
// A market is included if it matches ANY of the requested categories.
categories.some((category) => matchesCategory(market, category)),
);
}

if (params?.excludeSymbols?.length) {
const excluded = new Set(params.excludeSymbols);
result = result.filter((market) => !excluded.has(market.symbol));
}

if (params?.sortBy) {
result = sortMarkets({
markets: result,
sortBy: params.sortBy,
direction: params.direction,
});
}

if (params?.limit !== undefined) {
result = result.slice(0, params.limit);
}

return result;
}

/**
* Maximum length for market filter patterns (prevents DoS attacks)
Expand Down
Loading
Loading