Skip to content

fix(perps): wait for the client's init() across a disconnect-then-init reconnect - #10589

Merged
abretonc7s merged 7 commits into
mainfrom
TAT-4041-fix-fix-order-submit-ws-reconnect
Sep 30, 2026
Merged

abretonc7s merged 7 commits into
mainfrom
TAT-4041-fix-fix-order-submit-ws-reconnect

Conversation

@abretonc7s

@abretonc7s abretonc7s commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Explanation

Mobile reconnects Perps with PerpsController.disconnect(), a short cleanup delay, then init(). That covers a foreground resume after a failed ping, and account or network changes. #getActiveProviderWhenReady waited for the disconnect but not for the init() that follows it. An order submitted in that window woke up to an uninitialized controller and failed with CLIENT_NOT_INITIALIZED.

This PR changes only the controller:

  • If an action waited on a disconnect() and the controller is still uninitialized afterwards, it waits up to PERPS_CONSTANTS.ConnectionTimeoutMs for the client's init() to start, then runs. If no init() follows, it fails as before. The controller never starts a connection itself.
  • If the reconnect switched the selected account, the network or the active provider, the action fails with PROVIDER_LIFECYCLE_STALE instead of running under the new context. The context is recorded when the action is issued.

Behavior change: after a final disconnect with no follow-up init(), an action that was already in flight now takes up to 10 s to fail instead of failing right away. Actions issued after the disconnect are unaffected.

The provider-side part of TAT-4041 (order-path reads over HTTP during a WebSocket reconnect, the getMaxLeverage 3x cap, 429 retry) is split into the stacked draft #10590. It stays in draft until a transport stress test settles the long-term transport choice.

Not in this PR:

  • PROVIDER_LIFECYCLE_STALE for an order already in flight inside the provider when the account changes. Continuing under the new account would be wrong. How that code is shown to the user is up to the client.

Reproduction

The real headless PerpsController, SDK transports and signer ran against HyperLiquid testnet, with a client driver calling the real disconnect() and a delayed init().

Controlled condition Unfixed source (1f15b7c) With the controller change
Client disconnect() followed by delayed init() Disconnect finished at 2,970 ms, and the order failed with CLIENT_NOT_INITIALIZED at 2,970 ms. init() only started at 3,470 ms. init() finished at 5,260 ms. Order 61421723850 succeeded at 7,882 ms, was observed open, and was canceled by ID. 0 BTC orders and 0 positions after teardown.

That run was recorded on 9eb852d, which also contained the provider changes now in #10590. The controller code on this path is the same apart from narrowing: the wait now only follows a disconnect(), not a reinitialization. I have not rerun the testnet reproduction on 9149042. No Mobile or Extension UI was exercised.

Validation (9149042)

  • PerpsController suites: 8 passed, 465 passed.
  • The 4 new lifecycle tests all fail on main: order placed across disconnect then init, bounded wait, and refusal after an account switch or a network switch. With only the context guard removed, just the 2 switch tests fail.
  • oxlint and oxfmt clean; changelog:validate passes; the package build type-checks.

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

Orders submitted while the Perps connection reconnects failed with
CLIENT_NOT_INITIALIZED although the HTTP exchange client stays available.

- Read order-path data over HTTP: margin-mode lock, HIP-3 balances and
  spot metadata, unified-account and referral setup, asset-map rebuilds,
  and the #ensureReady gate. getMaxLeverage no longer requires the
  WebSocket clients, which forced a 3x cap during a reconnect.
- When an action waited on a disconnect or reinitialization, wait up to
  ConnectionTimeoutMs for the client's follow-up init() instead of failing
  in the gap between disconnect() and init().

Refs: TAT-4041
Retry rate-limited pre-order HTTP reads with jittered backoff.
@abretonc7s abretonc7s changed the title chore: prepare farmslot publication pkg-87070aed-mun0pb20 fix(perps): order path depends on the WebSocket info client during reconnect — CLIENT_NOT_INITIALIZED / PROVIDER_LIFECYCLE_STALE / 429 on submit Sep 30, 2026
@abretonc7s
abretonc7s marked this pull request as ready for review September 30, 2026 00:10
@abretonc7s
abretonc7s requested review from a team as code owners September 30, 2026 00:10
@abretonc7s
abretonc7s deployed to default-branch September 30, 2026 00:10 — with GitHub Actions Active
Keep only the controller change: an action waiting on disconnect() waits a
bounded time for the client's follow-up init(), and fails with
PROVIDER_LIFECYCLE_STALE if the account, network or provider changed.

The HTTP order-path reads, the 429 retry and the getMaxLeverage change move
to a stacked follow-up so the transport choice can be validated separately.

Refs: TAT-4041
@abretonc7s abretonc7s changed the title fix(perps): order path depends on the WebSocket info client during reconnect — CLIENT_NOT_INITIALIZED / PROVIDER_LIFECYCLE_STALE / 429 on submit fix(perps): wait for the client's init() across a disconnect-then-init reconnect Sep 30, 2026

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

I see the connection timeout constant is used in this method invocation but left a comment about using it as a default instead. Otherwise, LGTM.

* @param timeoutMs - Longest time to wait for init() to be called.
* @returns A promise that resolves when init starts or the timeout elapses.
*/
async #waitForInitializationStart(timeoutMs: number): Promise<void> {

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.

nit: wdyt about a default for timeoutMs here to inform callers in the future?

@abretonc7s
abretonc7s added this pull request to the merge queue Sep 30, 2026
Merged via the queue into main with commit 707a74c Sep 30, 2026
43 checks passed
@abretonc7s
abretonc7s deleted the TAT-4041-fix-fix-order-submit-ws-reconnect branch September 30, 2026 00:51
@abretonc7s abretonc7s mentioned this pull request Sep 30, 2026
3 of 4 tasks
github-merge-queue Bot pushed a commit that referenced this pull request Sep 30, 2026
## Explanation

- Release major perps-controller 19.0.0

Breaking changes (see changelog):
- `OrderFill.liquidation.liquidatedUser` is now optional
- `FrontendOrder.orderType` union adds `Twap Slice`, `Vault Close`,
`Spot Dust Conversion`
- Removed `LighterPersonalSigner` and the `personalSigner` / `l1Address`
fields of `LighterAuthConfig`

## References

- #10414, #10437, #10464, #10559, #10588, #10589, #10591

## 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
- [x] 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
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