feat(perps-controller): integrate Terminal API for market metadata with HyperLiquid fallback#9137
feat(perps-controller): integrate Terminal API for market metadata with HyperLiquid fallback#9137michalconsensys wants to merge 38 commits into
Conversation
… source with HyperLiquid fallback Add TerminalMarketService to fetch market data from the MetaMask Terminal API, gated behind the `perpsTerminalApiMarkets` remote feature flag. When enabled, the Terminal API is used as the primary source for market listings and metadata (name, keywords, tags, categories), with a silent fallback to HyperLiquid on failure. Also enhances market search to index against keyword fields.
…ets path The Terminal API path in getMarkets was returning all markets without respecting params.symbols or params.dex, unlike the provider fallback path which applies those filters. This could cause callers requesting specific symbols to receive the full market list.
…der data The comment stated the two fetches ran in parallel, but the code awaited the terminal metadata before starting the provider fetch. Use Promise.all so both requests fly concurrently, reducing wall-clock latency when the Terminal API flag is enabled.
…ormDependencies Adding a required field to PerpsPlatformDependencies is a breaking change for all existing consumers. Since the Terminal API feature is behind a remote flag, the URL should be optional — when omitted the service is not instantiated and the integration is disabled regardless of the flag.
…known values The #extractMetadata method cast item.marketType to MarketType with an unchecked `as` assertion. If the API returned an unrecognised value (e.g. "derivatives"), it would propagate silently and could cause downstream category filtering or UI issues. Now only values present in MARKET_CATEGORIES are accepted; unknown strings are dropped.
… responses Replace unsafe `as` type cast with per-item superstruct validation in TerminalMarketService. Items that fail validation are filtered out and logged instead of silently accepted. Also fix API path to /v1/perpetuals and add a temporary test script for manual verification.
… getMarkets path" This reverts commit 2e6e684.
…th provider data" This reverts commit 3478db9.
…rpsPlatformDependencies" This reverts commit 48d5c51.
…against known values" This reverts commit 122c8ba.
|
@metamaskbot publish-previews |
…for monorepo consistency
|
@metamaskbot publish-previews |
… useTerminalApi param Move Terminal API toggle from an internal remote feature flag read to a caller-provided `useTerminalApi` parameter on `GetMarketsParams` and `GetMarketDataWithPricesParams`, giving consumers direct control over the data source selection.
…Service and apply symbol filters - Standalone getMarkets now delegates to MarketDataService (consistent with getMarketDataWithPrices), so Terminal API is used when requested. - Terminal API path in getMarkets applies symbols filtering before returning results instead of returning the full unfiltered list.
…nal API filter Align Terminal API symbol filtering with HyperLiquidProvider behavior by comparing symbols case-insensitively.
Automated Review — PR #9137
Summary
Full review detailsNon-blocking
Downstream (mobile/ext on
|
…tegration - Remove useTerminalApi duplication from MarketDataService method options; read it from params instead of accepting it as a separate option - Reuse deps object in MarketDataService constructor by including terminalMarketService in the deps parameter via intersection type - Replace hardcoded 'perps' magic string with PERPS_CONSTANTS.FeatureName in TerminalMarketService - Make terminalApiBaseUrl optional in PerpsPlatformDependencies so existing client factories are non-breaking on upgrade - Add early guard in TerminalMarketService.fetchMarkets() for missing URL
…om config Endpoint URLs should only be passed from the client via PerpsPlatformDependencies.terminalApiBaseUrl, not stored in core.
abretonc7s
left a comment
There was a problem hiding this comment.
Re-review after the latest push. The earlier blocking compile break is resolved — terminalApiBaseUrl is now optional. Remaining issues below; requesting changes primarily on the filter bypass and changelog accuracy.
Blocking
MarketDataService.ts:764-785— the TerminalgetMarketspath filters only byparams.symbols, then returns early, bypassing the allowlist/blocklist filtering (andskipFilters/dex) that the provider path enforces viaprovider.getMarkets. WithuseTerminalApitrue, blocklisted markets can leak through. Apply the same filtering on the Terminal path.CHANGELOG.md:16— still saysPerpsPlatformDependenciesgains a requiredterminalApiBaseUrl: string; the field is now optional (types/index.ts:1678). Update the wording (the change is non-breaking).perpsTerminalApiMarketsis never read in Core —getMarkets/getMarketDataWithPricesgate only onparams.useTerminalApi. Either wire the remote flag or correct the changelog "behindperpsTerminalApiMarketsfeature flag" claim.
Non-blocking
marketDataTransform.ts:194— the newterminalMetadataparam is never passed in production (HyperLiquidProvider.ts:6811calls it with 4 args); live enrichment runs throughMarketDataService.#enrichWithTerminalMetadatawith different name/marketType precedence. Drop the param or wire it — two divergent enrichment paths.index.ts:473— exportedtransformMarketDatareferencesTerminalAssetMetadata, which is not exported.TerminalMarketService.ts:230—item.marketType as MarketTypecasts an unvalidated string straight into the enum; validate against known values first.MarketDataService.ts:~890— comment says Terminal metadata is fetched "in parallel" with provider data, but the awaits are sequential.
See also my two inline notes (full-URL injection vs base+path; the deps & { terminalMarketService } intersection).
Validation: 110/110 targeted tests pass on the pushed state; package build not completable locally due to a pre-existing dependency cycle (unbuilt account-tree-controller dist) — no error maps to a changed line.
…hangelog accuracy, dead code removal - Apply allowlist/blocklist filtering on the Terminal API getMarkets path so blocklisted HIP-3 markets no longer leak through when useTerminalApi is enabled. The controller now passes an isMarketAllowed callback that mirrors the provider's shouldIncludeMarket logic. - Fix changelog: terminalApiBaseUrl is optional (not required) and the feature is gated by useTerminalApi param (not a perpsTerminalApiMarkets remote flag that doesn't exist). - Rename terminalApiBaseUrl → terminalApiUrl; clients inject the full endpoint URL, removing the PerpetualPath constant. - Remove unused terminalMetadata param from transformMarketData (enrichment is handled by MarketDataService.#enrichWithTerminalMetadata). - Move TerminalAssetMetadata to types/index.ts and export it. - Validate marketType against MarketCategory enum instead of accepting any string. - Fix "in parallel" comment that described sequential awaits. - Replace intersection-type constructor with separate param for terminalMarketService.
Resolve CHANGELOG.md conflict by keeping both Added entries: Terminal API integration (this branch) and Discovery analytics constants (main).
…etService Split mixed import into separate type-only and value imports to satisfy the import-x/consistent-type-specifier-style lint rule.
The Terminal API entries were under the already-released [8.2.0] section, causing the changelog CI check to fail.
…formDependencies Add PerpsTerminalMarketService interface to PerpsPlatformDependencies so callers can inject any implementation. MarketDataService now reads the service from deps instead of a separate constructor argument. The controller auto-creates the service from terminalApiUrl when not explicitly provided, preserving backward compatibility.
# Conflicts: # yarn.lock
geositta
left a comment
There was a problem hiding this comment.
Requesting changes. Please preserve provider fallback when Terminal returns a non-empty response that does not contain the requested symbol or DEX, so partial Terminal coverage does not hide valid HyperLiquid markets.
| }); | ||
| } | ||
| traceData = { success: true }; | ||
| return filtered; |
There was a problem hiding this comment.
getMarkets() currently falls back only when the Terminal request fails or returns an empty response. The PR description also says static fallback data remains available for assets absent from Terminal, but when Terminal returns a non-empty list that lacks the requested symbol or DEX, the filtered result is returned as [] and the HyperLiquid fallback is skipped.
Please fall back to the provider when a constrained Terminal query (symbols or dex) produces no matches, so Terminal partial coverage does not make provider-backed markets undiscoverable
There was a problem hiding this comment.
Hello @geositta could you please share more details what you're expecting? Do you suggest we should fallback to HL if there are no results from our BE matching the search?
There was a problem hiding this comment.
@michalconsensys I would expect HL fallback only for symbol- or DEX specific getMarkets() lookups, not for every empty search result.
I checked mobile and this flow is possible: token details uses usePerpsMarketForAsset, which calls getMarkets({ symbols: [lookupSymbol], standalone: true }) to decide whether a spot asset has a perps market. If mobile enables useTerminalApi on that path and Terminal returns a nonempty list that does not include that symbol yet, the current code returns [] and skips HL, so the token would look like it has no perps market even if HL supports it.
For broad getMarkets({ useTerminalApi: true }), returning the Terminal list directly makes sense. The fallback that I am suggesting is only when the caller asks for a specific symbols or dex value and Terminal's filtered result is empty.
When getMarkets() is called with useTerminalApi and a specific symbols or dex filter, fall through to the HyperLiquid provider if the Terminal response does not contain a match. This prevents partial Terminal coverage from making provider-backed markets undiscoverable.
geositta
left a comment
There was a problem hiding this comment.
Approve. Thanks for addressing the getMarkets() fallback.
Explanation
The Perps Controller currently relies exclusively on HyperLiquid for market metadata (names, categories, market types). This is limiting because adding or categorising new markets requires code changes to static maps.
This PR integrates the MetaMask Terminal API as an optional market data source, controlled by the caller via
useTerminalApiparameter onGetMarketsParams/GetMarketDataWithPricesParams. When enabled:getMarkets()attempts the Terminal API first; on failure or empty response, falls back silently to the HyperLiquid provider. Terminal results respect the same allowlist/blocklist filtering as the provider path via a newisMarketAllowedfilter built byPerpsController.getMarketDataWithPrices()fetches live pricing from HyperLiquid as before, then enriches it with Terminal API metadata (human-readable name, keywords, tags, categories, marketType).getMarketMatchRank,rankMarketsByQuery) now indexes thekeywordsfield for richer results.getMarkets({ standalone: true })) now routes throughMarketDataServiceinstead of calling the provider directly, ensuring consistent filtering and Terminal API support.Key design decisions:
TerminalMarketServicevalidates each API response item individually with@metamask/superstruct; invalid items are filtered out and logged rather than rejecting the entire response.PerpsPlatformDependenciesgains optionalterminalApiUrl?: stringandterminalMarketService?: PerpsTerminalMarketServicefields — clients can inject a pre-built service instance or let the controller create one from the URL.PerpsMarketDatagains optionalkeywords,tags, andcategoriesfields.HYPERLIQUID_ASSET_NAMES,HIP3_ASSET_MARKET_TYPES) remain intact for assets absent from the Terminal API.References
Checklist
Note
Medium Risk
Changes core market discovery and listing behavior when
useTerminalApiis on, including fallback rules and filter parity; failures are logged but should be validated in client integrations.Overview
Adds an optional MetaMask Terminal API path for perps market metadata, gated by
useTerminalApionGetMarketsParamsandGetMarketDataWithPricesParams.When enabled,
getMarketstries Terminal first (with dex/symbol filtering and the same HIP-3 allowlist/blocklist via a newisMarketAllowedfilter fromPerpsController), then falls back to HyperLiquid on failure, empty data, or when a constrained query has no Terminal matches.getMarketDataWithPricesstill loads live prices from the provider and merges Terminal fields (name,keywords,tags,categories,marketType). StandalonegetMarketsnow goes throughMarketDataServicelike the normal path.Introduces
TerminalMarketService(5-minute cache,@metamask/superstructper-item validation), optionalterminalApiUrl/terminalMarketServiceon platform deps, and extends market search to rank onkeywords. Static HyperLiquid name/category maps remain as fallback.Reviewed by Cursor Bugbot for commit c7c4505. Bugbot is set up for automated code reviews on this repo. Configure here.