Skip to content

fix(perps): complete spot-balance parity cp-7.72.2#29110

Merged
aganglada merged 14 commits into
mainfrom
perps-spot-full-fix
Apr 21, 2026
Merged

fix(perps): complete spot-balance parity cp-7.72.2#29110
aganglada merged 14 commits into
mainfrom
perps-spot-full-fix

Conversation

@abretonc7s

@abretonc7s abretonc7s commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Description

Full fix for TAT-3016 — [PROD INCIDENT] MetaMask UI shows $0 balance for accounts with spot + perps funds on HyperLiquid. Builds on Matt's stream fix (#29089) and adds the two missing pieces uncovered during investigation.

What was broken

For HyperLiquid accounts that hold collateral as spot USDC (non-zero spotClearinghouseState.balances.USDC) but zero perps clearinghouse balance (clearinghouseState.withdrawable == 0, marginSummary.accountValue == 0), three independent code paths were under-reporting the balance:

Path Pre-fix totalBalance Pre-fix availableBalance Why
Streamed (HyperLiquidSubscriptionService webData2 + clearinghouseState callbacks) 0 0 adaptAccountStateFromSDK(data.clearinghouseState, undefined) never fetched/attached spotClearinghouseState
Standalone fetch (PerpsController.getAccountState({standalone: true})) 0 0 Same omission pattern in a separate provider path (HyperLiquidProvider.ts:5545-5573)
Full fetch (PerpsController.getAccountState()) correct (101.13…) 0 Only path that correctly queried both clearinghouse + spot

Why it surfaced now

Two independent changes in the week leading up to the incident made a long-standing omission visible at scale:

  1. feat(perps): disk-backed cold-start cache for instant data display (#27898, merged 2026-04-11) changed usePerpsLiveAccount to seed first render from the in-memory stream snapshot (streamManager.account.getSnapshot()) before falling back to the preloaded disk cache. The stream snapshot has always been spot-less because HyperLiquidSubscriptionService.ts:1406 and :1604 have passed spotState=undefined to adaptAccountStateFromSDK since December 2025 / February 2026 (git blame). Flipping the trust order from disk cache → live stream exposed the pre-existing zero on first paint.
  2. HyperLiquid Portfolio Margin alpha shipped on the 2026-04-18 network upgrade. PM pushes more users to hold collateral as spot USDC rather than transferring into perps clearinghouse, expanding the population hitting the spot-only account shape.

Neither change is the root cause. The fix is on the MetaMask side: the streamed and standalone account paths must read spotClearinghouseState alongside clearinghouseState and include spot balance in totalBalance for parity with the full-fetch path.

What this PR does

  • Spot-inclusive balance across all three account-state paths. Streamed, standalone, and full-fetch paths now fold spotClearinghouseState.balances into AccountState.totalBalance via the shared addSpotBalanceToAccountState helper. Only collateral-eligible coins contribute (SPOT_COLLATERAL_COINS = {USDC, USDH}) — non-collateral spot assets (HYPE, PURR, …) are excluded so they don't mis-gate the CTA for users who can't actually trade them.
  • USDH handled for HIP-3 USDH DEXs. The codebase already models USDH as auto-collateral (HyperLiquidProvider.#isUsdhCollateralDex / #getSpotUsdhBalance); including USDH in the allowlist keeps USDH-only HIP-3 users from hitting the same $0 regression.
  • Add Funds CTA gates on totalBalance. PerpsMarketDetailsView.showAddFundsCTA now checks totalBalance < threshold && defaultPayToken === null. "User has any money in the perps ecosystem → hide Add Funds." Also fixes the pre-existing edge case where funds locked in an open position incorrectly prompted Add Funds.
  • Order-form preselect keeps availableBalance. useDefaultPayWithTokenWhenNoPerpsBalance gates on withdrawable so spot-funded / margin-locked accounts still get an external pay token preselected in PerpsOrderView. CTA correctness is preserved by the component-level totalBalance guard.
  • Race-free spot state cache. #spotStateGeneration token + #spotStatePromiseUserAddress tracker in HyperLiquidSubscriptionService. #ensureSpotState only shares in-flight promises when the user matches; #refreshSpotState discards result + error if generation was bumped post-await; cleanUp / clearAll bump generation and null promise refs. Prevents user-A's spot fetch from re-populating the cache after a switch to user B.
  • Cold-start SDK init. #refreshSpotState now awaits ensureSubscriptionClient before getInfoClient() (which throws on fresh instances) so the first subscribeToAccount on a cold service gets the spot-adjusted snapshot instead of perps-only until resubscribe.
  • NaN guard in addSpotBalanceToAccountState keeps FallbackDataDisplay sentinels intact when upstream totalBalance is non-numeric.

What this PR deliberately does NOT change

  • Order-form slider and order-placement warnings (usePerpsOrderForm.ts, PerpsOrderView.tsx) keep reading availableBalance. Those surfaces need immediately-spendable withdrawable. On standard-margin (non-Unified/non-PM) HyperLiquid accounts, spot USDC is not directly usable as perps margin — users must transfer spot → perps clearinghouse first. Showing a max order size that HyperLiquid would reject at submit would be worse UX than the current behaviour. This is HL's model for standard accounts and outside the scope of the $0 balance incident.
  • No new fields on AccountState. Considered adding availableToTradeBalance (see #29090) or spotUsdcBalance (see #29092); both leak HL primitives into the shared protocol-agnostic contract and will need reshaping once Portfolio Margin graduates from pre-alpha. Reusing existing totalBalance for the CTA gate solves the incident with zero contract changes.
  • Portfolio Margin buying-power. PM pre-alpha uses HYPE-as-collateral with LTV-based borrow (token_balance * oracle_price * ltv, LTV 0.5 for HYPE, borrow_cap(USDC) = 1000 per user). Correct PM buying-power math needs live oracle prices, LTV queries, and account-mode detection — deferred until PM graduates and the API stabilises. The spot USDC/USDH fix here still handles PM users who happen to hold spot collateral.
  • Account-mode UI surface (standard / Unified / PM). Valuable UX signal, but independent of the balance math — tracked as a separate follow-up. The fix on this PR is correct whether or not we surface mode in the UI.
  • Core-side companion. Matt's core PR #8533 covers the stream fix. The standalone-path fix on this PR needs a 1-liner mirror in @metamask/perps-controller before mobile syncs that package — flagging as follow-up.

Changelog

CHANGELOG entry: Fixed Perps $0 balance display for accounts funded via HyperLiquid spot USDC

Related issues

Fixes: TAT-3016

Supersedes: #29090, #29092 (both introduce a new AccountState field; this PR achieves the same user-visible outcome via totalBalance without a contract change)

Manual testing steps

Feature: Perps balance visibility for spot-funded accounts

  Background:
    Given the user holds spot USDC on HyperLiquid mainnet
    And the user's HyperLiquid perps clearinghouse balance (withdrawable, marginSummary.accountValue) is 0
    And the user is on MetaMask mobile with Perps enabled

  Scenario: Header reflects spot-backed collateral
    When user navigates to the Perps tab
    Then the Perps header shows the spot USDC balance (e.g. $101.14), not $0
    And "$0.00 available" is shown as the subtitle (correctly reflecting withdrawable)

  Scenario: Market detail CTA respects total balance
    Given user is on the Perps tab with the spot-only account
    When user opens the BTC market detail view
    Then the Long and Short action buttons are visible
    And the "Add Funds" CTA is not shown

  Scenario: Standalone account-state fetch
    Given a developer queries Engine.context.PerpsController.getAccountState({ standalone: true, userAddress })
    Then totalBalance matches the full-fetch path and includes the spot USDC balance

Agentic recipe: evidence/recipe.json (also in this branch) replays the scenario via CDP and captures the stream / full-fetch / standalone values plus screenshots. Run:

bash scripts/perps/agentic/validate-recipe.sh evidence --no-hud --skip-manual

Expected captures (after fix): {stream,fetch,standalone}_totalBalance = "101.13506928" for the 0x316BDE155acd07609872a56Bc32CcfB0B13201fA Trading fixture; CTA state {addFundsVisible:false, longButtonVisible:true, shortButtonVisible:true}.

Screenshots/Recordings

Recipe: evidence/recipe.json on this branch — captures the 3 balance paths, screenshots PerpsHome + PerpsMarketDetails, and probes CTA testIDs on every run.

Before (pre-fix main) After (this PR)
Perps tab header
Shows $0 — the streamed value (spot-less). PerpsHome renders the PerpsEmptyBalance placeholder instead of Withdraw + Add Funds action buttons.
Perps tab header

$101.14 balance + "$0.00 available" subtitle + Withdraw / Add Funds row (non-empty funded-state UI)
PerpsMarketDetails (BTC)
Shows "Add Funds" CTA instead of Long / Short buttons. Trade path blocked for spot-only accounts.
PerpsMarketDetails (BTC)

Long + Short buttons, no "Add Funds" CTA

Visual before-fix screenshot was blocked by intermittent iOS Simulator crashes during this session (unrelated Apple libsystem_sim_platform issue). Trace-level evidence from the unfixed code stands:

// Streamed (HyperLiquidSubscriptionService #cachedAccount replayed via fresh subscribeToAccount listener)
{ "availableBalance": "0", "totalBalance": "0", ... }

// Standalone fetch: getAccountState({ standalone: true, userAddress: '0x316B...' })
{ "availableBalance": "0", "totalBalance": "0", ... }

// Full fetch: getAccountState() — the only path that was correct pre-fix
{ "availableBalance": "0", "totalBalance": "101.13506928", ... }

Three paths disagreed on the same account at the same moment. Matt's [PerpsDiag][ImportedAccount] Sentry trace from prod confirms the same spot-less streamed payload shape for multiple users hitting TAT-3016.

After-fix trace.json captures (from evidence/recipe.json run on commit 7f0e9def6f):

stream:     totalBalance = "101.13506928", availableBalance = "0"
fetch:      totalBalance = "101.13506928", availableBalance = "0"
standalone: totalBalance = "101.13506928", availableBalance = "0"
CTA probe:  addFundsVisible = false, longButtonVisible = true, shortButtonVisible = true

All three balance paths now agree; CTA probe confirms Long + Short visible, Add Funds hidden on the BTC market detail view.

Pre-merge author checklist

Performance checks (if applicable)

  • I've tested on Android
    • Ideally on a mid-range device; emulator is acceptable
  • I've tested with a power user scenario
    • Use these power-user SRPs to import wallets with many accounts and tokens
  • I've instrumented key operations with Sentry traces for production performance metrics

For performance guidelines and tooling, see the Performance Guide.

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.

Note

Medium Risk
Touches core perps balance reporting and adds new async spot-state caching logic, so regressions could impact displayed totals and streaming updates across accounts/DEXs. Changes are localized and covered by targeted unit tests, but still affect user-visible funded-state gating.

Overview
Fixes HyperLiquid spot-funded accounts showing a $0 perps balance by folding eligible spot collateral (USDC only) into AccountState.totalBalance across full fetch, standalone fetch, and WebSocket-streamed account updates via new getSpotBalance/addSpotBalanceToAccountState helpers.

Updates HyperLiquidSubscriptionService to fetch/cache spotClearinghouseState (with generation-based anti-stale guards) and apply spot-adjusted totals for both multi-DEX aggregation and single-DEX webData2 updates; HyperLiquidProvider’s standalone getAccountState path now also fetches spot state and applies the same adjustment.

Adjusts PerpsMarketDetailsView funding CTA logic to key off “has direct order funding path” (spendable balance above threshold or pay-with-token preselect available), adds coverage for the “total funded but not spendable/no direct order path” case, and updates a perps market list page-object selector to tap rows by test id instead of text.

Reviewed by Cursor Bugbot for commit 385c39c. Bugbot is set up for automated code reviews on this repo. Configure here.

geositta and others added 3 commits April 20, 2026 22:41
Mirrors the stream + full-fetch fix in the standalone path at
HyperLiquidProvider.getAccountState({ standalone: true }). Previously
this path queried only clearinghouseState and skipped spotClearinghouseState,
so spot-funded accounts returned totalBalance: 0 from standalone fetches
even after the stream fix landed.

Query spotClearinghouseState in parallel with standalone clearinghouse
states and reuse the addSpotBalanceToAccountState helper so streamed,
full-fetch, and standalone paths all report the same totalBalance.
PerpsMarketDetailsView and useDefaultPayWithTokenWhenNoPerpsBalance were
gating the Add Funds CTA on AccountState.availableBalance, which maps to
HyperLiquid withdrawable. Accounts funded via spot USDC have
withdrawable = 0 even when they hold collateral in spot, so they saw the
Add Funds CTA instead of Long/Short even after the upstream stream fix
made totalBalance spot-inclusive.

Switch the CTA gate to totalBalance. Semantic match: "user has any money
in the perps ecosystem" → hide Add Funds. Also fixes a pre-existing edge
case where accounts with all funds locked in an open position (marginUsed
high, withdrawable = 0) incorrectly prompted Add Funds.

Order-form slider and order-placement warnings intentionally keep
availableBalance since those need immediately-spendable withdrawable for
standard-margin accounts — spot-backed margin requires a transfer to
perps clearinghouse first on non-PM accounts.
@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.

@metamaskbotv2 metamaskbotv2 Bot added the team-perps Perps team label Apr 21, 2026
@abretonc7s abretonc7s marked this pull request as ready for review April 21, 2026 12:21
@abretonc7s abretonc7s requested a review from a team as a code owner April 21, 2026 12:21
Comment thread app/controllers/perps/utils/accountUtils.ts
Comment thread app/controllers/perps/services/HyperLiquidSubscriptionService.ts Outdated
- accountUtils.addSpotBalanceToAccountState: guard against non-finite
  totalBalance sentinel ("--") to avoid producing "NaN" when the
  standalone path aggregates an empty perps-DEX result alongside a
  successful spot query.
- HyperLiquidSubscriptionService.subscribeToAccount: drop the
  #cachedSpotState condition on the immediate cache push — new
  subscribers were starved indefinitely if the spot fetch had never
  resolved. Stale-but-present data is strictly better; the next
  aggregation after spot resolves pushes the spot-inclusive value.
@github-actions github-actions Bot added the risk-medium Moderate testing recommended · Possible bug introduction risk label Apr 21, 2026
Comment thread app/controllers/perps/services/HyperLiquidSubscriptionService.ts
Bind spot-state fetches to a generation token and user address so an
in-flight fetch for account A cannot re-populate the cache after a
switch to B. cleanUp and clearAll bump the generation and drop the
promise tracker; refreshSpotState discards its result and error when
the generation moved on, preventing cross-account contamination in
aggregateAndNotifySubscribers.
useDefaultPayWithTokenWhenNoPerpsBalance is shared by the market-details
Add Funds CTA and by useInitPerpsPaymentToken in PerpsOrderView. Gating
on totalBalance broke order-entry for accounts with availableBalance=0
but totalBalance>0 (spot-funded, or margin locked in a position) — they
were sent into the trade flow with Perps balance preselected and zero
usable amount.

Restore the availableBalance check inside the hook. The CTA correctness
is preserved by the component-level totalBalance guard in
PerpsMarketDetailsView (showAddFundsCTA combines totalBalance<threshold
AND defaultPayToken===null), which already hides the CTA for spot-funded
accounts regardless of what the hook returns.
@abretonc7s abretonc7s changed the title fix(perps): complete spot-balance parity for TAT-3016 fix(perps): complete spot-balance parity Apr 21, 2026
P1: #refreshSpotState now awaits ensureSubscriptionClient before
calling getInfoClient(). On a fresh service instance subscribeToAccount
fires the spot fetch before any webData3 path had a chance to init the
SDK — getInfoClient() would throw and the error swallower left the
first subscriber reporting a perps-only totalBalance until resubscribe.

P2: getSpotBalance now filters to USDC only (SPOT_COLLATERAL_COINS).
Hyperliquid only accepts USDC as perps margin; summing HYPE, PURR, or
any other spot asset into totalBalance would hide the Add Funds CTA
for users whose only spot holdings cannot back a perps trade.

Covered by updated accountUtils + standalone HyperLiquidProvider tests.
@github-actions github-actions Bot added risk-medium Moderate testing recommended · Possible bug introduction risk and removed risk-medium Moderate testing recommended · Possible bug introduction risk labels Apr 21, 2026
HIP-3 USDH DEXs pull collateral from spot USDH automatically
(see HyperLiquidProvider.#isUsdhCollateralDex / #getSpotUsdhBalance).
Restricting SPOT_COLLATERAL_COINS to USDC left USDH-collateralized
accounts with totalBalance = 0, reproducing the same header / CTA
regression the PR is meant to fix — for the HIP-3 cohort.

Adds USDH to the allowlist and covers it with unit tests.
@github-actions github-actions Bot added risk-medium Moderate testing recommended · Possible bug introduction risk and removed risk-medium Moderate testing recommended · Possible bug introduction risk labels Apr 21, 2026
Comment thread app/controllers/perps/utils/accountUtils.ts
USDH_CONFIG.TokenName was undefined at module load when
HyperLiquidWalletService imported accountUtils, due to the init order
between hyperLiquidConfig and the wallet service's own dependency
graph. That crashed the whole test suite at accountUtils.ts:102.

Use the string literal 'USDH' directly — same value, no import cycle.
@github-actions github-actions Bot added risk-medium Moderate testing recommended · Possible bug introduction risk and removed risk-medium Moderate testing recommended · Possible bug introduction risk labels Apr 21, 2026
Comment thread app/controllers/perps/services/HyperLiquidSubscriptionService.ts
…e flicker

The webData2 callback (single-DEX / non-HIP3 path) was storing perps-only
account state directly, bypassing addSpotBalanceToAccountState. This caused
totalBalance to flicker between the correct spot-inclusive value and $0 on
every WebSocket tick for single-DEX users.
@github-actions github-actions Bot added size-L and removed size-M risk-medium Moderate testing recommended · Possible bug introduction risk labels Apr 21, 2026
@github-actions github-actions Bot added the risk-medium Moderate testing recommended · Possible bug introduction risk label Apr 21, 2026
@aganglada aganglada changed the title fix(perps): complete spot-balance parity fix(perps): complete spot-balance parity cp-7.72.2 Apr 21, 2026
@github-actions github-actions Bot added risk-medium Moderate testing recommended · Possible bug introduction risk and removed risk-medium Moderate testing recommended · Possible bug introduction risk labels Apr 21, 2026

@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 2 potential issues.

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 e061239. Configure here.

// Today the in-app HyperLiquid market surface is USDC-collateralized only,
// so USDH must not inflate the shared funded-state path that hides Add Funds.
// Non-stablecoin spot assets (HYPE, PURR, …) also remain excluded.
const SPOT_COLLATERAL_COINS = new Set<string>(['USDC']);

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.

USDH excluded from spot collateral despite PR intent

Medium Severity

SPOT_COLLATERAL_COINS only contains 'USDC', but the PR description explicitly states it is {USDC, USDH} and that "including USDH in the allowlist keeps USDH-only HIP-3 users from hitting the same $0 regression." The old full-fetch path summed ALL spot balances (including USDH); the new addSpotBalanceToAccountState helper excludes USDH, creating a regression for USDH-collateral DEX users who hold only USDH in their spot wallet. The codebase already models USDH as auto-collateral via #isUsdhCollateralDex / #getSpotUsdhBalance.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e061239. Configure here.

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.

False positive for the current product as confirmed with MSO.


if (this.#dexAccountCache.size > 0) {
this.#aggregateAndNotifySubscribers();
}

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.

Spot refresh skips notification for single-DEX users

Medium Severity

In #refreshSpotState, the guard if (this.#dexAccountCache.size > 0) before calling #aggregateAndNotifySubscribers() is never true for single-DEX (webData2) users because webData2 updates #cachedAccount directly without populating #dexAccountCache. When the spot fetch completes for these users, subscribers are never notified — the spot-inclusive balance only appears on the next WebSocket tick, causing an unnecessary delay or flicker from $0 to the correct balance.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e061239. Configure here.

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.

In the single-DEX webData2 path we do populate #dexAccountCache before notifying:

  • app/controllers/perps/services/HyperLiquidSubscriptionService.ts:1520

So when #refreshSpotState completes, the existing if (this.#dexAccountCache.size > 0) path does run for single-DEX users:

  • app/controllers/perps/services/HyperLiquidSubscriptionService.ts:1087

That flows into #aggregateAndNotifySubscribers(), which re-applies spot balance and notifies on hash change. We also already have a focused regression test for this webData2 behavior:

  • app/controllers/perps/services/HyperLiquidSubscriptionService.test.ts:3755

So the reported root cause is false positive

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 68.42105% with 24 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.21%. Comparing base (64f79bb) to head (e061239).
⚠️ Report is 23 commits behind head on main.

Files with missing lines Patch % Lines
...s/perps/services/HyperLiquidSubscriptionService.ts 66.03% 10 Missing and 8 partials ⚠️
app/controllers/perps/utils/accountUtils.ts 73.33% 1 Missing and 3 partials ⚠️
...controllers/perps/providers/HyperLiquidProvider.ts 66.66% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #29110      +/-   ##
==========================================
+ Coverage   82.19%   82.21%   +0.02%     
==========================================
  Files        5069     5079      +10     
  Lines      133674   133962     +288     
  Branches    29969    30049      +80     
==========================================
+ Hits       109870   110134     +264     
- Misses      16334    16345      +11     
- Partials     7470     7483      +13     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@javiergarciavera javiergarciavera requested a review from a team as a code owner April 21, 2026 18:29
@github-actions github-actions Bot added risk-medium Moderate testing recommended · Possible bug introduction risk and removed risk-medium Moderate testing recommended · Possible bug introduction risk labels Apr 21, 2026
@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: 90%
click to see 🤖 AI reasoning details

E2E Test Selection:
All 10 changed files are scoped to the Perps feature area:

  1. tests/page-objects/Perps/PerpsMarketListView.ts (CRITICAL): Changed selectMarket() from text-based to ID-based element selection. This directly affects how E2E Perps tests interact with the market list - requires running SmokePerps to validate the page object still works correctly.

  2. PerpsMarketDetailsView.tsx: Logic refactor for when Add Funds CTA vs Long/Short buttons are shown. The showAddFundsCTA variable was replaced with hasDirectOrderFundingPath with inverted semantics, changing UI behavior for spot-funded accounts. This needs E2E validation.

  3. HyperLiquidSubscriptionService.ts: Significant addition of spot balance caching (#cachedSpotState, #ensureSpotState, #refreshSpotState) to ensure spot-inclusive totalBalance is reported consistently. This affects account balance display in Perps - critical for balance verification in E2E tests.

  4. HyperLiquidProvider.ts: Refactored standalone path to fetch spot state in parallel and use new addSpotBalanceToAccountState utility.

  5. accountUtils.ts: New getSpotBalance() and addSpotBalanceToAccountState() utilities with USDC-only collateral filtering (USDH and non-stablecoin assets excluded).

  6. useDefaultPayWithTokenWhenNoPerpsBalance.ts: Comment clarification only.

  7. Test files: Unit tests for all the above.

Tag selection rationale:

  • SmokePerps: Primary tag - all changes are in Perps feature area, page object change requires validation
  • SmokeWalletPlatform: Required per SmokePerps description: "Perps is also a section inside the Trending tab (SmokeWalletPlatform); changes to Perps views affect Trending. When selecting SmokePerps, also select SmokeWalletPlatform"
  • SmokeConfirmations: Required per SmokePerps description: "When selecting SmokePerps, also select SmokeConfirmations (Add Funds deposits are on-chain transactions)"

No other feature areas are impacted - changes are fully contained within Perps controllers, services, UI components, and test infrastructure.

Performance Test Selection:
The HyperLiquidSubscriptionService changes introduce async spot state fetching with caching logic (#ensureSpotState, #refreshSpotState, generation-based cache invalidation). This adds a new async operation on every subscribeToAccount call and could impact Perps performance - specifically balance loading times, subscription responsiveness, and the add funds flow. @PerformancePreps covers 'perps market loading, position management, add funds flow, and order execution' which directly aligns with these changes.

View GitHub Actions results

@aganglada aganglada enabled auto-merge April 21, 2026 18:37
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
77.8% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@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

@aganglada aganglada added this pull request to the merge queue Apr 21, 2026
Merged via the queue into main with commit 3e535e6 Apr 21, 2026
106 of 107 checks passed
@aganglada aganglada deleted the perps-spot-full-fix branch April 21, 2026 19:29
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 21, 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 21, 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-medium Moderate testing recommended · Possible bug introduction risk size-L skip-sonar-cloud Only used for bypassing sonar cloud when failures are not relevant to the changes. team-perps Perps team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants