Skip to content

feat(perps-controller): add Lighter perps venue (initial implementation, flag-gated) - #9889

Merged
abretonc7s merged 69 commits into
mainfrom
TAT-3766-feat-spike-lighter-perps-integratio
Aug 31, 2026
Merged

abretonc7s merged 69 commits into
mainfrom
TAT-3766-feat-spike-lighter-perps-integratio

Conversation

@abretonc7s

@abretonc7s abretonc7s commented Aug 15, 2026 •

Copy link
Copy Markdown
Contributor

Explanation

TAT-3766: add Lighter as a second perps venue. Started as a spike, now a full LighterProvider in @metamask/perps-controller, hardened through 23 rounds of adversarial cross-review and validated end-to-end against live Lighter testnet.

What's in it

  • Full trading surface behind the PerpsProvider interface: place/cancel/close orders, venue-linked OCO TP/SL (grouped trigger orders), isolated margin add/remove, leverage updates, signed L2 withdraw, historical orders/fills/funding/ledger, and live streams (prices, account, positions, orders, fills, order book, candles) over a shared WebSocket with REST-polling fallback.
  • Signer model: deterministic venue-key derivation via personal_sign (raw EVM key never leaves the keyring — hardware-wallet compatible). The Go/WASM signer is injected through a LighterSignerBridge seam so mobile's WebView bridge and the headless Node adapter are interchangeable.
  • Financial-safety architecture (the bulk of the review rounds): a durable nonce-dispatch ledger with exact tx-hash reconciliation — an ambiguous dispatch is never blindly retried; proven-committed outcomes are quarantined until explicitly acknowledged per-outcome (getRecoveredDispatches/acknowledgeRecoveredDispatch); a TP/SL settlement journal that survives crashes and restores nothing it cannot prove, parking unprovable protection as durable manual recovery (getPendingManualRecoveries); process-wide serialization of venue writes, the ledger, and the bridge's singleton WASM client; session fences on every account switch.
  • Controller wiring behind a flag: PerpsProviderType gains 'lighter'; enablement via providerCredentials.lighter.enabled or the perpsLighterProviderEnabled remote flag. New controller actions expose the safety states to clients.

How it was validated

  • Unit: 213 provider tests (3,031 package-wide), including deterministic fault-injection proofs for response loss, masked commits, restarts, account switches mid-flight, concurrent providers, and storage failures. Every review-round fix was proven RED against the prior commit before landing.
  • Live testnet e2e (real venue, real WASM signer built from source): sign-only 9/9 (pinned WASM identity contract), key registration, order-lifecycle 7/7, TP/SL 12/12 (including a real venue-linked OCO pair), close-position 5/5 with both fills observed on the trades stream, margin/leverage 7/7 on a fresh faucet account.
  • Mainnet read path: 227 active perp markets, full-catalog WS snapshot, candles.
  • On-device (iOS simulator): live prices/candles/positions over WS-only transport; WASM signer mount→ready ~2.1 s, round-trip 32–78 ms.

Open items

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 — not applicable: no breaking changes

Note

High Risk
Introduces a new trading venue, client signer bridge, and durable dispatch/recovery flows for ambiguous L2 submissions—areas that affect funds, leverage, and order settlement even when feature-gated.

Overview
Adds Lighter as an opt-in perps venue (PerpsProviderType now includes 'lighter', a breaking change for exhaustive switches). Registration mirrors MYX: dynamic import of LighterProvider, gated by providerCredentials.lighter.enabled or remote perpsLighterProviderEnabled only when the client supplies LighterSignerBridge (venue keys stay client-side; no deposit ads until bridge deposit flow exists). PROVIDER_CONFIG.LIGHTER_TESTNET_ONLY and cache keys follow the same pattern as MYX.

Exposes durable settlement on the controller and aggregator: getPendingManualRecoveries, getRecoveredDispatches, and acknowledgeRecoveredDispatch, with exported PerpsPendingManualRecovery / PerpsRecoveredDispatch types. Aggregated mode fans these out across providers that implement the contract.

