Skip to content

refactor(perps): split AccountState balances + mode-aware spot fold#29303

Merged
abretonc7s merged 51 commits into
mainfrom
refactor/tat-3047-balance-contract-reshape
May 4, 2026
Merged

refactor(perps): split AccountState balances + mode-aware spot fold#29303
abretonc7s merged 51 commits into
mainfrom
refactor/tat-3047-balance-contract-reshape

Conversation

@abretonc7s

@abretonc7s abretonc7s commented Apr 24, 2026

Copy link
Copy Markdown
Contributor

Description

Two outcomes:

  1. API surface — AccountState balances are purpose-built and self-documenting. Three fields with JSDoc-documented per-provider semantics replace the overloaded availableBalance / availableToTradeBalance pair:

    • totalBalance — venue equity (display only)
    • spendableBalance — max collateral for a new position (order entry)
    • withdrawableBalance — max reachable to the user's external wallet (withdraw)
  2. Full coverage of HyperLiquid abstraction-mode combinations. The provider, aggregation, and UI now read the right field for the right surface across every Unified / Standard / Portfolio Margin topology, validated end-to-end on mainnet against four fixture accounts and a live Unified ↔ Standard ↔ Unified flip.

User-visible fix: Unified-mode accounts whose collateral sits entirely in spot USDC now show the correct withdraw max (matches app.hyperliquid.xyz "Withdrawable") and submission proceeds. HL's Unified abstraction draws withdraw3 from the unified balance directly — no client-side sweep.

Mode-aware spot fold

