Skip to content

fix(perps): HL Unified-mode live balance — spotState ws + tradeable-balance + total-balance math cp-7.72.2#29226

Merged
abretonc7s merged 19 commits into
mainfrom
perps/hl-spotstate-live-balance
Apr 23, 2026
Merged

fix(perps): HL Unified-mode live balance — spotState ws + tradeable-balance + total-balance math cp-7.72.2#29226
abretonc7s merged 19 commits into
mainfrom
perps/hl-spotstate-live-balance

Conversation

@abretonc7s

@abretonc7s abretonc7s commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Description

TAT-3016 follow-up covering the remaining HL balance gaps after the initial spot-balance parity work.

What's broken on main

  1. AccountState.totalBalance is never updated live after a limit order is placed or cancelled — the HL spot clearinghouse state changes but nothing reflects it in our streamed cache. The REST #refreshSpotState path only runs on cold-start and standalone fetches, so the cached snapshot goes stale on every on-chain event (local trade, external-client trade, funding, liquidation, deposit, transfer).
  2. AccountState.availableToTradeBalance doesn't exist — order-entry surfaces read availableBalance (= HL withdrawable), which is always $0 on Unified-mode accounts whose collateral is held as spot USDC. Users with tradeable balance see the app refuse to open an order.
  3. totalBalance on the three account-state paths sums perps.accountValue + spot.total without subtracting spot.hold. On Unified/PM the held margin is reported in both fields, so pre-fix totalBalance inflates by the margin amount whenever a limit order is placed and deflates when cancelled — even though no wealth changed hands.

What this PR does

  • Subscribes to HL's spotState WebSocket channel alongside the existing webData2/3 user-data subscription. Handler updates #cachedSpotState, bumps #spotStateGeneration to invalidate any in-flight REST race, and re-runs #aggregateAndNotifySubscribers so UI consumers see spot-folded totals within one network round-trip of the change. REST fallback stays for cold-start and the standalone path.
  • Adds AccountState.availableToTradeBalance as a first-class optional field: withdrawable + (spot.total − spot.hold) for HL, availableBalance trivial default for other providers. Order-entry surfaces (PerpsMarketBalanceActions, PerpsMarketDetailsView, useDefaultPayWithTokenWhenNoPerpsBalance, usePerpsOrderForm) read availableToTradeBalance ?? availableBalance. Withdraw path (PerpsWithdrawView) keeps reading availableBalance unchanged so the withdraw row never leaks the spot fold.
  • Subtracts spot.hold from the totalBalance sum in both the single-DEX adapter and the aggregated fold helper. On Standard mode spot.hold = 0 so the subtraction is a no-op; on Unified/PM it cancels the double-count. Result: totalBalance no longer ping-pongs on limit place/cancel, matching HL web.
  • Exposes HyperLiquidProvider.getExchangeClient() as a non-interface escape hatch for agentic validation flows that drive HL mutations directly.
  • Adds a perps-withdraw-available-balance-text testID to anchor the non-regression check that withdraw keeps rendering availableBalance.
  • Adds the latest HL reference docs (account-abstraction-modes.md, portfolio-margin.md, margin-tiers.md, updated subscriptions.md + margining.md) so the code rationale can cite them.

Follow-ups (not in this PR)

  • Rename availableBalance → withdrawableBalance (TAT-3047) — pure rename against main, kept separate so OTA cherry-picks don't see symbol drift.
  • Agentic regression recipe exercising mode-flip + limit-cycle + REST parity — tracked separately.

Changelog

CHANGELOG entry: Fixed Perps balance not refreshing after trades, funding, or transfers for HyperLiquid users, and corrected total balance inflation on Unified-mode accounts.

Related issues

Fixes: TAT-3016

Supersedes: #29150, #29217 (both closed).

Manual testing steps

Feature: HL spotState live-balance refresh

  Scenario: Spot-funded Unified account sees live balance
    Given the Trading fixture (0x316B…01fA) is connected in Unified mode
    And the Perps screen is open
    Then AccountState.availableToTradeBalance reflects withdrawable + (spot.total − spot.hold)
    And PerpsMarketBalanceActions 'Available' row shows the same value
    And PerpsWithdrawView 'Available Perps balance' row shows $0 (withdrawable only)
    And PerpsOrderView 'Pay with' row defaults to 'Perps balance'

  Scenario: Limit order cycle leaves totalBalance stable
    When the user places a limit order on BTC
    Then AccountState.availableToTradeBalance drops by the reserved margin
    And AccountState.totalBalance is unchanged
    When the user cancels the limit
    Then AccountState.availableToTradeBalance returns to baseline within 5s
    And AccountState.totalBalance is still unchanged

  Scenario: Screenshot parity with HL web
    Then the total shown in the MetaMask Perps header matches app.hyperliquid.xyz (within $0.20)
    And the order-form 'Available' value matches HL 'Available to trade'

Screenshots/Recordings

Before

image

After

image

Pre-merge author checklist

Performance checks (if applicable)

  • I've tested on Android
  • I've tested with a power user scenario
  • I've instrumented key operations with Sentry traces for production performance metrics

Pre-merge reviewer checklist

  • I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed).
  • I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.

…eferences

Brings the in-repo HL reference set up to date with hyperliquid.gitbook.io as of 2026-04-23:

- account-abstraction-modes.md: Standard / Unified / Portfolio Margin / DEX
  abstraction definitions + the computeUnifiedAccountRatio reference
  implementation.
- portfolio-margin.md: LTV-based buying-power math, alpha caps, liquidation
  ratio formula.
- margin-tiers.md: mainnet + testnet tiered leverage tables.
- margining.md: HIP-3 cross-margin behaviour per abstraction mode,
  "strict isolated" rename, minor formatting alignment.
- subscriptions.md: spotState, allDexsClearinghouseState, allDexsAssetCtxs
  subscription types + WsSpotState / SpotState / UserBalance data formats,
  per-DEX filter on twapStates / clearinghouseState / openOrders.

Adds docs/perps/tat-3016-clarity.md: one-page incident brief and
architectural critique for the HL spot-balance incident (TAT-3016),
including the rationale for switching from REST-pull refresh to the
spotState WebSocket subscription.
…lance

Problem
-------

