fix(perps): tighten Scale ladder contract - #10065
Conversation
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ 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 e841a8b. Configure here.
Automated Review — PR #10065
SummaryThe type-narrowing half of this PR is correct and I verified it end to end. The Full review detailsBlocking findings1. Parity test previews
|
| Source | Reasons emitted |
|---|---|
PerpsController.getScalePriceLadder (src/PerpsController.ts:2944-2984) |
provider_unavailable, provider_not_routable, not_implemented |
AggregatedPerpsProvider.getScalePriceLadder (src/providers/AggregatedPerpsProvider.ts:336-361) |
provider_not_found, not_implemented, provider_not_routable |
HyperLiquidProvider.getScalePriceLadder (src/providers/HyperLiquidProvider.ts:1659-1685) |
provider_not_routable + whatever #getOrderCapabilityMarket returns |
HyperLiquidProvider.#getOrderCapabilityMarket (src/providers/HyperLiquidProvider.ts:1694-1743) |
invalid_symbol, provider_unavailable, market_not_found |
None produce strategy_market_unsupported. The narrowed
HyperLiquidOrderCapabilityMarket unavailable variant stays assignable to
DirectProviderOrderCapabilities, so getOrderCapabilities
(src/providers/HyperLiquidProvider.ts:1645-1649) still type-checks in its
unavailable branch. HyperLiquid never emits that reason at all — it advertises
HYPERLIQUID_ORDER_CAPABILITIES uniformly once the market resolves.
Export surface is additive: ScalePriceLadderUnavailableReason added at
src/index.ts:297; nothing removed or renamed. No runtime behavior change.
Downstream compatibility (Mobile / Extension)
No downstream impact.
- The whole
getScalePriceLadderAPI (feat(perps): add provider-routed Scale price normalization #10021) sits in## [Unreleased]above
## [15.0.0]. Nothing published carries the widereasontype, so narrowing
it before release cannot break a released consumer. - Local clones scanned (
~/dev/metamask/metamask-mobile-{1,2},
~/dev/metamask/metamask-extension-{1,2}; the../metamask-mobile/
../metamask-extensionpaths in the checklist do not exist on this host).
Zero references togetScalePriceLadder,PerpsScalePriceLadder, or
ScalePriceLadderUnavailableReason. - The only downstream
strategy_market_unsupportedhits are Mobile test
fixtures on the order-capabilities path, which this PR does not touch:app/components/UI/Perps/hooks/usePerpsProvider.test.ts:326(mocks
getOrderCapabilities)app/components/UI/Perps/Views/PerpsProMarketView/PerpsProMarketView.view.test.tsx:93
(mocksgetOrderCapabilities)
PerpsOrderCapabilities.reasonkeeps the full
OrderCapabilitiesUnavailableReasonunion, so both stay valid.
- Declared deps: Mobile
@metamask/perps-controller@^15.0.0, Extension
^12.0.0. Package version on this branch is still15.0.0(unreleased).
Semver: additive type export in an unreleased section → minor on next release.
Narrowing an unreleased type is not a breaking change and correctly carries no
**BREAKING:** prefix.
Validation
| Command | Result |
|---|---|
yarn workspace @metamask/perps-controller run jest --no-coverage tests/src/providers/HyperLiquidProvider.strategy-orders.test.ts |
PASS — 288/288 tests, 1 suite, 1.672 s |
yarn workspace @metamask/perps-controller run changelog:validate |
PASS — no output |
yarn eslint <5 changed source/test files> |
PASS — no violations |
yarn build (monorepo root) |
PASS — exit 0, ✔ Project built successfully |
grep ScalePriceLadderUnavailableReason packages/perps-controller/dist/**/*.d.cts |
Present at dist/index.d.cts:35 and dist/types/index.d.cts:1319,1328 |
Notes on the build step: yarn workspace @metamask/perps-controller run build
and build:all both fail standalone (ts-bridge project references need sibling
dist/ outputs), which is the documented environment property, not a defect in
this PR. Root yarn build is the correct command and succeeds.
Divergence-of-szDecimals evidence for finding 1 was produced by replaying the
formatHyperLiquidPrice / roundToSignificantFigures logic
(src/utils/hyperLiquidAdapter.ts:561, src/utils/significantFigures.ts:70)
against both fixture values in a scratch script; no repo files were modified.
Per the review contract this was a static-code pass: no project start, no CDP,
no recipes, no screenshots, no runtime QA. No Farmslot evidence was linked to
this PR, so there was none to audit.
abretonc7s
left a comment
There was a problem hiding this comment.
Automated review — see comment above for full details.
Narrow unavailable reasons to reachable states and verify fractional previews match submitted prices. Co-authored-by: Cursor <cursoragent@cursor.com>
Match the shared market lookup type to the reasons it can actually return so Scale ladder declaration builds remain type-safe. Co-authored-by: Cursor <cursoragent@cursor.com>
Associate the Unreleased Scale normalization entry with the current pull request so changelog CI can verify coverage. Co-authored-by: Cursor <cursoragent@cursor.com>
Use one precision-sensitive market for preview and placement, and centralize the direct-provider unavailable reason type. Co-authored-by: Cursor <cursoragent@cursor.com>
Automated Review — PR #10065
SummaryThe one blocking finding from generation 1 is fixed, and both non-blocking Full review detailsPrior findings — revalidation1. Parity test previews
|
| Symbol | szDecimals |
Decimal cap | Ladder for 12.341 → 12.381, count 3 |
|---|---|---|---|
ETH (used) |
4 | 2 | ['12.34', '12.36', '12.38'] |
BTC (old bug) |
3 | 3 | ['12.341', '12.361', '12.381'] |
Five significant figures is never reached at either cap here, so the decimal cap
decides the output. Reintroducing the symbol mismatch now fails the pinned
assertion instead of passing by convergence. Observation 2 from the prior review
(no concrete values pinned) is closed by the same line.
Observation 1 — duplicated Exclude<> — RESOLVED
src/types/index.ts:1625-1634 introduces the named alias and re-expresses the
routed type in terms of it:
export type DirectProviderScalePriceLadderUnavailableReason = Exclude<
DirectProviderOrderCapabilitiesUnavailableReason,
'strategy_market_unsupported'
>;
export type ScalePriceLadderUnavailableReason =
| DirectProviderScalePriceLadderUnavailableReason
| RoutedOrderCapabilitiesUnavailableReason;HyperLiquidProvider.ts:1268-1276 consumes the alias, so the exclusion is stated
once. The new ScalePriceLadderUnavailableReason is structurally identical to the
generation-1 definition: OrderCapabilitiesUnavailableReason is exactly
DirectProvider… | Routed… (src/types/index.ts:1599-1601) and
'strategy_market_unsupported' lives only in the direct half
(src/types/index.ts:1588-1592), so distributing the Exclude<> changes nothing
about the resolved union. No consumer-visible drift from generation 1.
New findings on the incremental delta
None blocking.
Non-blocking observations
src/providers/HyperLiquidProvider.ts:1269-1276— the unavailable variant of
HyperLiquidOrderCapabilityMarketis now hand-written (status/providerId
/reason) instead of derived via
Extract<DirectProviderOrderCapabilities, { status: 'unavailable' }>. That was
the price of narrowingreason, but it means a future field added to the
shared unavailable shape will not reach this type.Omit<Extract<…>, 'reason'> & { reason: DirectProviderScalePriceLadderUnavailableReason }would keep the
derivation; readability is arguably worse. Cosmetic either way.- The test's new
currentPrice: 12.36/usdAmount: '300'overrides are load
bearing —baseOrderis priced at3000, and without them the per-rung
notional falls under the minimum order value at the new bounds. No comment
explains that, so a later edit could strip them as noise. One trailing comment
would protect the fixture.
Correctness (re-verified on the current head)
The generation-1 conclusion still holds: no ladder code path can emit
strategy_market_unsupported. Reasons reachable per source —
PerpsController.getScalePriceLadder (src/PerpsController.ts:2944-2984):
provider_unavailable, provider_not_routable, not_implemented;
AggregatedPerpsProvider.getScalePriceLadder
(src/providers/AggregatedPerpsProvider.ts:336-361): provider_not_found,
not_implemented, provider_not_routable;
HyperLiquidProvider.getScalePriceLadder (src/providers/HyperLiquidProvider.ts:1656-1682):
provider_not_routable plus whatever #getOrderCapabilityMarket
(:1691-1740) returns — invalid_symbol, provider_unavailable,
market_not_found. None is the excluded reason.
getOrderCapabilities (src/providers/HyperLiquidProvider.ts:1642-1647) still
returns the narrowed unavailable variant in its DirectProviderOrderCapabilities
slot; the root build type-checks it, confirming assignability survived the
inlining.
PerpsController.ts:111 keeps importing OrderCapabilitiesUnavailableReason —
still used at :3003 by #getUnavailableOrderCapabilities, so no orphaned
import. ESLint agrees.
Export surface is additive only: DirectProviderScalePriceLadderUnavailableReason
and ScalePriceLadderUnavailableReason added at src/index.ts:297-298. Nothing
removed or renamed. No runtime behavior change anywhere in the diff.
Downstream compatibility (Mobile / Extension)
No downstream impact.
- The entire
getScalePriceLadderAPI (feat(perps): add provider-routed Scale price normalization #10021) is still under## [Unreleased]
above## [15.0.0], and the package version on this branch is15.0.0. Nothing
published carries the widereasontype, so narrowing it pre-release cannot
break a released consumer. - Clones scanned:
../metamask-mobile-{1,2},../metamask-extension-{1,2}. The
literal../metamask-mobile/../metamask-extensionpaths in the checklist do
not exist on this host. Zero references togetScalePriceLadder,
PerpsScalePriceLadder,ScalePriceLadderUnavailableReason,
DirectProviderScalePriceLadderUnavailableReason, or
OrderCapabilitiesUnavailableReasonin any of them. - The only downstream
strategy_market_unsupportedhits are two Mobile test
fixtures on the untouched order-capabilities path:
app/components/UI/Perps/hooks/usePerpsProvider.test.ts:326and
app/components/UI/Perps/Views/PerpsProMarketView/PerpsProMarketView.view.test.tsx:93,
both mockinggetOrderCapabilities.PerpsOrderCapabilities.reasonkeeps the
fullOrderCapabilitiesUnavailableReasonunion, so both stay valid. - Semver: two additive type exports in an unreleased section → minor on next
release. Narrowing an unreleased type is not breaking and correctly carries no
**BREAKING:**prefix. The changelog entry was updated to name both new types.
Validation
| Command | Result |
|---|---|
yarn workspace @metamask/perps-controller run jest --no-coverage tests/src/providers/HyperLiquidProvider.strategy-orders.test.ts -t 'submits the provider preview prices for fractional bounds' |
PASS — 1 passed, 287 skipped |
yarn workspace @metamask/perps-controller run jest --no-coverage tests/src/providers/HyperLiquidProvider.strategy-orders.test.ts |
PASS — 288/288, 1.326 s |
yarn workspace @metamask/perps-controller run changelog:validate |
PASS — exit 0, no output |
yarn eslint <5 changed source/test files> |
PASS — exit 0, no violations |
yarn build (monorepo root) |
PASS — ✔ Project built successfully |
grep DirectProviderScalePriceLadderUnavailableReason packages/perps-controller/dist/**/*.d.{cts,mts} |
Present at dist/index.d.cts:35, dist/index.d.mts:35, dist/types/index.d.cts:1319,1321 |
Root yarn build was run because the delta adds a public type export; per-package
build / build:all cannot run standalone here (ts-bridge project references),
which is a documented property of this monorepo, not a defect in the PR.
Per the review contract this was a static-code pass: no project start, no CDP, no
recipes, no screenshots, no runtime QA. No Farmslot evidence is linked to this PR,
so there was none to audit.
abretonc7s
left a comment
There was a problem hiding this comment.
Automated review — see comment above for full details.
b19ebca to
caa9bfe
Compare

Explanation
The Scale price ladder API introduced in #10021 reused
OrderCapabilitiesUnavailableReason, which exposedstrategy_market_unsupportedeven though no ladder path can return that reason. This made exhaustive consumer handling include an impossible state. The original tests also verified fractional preview formatting and integer placement separately, but did not directly prove that fractional preview prices match submitted wire prices.This PR:
ScalePriceLadderUnavailableReason, excluding the capability-onlystrategy_market_unsupportedreason.PerpsScalePriceLadderand the controller's Scale-specific unavailable response helper.getScalePriceLadderoutput with the prices sent by Scale placement.Runtime behavior and dependencies are unchanged.
References
Validation
@metamask/perps-controllertest suiteChecklist
Note
Low Risk
Type-level contract tightening and tests only; placement and routing behavior are unchanged, with minor compile-time impact for exhaustive handling of ladder unavailable reasons.
Overview
Tightens the Scale price ladder public contract so unavailable results no longer reuse the broader order-capabilities reason union.
PerpsScalePriceLadderandPerpsControllerScale-specific unavailable helpers now useScalePriceLadderUnavailableReasonandDirectProviderScalePriceLadderUnavailableReason, which drop the capability-onlystrategy_market_unsupportedreason that ladder APIs cannot return. The new types are exported from the package entry; HyperLiquid’s internal capability lookup typing is aligned with the direct-provider ladder reason.Adds a HyperLiquid strategy-order test that
getScalePriceLadderpreview prices for fractional bounds match the prices submitted onplaceOrderfor a scale ladder. Updates the Unreleased changelog to document the exported reason types.Reviewed by Cursor Bugbot for commit caa9bfe. Bugbot is set up for automated code reviews on this repo. Configure here.