accountUtils.addSpotBalanceToAccountState takes an { foldIntoCollateral: boolean } option. Provider owns the decision:

  • hyperliquid-types.ts hosts HyperLiquidAbstractionMode (alias of HL SDK's UserAbstractionResponse) and hyperLiquidModeFoldsSpot(mode)true for Unified / Portfolio / default (and unknown-mode where the majority sits), false for Standard / DEX-abstraction.
  • HyperLiquidProvider.getAccountState fetches userAbstraction alongside clearinghouse + spot state.
  • HyperLiquidSubscriptionService caches mode keyed by lowercase address, refreshes on spot WS ticks via ABSTRACTION_MODE_REFRESH_THROTTLE_MS = 60_000, and re-aggregates subscribers when the fold side flips.

Per-provider mapping

Provider totalBalance spendableBalance withdrawableBalance
HL Unified / Portfolio accountValue + spot.total − spot.hold withdrawable + freeSpotUSDC withdrawable + freeSpotUSDC
HL Standard accountValue + spot.total − spot.hold (display) withdrawable (perps-only) withdrawable (perps-only)
MYX walletBalance + marginUsed + unrealizedPnl walletBalance walletBalance

Upgrade safety

  • Migration 133 (12 cases) maps legacy persisted accountState:
    • spendableBalanceavailableToTradeBalance ?? availableBalance
    • withdrawableBalanceavailableBalance (perps-only, safe for Standard until first fresh fetch)
    • subAccountBreakdown entries migrate per-DEX
  • Disk-cache key bump PERPS_DISK_CACHE_USER_DATA_V2 prevents legacy-shape hydration. One cold start falls through to skeleton; WS backfills.

Scope

  • Type reshape + JSDoc in app/controllers/perps/types
  • HL adapter, accountUtils, subscription service aggregation
  • HL provider withdraw / margin / #getBalanceForDex field references
  • MYX adapter
  • State migration + disk-cache key bump
  • Mode detection + cache + throttled WS refresh + notify-on-fold-change
  • 30+ consumer files migrated by intent
  • ~40 test fixtures + mocks updated
  • 4 reusable agentic flows + multi-phase recipe with live Standard-mode flip evidence

Validation matrix + live captured values: docs/perps/perps-account-abstraction-and-balance-contract.md.

Not in this PR (deliberate)

  • @metamask/perps-controller core mirror — needs separate release coordination
  • Account-mode detection surface in UI (Unified / Standard / PM label)
  • HYPE-as-collateral / USDhl — not live

Changelog

CHANGELOG entry: Fixed HyperLiquid withdraw showing $0 max on Unified-mode accounts whose collateral sits in spot USDC; prevented over-approval of withdraws and orders on Standard-mode accounts where spot is a separate ledger.

Related issues

Fixes: TAT-3047

Manual testing steps

Feature: HL Unified-mode withdraw with spot-only collateral

  Scenario: Withdraw max on spot-funded Unified account
    Given the Trading fixture (0x316B…01fA) is connected in Unified Mode
    And perps.withdrawable = $0 and spot USDC > $0
    And the Perps withdraw screen is open

    Then the "Available" row shows withdrawableBalance (≈ spot USDC), not $0
    And the Max button enables

    When the user submits a withdraw for $1.01
    Then withdraw3 is called directly
    And HL draws from unified balance (spot USDC decreases by amount + fee)
    And the withdraw succeeds

  Scenario: Standard-mode correctness
    Given dev2 flipped from Unified to Standard via userSetAbstraction
    And the account has spot USDC but $0 perps withdrawable
    Then spendableBalance === withdrawableBalance === $0 (perps-only, no spot fold)
    And PerpsMarketBalanceActions shows the Add Funds CTA
    And withdraw / order-entry validation cannot approve an amount HL will reject

  Scenario: Live HL-web mode flip
    Given the user flips Unified ↔ Standard on HL web while the mobile app stays open
    When the spot WS tick arrives (within ~60s throttle window)
    Then mobile refreshes userAbstraction via REST
    And balance-dependent UI re-renders with the new fold semantics

  Scenario: Upgrade path (migration 133 + disk-cache V2 bump)
    Given an existing install with persisted accountState carrying availableBalance / availableToTradeBalance
    When the app is upgraded and state is rehydrated
    Then accountState.spendableBalance and accountState.withdrawableBalance are populated
    And PERPS_DISK_CACHE_USER_DATA_V2 is empty on first run, skeleton renders
    And the first WS tick backfills the new-shape cache

See docs/perps/perps-account-abstraction-and-balance-contract.md for the full matrix + live-run evidence.

Recipe used to validate this branch (multi-phase, mainnet)

The recipe orchestrates four composable flows (hl-balance-contract-check, hl-balance-math-check, hl-empty-state-check, hl-standard-mode-fold-check) plus hl-provision-fixture across four fixture accounts (Trading / dev1 / dev2 / Account 6) and exercises a live Unified ↔ Standard ↔ Unified mode flip on dev2.

Recipe: scripts/perps/agentic/teams/perps/recipes/hl-balance-contract.json

Run locally:

bash scripts/perps/agentic/validate-recipe.sh scripts/perps/agentic/teams/perps/recipes/hl-balance-contract

Screenshots/Recordings

Before

Unified Mode + spot-only USDC → withdraw max shows $0, submit disabled.

After

Unified Mode + spot-only USDC → withdraw max shows combined balance; submit proceeds via HL's Unified abstraction (verified via direct withdraw3 probe on Trading fixture: $1.01 withdrawn, spot USDC decreased by $1.01, perps unchanged).

Pre-merge author checklist

  • I've followed MetaMask Contributor Docs and MetaMask Mobile Coding Standards.
  • I've completed the PR template to the best of my ability
  • I've included tests if applicable — 12 migration cases, accountUtils fold-option coverage, mode-classifier full-enumeration
  • I've documented my code using JSDoc format (per-field contract on AccountState; per-helper on mode + fold util)
  • I've applied the right labels on the PR

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.

Note

Medium Risk
Medium risk because it changes which balance fields drive order validation, withdraw limits, empty-state gating, and deposit/withdraw progress completion; incorrect mapping could block trades/withdraws or mislead users about available funds.

Overview
Replaces the overloaded perps balance fields across the app by migrating consumers from availableBalance/availableToTradeBalance to the new AccountState contract: spendableBalance (order entry/margin add/Pay-with) and withdrawableBalance (withdraw + confirmations). This updates core UI surfaces (PerpsOrderView, PerpsMarketDetailsView, PerpsWithdrawView, balance tooltips/rows) and related hooks/utilities (usePerpsOrderForm, usePerpsOrderValidation, useWithdrawValidation, useDefaultPayWithTokenWhenNoPerpsBalance, token filter/payment tokens).

Tightens state/progress logic around balances: deposit completion/toasts and deposit-progress now key off spendableBalance (avoiding false clears from totalBalance PnL moves), withdraw-progress keys off withdrawableBalance, and the home balance component’s “empty state” now gates on totalBalance (treating non-finite sentinel values as empty) while displaying spendableBalance. Cache safety: bumps the disk user-data cache key to PERPS_DISK_CACHE_USER_DATA_V2 and introduces ABSTRACTION_MODE_REFRESH_THROTTLE_MS.

Updates extensive unit/e2e tests and mocks to the new fields and adds targeted edge-case coverage (e.g., funded users with spendableBalance=0 due to locked margin, and deposits where only totalBalance increases via unrealized PnL).

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

…o-sweep spot→perps on withdraw

AccountState now carries three purpose-built fields:
- totalBalance     — venue equity (display only)
- spendableBalance — max collateral for a new position (order-entry)
- withdrawableBalance — max reachable to user's wallet (withdraw)

Previously `availableBalance` was overloaded between order-entry and withdraw
paths, and the `availableToTradeBalance` hotfix (TAT-3016) carried an optional
fallback that every consumer had to re-derive. This commit retires both,
pushing per-intent semantics into the type with JSDoc per field.

Per-provider mapping is provider-internal:
- HL Unified:  withdrawable + freeSpotUSDC (both new fields fold spot)
- HL Standard: withdrawable (spot.hold = 0, math collapses)
- MYX:         walletBalance (spendable === withdrawable)

HyperLiquidProvider.withdraw() now auto-sweeps spot→perps via
usdClassTransfer when the perps clearinghouse alone can't cover the
requested amount but the user's spendable does. The caller (UI) never
branches on provider — it just reads withdrawableBalance. If the shortfall
exceeds free spot, the withdraw is rejected with a clear error.

Fixes: TAT-3047 — unblocks withdraw for Unified-mode accounts whose
collateral sits entirely in spot USDC (previously showed \$0 max and
refused to submit).

Scope:
- AccountState + SubAccountState types in app/controllers/perps/types
- HL adapter, accountUtils spot-fold, subscription-service aggregation
- HyperLiquidProvider.withdraw() sweep + polling
- MYX adapter
- 30+ consumer files migrated by intent (spendable vs withdrawable vs total)
- Hook params renamed (usePerpsOrderValidation, validateBalance,
  getMaxAllowedAmount)
- Test fixtures + mocks updated across ~40 test files
@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.

…tomic

HL's usdClassTransfer is applied at the validator layer and is visible to
the perps clearinghouse immediately on `status === 'ok'`. The 10s poll on
clearinghouseState was defensive paranoia added before SDK semantics were
verified. Removing it eliminates a failure mode where a delayed `ok`
response would throw a misleading "sweep did not confirm before timeout"
error while the funds had already moved.

After-sweep path is now: transfer → check status → withdraw3.
Leftover synonym from when the field was called availableToTradeBalance.
With the rename, the local alias just adds noise — local now matches the
field name.
- prevAvailableBalanceRef → prevSpendableBalanceRef (deposit hooks)
- prevAvailableBalanceRef → prevWithdrawableBalanceRef (withdraw progress)
- validateBalance JSDoc param description
- UsePerpsOrderValidationParams.spendableBalance JSDoc
- DevLogger label 'Available Perps Balance' → 'Withdrawable Balance'
… new balance fields

Persisted PerpsController.accountState carries the legacy
availableBalance / availableToTradeBalance keys for existing installs.
Without a migration, rehydration on upgrade produces an accountState
that consumers can't read, leaving balance UIs at 0/— until a fresh
fetch lands — which blocks order-entry and withdraw on cold start,
offline use, or slow startup.

Migration 133 maps:
- availableToTradeBalance ?? availableBalance → spendableBalance
- availableToTradeBalance ?? availableBalance → withdrawableBalance
- subAccountBreakdown[*].availableBalance → spendableBalance + withdrawableBalance

The in-repo agentic validation/provisioning flows under
scripts/perps/agentic/teams/perps/ (evals.json, pre-conditions.js,
hl-balance-validation.json, hl-provision-fixture.json) also referenced
the old field names. Renamed in place so the probes read real values
instead of silently seeing undefined/zero.
…, not folded accountState

hl-provision-fixture.json transfer-to-spot/to-perp previously read
accountState.{withdrawable,spendable}Balance when transferAmount='max'.
After the balance reshape those fields fold free spot USDC into both,
so:
- to-spot max would over-request (asks to move spot-inclusive amount
  from perps, but usdClassTransfer only moves perps-side funds)
- to-perp max computed as spendable-withdrawable is always 0, so the
  flow refuses to transfer even when spot USDC is available

Query HL REST directly for raw values:
- to-spot: clearinghouseState.withdrawable (perps clearinghouse only)
- to-perp: spotClearinghouseState USDC {total - hold} (free spot)

Mainnet/testnet URL picked from PerpsController.state.isTestnet.
@abretonc7s abretonc7s marked this pull request as ready for review April 24, 2026 07:20
@abretonc7s abretonc7s requested review from a team as code owners April 24, 2026 07:20
Comment thread app/components/UI/Perps/utils/orderCalculations.test.ts Outdated
@abretonc7s abretonc7s changed the title refactor(perps): TAT-3047 split AccountState balances + auto-sweep spot→perps on withdraw refactor(perps): split AccountState balances + auto-sweep spot→perps on withdraw Apr 24, 2026
… balance contract

Three fallout spots missed in the prior field rename:

1. hyperLiquidAdapter.test.ts asserted the adapter still returns
   availableToTradeBalance. Removed — the field no longer exists.
2. HyperLiquidSubscriptionService.test.ts expected subAccountBreakdown
   entries without withdrawableBalance. Production code emits it now.
3. tests/framework/fixtures/json/default-fixture.json +
   tests/component-view/renderers/perpsViewRenderer.tsx seed
   accountState directly (bypassing redux-persist migration 133), so
   legacy availableBalance would leave consumers reading 0 on any
   fixture-backed run. Migrated the seed shapes to the new fields.
Comment thread app/controllers/perps/providers/HyperLiquidProvider.ts Outdated
…State shape

The cold-start disk cache (PERPS_DISK_CACHE_USER_DATA) stores
AccountState snapshots via buildUserDataPayload and is hydrated into
cachedUserDataByProvider by hydrateFromDiskSync before the first WS
tick. Existing installs have legacy-shape accountState
(availableBalance / availableToTradeBalance) sitting in that cache, so
consumers that read spendable/withdrawableBalance via getPreloadedData
see undefined and render $0 / disabled controls until the first live
fetch lands.

The disk payload has no in-payload version field, so the simplest
no-tech-debt path is to bump the storage key. Upgraded installs see an
empty new cache on first run, fall through to skeleton/fallback, then
backfill from WS as normal. The orphan old-key blob stays on disk
(small, eventually cleaned by any reset/logout flow).
…ble contract

usePerpsAdjustMarginData exposes spendableBalance as the cap for
add-margin. On Unified accounts with spot USDC collateral that value
exceeds what updateIsolatedMargin() can actually draw (clearinghouse
withdrawable only), so amounts between clearinghouse and spendable
become selectable in the UI and then fail at submission.

Mirror the withdraw() sweep: before calling updateIsolatedMargin with
a positive (ADD) amount, if the shortfall vs perps clearinghouse is
non-zero, run usdClassTransfer({ toPerp: true }) for the gap. Symmetric
with withdraw, and keeps the provider honouring the spendableBalance
contract — UI reads one number, provider performs internal moves.

REMOVE direction is unaffected (no sweep).
@abretonc7s abretonc7s added DO-NOT-MERGE Pull requests that should not be merged labels Apr 24, 2026
Comment thread app/controllers/perps/providers/HyperLiquidProvider.ts
Comment thread app/store/migrations/133.ts
…Unified mode

Live probe against the Trading fixture (Unified-mode mainnet) shows HL
rejects usdClassTransfer with 'Action disabled when unified account is
active'. Both sweeps (withdraw + updateMargin) would fail on every
Unified-mode user — the default on app.hyperliquid.xyz and most of our
traffic. That is a worse regression than the original TAT-3047 symptom
(UI showed \$0, didn't submit).

Correct behaviour:
- Unified mode: HL unifies the spot + perps ledger internally. withdraw3
  and updateIsolatedMargin draw from the unified balance directly. No
  sweep needed.
- Standard mode: spot and perps are separate; moving funds requires
  explicit user action on HL web. Our mobile app did not handle this
  pre-refactor and this PR does not introduce it.

This PR therefore reduces to rename-only: the three-field contract
(totalBalance / spendableBalance / withdrawableBalance), the persisted-
state migration, and consumer updates. withdraw() and updateMargin()
bodies revert to their pre-refactor shapes aside from the field name
changes in validation.

Revert-of: 2708b11 (updateMargin sweep), portion of 5f07189 +
a3a1798 (withdraw sweep).
Comment thread app/controllers/perps/providers/HyperLiquidProvider.ts
…AT-3047

Three composable flows under scripts/perps/agentic/teams/perps/flows/:

- hl-balance-contract-check: asserts PerpsController.accountState exposes
  the new three-field contract (spendable / withdrawable / total) with no
  legacy keys. Inputs: address, phaseLabel. Runs against any account.

- hl-balance-math-check: asserts the spot-fold math by using the
  controller's subAccountBreakdown as perps-side truth (covers HIP-3
  multi-DEX) plus spotClearinghouseState REST as spot-side truth.
  Expected: spendable/withdrawable = Σ(breakdown.spendable) + freeSpot;
  total = Σ(breakdown.total) + spot.total − spot.hold; spendable ===
  withdrawable. Tolerates a small rounding epsilon.

- hl-empty-state-check: asserts zero-balance accounts surface the
  PerpsEmptyBalance affordance (Add Funds button) on PerpsMarketListView.

These compose cleanly via \`call\` so future balance-field refactors can
reuse them with different fixture accounts.

Recipe usage (local, .task/-gated): see .task/fix/tat-3047-0424-1139/
VALIDATION.md for the multi-account validation matrix and run output.
Matrix + composable flows + live-run evidence for the balance-contract
reshape. Documents coverage across three fixture accounts (Trading
Unified spot-funded + HIP-3, dev1 spot-only clean, Account 6 zero) and
calls out the Standard-mode follow-up as out of scope.
… live numbers

New composable flow hl-standard-mode-fold-check.json compares the
adapter's actual spendable/withdrawable against Standard-mode semantics
(perps-only, no spot fold) and asserts `observedInflation ≈ freeSpot`
within ε=0.05 — a regression guard that both documents the existing
limitation and fails if a future mode-aware fix unexpectedly removes
it without updating this assertion.

Recipe phase 2c now runs this on dev2 post Unified→Standard flip.
Live measurement at the time of this commit:
  spot.free = $10.01, standardSemanticExpected.spendable = $0,
  adapterActual.spendable = $10.01, observedInflation = $10.01 ≈ freeSpot ✓

VALIDATION.md updated with:
- HL web checkbox → abstraction mode mapping (explicit Unified vs Standard)
- Phase 2c live numbers captured as evidence
- Standard-mode fold limitation called out as pre-existing and out of
  scope for this PR, with the flow serving as its regression guard.
Comment thread docs/perps/perps-account-abstraction-and-balance-contract.md Outdated
Previously spot USDC was folded into spendable/withdrawable unconditionally,
which is correct for HL Unified/Portfolio (spot is unified with perps) but
inflates balances on Standard mode where spot is a separate ledger HL's
backend can't auto-draw from. UI approved submissions HL would reject.

Design — clean separation:
- accountUtils.addSpotBalanceToAccountState takes { foldIntoCollateral }
  option; provider-agnostic utility, no HL-specific imports.
- hyperliquid-types.ts owns HyperLiquidAbstractionMode (alias of HL SDK's
  UserAbstractionResponse) and hyperLiquidModeFoldsSpot(mode) helper.
- HyperLiquidProvider.getAccountState fetches userAbstraction in parallel
  with clearinghouse + spot, computes the flag, passes to the util.
- HyperLiquidSubscriptionService caches the mode alongside #cachedSpotState
  (refreshed together, cleared together on cleanup) and gates both fold
  call sites on the helper.

Migration 133 asymmetric mapping: legacy availableBalance maps to
withdrawableBalance (perps-only pre-refactor value);
availableToTradeBalance ?? availableBalance maps to spendableBalance.
Minimises cold-start inflation window for upgraded Standard-mode users.

Live proof on dev2 flipped to Standard:
  spot.free = \$10.01
  standardSemanticExpected.spendable = 0 (perps-only)
  adapterActual.spendable = 0
  observedInflation = 0 — gate works.

Recipe phase 2c passes hl-balance-math-check with foldIntoCollateral=false
and hl-standard-mode-fold-check asserting standardModeCorrect.

Tests: accountUtils.test.ts gains 2 cases; hyperliquid-types.test.ts is
new and covers all 5 modes plus null/undefined. Recipe 36/36 across
Trading + dev1 + dev2 Unified-to-Standard-to-Unified + Account 6.
… fixtures

- HyperLiquidSubscriptionService: throttle constant moved to perpsConfig,
  the doc-comment that documented it stayed behind above an unrelated field
  declaration. Remove the dangling comment.
- useTransactionCustomAmount tests: three perps-withdraw fixtures only
  seeded withdrawableBalance; pair them with spendableBalance to match the
  peer fixture in the same file.
… validation notes

- Added "schema_version": 1 to the JSON structure for better version control.
- Enhanced validation notes for shared-screenshot-market-list and shared-screenshot-withdraw actions to clarify expected behaviors and outcomes related to balance displays, addressing TAT-3047 fixes.

@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.

@abretonc7s Please see my comment regarding the dead map. I would approve, conditional on that. Since leaving the dead #abstractionModeLastFetchedAtByUser with no readers makes the next person to touch this file second guess whether they are missing a code path.

Comment thread app/controllers/perps/services/HyperLiquidSubscriptionService.ts Outdated
Map was set in two write sites and cleared in cleanUp/clearAll, but
never read. Throttle gate uses #abstractionModeLastWsRefreshAtByUser
exclusively. Removing the dead state so the intent of the remaining
map is unambiguous.
@abretonc7s abretonc7s enabled auto-merge April 29, 2026 09:46
…ance-contract-reshape

# Conflicts:
#	app/components/UI/Perps/Views/PerpsWithdrawView/PerpsWithdrawView.tsx
#	app/components/UI/Perps/hooks/usePerpsBalanceTokenFilter.ts
#	app/components/UI/Perps/hooks/usePerpsPaymentTokens.ts
#	app/components/UI/Perps/hooks/useWithdrawValidation.ts
#	app/components/Views/confirmations/components/perps-confirmations/perps-withdraw-balance/perps-withdraw-balance.tsx
#	app/components/Views/confirmations/hooks/alerts/useInsufficientPerpsBalanceAlert.ts
#	app/components/Views/confirmations/hooks/transactions/useTransactionCustomAmount.ts
#	app/controllers/perps/providers/HyperLiquidProvider.test.ts
#	app/controllers/perps/providers/HyperLiquidProvider.ts
#	app/controllers/perps/services/HyperLiquidSubscriptionService.test.ts
#	app/controllers/perps/services/HyperLiquidSubscriptionService.ts
#	app/controllers/perps/types/hyperliquid-types.test.ts
#	app/controllers/perps/types/hyperliquid-types.ts
#	app/controllers/perps/utils/accountUtils.test.ts
#	app/controllers/perps/utils/accountUtils.ts
@abretonc7s abretonc7s added the team-perps Perps team label May 4, 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 1 potential issue.

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


expect(accountState.availableToTradeBalance).toBe('0');
expect(accountState.spendableBalance).toBe('0');
expect(accountState.withdrawableBalance).toBe('0');

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.

Stale availableBalance in HyperLiquidProvider withdraw test mocks

Medium Severity

Three getAccountState mocks in HyperLiquidProvider.test.ts still return { availableBalance: '5000' } instead of the new { spendableBalance, withdrawableBalance } shape. The actual provider now reads accountState.spendableBalance (e.g., in the margin guard at line 4777) and accountState.withdrawableBalance (in #getBalanceForDex). These mocks will produce NaN / undefined when the provider parses the response, causing the withdraw-related tests to pass vacuously or for the wrong reason.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ece3bdb. Configure here.

@github-actions

github-actions Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

🔍 Smart E2E Test Selection

  • Selected E2E tags: SmokePerps, SmokeWalletPlatform, SmokeConfirmations
  • Selected Performance tags: None (no tests recommended)
  • Risk Level: medium
  • AI Confidence: 90%
click to see 🤖 AI reasoning details

E2E Test Selection:
This PR is a focused refactor of the PerpsController's AccountState balance fields: availableBalance is replaced by two purpose-built fields spendableBalance and withdrawableBalance. The changes span:

  1. Core type definition (app/controllers/perps/types/index.ts): Breaking rename of availableBalancespendableBalance + withdrawableBalance with distinct semantics per HyperLiquid abstraction mode (Unified vs Standard).

  2. UI components (PerpsWithdrawView, PerpsMarketBalanceActions): Now read withdrawableBalance and spendableBalance respectively instead of the old availableBalance/availableToTradeBalance pair.

  3. Confirmation hooks (useInsufficientPerpsBalanceAlert, useTransactionCustomAmount): Updated to use withdrawableBalance for perps withdrawal validation — directly affects the confirmation flow for perps withdrawals.

  4. E2E test infrastructure (default-fixture.json, perps-controller-mixin.ts, perps-e2e-mocks.ts): All updated to use new field names, meaning existing Perps E2E tests will exercise the new contract.

  5. Store migration 133: New migration handles upgrade path from old to new field names for existing users.

Tag selection rationale:

  • SmokePerps: Primary tag — all perps balance display (market list, withdraw view), order entry validation, and deposit flows are affected by the field rename. The E2E mock infrastructure has been updated to match.
  • SmokeWalletPlatform: Required per SmokePerps tag description — Perps is a section inside the Trending tab, so changes to Perps views affect Trending.
  • SmokeConfirmations: Required per SmokePerps description (Add Funds deposits are on-chain transactions); also directly affected because useInsufficientPerpsBalanceAlert and useTransactionCustomAmount (both in the confirmations hooks directory) were updated to use withdrawableBalance — this directly impacts the perps withdrawal confirmation flow.

The changes are well-contained to the Perps domain with no impact on non-Perps flows (no shared navigation, no shared controllers beyond PerpsController, no changes to Engine initialization).

Performance Test Selection:
The changes are a field rename/refactor within the PerpsController state (availableBalance → spendableBalance + withdrawableBalance). There are no new data fetching patterns, no list rendering changes, no new component renders, no startup/initialization changes, and no Redux store structure changes beyond the field rename. The migration is a simple key rename. No performance impact is expected.

View GitHub Actions results

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.29167% with 34 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.85%. Comparing base (51b6bbd) to head (ece3bdb).
⚠️ Report is 38 commits behind head on main.

Files with missing lines Patch % Lines
app/store/migrations/133.ts 86.27% 1 Missing and 6 partials ⚠️
...controllers/perps/providers/HyperLiquidProvider.ts 14.28% 6 Missing ⚠️
...ponents/UI/Perps/hooks/usePerpsWithdrawProgress.ts 16.66% 5 Missing ⚠️
...s/perps/services/HyperLiquidSubscriptionService.ts 91.66% 2 Missing and 2 partials ⚠️
app/components/UI/Perps/Debug/HIP3DebugView.tsx 0.00% 3 Missing ⚠️
...mponents/UI/Perps/hooks/usePerpsDepositProgress.ts 75.00% 0 Missing and 2 partials ⚠️
...components/UI/Perps/hooks/usePerpsDepositStatus.ts 81.81% 0 Missing and 2 partials ⚠️
app/controllers/perps/utils/accountUtils.ts 80.00% 1 Missing and 1 partial ⚠️
...s/UI/Perps/Views/PerpsOrderView/PerpsOrderView.tsx 75.00% 1 Missing ⚠️
app/components/UI/Perps/hooks/usePerpsOrderForm.ts 75.00% 0 Missing and 1 partial ⚠️
... and 1 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #29303      +/-   ##
==========================================
- Coverage   82.15%   81.85%   -0.31%     
==========================================
  Files        5178     5245      +67     
  Lines      137450   138757    +1307     
  Branches    31079    31470     +391     
==========================================
+ Hits       112924   113573     +649     
- Misses      16875    17458     +583     
- Partials     7651     7726      +75     

☔ 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.

@sonarqubecloud

sonarqubecloud Bot commented May 4, 2026

Copy link
Copy Markdown

@abretonc7s abretonc7s added this pull request to the merge queue May 4, 2026
Merged via the queue into main with commit 57a2e9c May 4, 2026
100 of 101 checks passed
@abretonc7s abretonc7s deleted the refactor/tat-3047-balance-contract-reshape branch May 4, 2026 14:03
@github-actions github-actions Bot locked and limited conversation to collaborators May 4, 2026
@metamaskbotv2 metamaskbotv2 Bot added the release-7.77.0 Issue or pull request that will be included in release 7.77.0 label May 4, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

release-7.77.0 Issue or pull request that will be included in release 7.77.0 size-XL team-perps Perps team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants