feat(perps): spot trading support and performance optimization#11183
Draft
feat(perps): spot trading support and performance optimization#11183
Conversation
Add ESubscriptionType entries (SPOT_ASSET_CTXS, ACTIVE_SPOT_ASSET_CTX), spot WS types (IWsSpotAssetCtxs, IWsActiveSpotAssetCtx), spot info types (ISpotUniverse, ISpotToken, ISpotAssetCtx), ISpotOrderParams, ISpotFormattedAssetCtx, and utility functions (formatSpotAssetCtx, isSpotInstrument, SPOT_TOKEN_DISPLAY_MAP, getSpotTokenDisplayName, formatSpotPairDisplayName).
Add tradingModeAtom ('perp'|'spot') to perps.ts, register all spot atom
names in atomNames.ts, export spot.ts from atoms/index.ts. spot.ts defines
spotActiveAssetAtom, spotActiveAssetCtxAtom, spotBalancesAtom,
spotAssetCtxsMapAtom, and related atoms.
ServiceHyperliquid: updateActiveSpotAssetCtx, extractSpotPricesFromAllMids, updateSpotAssetCtxsMap, spot price cache, spot mappings, spotBalancesAtom write. ServiceHyperliquidSubscription: coin format detection in ACTIVE_ASSET_CTX handler, tradingMode-aware buildRequiredSubscriptionsMap, SPOT_ASSET_CTXS handler. SubscriptionConfig: mode-exclusive per-asset subscriptions, spot mode branch. ServiceHyperliquidExchange: placeSpotOrder, _calculateSpotSlippagePrice. SimpleDbEntityPerp: getSpotMeta/setSpotMeta.
…ading Actions: switchTradeInstrument facade, changeActiveSpotAsset, _getActiveCoin, _buildActiveTradeInstrument, placeSpotOrder, mode-aware BBO/L2Book updates. Context atoms: activeTradeInstrumentAtom, tradeRouteViewStateAtom. PerpsGlobalEffects: mode-aware subscription updates via planTradeSubscriptions. TokenSelector: spot token detection and switchTradeInstrument call. TickerBar: spot data source switching, hide perps-only fields. TradingPanel: hide leverage/trigger/TPSL/reduceOnly for spot, Buy/Sell labels. OrderInfoPanel: SpotBalanceList in Balances tab.
Restore 25 UI component files with spot support: TokenSelector (Spot tab, SpotTokenSelectorRowInner, spot data loading/sorting), PerpOrderBook, MobilePerpMarketHeader, FavoriteTokenItem, FavoritesBar, FooterTicker, PerpCandles, TradingForm, OrderConfirmModal, PriceInput, usePerpsFavorites, usePopularTickers, useTradingCalculationsForSide, useTradingPrice, useLiquidationPrice, useOrderConfirm, usePerpOrderInfoPanel, usePerpTokenUrlSync, MobilePerpMarket.
- PriceInput: remove validateSpotPriceInput (not needed, reuse validatePriceInput) - ServiceHyperliquid: add debug log in updateActiveSpotAssetCtx - ServiceHyperliquidSubscription: add debug log in ACTIVE_ASSET_CTX handler Debug logs to diagnose why spotActiveAssetCtxAtom stays empty.
TickerBarMarkPrice, TickerBarChange24hPercent, TickerBar24hVolume now branch on tradingMode to read from spotActiveAssetCtxAtom when in spot mode. Show Volume in spot mode, only hide OraclePrice/OI/FundingRate.
refreshSpotMeta was using item.index directly instead of 10000 + item.index, causing spot orders to be sent with perps assetId.
Root cause: perpsWebSocketReadyStateAtom.set() on every WS message created new object references, triggering WebSocketSubscriptionUpdate effect continuously. - ServiceHyperliquidSubscription: cache _lastReadyState, skip redundant writes - PerpsGlobalEffects: extract primitive deps from object references in useEffect - actions: add _isTradeInstrumentEqual check before setting activeTradeInstrumentAtom - actions: skip setTradeRouteViewState when values unchanged
…in, force leverage=1
…m integer prices The regex /\.?0+$/ incorrectly stripped trailing zeros from integer prices (e.g. '60000' → '6'), causing spot limit orders to compute wrong sizes. Only strip trailing zeros after a decimal point.
… address - PerpOrderInfoPanel: Balances tab is now first, initialTabName=Balances - SpotBalanceList: build tokenContractMap from spotMeta.tokens.evmContract, populate IBalanceDisplayItem.contract for display - BalanceRow (mobile): show shortened contract address next to coin label
Extract ContractAddressCell component with IconButton (Copy3Outline) that calls useClipboard().copyText(). Used in both mobile and desktop BalanceRow.
…ccount switch, await cleanup, remove dead ACTIVE_SPOT_ASSET_CTX branch - placeSpotOrder: guard against undefined assetId (spotMeta load failure) - placeSpotOrder: pass szDecimals from env to exchange call - changeActivePerpsAccount: also reset spotBalancesAtom on account switch - _cleanupAllSubscriptions: await Promise.all before clearing active set - Remove dead ACTIVE_SPOT_ASSET_CTX handler branch and wire-type mapping (spot reuses ACTIVE_ASSET_CTX, routed by coin format in handler)
…' in OrderConfirmModal - Add SPOT_ASSET_ID_OFFSET = 10_000 to perp.constants.ts (was magic number) - refreshSpotMeta and ISpotOrderParams comment use the named constant - OrderConfirmModal: use discriminated union narrowing instead of 'as any' for activeInstrument.universe access (both szDecimals and baseName)
- Remove commented-out getOnekeyWalletClient in ServiceHyperliquidExchange - Remove dead '// debugger' comments in ServiceHyperliquidSubscription - Remove @ts-ignore on allMids dex param (SDK now supports it) - Remove dead commented ACTIVE_ASSET_DATA block + ts-ignore noise - Remove stale debug console.log in WS hot path and various trace logs - Drop unused lodash isEmpty import
… mode transition When switching from spot to perps, universe may briefly hold flat ISpotUniverse[] while mode has already changed to 'perp'. The code casts it to IPerpsUniverse[][] and iterates perpUniverse[dexIndex] which is not an array, causing 'assets is not iterable'. Add Array.isArray guard before iteration.
Root cause: usePromiseResult keeps stale data from the previous mode until
the new promise resolves. When switching spot→perps, 'mode' changes immediately
but 'universe' still holds flat ISpotUniverse[], causing 'assets is not iterable'
when cast to IPerpsUniverse[][].
Fix: tag the promise result with { mode, data } so the consumer can verify
the data matches the current mode. Stale cross-mode data is safely skipped
(returns [] until the correct data arrives). No more unsafe type casts.
Spot mode was showing perpetual loading skeletons because useTickerBarIsLoading checked isReady (perps WS connection state) which can be temporarily false during mode transitions. Spot should only check if spotActiveAssetCtxAtom has data.
…paration - selectAtom + isPerpsCtxEqual for per-asset notifications in atoms.ts - 1s leading+trailing throttle + startTransition for ALL_DEXS_ASSET_CTXS - usePerpsAssetCtx subscribes per-asset instead of full array - Split activeTabData/mockedListData into perpSortedList + spotSortedList + filter layers - initialNumToRender 10/15 → 5 on both platforms - Mobile: usePageMounted replaces hardcode 200ms setTimeout - Mobile: spot tab, spot universe fetch with cache-then-refresh fallback - Mobile: spot token selection via switchTradeInstrument - Desktop: spot loading spinner, cache-then-refresh for spotMeta - PerpOrderInfoPanel: add key to TabBar renderItem - Remove 3 stale console.log in actions.ts - Restore perp config cache maxAge to 1 hour - Remove commented-out empty effect in PerpsGlobalEffects
- MobileTokenSelector: add Spot tab with universe fetch, sort, search, selection - PerpMobileLayout: add Balances tab with SpotBalanceList - ServiceHyperliquid: restore perp config cache, clean spot price writer - ServiceHyperliquidSubscription: register ACTIVE_SPOT_ASSET_CTX, remove console.log - SubscriptionConfig: add spot subscription specs - spot.ts atoms: add spotTradesHistoryDataAtom, quantized cache timing - PerpTickerBarDesktop, useActiveTradeDisplay, usePopularTickers: spot mode adaptation - OrderConfirmModal: extract SPOT_ASSET_ID_OFFSET constant - SpotBalanceList, BalanceRow: minor fixes - perp.constants: add CACHE_TIME_QUANTIZE_MS
- Remove key={activeTab} from ListView — tab switch no longer destroys/recreates 150+ rows
- Pre-compute tokenName, tokenMaxLeverage, tokenSubtitle in parent list items
- Row reads static data from props instead of usePerpsAllAssetsFilteredAtom (was 150+ subscriptions)
- Remove usePerpsTokenSearchAliasesAtom from row (subtitle now in list item)
- Applied to both desktop and mobile token selectors
- Parallelize exchangeService.setup() with userRole() check (independent clients) - Parallelize checkBuilderFeeStatus with rebate binding check (no data dependency) - Move setReferrer fire-and-forget to after finally block to reduce bandwidth contention during critical loading path - Move accountAddress null check before setup to fail fast - Prettier auto-format on refreshSpotMeta map callback
…bscription - Remove DelayedRender(600ms) around WebSocketSubscriptionUpdate — gate conditions (isWebSocketConnected, !isLoading, shouldSyncSubscriptions) are now sufficient to prevent premature subscriptions - Extract _updateSubscriptionsCore from debounced wrapper - Skip 300ms debounce on first updateSubscriptions() call for faster initial data delivery - Reset _hasInitialSubscription on disconnect() so post-reconnect (including iOS foreground resume) also gets immediate subscription
spotEnabled was computed in subscriptionPlanner and passed through the entire pipeline but never used to gate any subscription. Wire it up to SPOT_STATE so spot balance streaming is only active when in spot mode or viewing the Balances tab.
…ription Avoids ~1s delay on first WS connection where socketOpenHandler calls updateSubscriptions before the React effect sets spotEnabled via setRouteSubscriptionState.
API rejection reasons (insufficient balance, price out of range, etc.) are useful for the user — should not be swallowed.
sidmorizon
previously approved these changes
Apr 14, 2026
…ps, SpotBalanceList
…list navigation SpotBalanceList was passing rawCoin (e.g. "HYPE") to changeActiveSpotAsset, but WS subscriptions and price cache are keyed by pair name (e.g. "@107").
Math.max(0, ...) prevents negative toFixed() when szDecimals > MAX_DECIMALS_SPOT.
- Reuse TradeSideToggle with isSpot prop for Buy/Sell labels - Show raw token balance (e.g. "1.234 HYPE") instead of USD value - Render single action button matching selected direction - Clear size input on side switch
HL spot token internal names (UBTC, HPENGU, FXRP, etc.) are scoped to SPOT_TOKEN_DISPLAY_MAP — disable cspell inline so new tokens don't require skipWords updates.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Intent & Context
This PR adds spot trading as a new trading mode alongside perpetual futures in the HyperLiquid integration. During development, HAR file analysis revealed significant first-screen performance issues (duplicate API requests, serial initialization chains, unnecessary WS delays), which were optimized in the same branch.
Design Decisions
tradingModeAtomwith instrument-level separation.loadTradesHistoryquantizesDate.now()to 10-second boundaries so near-simultaneous callers produce identical memoizee keys, eliminating duplicate 345KBuserFillsByTimerequests without changing the memoizee API.usePerpFeatureGuardswitched to cached version — The originalupdatePerpsConfigByServer()bypassed the 1-hour memoizee cache on every tab focus, causing duplicate 30KBperp-configfetches. The config is slow-changing (referrer rates, banners); 1-hour TTL is the intended production behavior.checkPerpsAccountStatus—exchangeService.setup()(local) anduserRole()(info client) are independent;checkBuilderFeeStatusandcheckInternalRebateBindingStatuswrite to differentstatusDetailsfields. Both groups run in parallel, withcheckAgentStatussequenced after both complete.finallyblock — Already fire-and-forget, but starting it during the critical path caused bandwidth contention on the same HTTP/2 connection as blockingfetchUserAbstraction. Moving it afterenableTradingLoadingclears avoids this.isWebSocketConnected && !isLoading && shouldSyncSubscriptions) are sufficient._hasInitialSubscriptionflag bypasses the trailing-edge debounce on the very firstupdateSubscriptions()call. Flag is reset indisconnect()so iOS foreground resume also gets immediate re-subscription.spotEnabledgatesSPOT_STATEsubscription — The field was computed insubscriptionPlannerand passed through the entire pipeline but never consumed incalculateRequiredSubscriptions. Now properly wired to gateSPOT_STATE, saving bandwidth when in pure perps mode.placeSpotOrderassetId assertion — Defense-in-depth: rejects orders withassetId < SPOT_ASSET_ID_OFFSETto prevent a stale perps instrument from being accidentally submitted through the spot code path during mode transitions.Changes Detail
packages/kit-bg/src/services/ServiceHyperLiquid/ServiceHyperliquid.ts— Spot service layer, perf parallelization, setReferrer defer, quantized cachepackages/kit-bg/src/services/ServiceHyperLiquid/ServiceHyperliquidExchange.ts— Spot order placement, slippage calculation, assetId validationpackages/kit-bg/src/services/ServiceHyperLiquid/ServiceHyperliquidSubscription.ts— Spot WS handlers, skip-debounce optimization, disconnect resetpackages/kit-bg/src/services/ServiceHyperLiquid/utils/SubscriptionConfig.ts— Spot subscription specs, spotEnabled gatepackages/kit/src/views/Perp/components/— Spot UI: token selector tabs, balance list, ticker bar mode branching, order confirmpackages/kit/src/states/jotai/contexts/hyperliquid/— Spot actions, atoms, context extensionspackages/kit-bg/src/states/jotai/atoms/spot.ts— Spot atom definitionspackages/shared/types/hyperliquid/— Spot types, constants (SPOT_ASSET_ID_OFFSET, CACHE_TIME_QUANTIZE_MS)Risk Assessment
Test plan
userFillsByTimeorperp-configrequests (verify via Network tab)