Skip to content

fix(perps-controller): guard userFills subscribe against reconnect race - #11160

Merged
javiergarciavera merged 7 commits into
mainfrom
fix/perps-userfills-reconnect-race
Nov 3, 2026
Merged

javiergarciavera merged 7 commits into
mainfrom
fix/perps-userfills-reconnect-race

Conversation

@javiergarciavera

@javiergarciavera javiergarciavera commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Explanation

After a HyperLiquid WebSocket reconnect, restoreSubscriptions clears and re-opens userFills. An in-flight subscribe that started before reconnect can still resolve afterward and write a live socket into the map that no one is listening to — or overwrite the post-reconnect subscription. Live fills then go silent until something else re-subscribes (e.g. lock/unlock).

This mirrors the existing TWAP pending + generation guards for order fills: share one in-flight open per account, bump a generation on teardown/reconnect/clearAll, and discard stale opens / frames when the generation no longer matches.

Verified locally in the extension by patching @metamask/perps-controller@20.1.0 with this change and running perps-tpsl E2E without the lock/unlock workaround — both TP and SL cases passed.

References

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

Note

Medium Risk
Changes live WebSocket subscription lifecycle for HyperLiquid order fills (reconnect/teardown races); behavior is well-tested but affects real-time trading UI data paths.

