Skip to content

feat(perps): spot trading support and performance optimization#11183

Draft
Minnzen wants to merge 71 commits intoxfrom
feat/spot-trading-and-perps-perf
Draft

feat(perps): spot trading support and performance optimization#11183
Minnzen wants to merge 71 commits intoxfrom
feat/spot-trading-and-perps-perf

Conversation

@Minnzen
Copy link
Copy Markdown
Contributor

@Minnzen Minnzen commented Apr 13, 2026

Summary

  • Add spot trading support: service layer, atoms, WS subscriptions, mobile/desktop UI (token selector, balances tab, order flow)
  • Optimize perps first-screen loading: deduplicate API requests, parallelize initialization, speed up WS subscriptions
  • Improve safety: add spot order assetId validation, gate SPOT_STATE subscription on spotEnabled, cleanup dead code

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

  • Spot trading shares the existing perps infrastructure (WS transport, subscription management, account system) rather than building a parallel stack. Mode switching is handled via tradingModeAtom with instrument-level separation.
  • CACHE_TIME_QUANTIZE_MS (10s)loadTradesHistory quantizes Date.now() to 10-second boundaries so near-simultaneous callers produce identical memoizee keys, eliminating duplicate 345KB userFillsByTime requests without changing the memoizee API.
  • usePerpFeatureGuard switched to cached version — The original updatePerpsConfigByServer() bypassed the 1-hour memoizee cache on every tab focus, causing duplicate 30KB perp-config fetches. The config is slow-changing (referrer rates, banners); 1-hour TTL is the intended production behavior.
  • Promise.all parallelization in checkPerpsAccountStatusexchangeService.setup() (local) and userRole() (info client) are independent; checkBuilderFeeStatus and checkInternalRebateBindingStatus write to different statusDetails fields. Both groups run in parallel, with checkAgentStatus sequenced after both complete.
  • setReferrer deferred to after finally block — Already fire-and-forget, but starting it during the critical path caused bandwidth contention on the same HTTP/2 connection as blocking fetchUserAbstraction. Moving it after enableTradingLoading clears avoids this.
  • 600ms DelayedRender removed — Originally added (Oct 2025) to prevent premature WS reconnections when subscription management lacked gate conditions. Current guards (isWebSocketConnected && !isLoading && shouldSyncSubscriptions) are sufficient.
  • First WS subscription skips 300ms debounce_hasInitialSubscription flag bypasses the trailing-edge debounce on the very first updateSubscriptions() call. Flag is reset in disconnect() so iOS foreground resume also gets immediate re-subscription.
  • spotEnabled gates SPOT_STATE subscription — The field was computed in subscriptionPlanner and passed through the entire pipeline but never consumed in calculateRequiredSubscriptions. Now properly wired to gate SPOT_STATE, saving bandwidth when in pure perps mode.
  • placeSpotOrder assetId assertion — Defense-in-depth: rejects orders with assetId < SPOT_ASSET_ID_OFFSET to 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 cache
  • packages/kit-bg/src/services/ServiceHyperLiquid/ServiceHyperliquidExchange.ts — Spot order placement, slippage calculation, assetId validation
  • packages/kit-bg/src/services/ServiceHyperLiquid/ServiceHyperliquidSubscription.ts — Spot WS handlers, skip-debounce optimization, disconnect reset
  • packages/kit-bg/src/services/ServiceHyperLiquid/utils/SubscriptionConfig.ts — Spot subscription specs, spotEnabled gate
  • packages/kit/src/views/Perp/components/ — Spot UI: token selector tabs, balance list, ticker bar mode branching, order confirm
  • packages/kit/src/states/jotai/contexts/hyperliquid/ — Spot actions, atoms, context extensions
  • packages/kit-bg/src/states/jotai/atoms/spot.ts — Spot atom definitions
  • packages/shared/types/hyperliquid/ — Spot types, constants (SPOT_ASSET_ID_OFFSET, CACHE_TIME_QUANTIZE_MS)

Risk Assessment

  • Risk Level: Medium
  • Affected Platforms: All (Desktop / Mobile / Web / Extension) — changes are in shared packages
  • Risk Areas:
    • Spot order flow (new feature, assetId mapping)
    • WS subscription timing changes (DelayedRender removal, debounce skip)
    • Promise.all parallelization in account status check (verified: no shared mutable state between parallel branches)

Test plan

  • Perps trading flow unchanged (open/close positions, TP/SL, limit orders)
  • Spot mode: switch to spot tab in token selector, place market/limit buy and sell orders
  • Spot balances tab: displays correct holdings with contract addresses
  • First-screen loading: no duplicate userFillsByTime or perp-config requests (verify via Network tab)
  • iOS: background → foreground → WS reconnects and data resumes without stale state
  • Account switching: spot balances reset, no stale data flash
  • Feature guard: perp-config uses cached version, tab redirect still works when server disables perps

Open with Devin

Minnzen added 30 commits April 13, 2026 17:06
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
…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.
devin-ai-integration[bot]

This comment was marked as resolved.

Minnzen and others added 3 commits April 14, 2026 11:03
…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 sidmorizon enabled auto-merge (squash) April 14, 2026 04:28
sidmorizon
sidmorizon previously approved these changes Apr 14, 2026
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

Minnzen and others added 4 commits April 14, 2026 16:19
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
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 3 new potential issues.

View 44 additional findings in Devin Review.

Open in Devin Review

@originalix originalix marked this pull request as draft April 14, 2026 11:05
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants