Skip to content

feat(perps-controller): sync controller from mobile 394fe407d2#8560

Merged
abretonc7s merged 3 commits intomainfrom
feat/perps/controller-apr-23rd
Apr 23, 2026
Merged

feat(perps-controller): sync controller from mobile 394fe407d2#8560
abretonc7s merged 3 commits intomainfrom
feat/perps/controller-apr-23rd

Conversation

@abretonc7s
Copy link
Copy Markdown
Contributor

@abretonc7s abretonc7s commented Apr 23, 2026

Syncs app/controllers/perps/ from mobile commit 394fe407d2.

Changes:

  • HL Unified-mode live balance: spotState WS + tradeable-balance + total-balance math (#29226)
  • Complete spot-balance parity with extension (#29110)
  • Preserve integer trailing zeros when szDecimals=0 (#29016)
  • Add coalescePerpsRestRequest util + accountUtils; account-scoped REST cache and guarded cache writes
  • Preserve candle pagination cancellation; skip coalesce for explicit-endTime candle paging
  • Parity with extension rate-limit fix + provider-agnostic forceRefresh
  • Pin resolved account id to forwarded provider params; defer account resolution to non-paginated cache path
  • Force-refresh activity mount and evict expired coalesce entries
  • Regenerate PerpsController method action types; shrink rate-limit diff and drop verbose history logs

Note

Medium Risk
Touches core market-data fetching, caching, and account-balance computation/WS subscriptions; regressions could surface stale/cross-account activity data or incorrect balances, but changes are localized and add explicit cache scoping and refresh escape hatches.

Overview
Adds a new service-layer REST request coalescing/TTL cache (coalescePerpsRestRequest) and wires it into MarketDataService (orders/fills/funding) and HyperLiquid candle snapshots to reduce duplicate REST traffic and 429s, with an end-to-end forceRefresh bypass for user-initiated refreshes.

Fixes HyperLiquid unified-mode balance parity by subscribing to spotState over WS, computing spot-adjusted totalBalance plus a new availableToTradeBalance, and centralizing spot math in accountUtils (including USDC-only spot collateral policy). Also scopes caches by resolved account via new PerpsProvider.getCurrentAccountId() and updates controller/provider method signatures and adapter/formatter behavior (e.g., preserve integer trailing zeros when szDecimals=0).

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

Syncs app/controllers/perps/ from mobile commit 394fe407d2.

Changes:
- HL Unified-mode live balance: spotState WS + tradeable-balance + total-balance math ([#29226](MetaMask/metamask-mobile#29226))
- Complete spot-balance parity with extension ([#29110](MetaMask/metamask-mobile#29110))
- Preserve integer trailing zeros when szDecimals=0 ([#29016](MetaMask/metamask-mobile#29016))
- Add coalescePerpsRestRequest util + accountUtils; account-scoped REST cache and guarded cache writes
- Preserve candle pagination cancellation; skip coalesce for explicit-endTime candle paging
- Parity with extension rate-limit fix + provider-agnostic forceRefresh
- Pin resolved account id to forwarded provider params; defer account resolution to non-paginated cache path
- Force-refresh activity mount and evict expired coalesce entries
- Regenerate PerpsController method action types; shrink rate-limit diff and drop verbose history logs
@abretonc7s abretonc7s requested a review from a team as a code owner April 23, 2026 09:28
Copy link
Copy Markdown

@cursor cursor Bot left a comment

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 7997c64. Configure here.

Comment thread packages/perps-controller/src/utils/hyperLiquidAdapter.ts Outdated
Comment thread packages/perps-controller/src/services/HyperLiquidSubscriptionService.ts Outdated
Syncs mobile fixes for the two bugbot comments on this PR:

- hyperLiquidAdapter: drop the dead spotState branch and optional
  parameter from adaptAccountStateFromSDK. Every caller was already
  passing a single argument; the spot branch summed balances across
  ALL coins, which would have silently diverged from the USDC-only
  policy enforced by addSpotBalanceToAccountState via
  SPOT_COLLATERAL_COINS if a future caller had wired it in.
- HyperLiquidSubscriptionService: normalize event.user.toLowerCase()
  before writing #cachedSpotStateUserAddress so the strict-equal
  check in #ensureSpotState hits the cache regardless of whether
  HyperLiquid returns a checksummed or lowercase address on the WS
  feed. Previously the mismatch triggered a redundant REST
  spotClearinghouseState refetch per subscribeToAccount call.

Mobile source: MetaMask/metamask-mobile#29243
@abretonc7s abretonc7s added this pull request to the merge queue Apr 23, 2026
Merged via the queue into main with commit 27b7d4b Apr 23, 2026
358 checks passed
@abretonc7s abretonc7s deleted the feat/perps/controller-apr-23rd branch April 23, 2026 11:08
@aganglada aganglada mentioned this pull request Apr 23, 2026
pull Bot pushed a commit to Reality2byte/metamask-mobile that referenced this pull request Apr 23, 2026
## **Description**

Addresses the two bugbot findings raised on the corresponding core sync
PR ([MetaMask/core#8560](MetaMask/core#8560)).
Both issues are sourced from mobile — fix here first, then re-sync to
core.

### 1. Dead spot branch in `adaptAccountStateFromSDK` (Medium)

`app/controllers/perps/utils/hyperLiquidAdapter.ts` — the exported
function still advertised an optional `spotState` param and ran a spot
branch that sums balances across **all** spot coins. Every current
caller passes a single argument only; the live path layers spot balances
afterwards via `addSpotBalanceToAccountState`, which restricts spot math
to **USDC only** (`SPOT_COLLATERAL_COINS`).

The dormant branch is a future-caller trap: anyone wiring `spotState` in
would silently get ALL-coins behavior, diverging from the USDC-only
policy enforced elsewhere. Dropped the param, the branch, and the
now-orphaned import.

### 2. Spot-state address casing causes redundant REST refetches (Low)

`app/controllers/perps/services/HyperLiquidSubscriptionService.ts` line
1147 — the spot-state WS callback filters `event.user` with
`.toLowerCase()` but then stores the raw `event.user` in
`#cachedSpotStateUserAddress`. The strict-equal check in
`#ensureSpotState` (line 1032) compares that cached value against the
wallet's lowercase address, so when HyperLiquid returns a checksummed
address on the WS feed the cache check always misses and a redundant
REST `spotClearinghouseState` fetch is triggered per
`subscribeToAccount` call (it self-heals once REST rewrites the cache,
but at a per-call cost until convergence).

Normalize at the store site to match the REST path's lowercase
convention.

## **Changelog**

CHANGELOG entry: null

## **Related issues**

Fixes:
[TAT-3066](https://consensyssoftware.atlassian.net/browse/TAT-3066)
Related:
[MetaMask/core#8560](MetaMask/core#8560) —
re-sync to core once this lands.

## **Manual testing steps**

```gherkin
Feature: Perps account balance aggregation (unchanged behavior)

  Scenario: Unified-mode user opens perps with spot balance
    Given the user is on the Perps Home screen with an open position
    Then availableToTradeBalance, totalBalance, marginUsed render as before the change
    And the USDC-only spot policy continues to be applied by addSpotBalanceToAccountState

  Scenario: WS spotState event arrives with a checksummed address
    Given subscribeToAccount is active
    When HyperLiquid pushes a spotState WS event with a checksummed `event.user`
    Then subsequent #ensureSpotState calls skip the REST refetch (cache hit)
    And metro.log shows no redundant `refreshSpotState` fetches between account updates
```

## **Screenshots/Recordings**

No visual change. Fix is in controller/service layer; account balance
values render identically.

### **Before**

N/A — code-level behavioral fixes, no UI surface.

### **After**

N/A — code-level behavioral fixes, no UI surface.

## **Pre-merge author checklist**

- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- Obsolete spot-branch tests removed alongside the dead code. The
remaining 61 tests in `hyperLiquidAdapter.test.ts` still pass.
- [x] I've documented my code using JSDoc format if applicable
- Added a short comment above `adaptAccountStateFromSDK` explaining why
spot logic is intentionally excluded.
- [ ] 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
- Fix is perf-positive (removes a redundant REST fetch) and does not
introduce new Sentry surfaces.

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR
- [ ] 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.

[TAT-3066]:
https://consensyssoftware.atlassian.net/browse/TAT-3066?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Updates core perps account-balance adaptation and spot-state caching
logic; mistakes could surface as incorrect balances or unnecessary
network calls, but changes are small and covered by existing call
patterns/tests.
> 
> **Overview**
> Fixes two perps core-sync issues.
> 
> `adaptAccountStateFromSDK` is now **perps-only**: it drops the
optional `spotState` parameter and removes the dormant spot-balance
summation branch, ensuring spot math is applied exclusively via
`addSpotBalanceToAccountState` (USDC-only policy) and simplifying
`availableToTradeBalance`/`totalBalance` to perps values.
> 
> Spot-state WebSocket handling now lowercases `event.user` when caching
`#cachedSpotStateUserAddress`, preventing cache misses that previously
triggered redundant REST `spotClearinghouseState` refetches when HL
returned checksummed addresses. Related unit tests that covered the
removed spot branch were deleted.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
d4c6805. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
pull Bot pushed a commit to Reality2byte/core that referenced this pull request Apr 23, 2026
## Explanation

This release bumps \`@metamask/perps-controller\` from \`3.2.0\` to
\`3.3.0\`.

### Changes included

**\`@metamask/perps-controller\` v3.3.0**
([MetaMask#8560](MetaMask#8560))

**Added**
- Add \`coalescePerpsRestRequest\` utility for deduplicating concurrent
REST requests with account-scoped cache keys
- Add \`accountUtils\` helpers for resolving the active perps account id
and pinning it to forwarded provider params

**Changed**
- Account-scope the REST cache and guard cache writes so mount load
stays cacheable without cross-account bleed
- Make \`forceRefresh\` provider-agnostic and align rate-limit handling
with the extension
- Regenerate \`PerpsController\` method action types; shrink rate-limit
diff and drop verbose history logs

**Removed**
- Drop the dead \`spotState\` parameter from
\`adaptAccountStateFromSDK\` — spot balances are layered on by
\`addSpotBalanceToAccountState\`, which enforces the USDC-only policy
via \`SPOT_COLLATERAL_COINS\`, removing the dormant branch keeps one
source of truth and prevents a future caller from silently getting
ALL-coins behavior

**Fixed**
- HyperLiquid Unified-mode live balance: subscribe to \`spotState\` WS
and compute tradeable/total balance from on-chain math
- Complete spot-balance parity with the extension consumer
- Preserve integer trailing zeros when \`szDecimals=0\` in
\`perpsFormatters\`
- Preserve candle pagination cancellation and skip coalesce for
explicit-\`endTime\` candle paging to avoid stale pages
- Defer account resolution on the non-paginated cache path to prevent
race conditions
- Force-refresh on activity mount and evict expired coalesce entries so
stale promises cannot resolve to cache
- Normalize \`event.user\` to lowercase when caching the spot-state WS
address so \`#ensureSpotState\` hits the cache instead of triggering a
redundant REST \`spotClearinghouseState\` refetch when HyperLiquid
returns a checksummed address

## References

- [MetaMask#8560](MetaMask#8560)

## Checklist

- I've updated the test suite for new or updated code as appropriate
- I've updated documentation (JSDoc, Markdown, etc.) for new or updated
code as appropriate
- I've communicated my changes to consumers by updating changelogs for
packages I've changed
- I've introduced breaking changes in this PR and have prepared draft
pull requests for clients and consumer packages to resolve them
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants