Skip to content
Closed
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
6 changes: 2 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,12 @@
"yieldxyz"
],
"scripts": {
"dev": "turbo dev --filter=@yieldxyz/perps-common --filter=@yieldxyz/perps-widget --filter=@yieldxyz/perps-dashboard",
"dev": "turbo dev",
"dev:widget": "turbo dev --filter=@yieldxyz/perps-common --filter=@yieldxyz/perps-widget",
"dev:dashboard": "turbo dev --filter=@yieldxyz/perps-common --filter=@yieldxyz/perps-dashboard",

"build": "turbo build",
"build:widget": "turbo build --filter=@yieldxyz/perps-widget",
"build:dashboard": "turbo build --filter=@yieldxyz/perps-dashboard",

"build": "turbo build",
"test": "turbo test",
"lint": "turbo lint",
"format": "turbo format",
Expand Down
5 changes: 2 additions & 3 deletions packages/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,12 @@
"dependencies": {
"@base-ui/react": "catalog:",
"@effect-atom/atom-react": "catalog:",
"@effect/experimental": "^0.58.0",
"@effect/experimental": "catalog:",
"@effect/platform": "catalog:",
"@effect/platform-node": "catalog:",
"@ledgerhq/wallet-api-client": "catalog:",
"@lucas-barake/effect-form-react": "catalog:",
"@nktkas/hyperliquid": "catalog:",
"@reown/appkit": "catalog:",
"@reown/appkit-adapter-wagmi": "catalog:",
"@stakekit/common": "catalog:",
Expand All @@ -86,8 +87,6 @@
"devDependencies": {
"@tanstack/devtools-vite": "catalog:",
"@tanstack/router-cli": "catalog:",
"@testing-library/dom": "catalog:",
"@testing-library/react": "catalog:",
"@tim-smart/openapi-gen": "catalog:",
"@types/node": "catalog:",
"@types/react": "catalog:",
Expand Down
5 changes: 1 addition & 4 deletions packages/common/src/atoms/actions-atoms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@ import type { ActionDto } from "../services/api-client/api-schemas";
import { runtimeAtom } from "../services/runtime";
import { portfolioReactivityKeysArray } from "./portfolio-atoms";

export const actionAtom = Atom.writable<ActionDto | null, ActionDto | null>(
() => null,
(ctx, value) => ctx.setSelf(value),
);
export const actionAtom = Atom.make<ActionDto | null>(null);

const getActionAtom = Atom.make(
Effect.fn(function* (ctx) {
Expand Down
8 changes: 8 additions & 0 deletions packages/common/src/atoms/hyperliquid-atoms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Stream } from "effect";
import { HyperliquidService, runtimeAtom } from "../services";

export const midPriceAtom = runtimeAtom.atom(
HyperliquidService.use((service) => service.subscribeMidPrice).pipe(
Stream.unwrapScoped,
),
);
1 change: 1 addition & 0 deletions packages/common/src/atoms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from "./actions-atoms";
export * from "./close-position-atoms";
export * from "./config-atom";
export * from "./edit-position-atoms";
export * from "./hyperliquid-atoms";
export * from "./markets-atoms";
export * from "./order-form-atoms";
export * from "./orders-pending-actions-atom";
Expand Down
49 changes: 47 additions & 2 deletions packages/common/src/atoms/markets-atoms.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import { Atom, AtomRef } from "@effect-atom/atom-react";
import { Data, Duration, Effect, Record, Schedule, Stream } from "effect";
import {
Array as _Array,
Data,
Duration,
Effect,
pipe,
Record,
Schedule,
Stream,
} from "effect";
import { ApiClientService } from "../services/api-client";
import type { ProviderDto } from "../services/api-client/api-schemas";
import { runtimeAtom } from "../services/runtime";
import { midPriceAtom } from "./hyperliquid-atoms";
import { selectedProviderAtom } from "./providers-atoms";

const DEFAULT_LIMIT = 50;
Expand Down Expand Up @@ -50,6 +60,20 @@ export const marketsAtom = runtimeAtom.atom(
}),
);

export const marketsBySymbolAtom = runtimeAtom.atom(
Effect.fn(function* (ctx) {
const markets = yield* ctx.result(marketsAtom);

return pipe(
Record.values(markets),
_Array.map(
(marketRef) => [marketRef.value.baseAsset.symbol, marketRef] as const,
),
Record.fromEntries,
);
}),
);

export class MarketNotFoundError extends Data.TaggedError(
"MarketNotFoundError",
) {}
Expand All @@ -74,7 +98,7 @@ export const refreshMarketsAtom = runtimeAtom.atom(
const selectedProvider = yield* ctx.result(selectedProviderAtom);

yield* Stream.fromSchedule(
Schedule.forever.pipe(Schedule.addDelay(() => Duration.seconds(10))),
Schedule.forever.pipe(Schedule.addDelay(() => Duration.minutes(1))),
).pipe(
Stream.mapEffect(() => getAllMarkets(selectedProvider)),
Stream.tap((markets) =>
Expand All @@ -96,3 +120,24 @@ export const refreshMarketsAtom = runtimeAtom.atom(
);
}),
);

export const updateMarketsMidPriceAtom = runtimeAtom.atom((ctx) =>
Effect.gen(function* () {
const { mids } = yield* ctx.result(midPriceAtom);

const markets = yield* ctx.result(marketsBySymbolAtom);

Record.toEntries(mids).forEach(([symbol, price]) => {
const marketRef = Record.get(markets, symbol);

if (marketRef._tag === "None") {
return;
}

marketRef.value.update((market) => ({
...market,
markPrice: Number(price),
}));
});
}),
);
54 changes: 21 additions & 33 deletions packages/common/src/atoms/order-form-atoms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,24 +32,18 @@ export const ORDER_SLIDER_STOPS = [0, 25, 50, 75, 100];

// Types
export type OrderType = "market" | "limit";
export type OrderSide = ApiTypes.PositionDtoSide;
export type OrderSide = ApiTypes.PositionSide;

// Schemas
export const LeverageRangesSchema = Schema.Data(
ApiSchemas.MarketDto.fields.leverageRange,
).pipe(Schema.brand("LeverageRange"));

// Order Type Atom
export const orderTypeAtom = Atom.writable<OrderType, OrderType>(
() => "market",
(ctx, value) => ctx.setSelf(value),
);
export const orderTypeAtom = Atom.make<OrderType>("market");

// Order Side Atom
export const orderSideAtom = Atom.writable<OrderSide, OrderSide>(
() => "long",
(ctx, value) => ctx.setSelf(value),
);
export const orderSideAtom = Atom.make<OrderSide>("long");

// Leverage Atom (family keyed by leverage ranges)
export const leverageAtom = Atom.family(
Expand All @@ -67,27 +61,21 @@ export const leverageAtom = Atom.family(
);

// TP/SL Settings Atom
export const tpOrSLSettingsAtom = Atom.writable<TPOrSLSettings, TPOrSLSettings>(
() => ({
takeProfit: {
option: null,
triggerPrice: null,
percentage: null,
},
stopLoss: {
option: null,
triggerPrice: null,
percentage: null,
},
}),
(ctx, value) => ctx.setSelf(value),
);
export const tpOrSLSettingsAtom = Atom.make<TPOrSLSettings>({
takeProfit: {
option: null,
triggerPrice: null,
percentage: null,
},
stopLoss: {
option: null,
triggerPrice: null,
percentage: null,
},
});

// Limit Price Atom
export const limitPriceAtom = Atom.writable<number | null, number | null>(
() => null,
(ctx, value) => ctx.setSelf(value),
);
export const limitPriceAtom = Atom.make<number | null>(null);

// Order Form Atom (family keyed by leverage ranges)
export const orderFormAtom = Atom.family(
Expand Down Expand Up @@ -145,7 +133,7 @@ export const orderFormAtom = Atom.family(
}: {
wallet: WalletConnected;
market: ApiSchemas.MarketDto;
side: ApiTypes.PositionDtoSide;
side: ApiTypes.PositionSide;
},
{ decoded },
) =>
Expand Down Expand Up @@ -183,16 +171,16 @@ export const orderFormAtom = Atom.family(
...(stopLossPrice && { stopLossPrice }),
...(takeProfitPrice && { takeProfitPrice }),
...(leverage && { leverage }),
...(limitPrice && { limitPrice: limitPrice }),
...(limitPrice && { limitPrice }),
},
});

registry.set(actionAtom, action);
}),
});

