diff --git a/packages/common/src/atoms/edit-position-atoms.ts b/packages/common/src/atoms/edit-position-atoms.ts index 9171492..3885548 100644 --- a/packages/common/src/atoms/edit-position-atoms.ts +++ b/packages/common/src/atoms/edit-position-atoms.ts @@ -1,8 +1,8 @@ -import { Atom, Registry, Result } from "@effect-atom/atom-react"; -import { Effect, Option } from "effect"; +import { Registry, Result } from "@effect-atom/atom-react"; +import { Effect, flow, Match, Option } from "effect"; +import { defined } from "effect/Match"; import type { TPOrSLConfiguration, - TPOrSLOption, TPOrSLSettings, } from "../components/molecules/tp-sl-dialog"; import type { WalletConnected } from "../domain/wallet"; @@ -15,59 +15,101 @@ import { runtimeAtom } from "../services/runtime"; import { actionAtom } from "./actions-atoms"; import { selectedProviderAtom } from "./providers-atoms"; -export const tpSlArgument = (tpOrSL: TPOrSLConfiguration) => - Option.some(tpOrSL).pipe( - Option.filterMap((v) => - v.triggerPrice !== null && v.option !== null - ? Option.some(v.triggerPrice) - : Option.none(), - ), - Option.getOrUndefined, - ); +export const tpSlArgument = flow( + Option.liftPredicate( + ( + tpOrSL: TPOrSLConfiguration, + ): tpOrSL is TPOrSLConfiguration & { triggerPrice: number } => + tpOrSL.triggerPrice !== null && tpOrSL.option !== null, + ), + Option.map((v) => v.triggerPrice), + Option.getOrUndefined, +); -export const editSLTPAtom = Atom.family((actionType: TPOrSLOption) => - runtimeAtom.fn( - Effect.fn(function* ({ - position, - wallet, - tpOrSLSettings, - }: { - position: PositionDto; - wallet: WalletConnected; - tpOrSLSettings: TPOrSLSettings; - }) { - const client = yield* ApiClientService; - const registry = yield* Registry.AtomRegistry; +export const editSLTPAtom = runtimeAtom.fn( + Effect.fn(function* ({ + position, + wallet, + tpOrSLSettings, + stopLossOrderId, + takeProfitOrderId, + }: { + position: PositionDto; + wallet: WalletConnected; + tpOrSLSettings: TPOrSLSettings; + stopLossOrderId?: string; + takeProfitOrderId?: string; + }) { + const client = yield* ApiClientService; + const registry = yield* Registry.AtomRegistry; - const selectedProvider = registry - .get(selectedProviderAtom) - .pipe(Result.getOrElse(() => null)); + const selectedProvider = registry + .get(selectedProviderAtom) + .pipe(Result.getOrElse(() => null)); - if (!selectedProvider) { - return yield* Effect.dieMessage("No selected provider"); - } + if (!selectedProvider) { + return yield* Effect.dieMessage("No selected provider"); + } - const newStopLossPrice: ArgumentsDto["stopLossPrice"] = tpSlArgument( - tpOrSLSettings.stopLoss, - ); + const newStopLossPrice: ArgumentsDto["stopLossPrice"] = tpSlArgument( + tpOrSLSettings.stopLoss, + ); - const newTakeProfitPrice: ArgumentsDto["takeProfitPrice"] = tpSlArgument( - tpOrSLSettings.takeProfit, - ); + const newTakeProfitPrice: ArgumentsDto["takeProfitPrice"] = tpSlArgument( + tpOrSLSettings.takeProfit, + ); - const action = yield* client.ActionsControllerExecuteAction({ - providerId: selectedProvider.id, - address: wallet.currentAccount.address, - action: actionType, + const actionArgs = Match.value({ + newStopLossPrice, + newTakeProfitPrice, + }).pipe( + Match.withReturnType<{ + action: "setTpAndSl" | "takeProfit" | "stopLoss"; + args: ArgumentsDto; + } | null>(), + Match.when( + { newStopLossPrice: defined, newTakeProfitPrice: defined }, + (v) => ({ + action: "setTpAndSl", + args: { + stopLossPrice: v.newStopLossPrice, + takeProfitPrice: v.newTakeProfitPrice, + ...(stopLossOrderId && { stopLossOrderId }), + ...(takeProfitOrderId && { takeProfitOrderId }), + }, + }), + ), + Match.when({ newTakeProfitPrice: defined }, () => ({ + action: "takeProfit", + args: { + takeProfitPrice: newTakeProfitPrice, + ...(takeProfitOrderId && { orderId: takeProfitOrderId }), + }, + })), + Match.when({ newStopLossPrice: defined }, () => ({ + action: "stopLoss", args: { - marketId: position.marketId, - ...(actionType === "stopLoss" - ? { stopLossPrice: newStopLossPrice } - : { takeProfitPrice: newTakeProfitPrice }), + stopLossPrice: newStopLossPrice, + ...(stopLossOrderId && { orderId: stopLossOrderId }), }, - }); + })), + Match.orElse(() => null), + ); - registry.set(actionAtom, action); - }), - ), + if (!actionArgs) { + return yield* Effect.dieMessage("No TP/SL settings provided"); + } + + const action = yield* client.ActionsControllerExecuteAction({ + providerId: selectedProvider.id, + address: wallet.currentAccount.address, + action: actionArgs.action, + args: { + marketId: position.marketId, + ...actionArgs.args, + }, + }); + + registry.set(actionAtom, action); + }), ); diff --git a/packages/common/src/atoms/position-pending-actions-atom.ts b/packages/common/src/atoms/position-pending-actions-atom.ts index 786bbe8..16458fa 100644 --- a/packages/common/src/atoms/position-pending-actions-atom.ts +++ b/packages/common/src/atoms/position-pending-actions-atom.ts @@ -1,59 +1,13 @@ import { Registry, Result } from "@effect-atom/atom-react"; import { Effect } from "effect"; -import type { - TPOrSLOption, - TPOrSLSettings, -} from "../components/molecules/tp-sl-dialog"; -import type { WalletAccount, WalletConnected } from "../domain/wallet"; +import type { WalletConnected } from "../domain/wallet"; import { ApiClientService } from "../services/api-client"; import type { PositionDto } from "../services/api-client/api-schemas"; import { runtimeAtom } from "../services/runtime"; import { actionAtom } from "./actions-atoms"; -import { tpSlArgument } from "./edit-position-atoms"; import { selectedProviderAtom } from "./providers-atoms"; -export const editSLOrTPAtom = runtimeAtom.fn( - Effect.fn(function* ({ - position, - walletAddress, - tpOrSLSettings, - actionType, - }: { - position: PositionDto; - walletAddress: WalletAccount["address"]; - tpOrSLSettings: TPOrSLSettings; - actionType: TPOrSLOption; - }) { - const client = yield* ApiClientService; - const registry = yield* Registry.AtomRegistry; - - const selectedProvider = registry - .get(selectedProviderAtom) - .pipe(Result.getOrElse(() => null)); - - if (!selectedProvider) { - return yield* Effect.dieMessage("No selected provider"); - } - - const newStopLossPrice = tpSlArgument(tpOrSLSettings.stopLoss); - - const newTakeProfitPrice = tpSlArgument(tpOrSLSettings.takeProfit); - - const action = yield* client.ActionsControllerExecuteAction({ - providerId: selectedProvider.id, - address: walletAddress, - action: actionType, - args: { - marketId: position.marketId, - ...(actionType === "stopLoss" - ? { stopLossPrice: newStopLossPrice } - : { takeProfitPrice: newTakeProfitPrice }), - }, - }); - - registry.set(actionAtom, action); - }), -); +export type UpdateMarginMode = "add" | "remove"; export const updateLeverageAtom = runtimeAtom.fn( Effect.fn(function* ({ diff --git a/packages/common/src/components/molecules/sign-transactions/index.tsx b/packages/common/src/components/molecules/sign-transactions/index.tsx index 4e2f717..0236ee0 100644 --- a/packages/common/src/components/molecules/sign-transactions/index.tsx +++ b/packages/common/src/components/molecules/sign-transactions/index.tsx @@ -320,14 +320,16 @@ function formatTransactionType(type: string): string { OPEN_POSITION: "Open Position", CLOSE_POSITION: "Close Position", UPDATE_LEVERAGE: "Update Leverage", + UPDATE_MARGIN: "Update Margin", STOP_LOSS: "Update Stop Loss", TAKE_PROFIT: "Update Take Profit", CANCEL_ORDER: "Cancel Order", FUND: "Fund Account", WITHDRAW: "Withdraw", + SET_TP_AND_SL: "Set TP and SL", }; - return typeMap[type] || type; + return typeMap[type] || formatSnakeCase(type.toLowerCase()); } function getErrorDescription(error: SignTransactionsState["error"]): string { diff --git a/packages/common/src/components/molecules/toggle-group.tsx b/packages/common/src/components/molecules/toggle-group.tsx index 1de3e20..b35f2ad 100644 --- a/packages/common/src/components/molecules/toggle-group.tsx +++ b/packages/common/src/components/molecules/toggle-group.tsx @@ -3,37 +3,37 @@ import { ToggleGroup as BaseToggleGroup } from "@base-ui/react/toggle-group"; import type { ComponentProps } from "react"; import { cn } from "../../lib/utils"; -export interface ToggleOption { - value: string; +export interface ToggleOption { + value: T; label: string; } -export interface ToggleGroupProps +export interface ToggleGroupProps extends Omit< ComponentProps, "value" | "onValueChange" > { - options: ToggleOption[]; - value: string; - onValueChange: (value: string) => void; + options: ToggleOption[]; + value: T; + onValueChange: (value: T) => void; variant?: "dark" | "light" | "compact"; } -export function ToggleGroup({ +export function ToggleGroup({ options, value, onValueChange, variant = "dark", className, ...props -}: ToggleGroupProps) { +}: ToggleGroupProps) { return ( { const newValue = values[values.length - 1]; if (newValue) { - onValueChange(newValue); + onValueChange(newValue as T); } }} className={cn( diff --git a/packages/common/src/hooks/use-edit-position.ts b/packages/common/src/hooks/use-edit-position.ts index aefbd3b..3e64ff0 100644 --- a/packages/common/src/hooks/use-edit-position.ts +++ b/packages/common/src/hooks/use-edit-position.ts @@ -1,25 +1,18 @@ -import { useAtomSet, useAtomValue } from "@effect-atom/atom-react"; +import { useAtom } from "@effect-atom/atom-react"; import { editSLTPAtom } from "../atoms/edit-position-atoms"; import { updateLeverageAtom } from "../atoms/position-pending-actions-atom"; export const useEditSLTP = () => { - const editTPResult = useAtomValue(editSLTPAtom("takeProfit")); - const editTP = useAtomSet(editSLTPAtom("takeProfit")); - - const editSLResult = useAtomValue(editSLTPAtom("stopLoss")); - const editSL = useAtomSet(editSLTPAtom("stopLoss")); + const [editSLTPResult, editSLTP] = useAtom(editSLTPAtom); return { - editTPResult, - editTP, - editSLResult, - editSL, + editSLTPResult, + editSLTP, }; }; export const useUpdateLeverage = () => { - const updateLeverageResult = useAtomValue(updateLeverageAtom); - const updateLeverage = useAtomSet(updateLeverageAtom); + const [updateLeverageResult, updateLeverage] = useAtom(updateLeverageAtom); return { updateLeverageResult, diff --git a/packages/common/src/hooks/use-position-actions.ts b/packages/common/src/hooks/use-position-actions.ts index 7180101..0bbd547 100644 --- a/packages/common/src/hooks/use-position-actions.ts +++ b/packages/common/src/hooks/use-position-actions.ts @@ -8,18 +8,24 @@ const UpdateLeverageSchema = Schema.Struct({ }), }); -const TpSlSchema = Schema.Struct({ - type: Schema.Literal("stopLoss", "takeProfit"), +const SetTpAndSlSchema = Schema.Struct({ + type: Schema.Literal("setTpAndSl"), args: Schema.Struct({ marketId: Schema.String, - orderId: Schema.optional(Schema.String), + stopLossOrderId: Schema.optional(Schema.String), + takeProfitOrderId: Schema.optional(Schema.String), }), }); -const PendingActionSchema = Schema.Union(UpdateLeverageSchema, TpSlSchema); +const PendingActionSchema = Schema.Union( + UpdateLeverageSchema, + SetTpAndSlSchema, +); -export const usePositionActions = (position: PositionDto) => { - return position.pendingActions.reduce( +export const getPositionActions = ( + pendingActions: PositionDto["pendingActions"], +) => { + return pendingActions.reduce( (acc, pa) => { const decoded = Schema.decodeUnknownOption(PendingActionSchema)(pa).pipe( Option.getOrNull, @@ -31,11 +37,8 @@ export const usePositionActions = (position: PositionDto) => { Match.when({ type: "updateLeverage" }, (v) => { acc.updateLeverage = v; }), - Match.when({ type: "stopLoss" }, (v) => { - acc.stopLoss = v; - }), - Match.when({ type: "takeProfit" }, (v) => { - acc.takeProfit = v; + Match.when({ type: "setTpAndSl" }, (v) => { + acc.setTpAndSl = v; }), ); @@ -43,12 +46,14 @@ export const usePositionActions = (position: PositionDto) => { }, { updateLeverage: null, - stopLoss: null, - takeProfit: null, + setTpAndSl: null, } as { updateLeverage: typeof UpdateLeverageSchema.Type | null; - stopLoss: typeof TpSlSchema.Type | null; - takeProfit: typeof TpSlSchema.Type | null; + setTpAndSl: typeof SetTpAndSlSchema.Type | null; }, ); }; + +export const usePositionActions = (position: PositionDto) => { + return getPositionActions(position.pendingActions); +}; diff --git a/packages/common/src/lib/math.ts b/packages/common/src/lib/math.ts index cda7017..11cbf9d 100644 --- a/packages/common/src/lib/math.ts +++ b/packages/common/src/lib/math.ts @@ -221,6 +221,59 @@ export const getPriceChangePercentToLiquidation = ({ pastOrFuturePrice: liquidationPrice, }); +export const getSignedUpdateMarginAmount = ({ + amount, + mode, +}: { + amount: number | string; + mode: "add" | "remove"; +}) => { + const parsedAmount = typeof amount === "string" ? Number(amount) : amount; + const absoluteAmount = Math.abs(parsedAmount); + + if (!Number.isFinite(absoluteAmount) || absoluteAmount <= 0) { + return null; + } + + return (mode === "add" ? absoluteAmount : -absoluteAmount).toString(); +}; + +/** + * Estimates liquidation price after changing isolated margin while keeping + * position size constant. + * + * This is a UI approximation based on current notional / projected margin. + */ +export const getEstimatedLiquidationPriceForProjectedMargin = ({ + position, + projectedMargin, +}: { + position: Pick; + projectedMargin: number; +}) => { + if (projectedMargin <= 0) return null; + + const notionalUsd = calcNotionalUsd({ + priceUsd: position.markPrice, + sizeBase: position.size, + }); + + if (notionalUsd <= 0) return null; + + const estimatedLeverage = Math.max( + MIN_LEVERAGE, + notionalUsd / projectedMargin, + ); + + const liquidationPrice = getLiquidationPrice({ + currentPrice: position.markPrice, + leverage: estimatedLeverage, + side: position.side, + }); + + return Number.isFinite(liquidationPrice) ? liquidationPrice : null; +}; + /** * Maps a leverage into slider percent space between `MIN_LEVERAGE` and `maxLeverage`. */ diff --git a/packages/common/src/services/index.ts b/packages/common/src/services/index.ts index 7e9c012..e15755d 100644 --- a/packages/common/src/services/index.ts +++ b/packages/common/src/services/index.ts @@ -1,6 +1,5 @@ export * from "./api-client"; export * as ApiSchemas from "./api-client/api-schemas"; -// export type * from "./api-client/client-factory"; export type * as ApiTypes from "./api-client/client-factory"; export * from "./config"; export * from "./constants"; diff --git a/packages/common/tests/adjust-margin.test.ts b/packages/common/tests/adjust-margin.test.ts new file mode 100644 index 0000000..1eb20b5 --- /dev/null +++ b/packages/common/tests/adjust-margin.test.ts @@ -0,0 +1,83 @@ +import { describe, expect, test } from "vitest"; +import { + getEstimatedLiquidationPriceForProjectedMargin, + getSignedUpdateMarginAmount, +} from "../src/lib"; + +describe("adjust margin helpers", () => { + test("lower long liquidation price when adding margin", () => { + const position = { + markPrice: 4_000, + side: "long" as const, + size: "1", + }; + + const lowerRiskPrice = getEstimatedLiquidationPriceForProjectedMargin({ + position, + projectedMargin: 800, + }); + const higherRiskPrice = getEstimatedLiquidationPriceForProjectedMargin({ + position, + projectedMargin: 400, + }); + + expect(lowerRiskPrice).not.toBeNull(); + expect(higherRiskPrice).not.toBeNull(); + + if (lowerRiskPrice === null || higherRiskPrice === null) { + throw new Error("Expected liquidation price estimates"); + } + + expect(lowerRiskPrice).toBeLessThan(higherRiskPrice); + }); + + test("raise short liquidation price when adding margin", () => { + const position = { + markPrice: 4_000, + side: "short" as const, + size: "1", + }; + + const lowerRiskPrice = getEstimatedLiquidationPriceForProjectedMargin({ + position, + projectedMargin: 800, + }); + const higherRiskPrice = getEstimatedLiquidationPriceForProjectedMargin({ + position, + projectedMargin: 400, + }); + + expect(lowerRiskPrice).not.toBeNull(); + expect(higherRiskPrice).not.toBeNull(); + + if (lowerRiskPrice === null || higherRiskPrice === null) { + throw new Error("Expected liquidation price estimates"); + } + + expect(lowerRiskPrice).toBeGreaterThan(higherRiskPrice); + }); + + test("return null for invalid projected margin", () => { + const position = { + markPrice: 4_000, + side: "long" as const, + size: "1", + }; + + expect( + getEstimatedLiquidationPriceForProjectedMargin({ + position, + projectedMargin: 0, + }), + ).toBeNull(); + }); + + test("format update margin amounts for add and remove flows", () => { + expect(getSignedUpdateMarginAmount({ amount: "125.5", mode: "add" })).toBe( + "125.5", + ); + expect(getSignedUpdateMarginAmount({ amount: 125.5, mode: "remove" })).toBe( + "-125.5", + ); + }); +}); diff --git a/packages/dashboard/src/components/molecules/positions/index.tsx b/packages/dashboard/src/components/molecules/positions/index.tsx index e078335..f5eb1c9 100644 --- a/packages/dashboard/src/components/molecules/positions/index.tsx +++ b/packages/dashboard/src/components/molecules/positions/index.tsx @@ -20,10 +20,6 @@ import { OrdersTabWithWallet } from "./orders-tab"; import { PositionsTabWithWallet } from "./positions-tab"; import { TableDisconnected } from "./shared"; -interface PositionsTableProps { - className?: string; -} - const PositionsTabLabel = ({ wallet }: { wallet: WalletConnected }) => { const positionsResult = useAtomValue( positionsAtom(wallet.currentAccount.address), @@ -50,7 +46,7 @@ const OrdersTabLabel = ({ wallet }: { wallet: WalletConnected }) => { ); }; -export function PositionsTable({ className }: PositionsTableProps) { +export function PositionsTable({ className }: { className?: string }) { const wallet = useAtomValue(walletAtom).pipe(Result.value, Option.getOrNull); const walletConnected = isWalletConnected(wallet); diff --git a/packages/dashboard/src/components/molecules/positions/positions-tab.tsx b/packages/dashboard/src/components/molecules/positions/positions-tab.tsx index 916d9e1..5d26d18 100644 --- a/packages/dashboard/src/components/molecules/positions/positions-tab.tsx +++ b/packages/dashboard/src/components/molecules/positions/positions-tab.tsx @@ -14,7 +14,6 @@ import { LeverageDialog, Text, TPOrSLDialog, - type TPOrSLOption, type TPOrSLSettings, } from "@yieldxyz/perps-common/components"; import type { WalletConnected } from "@yieldxyz/perps-common/domain"; @@ -184,7 +183,7 @@ function PositionRow({ const position = useAtomRef(positionRef); const market = useAtomRef(marketRef); const { updateLeverage } = useUpdateLeverage(); - const { editTP, editSL } = useEditSLTP(); + const { editSLTP } = useEditSLTP(); const setSelectedMarket = useAtomSet(selectedMarketAtom); const positionActions = usePositionActions(position); @@ -217,15 +216,14 @@ function PositionRow({ }), }; - const handleAutoCloseSubmit = ( - settings: TPOrSLSettings, - actionType: TPOrSLOption, - ) => { - if (actionType === "takeProfit") { - editTP({ position, wallet, tpOrSLSettings: settings }); - } else { - editSL({ position, wallet, tpOrSLSettings: settings }); - } + const handleAutoCloseSubmit = (settings: TPOrSLSettings) => { + editSLTP({ + position, + wallet, + tpOrSLSettings: settings, + stopLossOrderId: positionActions.setTpAndSl?.args.stopLossOrderId, + takeProfitOrderId: positionActions.setTpAndSl?.args.takeProfitOrderId, + }); }; const handleLeverageChange = (newLeverage: number) => { @@ -346,17 +344,14 @@ function PositionRow({ {/* TP column */} - {positionActions.takeProfit ? ( + {positionActions.setTpAndSl ? ( - handleAutoCloseSubmit(settings, "takeProfit") - } + onSettingsChange={handleAutoCloseSubmit} entryPrice={position.entryPrice} currentPrice={position.markPrice} liquidationPrice={position.liquidationPrice} side={position.side} - mode="takeProfit" > + + {Result.isSuccess(submitResult) && ( + + )} + + ); +} + +function AdjustMarginRouteWithWallet({ wallet }: { wallet: WalletConnected }) { + const { marketId } = useParams({ + from: "/position-details/$marketId/adjust-margin", + }); + const { mode } = useSearch({ + from: "/position-details/$marketId/adjust-margin", + }); + const selectedMode = mode ?? "add"; + const formKey = makeAdjustMarginFormKey({ + walletAddress: wallet.currentAccount.address, + marketId, + mode: selectedMode, + }); + const market = useAtomValue(marketAtom(marketId)); + const position = useAdjustMarginPosition(formKey); + const providerBalance = useSelectedProviderBalanceResult( + wallet.currentAccount.address, + ); + + if ( + Result.isInitial(market) || + Result.isInitial(position) || + Result.isInitial(providerBalance) + ) { + return ; + } + + if ( + !Result.isSuccess(market) || + !Result.isSuccess(position) || + !Result.isSuccess(providerBalance) + ) { + return ; + } + + return ( + + ); +} + +export function AdjustMarginRoute() { + return ( + + {(wallet) => } + + ); +} diff --git a/packages/widget/src/components/modules/PositionDetails/AdjustMargin/loading.tsx b/packages/widget/src/components/modules/PositionDetails/AdjustMargin/loading.tsx new file mode 100644 index 0000000..fcedc6c --- /dev/null +++ b/packages/widget/src/components/modules/PositionDetails/AdjustMargin/loading.tsx @@ -0,0 +1,28 @@ +import { Skeleton } from "@yieldxyz/perps-common/components"; + +export function AdjustMarginLoading() { + return ( +
+
+ + +
+ + +
+
+ +
+
+ + + +
+ + +
+ + +
+ ); +} diff --git a/packages/widget/src/components/modules/PositionDetails/AdjustMargin/sign.tsx b/packages/widget/src/components/modules/PositionDetails/AdjustMargin/sign.tsx new file mode 100644 index 0000000..5f0dbf5 --- /dev/null +++ b/packages/widget/src/components/modules/PositionDetails/AdjustMargin/sign.tsx @@ -0,0 +1,5 @@ +import { SignTransactionsRoute } from "../../../molecules/sign"; + +export function AdjustMarginSignRoute() { + return ; +} diff --git a/packages/widget/src/components/modules/PositionDetails/AdjustMargin/state.tsx b/packages/widget/src/components/modules/PositionDetails/AdjustMargin/state.tsx new file mode 100644 index 0000000..25a7319 --- /dev/null +++ b/packages/widget/src/components/modules/PositionDetails/AdjustMargin/state.tsx @@ -0,0 +1,290 @@ +import { + Atom, + Registry, + Result, + useAtomSet, + useAtomValue, +} from "@effect-atom/atom-react"; +import { FormBuilder, FormReact } from "@lucas-barake/effect-form-react"; +import { + actionAtom, + positionsAtom, + selectedProviderAtom, + selectedProviderBalancesAtom, + type UpdateMarginMode, +} from "@yieldxyz/perps-common/atoms"; +import { AmountField, ToggleGroup } from "@yieldxyz/perps-common/components"; +import { + type WalletAccount, + WalletAccountAddress, +} from "@yieldxyz/perps-common/domain"; +import { + clampPercent, + getEstimatedLiquidationPriceForProjectedMargin, + getSignedUpdateMarginAmount, + percentOf, + round, + valueFromPercent, +} from "@yieldxyz/perps-common/lib"; +import { + ApiClientService, + type ApiTypes, + runtimeAtom, +} from "@yieldxyz/perps-common/services"; +import { Effect, Option, Record, Schema } from "effect"; + +export const AdjustMarginFormKey = Schema.Struct({ + walletAddress: WalletAccountAddress, + marketId: Schema.String, + mode: Schema.Literal("add", "remove"), +}).pipe(Schema.Data, Schema.brand("AdjustMarginFormKey")); + +export type AdjustMarginFormKey = typeof AdjustMarginFormKey.Type; + +export const makeAdjustMarginFormKey = Schema.decodeSync(AdjustMarginFormKey); + +const adjustMarginPositionAtom = Atom.family( + (args: typeof AdjustMarginFormKey.Type) => + runtimeAtom.atom( + Effect.fn(function* (ctx) { + const positions = yield* ctx.result(positionsAtom(args.walletAddress)); + + const positionRef = Record.get(positions, args.marketId); + + if (positionRef._tag === "None") { + return yield* Effect.dieMessage("Position not found"); + } + + return positionRef.value.value; + }), + ), +); + +const adjustMarginFormAtom = Atom.family( + (key: typeof AdjustMarginFormKey.Type) => { + const adjustMarginFormBuilder = FormBuilder.empty + .addField( + "Amount", + Schema.NumberFromString.pipe( + Schema.annotations({ message: () => "Invalid amount" }), + Schema.greaterThan(0, { message: () => "Must be greater than 0" }), + ), + ) + .addField( + "Mode", + Schema.Literal("add", "remove").pipe( + Schema.annotations({ message: () => "Invalid mode" }), + ), + ) + .refineEffect((values) => + Effect.gen(function* () { + const registry = yield* Registry.AtomRegistry; + + const providerBalance = registry + .get(selectedProviderBalancesAtom(key.walletAddress)) + .pipe(Result.getOrElse(() => null)); + + const position = registry + .get(adjustMarginPositionAtom(key)) + .pipe(Result.getOrElse(() => null)); + + if (!providerBalance) { + return { path: ["Amount"], message: "Missing provider balance" }; + } + + if (!position) { + return { path: ["Amount"], message: "Missing position" }; + } + + if (position.marginMode !== "isolated") { + return { + path: ["Amount"], + message: "Only isolated positions can adjust margin", + }; + } + + const maxAmount = + values.Mode === "add" + ? providerBalance.availableBalance + : position.margin; + + if (maxAmount <= 0) { + return { + path: ["Amount"], + message: + values.Mode === "add" + ? "No available balance" + : "No margin to remove", + }; + } + + if (values.Amount > maxAmount) { + return { + path: ["Amount"], + message: + values.Mode === "add" + ? "Insufficient balance" + : "Amount exceeds position margin", + }; + } + }), + ); + + return FormReact.make(adjustMarginFormBuilder, { + runtime: runtimeAtom, + fields: { + Amount: AmountField, + Mode: ({ field }) => ( + + ), + }, + onSubmit: (_, { decoded }) => + Effect.gen(function* () { + const registry = yield* Registry.AtomRegistry; + + const position = registry + .get(adjustMarginPositionAtom(key)) + .pipe(Result.getOrElse(() => null)); + + if (!position) { + return yield* Effect.dieMessage("Position not found"); + } + + const client = yield* ApiClientService; + + const selectedProvider = registry + .get(selectedProviderAtom) + .pipe(Result.getOrElse(() => null)); + + if (!selectedProvider) { + return yield* Effect.dieMessage("No selected provider"); + } + + const signedAmount = getSignedUpdateMarginAmount({ + amount: decoded.Amount, + mode: decoded.Mode, + }); + + if (!signedAmount) { + return yield* Effect.dieMessage("Invalid margin amount"); + } + + const action = yield* client.ActionsControllerExecuteAction({ + providerId: selectedProvider.id, + address: key.walletAddress, + action: "updateMargin", + args: { + marketId: position.marketId, + amount: signedAmount, + }, + }); + + registry.set(actionAtom, action); + }), + }); + }, +); + +export const getAdjustMarginCalculations = ({ + position, + availableBalance, + amount, + mode, +}: { + position: ApiTypes.PositionDto; + availableBalance: number; + amount: number; + mode: UpdateMarginMode; +}) => { + const signedMarginDelta = mode === "add" ? amount : -amount; + const projectedMargin = position.margin + signedMarginDelta; + const maxAmount = mode === "add" ? availableBalance : position.margin; + + return { + maxAmount, + projectedMargin, + currentMargin: position.margin, + currentLiquidationPrice: position.liquidationPrice, + estimatedLiquidationPrice: getEstimatedLiquidationPriceForProjectedMargin({ + position, + projectedMargin, + }), + }; +}; + +export const useAdjustMarginPosition = ( + key: typeof AdjustMarginFormKey.Type, +) => { + return useAtomValue(adjustMarginPositionAtom(key)); +}; + +export const useAdjustMargin = ({ + key, + position, + availableBalance, + mode, +}: { + key: typeof AdjustMarginFormKey.Type; + position: ApiTypes.PositionDto; + availableBalance: number; + mode: UpdateMarginMode; +}) => { + const AdjustMarginForm = adjustMarginFormAtom(key); + + const amountFieldAtom = AdjustMarginForm.getFieldAtoms( + AdjustMarginForm.fields.Amount, + ); + + const setAmount = useAtomSet(amountFieldAtom.setValue); + + const amount = useAtomValue(amountFieldAtom.value).pipe( + Option.map(Number), + Option.filter((value) => !Number.isNaN(value)), + Option.getOrElse(() => 0), + ); + + const maxAmount = mode === "add" ? availableBalance : position.margin; + + const handlePercentageChange = (newPercentage: number) => { + if (newPercentage >= 100) { + return setAmount(round(maxAmount, 6).toString()); + } + + const nextAmount = valueFromPercent({ + total: maxAmount, + percent: newPercentage, + }); + + setAmount(round(nextAmount, 6).toString()); + }; + + const percentage = clampPercent( + round(percentOf({ part: amount, whole: maxAmount }), 2), + ); + + const submit = useAtomSet(AdjustMarginForm.submit); + const submitResult = useAtomValue(AdjustMarginForm.submit); + + return { + AdjustMarginForm, + amount, + mode, + submit, + submitResult, + handlePercentageChange, + percentage, + }; +}; + +export const useSelectedProviderBalanceResult = ( + walletAddress: WalletAccount["address"], +) => { + return useAtomValue(selectedProviderBalancesAtom(walletAddress)); +}; diff --git a/packages/widget/src/components/modules/PositionDetails/Overview/Position/index.tsx b/packages/widget/src/components/modules/PositionDetails/Overview/Position/index.tsx index 358c2ca..b091e13 100644 --- a/packages/widget/src/components/modules/PositionDetails/Overview/Position/index.tsx +++ b/packages/widget/src/components/modules/PositionDetails/Overview/Position/index.tsx @@ -18,7 +18,6 @@ import { Skeleton, Text, TPOrSLDialog, - type TPOrSLOption, type TPOrSLSettings, } from "@yieldxyz/perps-common/components"; import { @@ -41,6 +40,7 @@ import { } from "@yieldxyz/perps-common/lib"; import type { ApiTypes } from "@yieldxyz/perps-common/services"; import { Option, Record } from "effect"; +import { AdjustMarginDialog } from "../adjust-margin-dialog"; function PositionCardContent({ positionRef, @@ -54,7 +54,7 @@ function PositionCardContent({ wallet: WalletConnected; }) { const position = useAtomRef(positionRef); - const { editTPResult, editTP, editSLResult, editSL } = useEditSLTP(); + const { editSLTPResult, editSLTP } = useEditSLTP(); const { updateLeverageResult, updateLeverage } = useUpdateLeverage(); const positionActions = usePositionActions(position); @@ -75,15 +75,14 @@ function PositionCardContent({ }), }; - const handleAutoCloseSubmit = ( - settings: TPOrSLSettings, - actionType: TPOrSLOption, - ) => { - if (actionType === "takeProfit") { - editTP({ position, wallet, tpOrSLSettings: settings }); - } else { - editSL({ position, wallet, tpOrSLSettings: settings }); - } + const handleAutoCloseSubmit = (settings: TPOrSLSettings) => { + editSLTP({ + position, + wallet, + tpOrSLSettings: settings, + stopLossOrderId: positionActions.setTpAndSl?.args.stopLossOrderId, + takeProfitOrderId: positionActions.setTpAndSl?.args.takeProfitOrderId, + }); }; const handleLeverageChange = (newLeverage: number) => { @@ -105,7 +104,7 @@ function PositionCardContent({ }); const isPnlPositive = position.unrealizedPnl >= 0; - if (Result.isSuccess(editTPResult) || Result.isSuccess(editSLResult)) { + if (Result.isSuccess(editSLTPResult)) { return ( -
- {positionActions.takeProfit && ( - - handleAutoCloseSubmit(settings, "takeProfit") - } - entryPrice={position.entryPrice} - currentPrice={position.markPrice} - liquidationPrice={position.liquidationPrice} - side={position.side} - mode="takeProfit" - > - - - )} - {positionActions.stopLoss && ( +
+ {positionActions.setTpAndSl && ( - handleAutoCloseSubmit(settings, "stopLoss") - } + onSettingsChange={handleAutoCloseSubmit} entryPrice={position.entryPrice} currentPrice={position.markPrice} liquidationPrice={position.liquidationPrice} side={position.side} - mode="stopLoss" > )} @@ -287,6 +263,10 @@ function PositionCardContent({ )}
+ {position.marginMode === "isolated" && ( + + )} + + ( + + )} + /> + + + + + + + + Adjust Margin + +
+ {ADJUST_MARGIN_OPTIONS.map((option) => { + const Icon = option.icon; + + return ( + +
+ +
+
+ + {option.label} + + + {option.description} + +
+ + ); + })} +
+
+
+
+ + ); +} diff --git a/packages/widget/src/components/modules/PositionDetails/Overview/index.tsx b/packages/widget/src/components/modules/PositionDetails/Overview/index.tsx index cec5da0..5e406e7 100644 --- a/packages/widget/src/components/modules/PositionDetails/Overview/index.tsx +++ b/packages/widget/src/components/modules/PositionDetails/Overview/index.tsx @@ -4,7 +4,12 @@ import { useAtomRef, useAtomValue, } from "@effect-atom/atom-react"; -import { Link, useParams, useSearch } from "@tanstack/react-router"; +import { + Link, + useNavigate, + useParams, + useSearch, +} from "@tanstack/react-router"; import hyperliquidLogo from "@yieldxyz/perps-common/assets/hyperliquid.png"; import { marketAtom, @@ -33,7 +38,6 @@ import { } from "@yieldxyz/perps-common/lib"; import type { ApiTypes } from "@yieldxyz/perps-common/services"; import { Option, Record } from "effect"; -import { useState } from "react"; import { BackButton } from "../../../molecules/navigation/back-button"; import { PositionDetailsLoading } from "./loading"; import { ModifyDialog } from "./modify-dialog"; @@ -41,6 +45,8 @@ import { OrdersTabContent } from "./Orders"; import { OverviewTabContent } from "./overview-tab-content"; import { PositionTabContent } from "./Position"; +type PositionDetailsTab = "overview" | "position" | "orders"; + function BottomButtonsWithWallet({ wallet, market, @@ -120,13 +126,13 @@ function BottomButtons({ market }: { market: ApiTypes.MarketDto }) { function PositionDetailsContent({ marketRef, - initialTab, + activeTab, }: { marketRef: AtomRef.AtomRef; - initialTab?: "overview" | "position" | "orders"; + activeTab: PositionDetailsTab; }) { const market = useAtomRef(marketRef); - const [activeTab, setActiveTab] = useState(initialTab ?? "overview"); + const navigate = useNavigate({ from: "/position-details/$marketId/" }); const symbol = market.baseAsset.symbol; const logo = market.baseAsset.logoURI ?? getTokenLogo(symbol); @@ -183,7 +189,10 @@ function PositionDetailsContent({ - setActiveTab(value as "overview" | "position" | "orders") + navigate({ + replace: true, + search: (prev) => ({ ...prev, tab: value }), + }) } className="gap-2.5" > @@ -238,7 +247,10 @@ export function PositionDetails() { } return ( - + ); } diff --git a/packages/widget/src/routeTree.gen.ts b/packages/widget/src/routeTree.gen.ts index 8304fd6..f39d1cd 100644 --- a/packages/widget/src/routeTree.gen.ts +++ b/packages/widget/src/routeTree.gen.ts @@ -15,6 +15,7 @@ import { Route as AccountWithdrawRouteImport } from './routes/account/withdraw' import { Route as AccountDepositRouteImport } from './routes/account/deposit' import { Route as PositionDetailsMarketIdIndexRouteImport } from './routes/position-details/$marketId/index' import { Route as PositionDetailsMarketIdCloseRouteImport } from './routes/position-details/$marketId/close' +import { Route as PositionDetailsMarketIdAdjustMarginRouteImport } from './routes/position-details/$marketId/adjust-margin' import { Route as AccountWithdrawSignRouteImport } from './routes/account/withdraw_/sign' import { Route as AccountDepositSignRouteImport } from './routes/account/deposit_/sign' import { Route as PositionDetailsMarketIdEditLeverageIndexRouteImport } from './routes/position-details/$marketId/edit-leverage_/index' @@ -22,6 +23,7 @@ import { Route as OrderMarketIdSideIndexRouteImport } from './routes/order/$mark import { Route as PositionDetailsMarketIdEditSlTpSignRouteImport } from './routes/position-details/$marketId/edit-sl-tp_/sign' import { Route as PositionDetailsMarketIdCloseSignRouteImport } from './routes/position-details/$marketId/close_/sign' import { Route as PositionDetailsMarketIdCancelOrderSignRouteImport } from './routes/position-details/$marketId/cancel-order_/sign' +import { Route as PositionDetailsMarketIdAdjustMarginSignRouteImport } from './routes/position-details/$marketId/adjust-margin_/sign' import { Route as OrderMarketIdSideSignRouteImport } from './routes/order/$marketId/$side/sign' import { Route as OrderMarketIdSideIncreaseIndexRouteImport } from './routes/order/$marketId/$side/increase_/index' @@ -57,6 +59,12 @@ const PositionDetailsMarketIdCloseRoute = path: '/position-details/$marketId/close', getParentRoute: () => rootRouteImport, } as any) +const PositionDetailsMarketIdAdjustMarginRoute = + PositionDetailsMarketIdAdjustMarginRouteImport.update({ + id: '/position-details/$marketId/adjust-margin', + path: '/position-details/$marketId/adjust-margin', + getParentRoute: () => rootRouteImport, + } as any) const AccountWithdrawSignRoute = AccountWithdrawSignRouteImport.update({ id: '/account/withdraw_/sign', path: '/account/withdraw/sign', @@ -96,6 +104,12 @@ const PositionDetailsMarketIdCancelOrderSignRoute = path: '/position-details/$marketId/cancel-order/sign', getParentRoute: () => rootRouteImport, } as any) +const PositionDetailsMarketIdAdjustMarginSignRoute = + PositionDetailsMarketIdAdjustMarginSignRouteImport.update({ + id: '/position-details/$marketId/adjust-margin_/sign', + path: '/position-details/$marketId/adjust-margin/sign', + getParentRoute: () => rootRouteImport, + } as any) const OrderMarketIdSideSignRoute = OrderMarketIdSideSignRouteImport.update({ id: '/order/$marketId/$side/sign', path: '/order/$marketId/$side/sign', @@ -115,9 +129,11 @@ export interface FileRoutesByFullPath { '/account/': typeof AccountIndexRoute '/account/deposit/sign': typeof AccountDepositSignRoute '/account/withdraw/sign': typeof AccountWithdrawSignRoute + '/position-details/$marketId/adjust-margin': typeof PositionDetailsMarketIdAdjustMarginRoute '/position-details/$marketId/close': typeof PositionDetailsMarketIdCloseRoute '/position-details/$marketId/': typeof PositionDetailsMarketIdIndexRoute '/order/$marketId/$side/sign': typeof OrderMarketIdSideSignRoute + '/position-details/$marketId/adjust-margin/sign': typeof PositionDetailsMarketIdAdjustMarginSignRoute '/position-details/$marketId/cancel-order/sign': typeof PositionDetailsMarketIdCancelOrderSignRoute '/position-details/$marketId/close/sign': typeof PositionDetailsMarketIdCloseSignRoute '/position-details/$marketId/edit-sl-tp/sign': typeof PositionDetailsMarketIdEditSlTpSignRoute @@ -132,9 +148,11 @@ export interface FileRoutesByTo { '/account': typeof AccountIndexRoute '/account/deposit/sign': typeof AccountDepositSignRoute '/account/withdraw/sign': typeof AccountWithdrawSignRoute + '/position-details/$marketId/adjust-margin': typeof PositionDetailsMarketIdAdjustMarginRoute '/position-details/$marketId/close': typeof PositionDetailsMarketIdCloseRoute '/position-details/$marketId': typeof PositionDetailsMarketIdIndexRoute '/order/$marketId/$side/sign': typeof OrderMarketIdSideSignRoute + '/position-details/$marketId/adjust-margin/sign': typeof PositionDetailsMarketIdAdjustMarginSignRoute '/position-details/$marketId/cancel-order/sign': typeof PositionDetailsMarketIdCancelOrderSignRoute '/position-details/$marketId/close/sign': typeof PositionDetailsMarketIdCloseSignRoute '/position-details/$marketId/edit-sl-tp/sign': typeof PositionDetailsMarketIdEditSlTpSignRoute @@ -150,9 +168,11 @@ export interface FileRoutesById { '/account/': typeof AccountIndexRoute '/account/deposit_/sign': typeof AccountDepositSignRoute '/account/withdraw_/sign': typeof AccountWithdrawSignRoute + '/position-details/$marketId/adjust-margin': typeof PositionDetailsMarketIdAdjustMarginRoute '/position-details/$marketId/close': typeof PositionDetailsMarketIdCloseRoute '/position-details/$marketId/': typeof PositionDetailsMarketIdIndexRoute '/order/$marketId/$side/sign': typeof OrderMarketIdSideSignRoute + '/position-details/$marketId/adjust-margin_/sign': typeof PositionDetailsMarketIdAdjustMarginSignRoute '/position-details/$marketId/cancel-order_/sign': typeof PositionDetailsMarketIdCancelOrderSignRoute '/position-details/$marketId/close_/sign': typeof PositionDetailsMarketIdCloseSignRoute '/position-details/$marketId/edit-sl-tp_/sign': typeof PositionDetailsMarketIdEditSlTpSignRoute @@ -169,9 +189,11 @@ export interface FileRouteTypes { | '/account/' | '/account/deposit/sign' | '/account/withdraw/sign' + | '/position-details/$marketId/adjust-margin' | '/position-details/$marketId/close' | '/position-details/$marketId/' | '/order/$marketId/$side/sign' + | '/position-details/$marketId/adjust-margin/sign' | '/position-details/$marketId/cancel-order/sign' | '/position-details/$marketId/close/sign' | '/position-details/$marketId/edit-sl-tp/sign' @@ -186,9 +208,11 @@ export interface FileRouteTypes { | '/account' | '/account/deposit/sign' | '/account/withdraw/sign' + | '/position-details/$marketId/adjust-margin' | '/position-details/$marketId/close' | '/position-details/$marketId' | '/order/$marketId/$side/sign' + | '/position-details/$marketId/adjust-margin/sign' | '/position-details/$marketId/cancel-order/sign' | '/position-details/$marketId/close/sign' | '/position-details/$marketId/edit-sl-tp/sign' @@ -203,9 +227,11 @@ export interface FileRouteTypes { | '/account/' | '/account/deposit_/sign' | '/account/withdraw_/sign' + | '/position-details/$marketId/adjust-margin' | '/position-details/$marketId/close' | '/position-details/$marketId/' | '/order/$marketId/$side/sign' + | '/position-details/$marketId/adjust-margin_/sign' | '/position-details/$marketId/cancel-order_/sign' | '/position-details/$marketId/close_/sign' | '/position-details/$marketId/edit-sl-tp_/sign' @@ -221,9 +247,11 @@ export interface RootRouteChildren { AccountIndexRoute: typeof AccountIndexRoute AccountDepositSignRoute: typeof AccountDepositSignRoute AccountWithdrawSignRoute: typeof AccountWithdrawSignRoute + PositionDetailsMarketIdAdjustMarginRoute: typeof PositionDetailsMarketIdAdjustMarginRoute PositionDetailsMarketIdCloseRoute: typeof PositionDetailsMarketIdCloseRoute PositionDetailsMarketIdIndexRoute: typeof PositionDetailsMarketIdIndexRoute OrderMarketIdSideSignRoute: typeof OrderMarketIdSideSignRoute + PositionDetailsMarketIdAdjustMarginSignRoute: typeof PositionDetailsMarketIdAdjustMarginSignRoute PositionDetailsMarketIdCancelOrderSignRoute: typeof PositionDetailsMarketIdCancelOrderSignRoute PositionDetailsMarketIdCloseSignRoute: typeof PositionDetailsMarketIdCloseSignRoute PositionDetailsMarketIdEditSlTpSignRoute: typeof PositionDetailsMarketIdEditSlTpSignRoute @@ -276,6 +304,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof PositionDetailsMarketIdCloseRouteImport parentRoute: typeof rootRouteImport } + '/position-details/$marketId/adjust-margin': { + id: '/position-details/$marketId/adjust-margin' + path: '/position-details/$marketId/adjust-margin' + fullPath: '/position-details/$marketId/adjust-margin' + preLoaderRoute: typeof PositionDetailsMarketIdAdjustMarginRouteImport + parentRoute: typeof rootRouteImport + } '/account/withdraw_/sign': { id: '/account/withdraw_/sign' path: '/account/withdraw/sign' @@ -325,6 +360,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof PositionDetailsMarketIdCancelOrderSignRouteImport parentRoute: typeof rootRouteImport } + '/position-details/$marketId/adjust-margin_/sign': { + id: '/position-details/$marketId/adjust-margin_/sign' + path: '/position-details/$marketId/adjust-margin/sign' + fullPath: '/position-details/$marketId/adjust-margin/sign' + preLoaderRoute: typeof PositionDetailsMarketIdAdjustMarginSignRouteImport + parentRoute: typeof rootRouteImport + } '/order/$marketId/$side/sign': { id: '/order/$marketId/$side/sign' path: '/order/$marketId/$side/sign' @@ -349,9 +391,13 @@ const rootRouteChildren: RootRouteChildren = { AccountIndexRoute: AccountIndexRoute, AccountDepositSignRoute: AccountDepositSignRoute, AccountWithdrawSignRoute: AccountWithdrawSignRoute, + PositionDetailsMarketIdAdjustMarginRoute: + PositionDetailsMarketIdAdjustMarginRoute, PositionDetailsMarketIdCloseRoute: PositionDetailsMarketIdCloseRoute, PositionDetailsMarketIdIndexRoute: PositionDetailsMarketIdIndexRoute, OrderMarketIdSideSignRoute: OrderMarketIdSideSignRoute, + PositionDetailsMarketIdAdjustMarginSignRoute: + PositionDetailsMarketIdAdjustMarginSignRoute, PositionDetailsMarketIdCancelOrderSignRoute: PositionDetailsMarketIdCancelOrderSignRoute, PositionDetailsMarketIdCloseSignRoute: PositionDetailsMarketIdCloseSignRoute, diff --git a/packages/widget/src/routes/position-details/$marketId/adjust-margin.tsx b/packages/widget/src/routes/position-details/$marketId/adjust-margin.tsx new file mode 100644 index 0000000..1ddbc6e --- /dev/null +++ b/packages/widget/src/routes/position-details/$marketId/adjust-margin.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { Schema } from "effect"; +import { AdjustMarginRoute } from "../../../components/modules/PositionDetails/AdjustMargin"; + +export const Route = createFileRoute( + "/position-details/$marketId/adjust-margin", +)({ + component: AdjustMarginRoute, + validateSearch: Schema.standardSchemaV1( + Schema.Struct({ + mode: Schema.optional(Schema.Literal("add", "remove")), + }), + ), +}); diff --git a/packages/widget/src/routes/position-details/$marketId/adjust-margin_/sign.tsx b/packages/widget/src/routes/position-details/$marketId/adjust-margin_/sign.tsx new file mode 100644 index 0000000..be2a8ec --- /dev/null +++ b/packages/widget/src/routes/position-details/$marketId/adjust-margin_/sign.tsx @@ -0,0 +1,8 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { AdjustMarginSignRoute } from "../../../../components/modules/PositionDetails/AdjustMargin/sign"; + +export const Route = createFileRoute( + "/position-details/$marketId/adjust-margin_/sign", +)({ + component: AdjustMarginSignRoute, +});