Multi-provider routing for margin math: optional providerId on liquidation/maintenance params and on getMaxLeverage(asset, providerId?), with AggregatedPerpsProvider delegating to the owning venue instead of always using the default.

Ships Lighter constants/types exports (lighterConfig, signer bridge types) and changelog notes for the initial (disabled-by-default) implementation.

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

@abretonc7s
abretonc7s marked this pull request as ready for review August 17, 2026 10:55
@abretonc7s
abretonc7s requested review from a team as code owners August 17, 2026 10:55
Comment thread packages/perps-controller/src/constants/perpsConfig.ts
Comment thread packages/perps-controller/src/PerpsController.ts
@abretonc7s abretonc7s changed the title feat(perps): spike — investigate Lighter as second perps venue feat(perps-controller): add Lighter perps venue (initial implementation, flag-gated) Aug 17, 2026
Comment thread packages/perps-controller/src/providers/LighterProvider.ts Outdated
Comment thread packages/perps-controller/src/providers/LighterProvider.ts Outdated
Comment thread packages/perps-controller/tests/e2e/lighter/build-wasm.sh Outdated
Comment thread packages/perps-controller/src/providers/LighterProvider.ts
Comment thread packages/perps-controller/src/providers/LighterProvider.ts Outdated
Comment thread packages/perps-controller/src/providers/LighterProvider.ts
Comment thread packages/perps-controller/src/providers/LighterProvider.ts
Comment thread packages/perps-controller/recipes/lighter-e2e.recipe.json Outdated
Comment thread packages/perps-controller/src/providers/LighterProvider.ts
Adds a flag-gated LighterProvider to @metamask/perps-controller following
the MYX optional-provider pattern: live REST reads, real order write path
through the Lighter Go/WASM signer behind a transport-agnostic
LighterSignerBridge seam, deterministic venue-key derivation from an
EIP-191 personal_sign signature (hardware-wallet compatible), and
controller wiring with a client bridge-injection point on
PerpsPlatformDependencies. Provider files are excluded from the published
artifact; Lighter is testnet-only and disabled by default.
…rading surface

Adds a shared-socket stream manager to LighterProvider (market_stats,
user_stats, account_all_positions, account_all_orders, account_all_trades,
order_book and candle channels) with keepalive, reconnect, and an
injectable structural WebSocket seam. Implements closePosition (reduce-only
IOC market orders with a protection price), editOrder, withdraw signing,
and historical candles. Converts Lighter private methods to arrow-function
fields to survive stale global tslib helpers under Hermes, and replaces the
price replay cache with a merged per-symbol snapshot so late subscribers
receive their symbol immediately.
…eads

- updatePositionTPSL via OCO grouped orders (tx 28, grouping 2) with
  STOP_LOSS/TAKE_PROFIT trigger orders; trigger orders use the 28-day
  expiry sentinel, replace semantics cancels prior reduce-only triggers
- updateMargin (tx 29, direction 1=add/0=remove) and updateLeverage
  (tx 20, cross/isolated); leverage change verified via position
  initial-margin-fraction readback in e2e
- getOrderFills via /trades and getFunding via /positionFundings with
  fill adaptation
- e2e driver phases: history-reads, tpsl, margin-leverage (all green
  against live testnet)
…rResult type

OrderResult has no txHash field; drop it from editOrder and
updatePositionTPSL returns and coerce EditOrderParams.orderId to string
LIGHTER_TESTNET_ONLY off: Lighter follows the global network toggle like
HyperLiquid, exposing the full mainnet market catalog and streams. The
signer/venue-key flow is network-agnostic (chain id 300/304).
…nd connection state

- getOrders: full historical order lifecycle via accountInactiveOrders
  merged with open orders
- getUserHistory: deposit/withdraw history endpoints mapped to
  UserHistoryItem with venue status mapping
- getUserNonFundingLedgerUpdates: deposits + withdrawals + transfers
  merged into signed RawLedgerUpdate flows, newest first
- getDepositRoutes/getWithdrawalRoutes: USDC bridge routes per network,
  contract addresses sourced from the venue's layer1BasicInfo