Overview
Fixes HyperLiquid userFills going silent after a WebSocket reconnect when an in-flight subscribe races with restoreSubscriptions — stale sockets could land in the map or overwrite the live post-reconnect subscription, so live fills never reached listeners (MetaMask/metamask-extension#47287).

Order-fill subscription setup now mirrors the existing TWAP guards: one shared in-flight open per account (#pendingOrderFillSubscriptions), a per-account generation counter bumped on unsubscribe teardown, reconnect restore, and clearAll, plus a dedicated #createOrderFillSubscription path that drops stale opens (unsubscribe + skip map registration) and ignores frames when the generation no longer matches.

Reconnect and full teardown also clear pending order-fill promises and bump generations for every account touched by subscribers, active subs, or pending opens. Tests cover reconnect re-subscribe, stale-open discard, frame routing after reconnect, pending rejection, subscriber churn, and clearAll.

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

javiergarciavera and others added 3 commits October 28, 2026 13:32
Pending + generation guards (same pattern as TWAP) so an in-flight
userFills open that resolves after restoreSubscriptions cannot rehydrate
a dead listener and leave live fills silent (extension #47287).

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…xfmt

Co-authored-by: Cursor <cursoragent@cursor.com>
…er stale-frame drops

Waiters on an in-flight userFills open now swallow its rejection and open
their own, re-check the pending map so only one waiter reopens, and skip
opening when no subscribers remain. Adds tests for frame delivery across
reconnect, rejection fallthrough, last-subscriber teardown and clearAll
while an open is in flight.

Co-Authored-By: Claude <noreply@anthropic.com>
michalconsensys
michalconsensys previously approved these changes Nov 2, 2026
…econnect-race

# Conflicts:
#	packages/perps-controller/CHANGELOG.md
javiergarciavera and others added 2 commits November 3, 2026 10:46
Main released 22.1.0 while this PR was open, so the merge left the entry
inside the already-released section.

Co-Authored-By: Claude <noreply@anthropic.com>
…econnect-race

# Conflicts:
#	packages/perps-controller/CHANGELOG.md
@javiergarciavera
javiergarciavera marked this pull request as ready for review November 3, 2026 10:29
@javiergarciavera
javiergarciavera requested review from a team as code owners November 3, 2026 10:29
@javiergarciavera
javiergarciavera added this pull request to the merge queue Nov 3, 2026
Merged via the queue into main with commit b6a4aa0 Nov 3, 2026
50 checks passed
@javiergarciavera
javiergarciavera deleted the fix/perps-userfills-reconnect-race branch November 3, 2026 10:35
Naz-Ovh pushed a commit to 0x-fork/metamask-core that referenced this pull request Nov 4, 2026
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Explanation

Releases `@metamask/perps-controller@22.1.1` (patch) so the fix from
MetaMask#11160 can be consumed by `metamask-extension`.

This version migrates the package's existing `Unreleased` entries into
`22.1.1`:

### Changed

- Bump `uuid` from `^14.0.0` to `^14.0.1` (MetaMask#11179)

### Fixed

- Prevent HyperLiquid `userFills` from going silent after a WebSocket
reconnect when an in-flight subscribe races with `restoreSubscriptions`
(MetaMask#11160)

## Notes

- Dependencies of `perps-controller` that have unreleased changes
(`@metamask/base-controller`, `@metamask/controller-utils`,
`@metamask/messenger`, `@metamask/utils`) were intentionally skipped —
`22.1.1` does not rely on new versions of them.
- `@metamask/transaction-pay-middleware`'s dependency range on
`perps-controller` was bumped to `^22.1.1` automatically by the release
tooling (its own version is unchanged).
- Dropped two non-consumer-facing CI/test commits (MetaMask#11261, MetaMask#11199) that
the tooling had added under `Uncategorized`, per the release changelog
guidelines.
- `yarn changelog:validate` passes for `@metamask/perps-controller`.

## Checklist

- [x] I've updated the test suite for new or updated code as appropriate
- [x] 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](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md)
- [ ] I've introduced [breaking
changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md)
in this PR and have prepared draft pull requests for clients and
consumer packages to resolve them
<!-- CURSOR_AGENT_PR_BODY_END -->

[Slack
Thread](https://consensys.slack.com/archives/D0AD7VDJ3U0/p1782396245326519?thread_ts=1782396245.326519&cid=D0AD7VDJ3U0)

<div><a
href="https://cursor.com/agents/bc-bc8c3875-2395-5248-9807-fdb8636d9cc3?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-web-light.png"><img
alt="Open in Web" width="114" height="28"
src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a
href="https://cursor.com/background-agent?bcId=bc-bc8c3875-2395-5248-9807-fdb8636d9cc3&cursor_ref=pr_footer&cursor_cta=open_in_cursor"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img
alt="Open in Cursor" width="131" height="28"
src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</div>

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Version and changelog-only release PR with dependency range updates;
functional changes were already merged in prior PRs.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
e397b02. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Amélie <ameliejyc@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
n3ps pushed a commit to n3ps/metamask-extension that referenced this pull request Nov 4, 2026
)

<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## **Description**

Bumps `@metamask/perps-controller` from `^20.1.0` to `^22.1.1` so the
extension picks up
[MetaMask/core#11160](MetaMask/core#11160),
which prevents HyperLiquid `userFills` from going silent after a
WebSocket reconnect when an in-flight subscribe races with
`restoreSubscriptions`. That fix was released in core release
`1384.0.0`.

Notes on the bump:
- This spans two majors (`21.0.0`, `22.0.0`). `@metamask/[email
protected]` was already resolved transitively via
`@metamask/transaction-pay-middleware`, so the version bump itself
compiles with no changes; the UI adaptations below (ported from the
earlier, now-closed
[MetaMask#47337](MetaMask#47337))
keep perps behavior correct on 22.x.
- **UI changes (ported from MetaMask#47337):**
- `ui/components/app/perps/hooks/usePerpsNetworkManagement.ts`: on
testnet, ensure the Arbitrum **Sepolia** network (where HyperLiquid
testnet deposits now settle USDC2 after the 22.0.0 change) instead of
always Arbitrum One, gated on `selectPerpsIsTestnet`.
- `ui/hooks/perps/usePerpsOrderFees.ts`: treat the new `'grant'`
`PerpsFeeSource` as a fee-reducing source alongside
`rewards`/`subscription`.
- `ui/pages/perps/perps-withdraw-page.test.tsx`: update the testnet USDC
asset-id mock to USDC2.
  - Plus colocated test updates for the hooks above.
- `yarn.lock` now dedupes the direct dependency and
`transaction-pay-middleware`'s `^22.0.0` range onto a single `22.1.1`,
and perps-controller now pulls `uuid@^14` (already present in the tree).
- LavaMoat policies were regenerated (`yarn lavamoat:auto`): the
duplicate `transaction-pay-middleware>@metamask/perps-controller`
entries collapse into the shared `@metamask/perps-controller`, and
`@metamask/perps-controller>uuid` moves from `crypto` to the
finer-grained `crypto.getRandomValues` / `crypto.randomUUID` that
`uuid@14` uses.
- `yarn lint:tsc` passes and the affected perps unit suites (72 tests
across the three touched files) pass.

## **Changelog**

CHANGELOG entry: Fixed HyperLiquid live fills going silent after a
WebSocket reconnect by updating `@metamask/perps-controller` to
`22.1.1`.

## **Related issues**

Related to: MetaMask#47287

## **Manual testing steps**

1. Build the extension and open a HyperLiquid perps market with an open
position.
2. Place or close an order so fills are streaming on the
position/activity view.
3. Force a WebSocket reconnect (e.g. drop/restore the network
connection) while a subscribe is in flight.
4. Verify live fills keep updating after the reconnect instead of going
silent.
5. In perps testnet mode, start a deposit and verify the preflight
adds/uses the Arbitrum Sepolia network (not Arbitrum One) so the testnet
deposit succeeds.

<!--
## **Screenshots/Recordings**

### **Before**

### **After**
-->

## **Pre-merge author checklist**

- [ ] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [ ] I've completed the PR template to the best of my ability
- [ ] I’ve included tests if applicable
- [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **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.
<!-- CURSOR_AGENT_PR_BODY_END -->

[Slack
Thread](https://consensys.slack.com/archives/D0AD7VDJ3U0/p1782396245326519?thread_ts=1782396245.326519&cid=D0AD7VDJ3U0)

<div><a
href="https://cursor.com/agents/bc-cb6458b0-6c9e-54df-a32c-5b5c5c0131bb?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-web-light.png"><img
alt="Open in Web" width="114" height="28"
src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a
href="https://cursor.com/background-agent?bcId=bc-cb6458b0-6c9e-54df-a32c-5b5c5c0131bb&cursor_ref=pr_footer&cursor_cta=open_in_cursor"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img
alt="Open in Cursor" width="131" height="28"
src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</div>


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes deposit network preflight and fee UI for testnet/grant paths,
plus dependency and LavaMoat policy updates around crypto globals used
by perps flows.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
a3f83e6. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Amélie <ameliejyc@users.noreply.github.com>
LabinatorSolutions pushed a commit to LabinatorSolutions/metamask-extension that referenced this pull request Nov 6, 2026
…taMask#47643)

<!--
Please submit this PR as a draft initially.
Do not mark it as "Ready for review" until the template has been
completely filled out, and PR status checks have passed at least once.
-->

## **Description**

Opening Perps Activity force-refreshes transaction history, but that
refresh only cleared the UI-side coalesce cache. `perpsGetOrderFills`,
`perpsGetOrders` and `perpsGetFunding` still reached `PerpsController`
without `forceRefresh`, so the controller's 60s REST coalesce cache
could return a result from before a just-closed trade. The trade then
only appeared if the live `userFills` stream delivered it. That is why
the TP/SL E2E specs fail intermittently on Firefox after the
`@metamask/perps-controller@22.1.1` bump.

1. **Reason for the change:** Activity must not show a result cached
before the fill it is meant to show.
2. **Solution:** `refetch()` (used by `forceFreshOnMount` on Activity
and by pull-to-refresh) now passes `{ forceRefresh: true }` to the three
REST reads. Passive fetches, such as the Recent activity preview on
Perps home, keep the controller cache. The E2E flow
`assertPerpsActivityShowsCloseFill` no longer pushes `userFills` a
second time, because the forced REST read now returns the fill from the
mock.

Related:
[MetaMask#47600](MetaMask#47600),
[MetaMask/core#11160](MetaMask/core#11160).

## **Changelog**

CHANGELOG entry: Fixed a bug where a just-closed Perps trade could be
missing from Perps Activity

(This helps the Release Engineer do their job more quickly and
accurately)

## **Related issues**

Fixes:

## **Manual testing steps**

1. Run the extension with `PERPS_ENABLED=true` and sign in with an
account that has an open ETH long position.
2. Open the ETH position and set a take-profit at a price the market
will reach, or a stop-loss that will trigger.
3. Wait for the position to close, then return to the Perps home.
4. Under "Recent activity", select "See all".
5. Verify the "Closed long" trade row for ETH appears in Perps Activity.
6. Open Perps Activity a second time, then use pull-to-refresh if
available, and verify the row is still shown.

## **Screenshots/Recordings**

<!--
## **Screenshots/Recordings**

### **Before**

### **After**
-->

## **Pre-merge author checklist**

- [ ] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [ ] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **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.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes Perps transaction history fetch semantics and HyperLiquid REST
traffic on Activity open/refetch; behavior is covered by new unit tests
and a simplified E2E flow.
> 
> **Overview**
> Fixes **missing just-closed trades on Perps Activity** when the UI
cleared its own coalesce cache but **`PerpsController` still served
60s-cached** `perpsGetOrderFills` / orders / funding reads (often after
the home “Recent activity” preview had already fetched).
> 
> **`usePerpsTransactionHistory`** now treats **`refetch()`**
(pull-to-refresh and **`forceFreshOnMount`**) as a true refresh:
background calls include **`{ forceRefresh: true }`**, and the UI
coalescer uses **`replaceInFlight`** so a forced read does not join an
in-flight passive request that would omit `forceRefresh`. Passive
previews keep cache-friendly reads.
> 
> **`coalesceBackgroundRequest`** gains an optional
**`replaceInFlight`** flag (with tests). The E2E close-fill flow drops
the extra **`pushUserFills()`** retry now that the forced REST path is
expected to return the fill from the mock.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
b2641ab. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Cursor <cursoragent@cursor.com>

This branch was previously deployed

1 inactive deployment
default-branch — f7c85bc5 Deployed Nov 3, 2026 by javiergarciavera via Determine whether this PR is a release PR #6391
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.

[Bug]: Perps - userFills WebSocket listener stops delivering live fills after subscribing; closed trades missing from Activity

2 participants