const setAmountFieldAtom = OrderForm.setValue(OrderForm.fields.Amount);
const amountFieldAtom = OrderForm.getFieldValue(OrderForm.fields.Amount);
const { value: amountFieldAtom, setValue: setAmountFieldAtom } =
OrderForm.getFieldAtoms(OrderForm.fields.Amount);

return Atom.readable(() => ({
form: OrderForm,
Expand All @@ -207,7 +195,7 @@ export const getOrderCalculations = (
amount: number,
leverage: number,
market: ApiSchemas.MarketDto,
side: ApiTypes.PositionDtoSide,
side: ApiTypes.PositionSide,
) => {
const cryptoAmount = calcBaseAmountFromUsd({
usdAmount: amount,
Expand Down
37 changes: 34 additions & 3 deletions packages/common/src/atoms/portfolio-atoms.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Atom } from "@effect-atom/atom-react";
import { Duration, Effect } from "effect";
import { Atom, AtomRef } from "@effect-atom/atom-react";
import { Duration, Effect, Record } from "effect";
import type { WalletAccount } from "../domain/wallet";
import { ApiClientService } from "../services/api-client";
import { runtimeAtom, withReactivity } from "../services/runtime";
import { midPriceAtom } from "./hyperliquid-atoms";
import { marketsBySymbolAtom } from "./markets-atoms";
import { providersAtom, selectedProviderAtom } from "./providers-atoms";
import { withRefreshAfter } from "./utils";

Expand All @@ -25,10 +27,15 @@ export const positionsAtom = Atom.family(
const client = yield* ApiClientService;
const selectedProvider = yield* ctx.result(selectedProviderAtom);

return yield* client.PortfolioControllerGetPositions({
const positions = yield* client.PortfolioControllerGetPositions({
address: walletAddress,
providerId: selectedProvider.id,
});

return Record.fromIterableBy(
positions.map((position) => AtomRef.make(position)),
(ref) => ref.value.marketId,
);
}),
)
.pipe(
Expand Down Expand Up @@ -105,3 +112,27 @@ export const selectedProviderBalancesAtom = Atom.family(
Atom.keepAlive,
),
);

export const updatePositionsMidPriceAtom = Atom.family(
(walletAddress: WalletAccount["address"]) =>
runtimeAtom.atom((ctx) =>
Effect.gen(function* () {
const { mids } = yield* ctx.result(midPriceAtom);
const markets = yield* ctx.result(marketsBySymbolAtom);
const positions = yield* ctx.result(positionsAtom(walletAddress));

Record.toEntries(mids).forEach(([symbol, price]) => {
const marketRef = Record.get(markets, symbol);
if (marketRef._tag === "None") return;

const positionRef = Record.get(positions, marketRef.value.value.id);
if (positionRef._tag === "None") return;

positionRef.value.update((position) => ({
...position,
markPrice: Number(price),
}));
});
}),
),
);
1 change: 1 addition & 0 deletions packages/common/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export * from "./molecules/leverage-dialog";
export * from "./molecules/limit-price-dialog";
export * from "./molecules/order-type-dialog";
export * from "./molecules/percentage-slider";
export * from "./molecules/price-flash";
export * from "./molecules/sign-transactions";
export * from "./molecules/toggle-group";
export * from "./molecules/token-icon";
Expand Down
6 changes: 5 additions & 1 deletion packages/common/src/components/molecules/leverage-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type LeverageDialogProps = Pick<
"leverage" | "onLeverageChange" | "currentPrice" | "maxLeverage" | "side"
> & {
children: React.ReactElement;
nativeButton?: boolean;
};

export const LeverageDialog = (props: LeverageDialogProps) => {
Expand All @@ -37,7 +38,10 @@ export const LeverageDialog = (props: LeverageDialogProps) => {

return (
<Dialog.Root actionsRef={actionsRef}>
<Dialog.Trigger render={props.children} />
<Dialog.Trigger
nativeButton={props.nativeButton}
render={props.children}
/>

<Dialog.Portal>
<Dialog.Backdrop />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,7 @@ function LimitPriceDialogContent({
onLimitPriceChange,
currentPrice,
}: LimitPriceDialogContentProps) {
const setAmount = useAtomSet(
LimitPriceForm.setValue(LimitPriceForm.fields.Amount),
);
const setAmount = useAtomSet(setAmountFieldAtom);
const submit = useAtomSet(LimitPriceForm.submit);

const handleQuickAdjust = (percent: number) => {
Expand Down Expand Up @@ -192,3 +190,7 @@ const LimitPriceForm = FormReact.make(limitPriceFormBuilder, {
{ decoded },
) => args.onSubmit(decoded.Amount),
});

const { setValue: setAmountFieldAtom } = LimitPriceForm.getFieldAtoms(
LimitPriceForm.fields.Amount,
);
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export function OrderTypeDialog({
<Dialog.Backdrop />

<Dialog.Popup>
<Dialog.Content className="pb-5 pt-6 px-6">
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>Order type</Dialog.Title>
</Dialog.Header>
Expand Down
32 changes: 32 additions & 0 deletions packages/common/src/components/molecules/price-flash.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useEffect, useRef } from "react";

export const PriceFlash = ({
price,
children,
}: {
price: number;
children: React.ReactNode;
}) => {
const prevPrice = useRef(price);
const ref = useRef<HTMLElement>(null);

useEffect(() => {
const el = ref.current;

if (!el || price === prevPrice.current) {
prevPrice.current = price;
return;
}

const cls =
price > prevPrice.current ? "price-flash-up" : "price-flash-down";
prevPrice.current = price;

el.classList.remove("price-flash-up", "price-flash-down");
// Force reflow to restart animation when direction is the same
void el.offsetWidth;
el.classList.add(cls);
}, [price]);

return <span ref={ref}>{children}</span>;
};
Loading
Loading