Commit 785ff8c
authored
feat(perps): [perps-controller] Add Chase, TWAP, and Scale order types (#9832)
## Explanation
`@metamask/perps-controller` supported six order types — `market`,
`limit`, and the four trigger
placements — all of which resolve to a single order submitted through
one code path. Chase, TWAP and
Scale are execution *strategies*: one request expands into a schedule of
orders, and each needs a
different submission path, a different cancellation path, and rules of
its own. None of that was
expressible.
This adds `twap`, `scale` and `chase` to `OrderType`, with the
parameters, validation, placement and
cancellation each needs.
**Protocol research (the ticket's open question).** Answered from the
SDK the package already depends
on, `@nktkas/hyperliquid@0.33.1`: TWAP **is** native (`twapOrder` /
`twapCancel`, with their own
endpoints); Scale is **not** — it is a batch of ordinary limit orders;
Chase is **not** — no such
action exists anywhere in the SDK, so it is emulated client-side. The
SDK schema also pins the TWAP
window to a whole number of minutes in `[5, 1440]`, stricter than the
ticket's "twapDuration > 0", so
validation enforces the real bound.
**What each placement does.**
- **TWAP** is submitted through the venue's TWAP action rather than the
order book, and returns the
venue's TWAP id. Cancelling it uses the TWAP cancel endpoint, never the
order-book cancel.
- **Scale** fans out `scaleNumOrders` limit orders on an inclusive
ladder between `scaleMinPrice` and
`scaleMaxPrice`, submitted as one batch so the ladder rests together or
fails together. Sizes are
split in whole units of the asset's size grid, so the rungs sum to
exactly the submitted size.
Children come back as `OrderResult.childOrderIds`; the returned handle
cancels all of them at once.
- **Chase** rests a post-only order at the near touch and returns a
session handle immediately; a
background tick re-prices it as the touch moves, stopping at the
repricing cap, at the window
deadline, when the order leaves the book, on cancel, or on
`disconnect()`.
**Notable API effects.** `OrderType` is a wider union, which is breaking
in the same way the trigger
types were in 11.0.0: consumer signatures that narrow it back to a
smaller set must widen. `OrderParams`
gains eight optional strategy fields, `OrderResult` gains
`childOrderIds`, and `CancelOrderParams`
gains an optional `orderType` that selects the cancellation path —
omitting it, which every existing
caller does, keeps today's behaviour exactly. Invalid strategy
parameters (inverted scale range,
out-of-range TWAP duration, a strategy field on a non-strategy order, a
limit price on a strategy) are
rejected with a typed `PERPS_ERROR_CODES` value before any network call.
The public model stays provider-agnostic — no venue vocabulary reaches
`OrderParams`,
`OrderResult` or `CancelOrderParams`. The one genuinely venue-specific
constant is named for its
venue, `HYPERLIQUID_TWAP_LIMITS`, beside the existing
`HYPERLIQUID_ORDER_LIMITS`.
One incidental fix: `TriggerOrderType` was `Exclude<OrderType, 'market'
| 'limit'>`, so the three new
members would have been silently absorbed into the trigger union and
started demanding a trigger
price. It is now spelled out. The resolved type is unchanged for
existing consumers.
**Validation.** 108 new unit tests across three suites, driving the real
provider against a stubbed
exchange client and asserting the exact actions submitted — that a TWAP
reaches the TWAP action and
not the order action, that its cancel reaches the TWAP cancel endpoint
and not the order cancel, the
exact scale ladder prices and the size split, the post-only chase
placement at the touch and its
re-pricing loop, and that no exchange call is made for an invalid
placement. The full package suite
passes with coverage thresholds met, the root build emits the new
symbols, and the six pre-existing
order suites pass unmodified. A live read against HyperLiquid testnet
confirms the controller still
instantiates and reads positions, orders and account state with the
widened union in place.
**Follow-ups, deliberately out of scope.** Strategy-handle correlation
is session-scoped (children
remain cancellable via `childOrderIds` after a restart); TWAP progress
is not surfaced in controller
state, because the venue reports TWAPs through feeds the open-orders
normalisation does not read; and
strategy placements are refused on sub-exchange (HIP-3) markets, whose
pre-order margin transfer and
rollback are wired into the single-order submit path.
## References
- https://consensyssoftware.atlassian.net/browse/TAT-3723
- Client follow-up: mobile/extension order forms need to send the new
fields and to pass `orderType`
when cancelling a strategy handle. UI for these order types is
explicitly out of scope here.
## 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
- The `OrderType` widening is breaking in the same way as the trigger
types in 11.0.0 and is marked
**BREAKING** in the changelog, but no client draft PRs are prepared yet:
the client work is a
separate ticket, and no consumer in this repo narrows `OrderType`.
## **Screenshots/Recordings**
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **High Risk**
> Widens the public `OrderType` union (breaking) and adds new signed
trading paths, including a client-side chase loop with disconnect/cancel
races and concurrency limits.
>
> **Overview**
> Adds **strategy order types** `twap`, `scale`, and `chase` to
`OrderType`, so one `placeOrder` request can expand into an execution
schedule instead of a single resting order.
>
> **TWAP** goes through HyperLiquid’s native TWAP action/cancel.
**Scale** submits a batch limit ladder and tracks children under a group
handle. **Chase** is emulated client-side: a post-only order rests one
tick inside the spread and is cancel/replaced as the touch moves,
bounded by interval, duration, and repricing caps.
>
> `OrderResult.orderId` is a strategy *handle* for these types, with
exchange ids in `childOrderIds`. `CancelOrderParams.orderType` selects
the strategy cancel path; omitting it keeps ordinary single-order
cancel. `editOrder` rejects strategy edits. Invalid strategy params fail
with new typed `PERPS_ERROR_CODES` before signing.
>
> Also **breaks** consumers that narrow `OrderType`, narrows
single-order helpers/`closePosition` to `OrdinaryOrderType`, and spells
out `TriggerOrderType` so the new types are not pulled into the trigger
union.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
2a22d0e. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->1 parent f2cf82d commit 785ff8c
14 files changed
Lines changed: 6673 additions & 242 deletions
File tree
- packages/perps-controller
- src
- constants
- providers
- types
- utils
- tests/src
- providers
- utils
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
10 | 57 | | |
11 | 58 | | |
12 | 59 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
260 | 260 | | |
261 | 261 | | |
262 | 262 | | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
263 | 267 | | |
264 | 268 | | |
265 | 269 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
113 | 113 | | |
114 | 114 | | |
115 | 115 | | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
116 | 146 | | |
117 | 147 | | |
118 | 148 | | |
| |||
252 | 282 | | |
253 | 283 | | |
254 | 284 | | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
255 | 307 | | |
256 | 308 | | |
257 | 309 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
158 | 158 | | |
159 | 159 | | |
160 | 160 | | |
| 161 | + | |
| 162 | + | |
161 | 163 | | |
162 | 164 | | |
163 | 165 | | |
| |||
437 | 439 | | |
438 | 440 | | |
439 | 441 | | |
| 442 | + | |
440 | 443 | | |
441 | 444 | | |
442 | 445 | | |
443 | 446 | | |
| 447 | + | |
444 | 448 | | |
445 | 449 | | |
446 | 450 | | |
| |||
482 | 486 | | |
483 | 487 | | |
484 | 488 | | |
| 489 | + | |
| 490 | + | |
485 | 491 | | |
| 492 | + | |
486 | 493 | | |
487 | 494 | | |
488 | 495 | | |
489 | 496 | | |
490 | 497 | | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
491 | 502 | | |
492 | 503 | | |
493 | 504 | | |
| |||
0 commit comments