- subscribeToConnectionState/reconnect/getWebSocketConnectionState wired
  to the shared WebSocket manager with real state transitions
- e2e phases parity-history (4/4) and connection-state (4/4); recipe now
  66 nodes, all green against live testnet
- ensureAccountIndex: fail with a clear error when the L1 address has no
  Lighter account instead of TypeError on empty reduce
- disconnect(): clear fill/order-book/candle subscriber sets too
- WS keepalive: replace the timer unconditionally on open; ??= kept a
  timer bound to a dead socket when a new one opened before onclose
- validateClosePosition/validateWithdrawal: validate for the implemented
  operations instead of rejecting with not-supported
- drop always-failing cancelOrders/closePositions stubs so the
  controller falls back to per-item operations (optional members)
- trading-op catch blocks log with per-operation error context
- move the Lighter signer bridge off PerpsPlatformDependencies onto
  LighterCredentials.signerBridge so the shared platform surface stays
  venue-agnostic
- Bind the venue session (account index, signer, auth token, streams) to
  the selected wallet address; switching accounts resets it atomically so
  reads/writes can never target the previous account
- Serialize all nonce-consuming venue writes through a per-provider
  queue; concurrent controller batch fallbacks no longer race
  fetch-nonce/submit pairs
- placeOrder honesty: reject attached TP/SL (directing to
  updatePositionTPSL), post-only TIF, non-positive sizes, and
  below-minimum sizes for position-increasing orders (reduce-only and
  full closes keep the venue-minimum bump since execution clamps to the
  position); honor IOC for limit orders; apply requested leverage via
  UpdateLeverage (tx 20) when the market has no position/resting order
- editOrder now refuses with a clear error: the venue accepts but does
  not apply ModifyOrder (raised with Lighter); cancel + re-place instead
- Invalidate the cached signer session when the bridge reports the WASM
  client is gone (WebView reload) so the next call re-runs setup
- Re-mint the auth token when re-subscribing authenticated WS channels
  on reconnect instead of replaying a possibly-expired token
- Ship Lighter in the published package (remove files exclusions) and
  update the changelog accordingly
- Pin the WASM signer build to an exact lighter-go commit
  (LIGHTER_GO_REF) instead of the moving web-wasm HEAD
- Make LighterCreateClientResult.prv optional: the mobile WebView now
  redacts it before results cross the bridge
- Session binding hardened: a generation counter invalidates in-flight
  account/auth resolutions started under the previous wallet account, and
  an account switch now rebuilds the stream channels implied by surviving
  subscribers (market stats, order books, candles, account channels for
  the new account) instead of leaving a fresh socket subscribed to
  nothing; both proven by adversarial unit tests
- placeOrder honors the full sizing contract: usdAmount as source of
  truth, maxSlippageBps/slippage-driven protection price, and a
  priceAtCalculation drift check; below-minimum sizes are bumped only
  for full closes (incl. dust detected against the live position) and
  rejected for partial reduce-only orders, which a bump would over-close
- Requested leverage is never silently dropped: already-in-effect
  leverage no-ops, otherwise UpdateLeverage is attempted and a venue
  rejection fails the placement
- validateOrder mirrors every placeOrder rejection
- Honest calculations: liquidation price and maintenance margin use the
  standard cross approximation, fees come from the venue's per-market
  metadata, and getHistoricalPortfolio reconstructs the 1d-ago account
  value from the venue pnl flows
- Remote-flag enablement now requires the client to have wired the
  venue signer bridge, so a remote flag cannot register a provider whose
  signer the client never mounted
- Signer-session invalidation covers reload/timeout/not-ready errors
… venue data

- Writes are bound to the wallet account they were INITIATED under: a
  generation captured at method entry aborts a queued write, signer
  setup, or account-channel request that outlives an account switch;
  candle series state is recreated on rebind so live candles keep
  flowing; proofs: queued-write cancellation test, exact new-account
  channel test (user_stats/900 and never user_stats/28)
