diff --git a/packages/perps-controller/CHANGELOG.md b/packages/perps-controller/CHANGELOG.md index f4ed9f0c52..df0cc83562 100644 --- a/packages/perps-controller/CHANGELOG.md +++ b/packages/perps-controller/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add hardcoded market collections registry with 175 perps markets and 27 collection tags ([#9106](https://github.com/MetaMask/core/pull/9106)) + - New `PerpsMarketCollectionTag` enum, `PerpsMarketDefinition` type, `PERPS_MARKET_DEFINITIONS` and `PERPS_MARKET_COLLECTION_TAGS` constants + - New utility functions `getMarketDefinitionByTicker` and `getMarketDefinitionsByCollection` + - New controller methods `getMarketCollections()`, `getMarketDefinitions()`, `getMarketDefinitionByTicker()`, and `getMarketDefinitionsByCollection()` + ### Changed - Bump `@metamask/utils` from `^11.9.0` to `^11.11.0` ([#9074](https://github.com/MetaMask/core/pull/9074)) diff --git a/packages/perps-controller/src/PerpsController-method-action-types.ts b/packages/perps-controller/src/PerpsController-method-action-types.ts index 1351266616..76196a1593 100644 --- a/packages/perps-controller/src/PerpsController-method-action-types.ts +++ b/packages/perps-controller/src/PerpsController-method-action-types.ts @@ -591,6 +591,49 @@ export type PerpsControllerGetMarketCategoriesAction = { handler: PerpsController['getMarketCategories']; }; +/** + * Get the ordered list of all market collection tags. + * Used by the UI to render collection filter pills. + * + * @returns Ordered array of {@link PerpsMarketCollectionTag} values. + */ +export type PerpsControllerGetMarketCollectionsAction = { + type: `PerpsController:getMarketCollections`; + handler: PerpsController['getMarketCollections']; +}; + +/** + * Get the full list of hardcoded perps market definitions. + * + * @returns Array of all {@link PerpsMarketDefinition} entries. + */ +export type PerpsControllerGetMarketDefinitionsAction = { + type: `PerpsController:getMarketDefinitions`; + handler: PerpsController['getMarketDefinitions']; +}; + +/** + * Look up a single market definition by its ticker symbol. + * + * @param ticker - The ticker to look up (e.g. 'BTC', 'ETH'). + * @returns The matching definition, or `undefined` if not found. + */ +export type PerpsControllerGetMarketDefinitionByTickerAction = { + type: `PerpsController:getMarketDefinitionByTicker`; + handler: PerpsController['getMarketDefinitionByTicker']; +}; + +/** + * Return all market definitions that belong to a given collection tag. + * + * @param collection - The collection tag to filter by. + * @returns Array of matching market definitions (may be empty). + */ +export type PerpsControllerGetMarketDefinitionsByCollectionAction = { + type: `PerpsController:getMarketDefinitionsByCollection`; + handler: PerpsController['getMarketDefinitionsByCollection']; +}; + /** * Get the current WebSocket connection state from the active provider. * Used by the UI to monitor connection health and show notifications. @@ -1068,6 +1111,10 @@ export type PerpsControllerMethodActions = | PerpsControllerSwitchProviderAction | PerpsControllerGetCurrentNetworkAction | PerpsControllerGetMarketCategoriesAction + | PerpsControllerGetMarketCollectionsAction + | PerpsControllerGetMarketDefinitionsAction + | PerpsControllerGetMarketDefinitionByTickerAction + | PerpsControllerGetMarketDefinitionsByCollectionAction | PerpsControllerGetWebSocketConnectionStateAction | PerpsControllerSubscribeToConnectionStateAction | PerpsControllerReconnectAction diff --git a/packages/perps-controller/src/PerpsController.ts b/packages/perps-controller/src/PerpsController.ts index 8e57403634..fa2356a833 100644 --- a/packages/perps-controller/src/PerpsController.ts +++ b/packages/perps-controller/src/PerpsController.ts @@ -16,6 +16,12 @@ import { PERPS_EVENT_VALUE, } from './constants/eventNames'; import { USDC_SYMBOL } from './constants/hyperLiquidConfig'; +import { + PERPS_MARKET_COLLECTION_TAGS, + PERPS_MARKET_DEFINITIONS, + getMarketDefinitionByTicker as getMarketDefinitionByTickerUtil, + getMarketDefinitionsByCollection as getMarketDefinitionsByCollectionUtil, +} from './constants/marketCollections'; import { PerpsMeasurementName } from './constants/performanceMetrics'; import type { SortOptionId } from './constants/perpsConfig'; import { @@ -107,6 +113,8 @@ import type { PerpsLogger, PerpsActiveProviderMode, PerpsProviderType, + PerpsMarketCollectionTag, + PerpsMarketDefinition, PerpsSelectedPaymentToken, PerpsRemoteFeatureFlagState, PerpsTransactionParams, @@ -726,7 +734,11 @@ const MESSENGER_EXPOSED_METHODS = [ 'getFunding', 'getHistoricalPortfolio', 'getMarketCategories', + 'getMarketCollections', 'getMarketDataWithPrices', + 'getMarketDefinitionByTicker', + 'getMarketDefinitions', + 'getMarketDefinitionsByCollection', 'getMarketFilterPreferences', 'getMarkets', 'getMaxLeverage', @@ -3987,6 +3999,49 @@ export class PerpsController extends BaseController< return MARKET_CATEGORIES; } + /** + * Get the ordered list of all market collection tags. + * Used by the UI to render collection filter pills. + * + * @returns Ordered array of {@link PerpsMarketCollectionTag} values. + */ + getMarketCollections(): PerpsMarketCollectionTag[] { + return [...PERPS_MARKET_COLLECTION_TAGS]; + } + + /** + * Get the full list of hardcoded perps market definitions. + * + * @returns Array of all {@link PerpsMarketDefinition} entries. + */ + getMarketDefinitions(): PerpsMarketDefinition[] { + return [...PERPS_MARKET_DEFINITIONS]; + } + + /** + * Look up a single market definition by its ticker symbol. + * + * @param ticker - The ticker to look up (e.g. 'BTC', 'ETH'). + * @returns The matching definition, or `undefined` if not found. + */ + getMarketDefinitionByTicker( + ticker: string, + ): PerpsMarketDefinition | undefined { + return getMarketDefinitionByTickerUtil(ticker); + } + + /** + * Return all market definitions that belong to a given collection tag. + * + * @param collection - The collection tag to filter by. + * @returns Array of matching market definitions (may be empty). + */ + getMarketDefinitionsByCollection( + collection: PerpsMarketCollectionTag, + ): PerpsMarketDefinition[] { + return getMarketDefinitionsByCollectionUtil(collection); + } + /** * Get the current WebSocket connection state from the active provider. * Used by the UI to monitor connection health and show notifications. diff --git a/packages/perps-controller/src/constants/index.ts b/packages/perps-controller/src/constants/index.ts index aedcaaec56..7568880f3f 100644 --- a/packages/perps-controller/src/constants/index.ts +++ b/packages/perps-controller/src/constants/index.ts @@ -9,3 +9,4 @@ export * from './perpsConfig'; export * from './transactionsHistoryConfig'; export * from './performanceMetrics'; export * from './myxConfig'; +export * from './marketCollections'; diff --git a/packages/perps-controller/src/constants/marketCollections.ts b/packages/perps-controller/src/constants/marketCollections.ts new file mode 100644 index 0000000000..a0f0416321 --- /dev/null +++ b/packages/perps-controller/src/constants/marketCollections.ts @@ -0,0 +1,258 @@ +import { PerpsMarketCollectionTag } from '../types'; +import type { PerpsMarketDefinition } from '../types'; + +const Tag = PerpsMarketCollectionTag; + +/** + * All market collection tags derived from the enum. + * Order follows the enum declaration order. + */ +export const PERPS_MARKET_COLLECTION_TAGS: readonly PerpsMarketCollectionTag[] = + Object.values(PerpsMarketCollectionTag); + +/** + * Canonical registry of all supported perps markets. + * Each entry defines the ticker and thematic collections. + */ +export const PERPS_MARKET_DEFINITIONS: readonly PerpsMarketDefinition[] = [ + { + ticker: 'BTC', + collections: [Tag.L1, Tag.BitcoinEcosystem, Tag.StoreOfValue], + }, + { ticker: 'ETH', collections: [Tag.L1, Tag.SmartContractPlatform] }, + { + ticker: 'ATOM', + collections: [Tag.L1, Tag.CosmosEcosystem, Tag.Interoperability], + }, + { + ticker: 'DYDX', + collections: [Tag.DeFi, Tag.CosmosEcosystem, Tag.ExchangeToken], + }, + { ticker: 'SOL', collections: [Tag.L1, Tag.SolanaEcosystem] }, + { ticker: 'AVAX', collections: [Tag.L1, Tag.SmartContractPlatform] }, + { + ticker: 'BNB', + collections: [Tag.L1, Tag.ExchangeToken, Tag.SmartContractPlatform], + }, + { ticker: 'APE', collections: [Tag.GamingNft] }, + { ticker: 'OP', collections: [Tag.L2Scaling] }, + { ticker: 'LTC', collections: [Tag.L1, Tag.BitcoinEcosystem, Tag.Payments] }, + { ticker: 'ARB', collections: [Tag.L2Scaling] }, + { ticker: 'DOGE', collections: [Tag.L1, Tag.Memecoin, Tag.Payments] }, + { ticker: 'INJ', collections: [Tag.L1, Tag.CosmosEcosystem, Tag.DeFi] }, + { ticker: 'SUI', collections: [Tag.L1, Tag.MoveEcosystem] }, + { ticker: 'kPEPE', collections: [Tag.Memecoin] }, + { ticker: 'CRV', collections: [Tag.DeFi] }, + { ticker: 'LDO', collections: [Tag.DeFi, Tag.LiquidStaking] }, + { ticker: 'LINK', collections: [Tag.Infrastructure, Tag.Oracle] }, + { ticker: 'STX', collections: [Tag.L2Scaling, Tag.BitcoinEcosystem] }, + { ticker: 'CFX', collections: [Tag.L1, Tag.SmartContractPlatform] }, + { ticker: 'GMX', collections: [Tag.DeFi, Tag.ExchangeToken] }, + { ticker: 'SNX', collections: [Tag.DeFi] }, + { ticker: 'XRP', collections: [Tag.L1, Tag.Payments] }, + { ticker: 'BCH', collections: [Tag.L1, Tag.BitcoinEcosystem, Tag.Payments] }, + { ticker: 'APT', collections: [Tag.L1, Tag.MoveEcosystem] }, + { ticker: 'AAVE', collections: [Tag.DeFi] }, + { ticker: 'COMP', collections: [Tag.DeFi] }, + { ticker: 'WLD', collections: [Tag.AiDepin] }, + { ticker: 'YGG', collections: [Tag.GamingNft] }, + { ticker: 'TRX', collections: [Tag.L1, Tag.SmartContractPlatform] }, + { ticker: 'kSHIB', collections: [Tag.Memecoin] }, + { ticker: 'UNI', collections: [Tag.DeFi, Tag.ExchangeToken] }, + { ticker: 'SEI', collections: [Tag.L1, Tag.CosmosEcosystem, Tag.DeFi] }, + { ticker: 'RUNE', collections: [Tag.BitcoinEcosystem, Tag.DeFi] }, + { ticker: 'ZRO', collections: [Tag.Infrastructure, Tag.Interoperability] }, + { ticker: 'DOT', collections: [Tag.L1, Tag.Interoperability] }, + { ticker: 'BANANA', collections: [Tag.Memecoin, Tag.DeFi] }, + { ticker: 'TRB', collections: [Tag.Infrastructure, Tag.Oracle] }, + { ticker: 'FTT', collections: [Tag.ExchangeToken] }, + { ticker: 'ARK', collections: [Tag.L1] }, + { ticker: 'BIGTIME', collections: [Tag.GamingNft] }, + { ticker: 'KAS', collections: [Tag.L1, Tag.BitcoinEcosystem] }, + { ticker: 'BLUR', collections: [Tag.GamingNft, Tag.DeFi] }, + { ticker: 'TIA', collections: [Tag.L1, Tag.ZkModular, Tag.CosmosEcosystem] }, + { ticker: 'BSV', collections: [Tag.L1, Tag.BitcoinEcosystem] }, + { ticker: 'ADA', collections: [Tag.L1, Tag.SmartContractPlatform] }, + { ticker: 'TON', collections: [Tag.L1, Tag.TonEcosystem] }, + { ticker: 'MINA', collections: [Tag.L1, Tag.ZkModular] }, + { ticker: 'POLYX', collections: [Tag.RwaStablecoin, Tag.Infrastructure] }, + { ticker: 'GAS', collections: [Tag.Infrastructure] }, + { ticker: 'PENDLE', collections: [Tag.DeFi, Tag.LiquidStaking] }, + { ticker: 'FET', collections: [Tag.AiDepin, Tag.Infrastructure] }, + { ticker: 'NEAR', collections: [Tag.L1, Tag.SmartContractPlatform] }, + { ticker: 'MEME', collections: [Tag.Memecoin] }, + { ticker: 'ORDI', collections: [Tag.BitcoinEcosystem] }, + { ticker: 'NEO', collections: [Tag.L1, Tag.SmartContractPlatform] }, + { ticker: 'ZEN', collections: [Tag.L1, Tag.Privacy] }, + { ticker: 'FIL', collections: [Tag.L1, Tag.StorageData] }, + { + ticker: 'PYTH', + collections: [Tag.Infrastructure, Tag.Oracle, Tag.SolanaEcosystem], + }, + { ticker: 'SUSHI', collections: [Tag.DeFi, Tag.ExchangeToken] }, + { ticker: 'IMX', collections: [Tag.GamingNft, Tag.L2Scaling] }, + { ticker: 'kBONK', collections: [Tag.Memecoin, Tag.SolanaEcosystem] }, + { ticker: 'GMT', collections: [Tag.GamingNft] }, + { ticker: 'SUPER', collections: [Tag.GamingNft] }, + { + ticker: 'JUP', + collections: [Tag.DeFi, Tag.SolanaEcosystem, Tag.ExchangeToken], + }, + { ticker: 'kLUNC', collections: [Tag.Memecoin] }, + { ticker: 'RSR', collections: [Tag.DeFi, Tag.RwaStablecoin] }, + { ticker: 'GALA', collections: [Tag.GamingNft] }, + { + ticker: 'JTO', + collections: [Tag.DeFi, Tag.SolanaEcosystem, Tag.LiquidStaking], + }, + { ticker: 'ACE', collections: [Tag.GamingNft] }, + { ticker: 'WIF', collections: [Tag.Memecoin, Tag.SolanaEcosystem] }, + { ticker: 'CAKE', collections: [Tag.DeFi, Tag.ExchangeToken] }, + { ticker: 'PEOPLE', collections: [Tag.Memecoin] }, + { ticker: 'ENS', collections: [Tag.Infrastructure] }, + { ticker: 'ETC', collections: [Tag.L1, Tag.BitcoinEcosystem] }, + { ticker: 'XAI', collections: [Tag.GamingNft, Tag.L2Scaling] }, + { ticker: 'MANTA', collections: [Tag.L2Scaling, Tag.ZkModular] }, + { ticker: 'UMA', collections: [Tag.Infrastructure, Tag.Oracle, Tag.DeFi] }, + { ticker: 'ONDO', collections: [Tag.RwaStablecoin, Tag.DeFi] }, + { ticker: 'ALT', collections: [Tag.L2Scaling, Tag.ZkModular] }, + { ticker: 'ZETA', collections: [Tag.L1, Tag.Interoperability] }, + { ticker: 'DYM', collections: [Tag.L1, Tag.ZkModular] }, + { ticker: 'W', collections: [Tag.L1, Tag.Interoperability] }, + { ticker: 'STRK', collections: [Tag.L2Scaling, Tag.ZkModular] }, + { ticker: 'TAO', collections: [Tag.L1, Tag.AiDepin] }, + { ticker: 'AR', collections: [Tag.L1, Tag.StorageData] }, + { ticker: 'kFLOKI', collections: [Tag.Memecoin] }, + { ticker: 'BOME', collections: [Tag.Memecoin, Tag.SolanaEcosystem] }, + { ticker: 'ETHFI', collections: [Tag.DeFi, Tag.LiquidStaking] }, + { ticker: 'ENA', collections: [Tag.DeFi, Tag.RwaStablecoin] }, + { ticker: 'MNT', collections: [Tag.L2Scaling, Tag.ExchangeToken] }, + { ticker: 'TNSR', collections: [Tag.GamingNft, Tag.SolanaEcosystem] }, + { ticker: 'SAGA', collections: [Tag.L1, Tag.GamingNft] }, + { ticker: 'MERL', collections: [Tag.L2Scaling, Tag.BitcoinEcosystem] }, + { ticker: 'HBAR', collections: [Tag.L1, Tag.SmartContractPlatform] }, + { ticker: 'POPCAT', collections: [Tag.Memecoin, Tag.SolanaEcosystem] }, + { ticker: 'EIGEN', collections: [Tag.Infrastructure, Tag.LiquidStaking] }, + { ticker: 'REZ', collections: [Tag.DeFi, Tag.LiquidStaking] }, + { ticker: 'NOT', collections: [Tag.Memecoin, Tag.TonEcosystem] }, + { ticker: 'TURBO', collections: [Tag.AiDepin, Tag.Memecoin] }, + { ticker: 'BRETT', collections: [Tag.Memecoin] }, + { ticker: 'IO', collections: [Tag.AiDepin] }, + { ticker: 'ZK', collections: [Tag.L2Scaling, Tag.ZkModular] }, + { ticker: 'BLAST', collections: [Tag.L2Scaling, Tag.DeFi] }, + { ticker: 'RENDER', collections: [Tag.AiDepin] }, + { ticker: 'POL', collections: [Tag.L2Scaling] }, + { ticker: 'CELO', collections: [Tag.L1, Tag.Payments] }, + { ticker: 'HMSTR', collections: [Tag.Memecoin, Tag.TonEcosystem] }, + { ticker: 'kNEIRO', collections: [Tag.Memecoin] }, + { ticker: 'GOAT', collections: [Tag.AiDepin, Tag.Memecoin] }, + { ticker: 'MOODENG', collections: [Tag.Memecoin] }, + { ticker: 'GRASS', collections: [Tag.AiDepin] }, + { ticker: 'PURR', collections: [Tag.Memecoin, Tag.HyperliquidEcosystem] }, + { ticker: 'PNUT', collections: [Tag.Memecoin, Tag.SolanaEcosystem] }, + { ticker: 'XLM', collections: [Tag.L1, Tag.Payments] }, + { ticker: 'CHILLGUY', collections: [Tag.Memecoin, Tag.SolanaEcosystem] }, + { ticker: 'SAND', collections: [Tag.GamingNft, Tag.Metaverse] }, + { ticker: 'IOTA', collections: [Tag.L1, Tag.IotInfrastructure] }, + { ticker: 'ALGO', collections: [Tag.L1, Tag.SmartContractPlatform] }, + { + ticker: 'HYPE', + collections: [Tag.L1, Tag.ExchangeToken, Tag.HyperliquidEcosystem], + }, + { ticker: 'ME', collections: [Tag.GamingNft, Tag.SolanaEcosystem] }, + { ticker: 'MOVE', collections: [Tag.L1, Tag.MoveEcosystem] }, + { ticker: 'VIRTUAL', collections: [Tag.AiDepin] }, + { + ticker: 'PENGU', + collections: [Tag.Memecoin, Tag.GamingNft, Tag.SolanaEcosystem], + }, + { ticker: 'USUAL', collections: [Tag.DeFi, Tag.RwaStablecoin] }, + { ticker: 'FARTCOIN', collections: [Tag.Memecoin] }, + { ticker: 'AIXBT', collections: [Tag.AiDepin, Tag.Memecoin] }, + { ticker: 'BIO', collections: [Tag.AiDepin] }, + { ticker: 'GRIFFAIN', collections: [Tag.AiDepin, Tag.Memecoin] }, + { ticker: 'SPX', collections: [Tag.AiDepin, Tag.Memecoin] }, + { ticker: 'S', collections: [Tag.L1, Tag.SmartContractPlatform] }, + { ticker: 'MORPHO', collections: [Tag.DeFi] }, + { ticker: 'TRUMP', collections: [Tag.Memecoin, Tag.Political] }, + { ticker: 'MELANIA', collections: [Tag.Memecoin, Tag.Political] }, + { ticker: 'ANIME', collections: [Tag.GamingNft, Tag.Memecoin] }, + { ticker: 'VINE', collections: [Tag.Memecoin] }, + { ticker: 'VVV', collections: [Tag.DeFi] }, + { ticker: 'BERA', collections: [Tag.L1, Tag.DeFi] }, + { ticker: 'TST', collections: [Tag.Memecoin] }, + { ticker: 'LAYER', collections: [Tag.LiquidStaking, Tag.SolanaEcosystem] }, + { ticker: 'IP', collections: [Tag.Infrastructure] }, + { ticker: 'KAITO', collections: [Tag.AiDepin, Tag.Infrastructure] }, + { ticker: 'NIL', collections: [Tag.L1, Tag.ZkModular] }, + { ticker: 'PAXG', collections: [Tag.RwaStablecoin] }, + { ticker: 'BABY', collections: [Tag.Memecoin] }, + { ticker: 'WCT', collections: [Tag.Infrastructure] }, + { ticker: 'HYPER', collections: [Tag.AiDepin] }, + { ticker: 'ZORA', collections: [Tag.GamingNft, Tag.L2Scaling] }, + { ticker: 'INIT', collections: [Tag.L1, Tag.SmartContractPlatform] }, + { ticker: 'DOOD', collections: [Tag.GamingNft, Tag.Memecoin] }, + + { ticker: 'SOPH', collections: [Tag.L2Scaling, Tag.AiDepin] }, + { ticker: 'RESOLV', collections: [Tag.DeFi, Tag.RwaStablecoin] }, + { ticker: 'SYRUP', collections: [Tag.DeFi] }, + { ticker: 'PUMP', collections: [Tag.Memecoin, Tag.SolanaEcosystem] }, + { ticker: 'PROVE', collections: [Tag.ZkModular, Tag.Infrastructure] }, + + { ticker: 'WLFI', collections: [Tag.DeFi] }, + { ticker: 'LINEA', collections: [Tag.L2Scaling, Tag.ZkModular] }, + { ticker: 'SKY', collections: [Tag.DeFi, Tag.RwaStablecoin] }, + { ticker: 'ASTER', collections: [Tag.DeFi, Tag.ExchangeToken] }, + + { ticker: 'STBL', collections: [Tag.RwaStablecoin] }, + { ticker: '0G', collections: [Tag.AiDepin, Tag.StorageData] }, + { ticker: 'HEMI', collections: [Tag.L2Scaling, Tag.BitcoinEcosystem] }, + { ticker: 'APEX', collections: [Tag.DeFi, Tag.ExchangeToken] }, + + { ticker: 'ZEC', collections: [Tag.L1, Tag.Privacy] }, + { ticker: 'MON', collections: [Tag.L1, Tag.SmartContractPlatform] }, + { ticker: 'MET', collections: [Tag.L2Scaling] }, + { ticker: 'MEGA', collections: [Tag.L2Scaling] }, + + { ticker: 'ICP', collections: [Tag.L1, Tag.Infrastructure] }, + { ticker: 'AERO', collections: [Tag.DeFi, Tag.ExchangeToken] }, + { ticker: 'STABLE', collections: [Tag.RwaStablecoin] }, + + { ticker: 'LIT', collections: [Tag.Infrastructure, Tag.Privacy] }, + { ticker: 'XMR', collections: [Tag.L1, Tag.Privacy] }, + { ticker: 'AXS', collections: [Tag.GamingNft] }, + { ticker: 'DASH', collections: [Tag.L1, Tag.Privacy, Tag.Payments] }, + + { ticker: 'AZTEC', collections: [Tag.L2Scaling, Tag.ZkModular, Tag.Privacy] }, +] as const; + +const marketsByTicker = new Map( + PERPS_MARKET_DEFINITIONS.map((market) => [market.ticker, market]), +); + +/** + * Look up a single market definition by its ticker symbol. + * O(1) via pre-built Map. + * + * @param ticker - The ticker to look up (e.g. 'BTC', 'ETH'). + * @returns The matching definition, or `undefined` if not found. + */ +export function getMarketDefinitionByTicker( + ticker: string, +): PerpsMarketDefinition | undefined { + return marketsByTicker.get(ticker); +} + +/** + * Return all market definitions that belong to a given collection tag. + * + * @param collection - The collection tag to filter by. + * @returns Array of matching market definitions (may be empty). + */ +export function getMarketDefinitionsByCollection( + collection: PerpsMarketCollectionTag, +): PerpsMarketDefinition[] { + return PERPS_MARKET_DEFINITIONS.filter((market) => + market.collections.includes(collection), + ); +} diff --git a/packages/perps-controller/src/index.ts b/packages/perps-controller/src/index.ts index c82cc7d4e1..abccb0de56 100644 --- a/packages/perps-controller/src/index.ts +++ b/packages/perps-controller/src/index.ts @@ -73,6 +73,10 @@ export type { PerpsControllerGetMarketDataWithPricesAction, PerpsControllerGetMarketFilterPreferencesAction, PerpsControllerGetMarketCategoriesAction, + PerpsControllerGetMarketCollectionsAction, + PerpsControllerGetMarketDefinitionByTickerAction, + PerpsControllerGetMarketDefinitionsAction, + PerpsControllerGetMarketDefinitionsByCollectionAction, PerpsControllerGetMarketsAction, PerpsControllerGetMaxLeverageAction, PerpsControllerGetOpenOrdersAction, @@ -139,6 +143,8 @@ export { MARKET_CATEGORIES, MarketCategory, } from './types'; +export { PerpsMarketCollectionTag } from './types'; +export type { PerpsMarketDefinition } from './types'; export type { RawLedgerUpdate, UserHistoryItem, @@ -413,6 +419,12 @@ export { MYX_MINIMUM_ORDER_SIZE_USD, MYX_EXECUTION_FEE_TOKEN, } from './constants'; +export { + PERPS_MARKET_COLLECTION_TAGS, + PERPS_MARKET_DEFINITIONS, + getMarketDefinitionByTicker, + getMarketDefinitionsByCollection, +} from './constants'; export { PERPS_CONSTANTS, WITHDRAWAL_CONSTANTS, diff --git a/packages/perps-controller/src/types/index.ts b/packages/perps-controller/src/types/index.ts index 24f8cbbdd4..f3025046e1 100644 --- a/packages/perps-controller/src/types/index.ts +++ b/packages/perps-controller/src/types/index.ts @@ -1787,6 +1787,56 @@ export function isVersionGatedFeatureFlag( ); } +// ============================================================================ +// Market Collections +// ============================================================================ + +/** + * Tag identifying a thematic collection/grouping that a perps market belongs to. + * A single market may belong to zero or more collections. + */ +export enum PerpsMarketCollectionTag { + L1 = 'L1', + L2Scaling = 'L2 / Scaling', + BitcoinEcosystem = 'Bitcoin Ecosystem', + SolanaEcosystem = 'Solana Ecosystem', + CosmosEcosystem = 'Cosmos Ecosystem', + TonEcosystem = 'TON Ecosystem', + HyperliquidEcosystem = 'Hyperliquid Ecosystem', + MoveEcosystem = 'Move Ecosystem', + SmartContractPlatform = 'Smart Contract Platform', + DeFi = 'DeFi', + ExchangeToken = 'Exchange Token', + Memecoin = 'Memecoin', + GamingNft = 'Gaming / NFT', + AiDepin = 'AI / DePIN', + Infrastructure = 'Infrastructure', + Oracle = 'Oracle', + Interoperability = 'Interoperability', + Payments = 'Payments', + LiquidStaking = 'LSD / Liquid Staking', + RwaStablecoin = 'RWA / Stablecoin', + ZkModular = 'ZK / Modular', + Privacy = 'Privacy', + StorageData = 'Storage / Data', + StoreOfValue = 'Store of Value', + Metaverse = 'Metaverse', + IotInfrastructure = 'IoT / Infrastructure', + Political = 'Political', +} + +/** + * Static definition of a supported perps market. + * These entries are hardcoded and represent the canonical set of markets + * available for perpetual trading. + */ +export type PerpsMarketDefinition = { + /** Asset ticker symbol (e.g. 'BTC', 'ETH', 'kPEPE'). */ + ticker: string; + /** Thematic collections this market belongs to. May be empty. */ + collections: PerpsMarketCollectionTag[]; +}; + // ============================================================================ // Sub-module type re-exports // These types live in separate files within types/ and need to be accessible diff --git a/packages/perps-controller/tests/src/constants/marketCollections.test.ts b/packages/perps-controller/tests/src/constants/marketCollections.test.ts new file mode 100644 index 0000000000..c7edf22b4e --- /dev/null +++ b/packages/perps-controller/tests/src/constants/marketCollections.test.ts @@ -0,0 +1,171 @@ +import { + PERPS_MARKET_COLLECTION_TAGS, + PERPS_MARKET_DEFINITIONS, + getMarketDefinitionByTicker, + getMarketDefinitionsByCollection, +} from '../../../src/constants/marketCollections'; +import { PerpsMarketCollectionTag } from '../../../src/types'; + +describe('PERPS_MARKET_DEFINITIONS', () => { + it('contains 175 market entries', () => { + expect(PERPS_MARKET_DEFINITIONS).toHaveLength(175); + }); + + it('has unique tickers', () => { + const tickers = PERPS_MARKET_DEFINITIONS.map((market) => market.ticker); + expect(new Set(tickers).size).toBe(tickers.length); + }); + + it('has a non-empty ticker for every entry', () => { + for (const market of PERPS_MARKET_DEFINITIONS) { + expect(market.ticker.length).toBeGreaterThan(0); + } + }); + + it('only uses collection tags that exist in PERPS_MARKET_COLLECTION_TAGS', () => { + const validTags = new Set(PERPS_MARKET_COLLECTION_TAGS); + for (const market of PERPS_MARKET_DEFINITIONS) { + for (const tag of market.collections) { + expect(validTags.has(tag)).toBe(true); + } + } + }); + + it('includes known markets with correct data', () => { + const btc = PERPS_MARKET_DEFINITIONS.find( + (market) => market.ticker === 'BTC', + ); + expect(btc).toStrictEqual({ + ticker: 'BTC', + collections: [ + PerpsMarketCollectionTag.L1, + PerpsMarketCollectionTag.BitcoinEcosystem, + PerpsMarketCollectionTag.StoreOfValue, + ], + }); + + const eth = PERPS_MARKET_DEFINITIONS.find( + (market) => market.ticker === 'ETH', + ); + expect(eth).toStrictEqual({ + ticker: 'ETH', + collections: [ + PerpsMarketCollectionTag.L1, + PerpsMarketCollectionTag.SmartContractPlatform, + ], + }); + }); + + it('has no markets with empty collections', () => { + for (const market of PERPS_MARKET_DEFINITIONS) { + expect(market.collections.length).toBeGreaterThan(0); + } + }); +}); + +describe('PERPS_MARKET_COLLECTION_TAGS', () => { + it('contains 27 collection tags', () => { + expect(PERPS_MARKET_COLLECTION_TAGS).toHaveLength(27); + }); + + it('has unique tags', () => { + expect(new Set(PERPS_MARKET_COLLECTION_TAGS).size).toBe( + PERPS_MARKET_COLLECTION_TAGS.length, + ); + }); + + it('covers every tag used by any market definition', () => { + const usedTags = new Set(); + for (const market of PERPS_MARKET_DEFINITIONS) { + for (const tag of market.collections) { + usedTags.add(tag); + } + } + const declaredTags = new Set(PERPS_MARKET_COLLECTION_TAGS); + for (const tag of usedTags) { + expect(declaredTags.has(tag)).toBe(true); + } + }); +}); + +describe('getMarketDefinitionByTicker', () => { + it('returns the correct entry for BTC', () => { + const btc = getMarketDefinitionByTicker('BTC'); + expect(btc).toBeDefined(); + expect(btc?.ticker).toBe('BTC'); + expect(btc?.collections).toContain( + PerpsMarketCollectionTag.BitcoinEcosystem, + ); + }); + + it('returns undefined for a non-existent ticker', () => { + expect(getMarketDefinitionByTicker('NONEXISTENT')).toBeUndefined(); + }); + + it('is case-sensitive', () => { + expect(getMarketDefinitionByTicker('btc')).toBeUndefined(); + expect(getMarketDefinitionByTicker('Btc')).toBeUndefined(); + }); + + it('returns the correct entry for a ticker with special prefix', () => { + const kPepe = getMarketDefinitionByTicker('kPEPE'); + expect(kPepe).toBeDefined(); + expect(kPepe?.collections).toStrictEqual([ + PerpsMarketCollectionTag.Memecoin, + ]); + }); +}); + +describe('getMarketDefinitionsByCollection', () => { + it('returns all Memecoin markets', () => { + const memecoins = getMarketDefinitionsByCollection( + PerpsMarketCollectionTag.Memecoin, + ); + expect(memecoins.length).toBeGreaterThan(0); + for (const market of memecoins) { + expect(market.collections).toContain(PerpsMarketCollectionTag.Memecoin); + } + }); + + it('returns all DeFi markets', () => { + const defi = getMarketDefinitionsByCollection( + PerpsMarketCollectionTag.DeFi, + ); + expect(defi.length).toBeGreaterThan(0); + for (const market of defi) { + expect(market.collections).toContain(PerpsMarketCollectionTag.DeFi); + } + }); + + it('returns BTC for Bitcoin Ecosystem', () => { + const btcEco = getMarketDefinitionsByCollection( + PerpsMarketCollectionTag.BitcoinEcosystem, + ); + const tickers = btcEco.map((market) => market.ticker); + expect(tickers).toContain('BTC'); + expect(tickers).toContain('LTC'); + }); + + it('returns results for every tag that appears in market definitions', () => { + const usedTags = new Set( + PERPS_MARKET_DEFINITIONS.flatMap((market) => market.collections), + ); + for (const tag of PERPS_MARKET_COLLECTION_TAGS) { + const results = getMarketDefinitionsByCollection(tag); + expect(results.length).toBe(usedTags.has(tag) ? results.length : 0); + expect(results.length).toBeGreaterThanOrEqual(0); + } + }); + + it('returns markets with the correct tag for Store of Value', () => { + const storeOfValue = getMarketDefinitionsByCollection( + PerpsMarketCollectionTag.StoreOfValue, + ); + expect(storeOfValue.length).toBeGreaterThanOrEqual(1); + for (const market of storeOfValue) { + expect(market.collections).toContain( + PerpsMarketCollectionTag.StoreOfValue, + ); + } + }); +});