HyperLiquid's spot clearinghouse state (per-coin total + hold) changes on
every on-chain event — local trades, external-client trades, hourly
funding, liquidations, deposits, transfers, usdClassTransfer — but none
of those are surfaced through webData2/webData3/clearinghouseState. The
existing REST path (#refreshSpotState -> infoClient.spotClearinghouseState)
only runs on cold-start or standalone getAccountState calls, so the cached
spot snapshot goes stale the moment anything moves it. UI surfaces that
read availableToTradeBalance or totalBalance see stale values until the
next account-state screen refresh.

Fix
---

Subscribe to HL's existing `spotState` WebSocket feed alongside the
webData2/webData3 user-data subscriptions already established in
subscribeToAccount. The push handler:

- updates #cachedSpotState + #cachedSpotStateUserAddress with the pushed
  SpotClearinghouseStateResponse
- bumps #spotStateGeneration so any in-flight REST #refreshSpotState
  drops its result rather than overwriting the fresh WS snapshot
- re-runs #aggregateAndNotifySubscribers when #dexAccountCache is
  populated, so subscribers see the spot-folded totalBalance /
  availableToTradeBalance immediately

REST fallback is left in place: #refreshSpotState still covers cold
start before the first WS tick and the standalone getAccountState path
that does not own a subscription.

Shape
-----

- #spotStateSubscriptions: Map<userAddress, ISubscription> — one sub
  per user, mirrors the per-user lifecycle of #orderFillSubscriptions.
- #spotStateSubscriptionPromises: Map<userAddress, Promise<void>> —
  in-flight guard so two concurrent subscribeToAccount calls share a
  single WS handshake.
- Cleanup in both #cleanupSharedWebData3ISubscription (ref-counted)
  and clearAll (shutdown).

Test coverage
-------------

Five new tests in the "spotState WebSocket Subscription" block cover:
establishment on subscribeToAccount, de-duplication across concurrent
subscribes, re-notification on push, user-filter guarding cross-account
events, and unsubscribe on last-subscriber removal.

Notes
-----

No change to #ensureSpotState, #refreshSpotState, #spotStateGeneration,
or #cachedSpotState semantics — those remain authoritative for the
REST-driven cold-start and standalone paths. This commit adds a push
source for the same cache without touching the REST writers.
Adds a public `getExchangeClient()` method on HyperLiquidProvider that
delegates to HyperLiquidClientService.getExchangeClient(). Not part of
the PerpsProvider interface — documented as an escape hatch for agentic
validation flows and test harnesses that need to drive HL mutations
directly (order, usdClassTransfer, userSetAbstraction, etc.) while
bypassing TradingService, metrics, error-handler wrappers, and cache
invalidation.

Needed by the upcoming tat-3016 regression recipe to probe that a
mutation originating outside TradingService still propagates through
the spotState WebSocket subscription added in the previous commit.
Production code paths continue to go through the provider's own methods.

Tests cover delegation and error propagation.
@abretonc7s abretonc7s added type-bug Something isn't working team-perps Perps team labels Apr 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes.

Validates that the spotState WebSocket subscription added earlier in this
branch keeps AccountState.availableToTradeBalance + totalBalance fresh
without any manual refresh, across three mutation classes:

1. UI-driven open — trade-open-market flow places a $11 BTC long.
   Recipe waits on marginUsed > 0 from the pushed state, then re-runs
   hl-balance-validation to assert the fold invariant + UI parity.
2. UI-driven close — trade-close-position mirror.
3. External-client — drives provider.getExchangeClient().usdClassTransfer
   directly, bypassing TradingService entirely. Proves the subscription
   catches mutations that never touched any of the TradingService
   refresh sites closed-#29217 tried to hook.

Composes the existing hl-balance-validation + trade-open-market +
trade-close-position flows. Reuses the getExchangeClient escape hatch
added by the preceding commit. Restores the USDC moved by the external
probe at end of run so the fixture account finishes clean.

Mainnet-only: the external probe relies on usdClassTransfer which is
an abstraction-aware mutation. On testnet the recipe will run through
the UI-driven portion but skip the external-probe branch cleanly.

Schema: passes node scripts/perps/agentic/validate-flow-schema.js.

Run: bash scripts/perps/agentic/validate-recipe.sh evidence/
@github-actions github-actions Bot added size-XL and removed size-L labels Apr 23, 2026
… debug markers

Problem caught during recipe validation
---------------------------------------

Running evidence/recipe.json on main-with-spotState-subscription showed
that `totalBalance` updated correctly via the push, but
`availableToTradeBalance` stayed at 0 / undefined across every snapshot.
Root cause: the field declared by the order-entry story was never actually
populated in the data pipeline. `addSpotBalanceToAccountState` bumped
`totalBalance` only; `adaptAccountStateFromSDK` and `aggregateAccountStates`
did not touch the field at all. All UI surfaces reading the fallback
`availableToTradeBalance ?? availableBalance` therefore reverted to the
HL `withdrawable` value, which is 0 on Unified/spot-only accounts.

Fix
---

- `AccountState.availableToTradeBalance?: string` is now a first-class
  field in the contract (still optional so MYX / providers that don't
  need it can omit).
- `adaptAccountStateFromSDK(perps, spot?)` computes it inline:
  `withdrawable + max(0, spot.total - spot.hold)` across the
  SPOT_COLLATERAL_COINS allowlist.
- `getSpotHold()` helper mirrors `getSpotBalance()` for the hold side.
- `addSpotBalanceToAccountState()` now writes
  `availableToTradeBalance = availableBalance + freeSpot` and keeps the
  existing `totalBalance += spotBalance` fold. When no spot is held it
  still sets the field to the current withdrawable so consumers always
  see a concrete value.
- `aggregateAccountStates()` sums `availableToTradeBalance` across
  per-DEX states when both sides have it defined; falls back to
  `availableBalance` otherwise.

UI surface
----------

- `PerpsMarketBalanceActions.tsx` (renders
  `perps-market-available-balance-text`) now reads
  `perpsAccount?.availableToTradeBalance ?? perpsAccount?.availableBalance`.
  Withdraw surfaces deliberately untouched so `PerpsWithdrawView`
  continues to render the raw `availableBalance`.

Instrumentation
---------------

Adds DevLogger markers under the `[TAT-3016]` prefix so the data flow is
greppable in Metro logs:

- `HyperLiquidSubscriptionService` spotState handler logs pushed
  balance/hold, user filter rejections, and the "deferred because no
  perps data cached yet" branch.
- `#aggregateAndNotifySubscribers` logs the post-aggregation
  AccountState shape.
- `addSpotBalanceToAccountState` logs inputs and derived `freeSpot`.

All markers are temporary diagnostics for incident validation and will
be removed in a follow-up commit once the recipe is green.
…ance-validation recipe

Withdraw-path non-regression anchor for the hl-balance-validation flow
on main. The agentic flow asserts that the text rendered at this testID
matches AccountState.availableBalance (withdrawable) and never leaks the
availableToTradeBalance spot fold — the invariant TAT-3016 hinges on.
Previously the flow's settle-withdraw wait_for timed out because the
testID was only referenced in flow JSON, not rendered by the view.
Spot-funded Unified-mode accounts were auto-steered onto external USDC
pay-token because useDefaultPayWithTokenWhenNoPerpsBalance gated on raw
availableBalance (= withdrawable = 0 on Unified). With the field now
populated end-to-end, switch the gate to
availableToTradeBalance ?? availableBalance so the order form defaults
to Perps balance when spot collateral is available.

Caught by evidence/recipe.json: capture-order-form step showed
paySymbol USDC instead of Perps on the Trading fixture.
PerpsMarketDetailsView.hasDirectOrderFundingPath only checked raw
availableBalance (= withdrawable = 0 on Unified-mode spot-funded) plus
the external pay-token fallback. After the pay-token hook learned to
skip the external steer when availableToTradeBalance is non-zero, the
gate flipped false and the Long/Short buttons disappeared, leaving
Add-Funds as the only CTA on Unified-mode accounts with spot collateral.

Add tradeableBalance (availableToTradeBalance ?? availableBalance) as
the primary direct-funding signal. Original availableBalance check
kept for provider/mode compatibility.
… TAT-3050 state

Recipe adjustments after first run:
- Reset leaked selectedPaymentToken + pending BTC trade config as the
  first node so baseline starts from a clean state (TAT-3050 sidestep).
- Focus the graph on the hl-balance-validation pass which already
  covers: fold invariant, market-list balance text, withdraw non-regression
  (fold must NOT leak), and Pay-with row defaulting to Perps balance
  when availableToTradeBalance > 0.
- External-client usdClassTransfer probe + UI-driven trade-open-market
  cycle deferred to a follow-up recipe — usdClassTransfer is abstraction-
  sensitive on Unified accounts and trade-open-market has a pre-existing
  gap on the RedesignedConfirmations route. Scope kept to what the
  current PR affects.

Pre-conditions:
- perps.sufficient_balance now gates on availableToTradeBalance
  (fallback: availableBalance). Previously read raw availableBalance
  which is 0 on HL Unified mode even when spot collateral covers orders
  — recipe pre-condition failed on accounts the product considers
  tradeable.
PerpsOrderView showed 'Insufficient balance. Required: $3.37, Available: $0'
on Unified-mode accounts with spot-only collateral because usePerpsOrderForm
fed availableBalance (= withdrawable = 0) into balanceForValidation,
maxPossibleAmount, and initialBalancePercent. With the field now populated,
prefer availableToTradeBalance when no external pay-token is selected.

Consumer surfaces fixed in this commit:
- perps-order-view slider/keypad max amount
- perps-order-view percentage buttons
- perps-order-view margin-vs-required gate
- 'Insufficient funds' red text at the bottom of the form
- Place Order button enablement
…on Unified mode

Symptom
-------

On HyperLiquid Unified/Portfolio Margin accounts opening a position
caused totalBalance to increase by the position margin, not by the
unrealized PnL. Example: $106 spot, 0 perps, no position → total $106.
Open a $11 long (~$3.37 margin): spot stays $106, spot.hold becomes
$3.37, perps accountValue becomes $3.37 — previous formula
`perpsBalance + spotBalance` reported $109.37. No wealth changed hands
but the displayed total went up.

Root cause
----------

marginSummary.accountValue on Unified/PM equals marginUsed +
unrealizedPnl + withdrawable_perps, where marginUsed is the margin
reserved from spot (reported by HL as spot.hold). Adding accountValue
to spotBalance double-counts that reservation.

Fix
---

Subtract spotHold from the totalBalance sum in both the single-DEX
adapter (adaptAccountStateFromSDK) and the aggregated fold
(addSpotBalanceToAccountState):

  totalBalance = perpsBalance + spotBalance - spotHold

On Standard mode spotHold = 0, so the formula reduces to the previous
`perpsBalance + spotBalance` and no behaviour changes. On Unified/PM
the subtraction cancels the held-margin double-count, so totalBalance
now moves only with unrealizedPnl — matching HL's own "Perp Equity"
semantics.

availableToTradeBalance is unaffected: it already computes
`availableBalance + (spotTotal - spotHold)` which correctly nets the
reservation out of the tradeable pool.
@abretonc7s abretonc7s marked this pull request as ready for review April 23, 2026 03:24
@abretonc7s abretonc7s requested a review from a team as a code owner April 23, 2026 03:24
@abretonc7s abretonc7s changed the title fix(perps): wire HyperLiquid spotState websocket for live balance (TAT-3016) fix(perps): HL Unified-mode live balance — spotState ws + tradeable-balance + total-balance math Apr 23, 2026
@github-actions github-actions Bot added the risk:high AI analysis: high risk label Apr 23, 2026
@abretonc7s abretonc7s changed the title fix(perps): HL Unified-mode live balance — spotState ws + tradeable-balance + total-balance math fix(perps): HL Unified-mode live balance — spotState ws + tradeable-balance + total-balance math cp-7.72.2 Apr 23, 2026
Comment thread app/controllers/perps/utils/accountUtils.ts Outdated
Comment thread app/controllers/perps/services/HyperLiquidSubscriptionService.ts
Comment thread app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.tsx Outdated
Review-prep cleanup pass:
- Remove [TAT-3016] DevLogger diagnostics added for in-flight debugging.
- Drop verbose block comments on the pay-token / order-form / market-details
  changes where the code is self-describing via identifier names.
- Collapse the market-details direct-funding gate to a single
  availableToTradeBalance check (with fallback to availableBalance).
- Drop unused DevLogger import in accountUtils.
- Keep only the non-obvious WHY comments: double-count subtraction
  rationale, sentinel preservation, and the withdraw-testID constraint.
- Rename the regression recipe off the Jira ticket reference.

@cursor cursor Bot left a comment

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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 9619d0c. Configure here.

Comment thread app/controllers/perps/utils/hyperLiquidAdapter.ts
Close the spotState subscribe-vs-cleanup race (bugbot #3128164456).
#spotStateSubscriptionPromises was not cleared on cleanup, so an
in-flight #ensureSpotStateSubscription continuation could resolve
after clearAll/cleanupSharedWebData3 and rehydrate
#spotStateSubscriptions with a now-orphaned subscription. The next
reconnect then short-circuited on the stale entry, leaving spot
balance updates silent for the session.

Adds #spotStateSubscriptionGeneration bumped at both cleanup sites;
the async body captures the generation before subscribing and
unsubscribes + drops the fresh sub if the generation has moved.
…recipe

HyperLiquidProvider.getExchangeClient() now returns Promise<ExchangeClient>
to match the shape hl-provision-fixture already assumes (it chains .then
on the return value). The underlying client-service call stays sync; the
wrapper is async so callers await a single pattern.

Recipe overhauled to cycle abstraction modes:
- setup captures initial state + resets payment-token
- workflow: set Standard → validate + parity → set Unified → validate +
  parity + order-form wiring → manual gate
- teardown restores Unified

Runs on both testnet and mainnet.
…ardown cleanup

Scenario from a reviewer report: open limit orders, 'cancel all',
balance stuck in pre-cancel state. Adds limit-cycle block that:
- seeds baseline (tradeable ≈ total)
- places a BTC limit at 50% of mid (won't fill)
- asserts tradeable drops below total (hold reserved)
- calls cancelOrders({cancelAll:true})
- asserts tradeable returns within $0.20 of total (hold released)

Recipe now also pre/post cancels any leftover orders so abstraction
mode switches don't fail on 'cannot disable unified account with open
orders'.

@gambinish gambinish left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I've tested this and I think this is definitely the approach we need. Thanks for figuring this out 👍

A couple of requests:

  1. Please do some validation on Limit Orders, I noticed that the available balance doesn't always account for newly created limit orders or newly cancelled limit orders. I was testing single cancellations and also "cancel all" flow.

  2. For the cherry pick, please exclude the files in the following directories: docs/perps, evidence and scripts/perps/agentic. These are valuable, but should be introduced into main separately, rather than as a cherry pick to an OTA update (per release guidelines)

…ance + populated availableToTradeBalance

Five existing tests asserted pre-fix behavior:
- HyperLiquidProvider.test getAccountState expected 20500 (perps 10500
  + spot.total 10000, double-counting spot.hold 1000). New correct value
  is 19500 after the fix subtracts spot.hold from the sum.
- Same for the HIP-3 partial-account-state test.
- accountUtils.test 'ignores non-collateral', 'excludes USDH-only', and
  the zero-spot path all compared with .toBe(accountState) (identity).
  Since addSpotBalanceToAccountState now always populates
  availableToTradeBalance (defaulting to availableBalance when no
  collateral is held) the return is a new object; asserting field
  equivalence instead.
Applies fs-recipe-quality review:
- Screenshots at each validation milestone so a cold reviewer can eyeball
  the evidence without re-running (phase-standard-summary, unified-order-form,
  limit-cycle-0-baseline / 1-hold-applied / 2-hold-released).
- Teardown now asserts ok:true (was silently swallowing errors).
- Setup + teardown both close any leftover positions before mode flips,
  not just orders. Abstraction switch rejects if positions OR orders OR
  TWAPs are open; previous runs were getting blocked on stale positions.
- Manual-vs-hl-web gate removed: the runner skips it silently under
  --skip-manual anyway; replaced with unconditional screenshots that
  live in the artifact folder for async reviewer comparison.
Removing three files that will be shared/reviewed separately, not as
part of this spotState subscription PR:
- docs/perps/tat-3016-clarity.md — incident brief + architectural
  rationale. Lives better as a team doc than in the PR diff.
- evidence/recipe.json — regression recipe. Will be committed through
  a separate agentic-validation track.
- scripts/perps/agentic/teams/perps/pre-conditions.js — reverted to
  main. The availableToTradeBalance fallback in perps.sufficient_balance
  was needed to run the recipe; now that the recipe is out of scope,
  leave main's version untouched.

This PR is now strictly: HL spotState subscription wiring,
AccountState.availableToTradeBalance population, UI consumer rewiring,
and supporting unit tests.

@geositta geositta left a comment

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.

The spot WebSocket direction is a good simplification for the live available to trade problem, and I agree with moving the free collateral signal closer to the source.

Comment thread app/controllers/perps/utils/accountUtils.ts
@github-actions github-actions Bot added size-L and removed size-XL labels Apr 23, 2026
…DK output

The hyperLiquidAdapter test file lives under app/components/UI/Perps/utils
but imports the real adapter at app/controllers/perps/utils. Three
account-state tests used toEqual with a fixed shape and failed on CI
once the adapter started returning availableToTradeBalance. Added the
expected values: withdrawable + free spot for each scenario. totalBalance
is unchanged because all three scenarios have spot.hold = 0 so the
subtraction is a no-op.
@abretonc7s abretonc7s enabled auto-merge April 23, 2026 05:12
@github-actions

Copy link
Copy Markdown
Contributor

AI PR Analysis

🚫 Merge safe: false | 🟠 Risk: high

Merge decision: AI analysis did not complete — manual review required before merging.

AI analysis did not complete. Manual review recommended.

View run

@github-actions

Copy link
Copy Markdown
Contributor

🔍 Smart E2E Test Selection

  • Selected E2E tags: SmokePerps, SmokeWalletPlatform, SmokeConfirmations
  • Selected Performance tags: @PerformancePreps
  • Risk Level: medium
  • AI Confidence: 88%
click to see 🤖 AI reasoning details

E2E Test Selection:
The PR introduces a new availableToTradeBalance field to the Perps AccountState type, representing withdrawable + unreserved spot collateral for order-entry paths. Key changes:

  1. Balance calculation logic (hyperLiquidAdapter.ts): New availableToTradeBalance computation and fixed totalBalance for Unified/PM account modes (subtracts spotHold to avoid double-counting margin).

  2. Real-time spot state subscription (HyperLiquidSubscriptionService.ts): New WebSocket subscription for spot state updates, ensuring balance reflects live data.

  3. UI components updated to use availableToTradeBalance instead of availableBalance for order-entry surfaces:

    • usePerpsOrderForm.ts - order form balance
    • useDefaultPayWithTokenWhenNoPerpsBalance.ts - "pay with token" preselection logic
    • PerpsMarketDetailsView.tsx - funding path determination
    • PerpsMarketBalanceActions.tsx - balance display in order entry
  4. PerpsWithdrawView.tsx: Added AVAILABLE_BALANCE_TEXT testID (withdraw view intentionally keeps availableBalance, not availableToTradeBalance).

SmokePerps: Directly tests Perps Add Funds flow, balance verification, and balance updates - all directly impacted by these changes.

SmokeWalletPlatform: Per tag description, Perps is a section inside the Trending tab; changes to Perps views (PerpsMarketDetailsView, PerpsWithdrawView, PerpsMarketBalanceActions) affect Trending.

SmokeConfirmations: Per SmokePerps tag description, "Add Funds deposits are on-chain transactions" - required when selecting SmokePerps.

Documentation files and unit test files don't require additional E2E coverage beyond what's already selected.

Performance Test Selection:
The HyperLiquidSubscriptionService changes add a new WebSocket subscription for spot state updates that fires on every account subscription. This adds a new async subscription path that could affect perps market loading and balance display performance. @PerformancePreps covers perps market loading, position management, add funds flow, and order execution - all areas touched by these changes.

View GitHub Actions results

@github-actions

Copy link
Copy Markdown
Contributor

E2E Fixture Validation — Schema is up to date
12 value mismatches detected (expected — fixture represents an existing user).
View details

@sonarqubecloud

Copy link
Copy Markdown

@abretonc7s abretonc7s dismissed gambinish’s stale review April 23, 2026 05:38

cleaned up as requested.

@abretonc7s abretonc7s added this pull request to the merge queue Apr 23, 2026
Merged via the queue into main with commit 756b701 Apr 23, 2026
117 of 118 checks passed
@abretonc7s abretonc7s deleted the perps/hl-spotstate-live-balance branch April 23, 2026 05:59
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 23, 2026
@metamaskbotv2 metamaskbotv2 Bot added the release-7.75.0 Issue or pull request that will be included in release 7.75.0 label Apr 23, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

release-7.75.0 Issue or pull request that will be included in release 7.75.0 risk:high AI analysis: high risk size-L team-perps Perps team type-bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants