Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/perps-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **BREAKING:** Add `PerpsController.previewPositionModify` and `PerpsProvider.previewPositionModify` so clients can read an isolated-margin post-trade projection without placing an order ([#9968](https://github.com/MetaMask/core/pull/9968))
- Mobile supplies the live position and proposed order; the HyperLiquid provider fetches the asset's margin table and applies selected leverage to the whole resulting position (matching `updateLeverage` before placement).
- The result is a discriminated union (`none` / `unsupported` / `full_close` / `open`) so a non-modifying preview cannot carry a flip kind and a full close cannot report remaining size. Margin and liquidation availability are independent: a missing live liquidation or missing multi-tier table withholds only liquidation.
- Isolated increases, leverage changes (up or down), reductions, flips, and full closes are projected for both longs and shorts. `price` is the expected fill or resting limit; the preview does not distinguish order types. Same-direction `reduceOnly` and increases/flips without a positive price return `{ status: 'none' }`. `resulting.leverage` is mark notional / remaining isolated margin. Liquidation uses the projected mark (not average entry) because isolated `marginUsed` is mark-based equity. A missing margin-table identity withholds liquidation rather than inventing a single-tier schedule. Aggregated providers route by `providerId` / `position.providerId`. Cross-margin returns `{ status: 'unsupported', reason: 'cross_margin' }`. MYX returns `{ status: 'unsupported', reason: 'provider' }`.
- Consumers that implement `PerpsProvider` must add `previewPositionModify`. Clients should use `resulting.direction` (not the order direction) when validating TP/SL against the projected liquidation.

## [13.0.0]

### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,19 @@ export type PerpsControllerCalculateLiquidationPriceAction = {
handler: PerpsController['calculateLiquidationPrice'];
};

/**
* Project the isolated position that would remain after a proposed order.
* Margin and liquidation availability are independent: a missing liquidation
* does not hide a valid margin projection. Cross-margin returns unsupported.
*
* @param params - Live position plus the proposed order.
* @returns Discriminated preview of the resulting position.
*/
export type PerpsControllerPreviewPositionModifyAction = {
type: `PerpsController:previewPositionModify`;
handler: PerpsController['previewPositionModify'];
};

/**
* Calculate maintenance margin for a specific asset
* Returns a percentage (e.g., 0.0125 for 1.25%)
Expand Down Expand Up @@ -1311,6 +1324,7 @@ export type PerpsControllerMethodActions =
| PerpsControllerGetAvailableDexsAction
| PerpsControllerFetchHistoricalCandlesAction
| PerpsControllerCalculateLiquidationPriceAction
| PerpsControllerPreviewPositionModifyAction
| PerpsControllerCalculateMaintenanceMarginAction
| PerpsControllerGetMaxLeverageAction
| PerpsControllerValidateOrderAction
Expand Down
23 changes: 23 additions & 0 deletions packages/perps-controller/src/PerpsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ import type {
LiquidationPriceParams,
LiveDataConfig,
MaintenanceMarginParams,
PositionModifyPreviewParams,
PositionModifyPreviewResult,
MarginResult,
MarketInfo,
Order,
Expand Down Expand Up @@ -976,6 +978,7 @@ const MESSENGER_EXPOSED_METHODS = [
'markFirstOrderCompleted',
'markTutorialCompleted',
'placeOrder',
'previewPositionModify',
'reconnect',
'recordMarketViewed',
'refreshEligibility',
Expand Down Expand Up @@ -4820,6 +4823,26 @@ export class PerpsController extends BaseController<
});
}

/**
* Project the isolated position that would remain after a proposed order.
* Margin and liquidation availability are independent: a missing liquidation
* does not hide a valid margin projection. Cross-margin returns unsupported.
*
* @param params - Live position plus the proposed order.
* @returns Discriminated preview of the resulting position.
*/
async previewPositionModify(
params: PositionModifyPreviewParams,
): Promise<PositionModifyPreviewResult> {
const provider = this.getActiveProvider();
const context = this.#createServiceContext('previewPositionModify');
return this.#marketDataService.previewPositionModify({
provider,
params,
context,
});
}

/**
* Calculate maintenance margin for a specific asset
* Returns a percentage (e.g., 0.0125 for 1.25%)
Expand Down
19 changes: 19 additions & 0 deletions packages/perps-controller/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export type {
PerpsControllerCalculateFeesAction,
PerpsControllerCalculateLiquidationPriceAction,
PerpsControllerCalculateMaintenanceMarginAction,
PerpsControllerPreviewPositionModifyAction,
PerpsControllerCancelOrderAction,
PerpsControllerCancelOrdersAction,
PerpsControllerClearDepositResultAction,
Expand Down Expand Up @@ -269,6 +270,16 @@ export type {
SubscribeOrderBookParams,
LiquidationPriceParams,
MaintenanceMarginParams,
PositionModifyPreviewParams,
PositionModifyPreviewResult,
PositionModifyPreviewSource,
PositionModifyPreviewKind,
PositionPreviewValue,
PositionModifyPreviewCurrent,
PositionModifyPreviewOpen,
PositionModifyPreviewFullClose,
PositionModifyPreviewUnsupported,
PositionModifyPreviewNone,
FeeCalculationParams,
FeeCalculationResult,
GetOrderCapabilitiesParams,
Expand Down Expand Up @@ -648,6 +659,14 @@ export {
parseAssetName,
adaptHyperLiquidLedgerUpdateToUserHistoryItem,
} from './utils/index.js';
export {
previewHyperLiquidIsolatedPositionModify,
resolveHyperLiquidMarginTiers,
buildMaintenanceSchedule,
estimateIsolatedLiquidationPrice,
estimateIsolatedLiquidationPriceAtTier,
} from './utils/index.js';
export type { HyperLiquidMarginTier } from './utils/index.js';
export { getEnvironment } from './utils/index.js';
export type { FiatRangeConfig } from './utils/index.js';
export {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ import type {
LiquidationPriceParams,
LiveDataConfig,
MaintenanceMarginParams,
PositionModifyPreviewParams,
PositionModifyPreviewResult,
MarginResult,
MarketInfo,
Order,
Expand Down Expand Up @@ -752,6 +754,15 @@ export class AggregatedPerpsProvider implements PerpsProvider {
return provider.calculateFees(params);
}

async previewPositionModify(
params: PositionModifyPreviewParams,
): Promise<PositionModifyPreviewResult> {
const [, provider] = this.#getProviderOrDefault(
params.providerId ?? params.position.providerId,
);
return provider.previewPositionModify(params);
}
Comment thread
cursor[bot] marked this conversation as resolved.

// ============================================================================
// Subscriptions (Multiplex via SubscriptionMultiplexer)
// ============================================================================
Expand Down
52 changes: 52 additions & 0 deletions packages/perps-controller/src/providers/HyperLiquidProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ import type {
LiquidationPriceParams,
LiveDataConfig,
MaintenanceMarginParams,
PositionModifyPreviewParams,
PositionModifyPreviewResult,
MarginResult,
MarketInfo,
Order,
Expand Down Expand Up @@ -176,6 +178,10 @@ import {
HYPERLIQUID_SCALE_CLOID_MARKER,
parseAssetName,
} from '../utils/hyperLiquidAdapter.js';
import {
previewHyperLiquidIsolatedPositionModify,
resolveHyperLiquidMarginTiers,
} from '../utils/hyperLiquidPositionPreview.js';
import {
createErrorResult,
getMaxOrderValue,
Expand Down Expand Up @@ -12941,6 +12947,52 @@ export class HyperLiquidProvider implements PerpsProvider {
}
}

/**
* Project the isolated position that would remain after a proposed order.
*
* Fetches the asset's margin table from cached meta so liquidation uses the
* maintenance tier at the resulting liquidation notional. Cross-margin
* positions return unsupported without a table lookup.
*
* @param params - Live position plus the proposed order.
* @returns Discriminated preview; margin and liquidation are independently available.
*/
async previewPositionModify(
params: PositionModifyPreviewParams,
): Promise<PositionModifyPreviewResult> {
if (params.position.leverage.type === 'cross') {
return { status: 'unsupported', reason: 'cross_margin' };
}

const { dex: dexName } = parseAssetName(params.position.symbol);
let marginTiers = null;

try {
const meta = await this.#getCachedMeta({ dexName });
const assetInfo = meta.universe.find(
(universeItem) => universeItem.name === params.position.symbol,
);
marginTiers = resolveHyperLiquidMarginTiers({
marginTableId: assetInfo?.marginTableId,
maxLeverage: assetInfo?.maxLeverage ?? params.position.maxLeverage,
marginTables: meta.marginTables,
});
} catch (error) {
this.#deps.debugLogger.log(
'HyperLiquidProvider: margin table unavailable for position preview',
{
symbol: params.position.symbol,
error,
},
);
}

return previewHyperLiquidIsolatedPositionModify({
...params,
marginTiers,
});
}

/**
* Calculate liquidation price using HyperLiquid's formula
* Formula: liq_price = price - side * margin_available / position_size / (1 - maintenanceMarginRatio * side)
Expand Down
8 changes: 8 additions & 0 deletions packages/perps-controller/src/providers/MYXProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ import type {
LiquidationPriceParams,
LiveDataConfig,
MaintenanceMarginParams,
PositionModifyPreviewParams,
PositionModifyPreviewResult,
MarginResult,
MarketInfo,
Order,
Expand Down Expand Up @@ -927,6 +929,12 @@ export class MYXProvider implements PerpsProvider {
};
}

async previewPositionModify(
_params: PositionModifyPreviewParams,
): Promise<PositionModifyPreviewResult> {
return { status: 'unsupported', reason: 'provider' };
}

// ============================================================================
// Subscriptions (Stage 1 - No-op)
// ============================================================================
Expand Down
34 changes: 34 additions & 0 deletions packages/perps-controller/src/services/MarketDataService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import type {
GetAvailableDexsParams,
LiquidationPriceParams,
MaintenanceMarginParams,
PositionModifyPreviewParams,
PositionModifyPreviewResult,
FeeCalculationParams,
FeeCalculationResult,
OrderParams,
Expand Down Expand Up @@ -1208,6 +1210,38 @@ export class MarketDataService {
}
}

/**
* Project the position that would remain after a proposed order.
*
* @param options - The configuration options.
* @param options.provider - The perps provider instance.
* @param options.params - Live position plus the proposed order.
* @param options.context - The service context for dependencies.
* @returns Discriminated preview of the resulting position.
*/
async previewPositionModify(options: {
provider: PerpsProvider;
params: PositionModifyPreviewParams;
context: ServiceContext;
}): Promise<PositionModifyPreviewResult> {
const { provider, params } = options;

try {
return await provider.previewPositionModify(params);
} catch (error) {
this.#deps.logger.error(
ensureError(error, 'MarketDataService.previewPositionModify'),
{
context: {
name: 'MarketDataService.previewPositionModify',
data: { params },
},
},
);
throw error;
}
}

/**
* Calculate maintenance margin for a position
*
Expand Down
Loading