- Market orders always resolve a fresh venue price as the sizing
  reference (the caller's snapshot can no longer satisfy its own drift
  check), usdAmount converts at the reference price rather than the
  protection price, and the protection offset applies only to the
  signed execution price; leverage update and order placement share one
  write-lock acquisition so no concurrent write can interleave
- isFullClose is a hint, never trusted: below-minimum bumps require the
  live position to verify a full close; a false claim is rejected
- Fills carry the venue's per-side realized pnl and the capitalized
  Buy/Sell direction vocabulary client transforms recognize
- Max leverage, maintenance margin, and market maxLeverage come from
  the venue's per-market margin fractions (orderBookDetails) instead of
  the 50x constant
- Signer bridge exposes onReset; the provider invalidates its session
  the moment the bridge resets instead of on the next failed call
…emantics, unified fills

- Signer setup is generation-fenced at every await (a stale _createClient
  can no longer clobber the current account's WASM client) and only the
  exact promise that failed is cleared; bridge reset advances the
  generation so pre-reset work aborts
- The write critical section re-fences at each nonce fetch and
  immediately before submission, not only at lock entry
- Frames from a replaced WebSocket are dropped before the router
- Composite writes carry ONE intent generation end to end: closePosition
  reads and places under the same identity, updatePositionTPSL's nested
  cancels inherit it and the whole operation aborts on a switch
- closePosition preserves caller semantics: limit closes with the
  requested price (rejected without one), usdAmount sizing, slippage
  tolerance and price-drift protection ride through placement; only
  market/limit accepted
- One fill adapter serves REST history and the live account_all_trades
  stream: per-side realized pnl, Buy/Sell vocabulary, side-appropriate
  maker/taker fee when the venue includes it; real captured payload is
  the test fixture
- resolveLeverageIntent reads venue state only; invalid usdAmount is
  rejected instead of falling back; a missing live market price fails
  closed; market data and position adapters use per-market margin
  fractions; liquidation preview clearly marked single-position only

Adversarial regression tests for each reviewer scenario: deferred
account-A signer setup cannot overwrite B, a paused write never signs
after B initializes, stale socket frames are ignored, closePosition and
TPSL abort mid-sequence switches, limit/market close params verified.
…st venue data

- Signer creation AND venue-key registration run inside the venue write
  lock: a stale previous-account _createClient aborts at the lock's
  fence before touching the bridge singleton, no other account's setup
  or write can interleave, and registration signs/submits through the
  fenced nonce+submit helpers with generation checks at every await
- #assertSession also notices a wallet switch nothing has rebound yet
  (live address comparison, not only the lazily-advanced generation);
  nextNonce re-fences after the fetch resolves
- Fill lifecycle derived from the venue's position-before context:
  Open Long/Short, Close Long/Short, and flips (Long > Short) from
  absolute size-before + sign-changed + trade side, with pnl
  disambiguating partial reduces; side-only Buy/Sell only when context
  is absent; real captured payload proves the taker sell is Close Long
- Integer venue fees are treated as unavailable (0) until a captured
  nonzero payload proves their unit — the official model gives no scale
- calculateLiquidationPrice is capability-gated for Lighter: cross
  liquidation needs account-level inputs, so the preview reports
  unavailable instead of a plausible wrong number
- Explicit non-positive leverage rejected at placement and validation;
  validateOrder rejects invalid usdAmount like placeOrder; margin cache
  warmed before REST and WS position adaptation

Race proofs per reviewer spec: stalled A _createClient with B pending
behind the lock (B ends as the actual signer), warmed-signer paused
write that never signs after B initializes, plus the existing switch
fences — all bounded and deterministic.
…st fees, unique client ids

- Session identity is atomic and fail-closed: assertSession cancels on a
  null binding (a configured account index alone can never act), on a
  live-address mismatch (rebinding to the new account before cancelling),
  and on full deselection; disconnect invalidates the whole session so a
  paused write cannot submit after teardown; ensureAccountIndex,
  getAuthToken and account-channel setup are address-aware after every
  await, and a failed/no-account channel setup clears its promise so the
  next bind retries
- Every account-bound read fences its captured identity after its final
  await (account state, open orders, order merge incl. the open leg,
  positions before the empty early-return, fills, funding, history,
  ledger); WS onopen and its deferred auth re-mint are fenced by
  socket+generation+still-wanted channel
- Client order ids come from a synchronous monotonic allocator (venue
  requires uniqueness across ALL markets): no same-millisecond
  collisions under Promise.all, no modulo wrap; grouped TP/SL reserves
  its pair atomically
- Fee honesty completed: only Standard (type 0) accounts are supported —
  resolution fails closed on Premium or unverifiable types, calculateFees
  gates the tier before quoting zero, and the fill adapter refuses OUR
  side's nonzero fee (unverified unit) while keeping fills whose Premium
  counterparty paid the fee
- Historical portfolio capability-gated: PnLEntry carries pool/spot/
  staking flows whose semantics are unverified; a partial reconstruction
  would show false daily history
- Fill lifecycle: break-even partials fall back to side-only vocabulary
  (never asserted Open without evidence); flips carry SIGNED
  startPosition for post-flip sizing
- Validate/execute parity shared: close-shape and live full-close checks
  are the same code in validateOrder, validateClosePosition, closePosition
  and placement; full-close verification is exact (float epsilon), so a
  deliberate 99% dust partial is rejected instead of bumped to 100%

23 new adversarial regressions, all bounded/deterministic.
Comment thread packages/perps-controller/src/utils/lighterAdapter.ts
@geositta
geositta self-requested a review August 28, 2026 21:52

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

Thank you for addressing the prior review. I re-verified the updated branch and confirmed that the deposit route, transaction-status mapping, key-registration behavior, signer-key ownership, account-switch reset, order-book continuity, documented order statuses, and e2e credential handling are now corrected.

I am keeping the review at Request changes for five remaining items:

  • Lighter's maker-only minimums are still applied to market/IOC orders, which rejects valid small taker orders and partial closes.
  • Fill-history time and symbol filters operate on only the newest page, so valid older results can be reported as absent.
  • Newly strict order decoding is not applied transactionally: REST errors become empty state, and a malformed WebSocket snapshot can clear valid cached orders before failing.
  • The public WASM bridge still lets callers independently assert any result type for any function and argument list.
  • The new REST schemas verify that financial decimals are strings but do not verify their numeric grammar or domain before adapters consume them.

These are concrete correctness and contract-boundary issues rather than requests to broaden the feature. The package test suite passes locally and the latest CI is green; additional tests are needed for these failure cases. No React or React Native component code changed in this revision, but the bridge typing directly affects the safety of React Native WebView implementations.

Comment thread packages/perps-controller/src/providers/LighterProvider.ts Outdated
Comment thread packages/perps-controller/src/providers/LighterProvider.ts Outdated
Comment thread packages/perps-controller/src/providers/LighterProvider.ts
Comment thread packages/perps-controller/src/types/lighter-types.ts Outdated
Comment thread packages/perps-controller/src/services/LighterClientService.ts
Comment thread packages/perps-controller/src/providers/LighterProvider.ts
@abretonc7s
abretonc7s enabled auto-merge August 29, 2026 03:35

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

Thank you for the follow-up work. I verified that the maker-only minimum handling, cursor-based fill history, transactional REST/WebSocket order updates, and function-keyed signer bridge are now correctly addressed. The disconnect and cache fences added in the subsequent commits are also well covered.

I cannot approve this revision yet. The remaining issues share one correctness requirement: when authoritative Lighter financial data is missing, malformed, or incomplete, the provider must surface that state rather than substitute plausible values or empty histories. Specifically:

  • Financial decimal schemas validate syntax but not field domains, and documented trade context can still be omitted and converted into zero/neutral history.
  • Liquidation and leverage APIs fabricate 1%/50x risk parameters when market metadata cannot be established.
  • Funding, ledger, and user-history reads convert integrity failures into empty histories.
  • Account-channel setup converts integrity failures into empty account, position, and order state.
  • TP/SL recovery repeatedly stops at the same 1,000 inactive orders without reporting that authoritative history was truncated.

These paths can produce materially incorrect Pro trading state while presenting a successful result, so they need to fail closed before merge. The exact-head package tests and all current CI checks pass; additional tests should exercise malformed/missing financial fields, metadata failure, integrity failures in every history/stream path, and recovery targets beyond the traversal bound. All comments are on code introduced by this PR relative to origin/main.

Comment thread packages/perps-controller/src/services/LighterClientService.ts
Comment thread packages/perps-controller/src/providers/LighterProvider.ts Outdated
Comment thread packages/perps-controller/src/providers/LighterProvider.ts
Comment thread packages/perps-controller/src/providers/LighterProvider.ts
Comment thread packages/perps-controller/src/providers/LighterProvider.ts
geositta
geositta previously approved these changes Aug 31, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

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

) {
throw new Error(
`${LIGHTER_DATA_INTEGRITY_PREFIX} missing authoritative leverage for ${market.symbol}`,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Market list fails on cache skew

Medium Severity

getMarkets now throws an integrity error for the entire catalog when any perp from a fresh getOrderBooks result lacks minInitial in #marginBySymbol. #ensureMarketMargins can still return a TTL-fresh cache that predates a newly listed market, so one missing symbol takes down every Lighter market until the cache expires.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f8a378e. Configure here.

@abretonc7s
abretonc7s added this pull request to the merge queue Aug 31, 2026
Merged via the queue into main with commit 1103332 Aug 31, 2026
47 checks passed
@abretonc7s
abretonc7s deleted the TAT-3766-feat-spike-lighter-perps-integratio branch August 31, 2026 02:05
pull Bot pushed a commit to Reality2byte/core that referenced this pull request Aug 31, 2026
## Description

Reject malformed Lighter WebSocket fill fields and invalid margin
fractions before they reach perps state. Adds coverage for
inactive-history cursor nonprogress found during the final MetaMask#9889 review.

## Changelog

The existing unreleased Lighter entry already covers this pre-release
correction.

## Related issues

Follow-up to MetaMask#9889.

## Testing

- Focused Lighter suites: 368 passed
- Full perps package: 84 suites, 3,675 passed, 40 skipped
- TypeScript, ESLint, Prettier, changelog validation, and diff checks
- Root build

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes leverage derivation and fill ingestion boundaries for Lighter;
overly strict validation could reject otherwise-usable venue rows or
block market/position reads until metadata is fixed.
> 
> **Overview**
> Tightens **Lighter** venue data handling so bad margin metadata and
trade payloads fail before they affect markets, leverage, or fill
streams.
> 
> **Max leverage** is now derived through a shared
`deriveLighterMaxLeverage` helper that requires
`minInitialMarginFraction` to be a safe integer in **1–10,000** and
rejects leverage below **1x**. `getMarkets`, margin cache refresh,
`getMaxLeverage`, and internal leverage lookups use this path; refresh
also validates **maintenance** margin fractions in the same range. REST
parsing swaps generic positive numbers for a **`MarginFractionStruct`**
on initial/default/maintenance margin fields.
> 
> **Fill adaptation** (`adaptFillFromLighterTrade`) adds checks on trade
identifiers, requires **`isMakerAsk` to be a boolean**, and parses
**maker/taker** fees via `parseLighterTradeFee` so malformed
counterparty fee fields invalidate the row while still allowing valid
nonzero counterparty fees. Matching WebSocket fill drops and unit tests
cover these cases.
> 
> Tests add rejection paths for out-of-range/non-integer margin
fractions, malformed fill fields, and **inactive-order history** paging
that advances the cursor with zero rows (TP/SL recovery fails closed).
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
6f8126d. 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 Aug 31, 2026
## Explanation

Releases `@metamask/perps-controller` **14.0.0 → 15.0.0**. The monorepo
version goes **1217.0.0 → 1218.0.0**.

No other package is being published. Perps-controller has no in-monorepo
dependents that need a workspace range bump.

The bump is **major**. Adding a new member to the public
`PerpsProviderType` union is a breaking type change for exhaustive
switches:

- **Added (breaking):** Lighter as a perps venue (initial
implementation, disabled by default)
([MetaMask#9889](MetaMask#9889),
[MetaMask#10015](MetaMask#10015)).
`PerpsProviderType` gains `'lighter'`. Enablement requires client opt-in
(`providerCredentials.lighter.enabled`, or the
`perpsLighterProviderEnabled` remote feature flag plus a client-supplied
`providerCredentials.lighter.signerBridge`). Without a bridge the
provider is read-only. Lighter is off by default, so clients that do not
opt in can upgrade without runtime behavior change; exhaustive
`PerpsProviderType` switches still need a new case to typecheck.
- **Fixed:** Preserve accepted HyperLiquid Scale orders when part of a
batch is rejected ([MetaMask#9989](MetaMask#9989)).

## Changelog

Moved Unreleased entries in `packages/perps-controller/CHANGELOG.md`
under `[15.0.0]`. Folded the pre-release Lighter validation follow-up
(MetaMask#10015) into the Lighter Added entry (as that PR intended). Breaking
entry is prefixed with `**BREAKING:**` and includes adaptation notes.

## References

- Source PRs: [MetaMask#9889](MetaMask#9889),
[MetaMask#10015](MetaMask#10015),
[MetaMask#9989](MetaMask#9989)

- No draft Mobile/Extension adoption PRs required to keep current
clients compiling: Lighter stays disabled until clients opt in. Client
work to enable Lighter (credentials, signer bridge, UI) is separate.

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


Made with [Cursor](https://cursor.com)

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
pull Bot pushed a commit to Reality2byte/core that referenced this pull request Sep 4, 2026
## Explanation

Lighter retains inactive markets for historical identity and can zero
their order minimums and margin fractions. The strict response decoder
rejected those retired rows, which prevented otherwise valid active
markets from loading.

This change accepts zero constraints only for inactive rows, omits
retired markets whose risk metadata cannot produce canonical market
data, and preserves inactive rows with valid metadata as delisted.
Positions left on retired markets remain readable using their current
position leverage as a non-tradable cap. Active markets remain strict
and fail closed on zero or malformed constraints.

Validated with:

- 322 focused Lighter service/provider tests
- Full `@metamask/perps-controller` test suite and Core build
- Targeted ESLint, formatting, and changelog validation
- Mobile yalc integration using package `16.1.0` (`yalc.sig`
`ee73aeec8330f58ce0a6e9c6b4cf42a3`)
- A 36/36-node iOS recipe covering Lighter and Hyperliquid on mainnet
and testnet
- Revalidated Lighter mainnet on iOS `mm-1`: live markets loaded and the
invalid inactive-market metadata error was absent

## References

- Related to https://consensyssoftware.atlassian.net/browse/TAT-3863
- Related to https://consensyssoftware.atlassian.net/browse/TAT-3766
- Unblocks MetaMask/metamask-mobile#34865
- Follow-up to MetaMask#9889 and
MetaMask#10015

## 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 — not applicable; this change is
non-breaking

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Scoped to Lighter public market-metadata parsing and listing; active
markets remain fail-closed on bad constraints, with no changes to
signing, deposits, or position operations.
>
> **Overview**
> Fixes Lighter market loading when the venue returns **inactive** perp
rows with zeroed minimum order sizes or margin fractions—metadata
Lighter keeps for history but that previously made the strict HTTP
decoder reject the whole response and block active markets.
>
> **`LighterClientService`** now allows zero minimums and margin
fractions only when `status === 'inactive'`, with `refine` validators
still requiring positive values for **active** markets.
**`LighterProvider`** adds `isInactiveMarketWithoutUsableRiskMetadata`
to drop those retired rows from `getMarketDataWithPrices` and margin
caching, skips them in `getMarkets` when leverage metadata is omitted
(instead of throwing), and still surfaces inactive markets with valid
risk data as **delisted**. Tests cover both omission and preservation
paths.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
445456c. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
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