Skip to content
Closed
Show file tree
Hide file tree
Changes from 11 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
30 changes: 30 additions & 0 deletions packages/common/src/atoms/candle-atoms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Atom } from "@effect-atom/atom-react";
import { Effect, Schema, Stream } from "effect";
import {
CandleIntervalSchema,
CoinSchema,
HyperliquidService,
} from "../services/hyperliquid";
import { runtimeAtom } from "../services/runtime";

export const CandleSubscriptionParams = Schema.Data(
Schema.Struct({
coin: CoinSchema,
interval: CandleIntervalSchema,
}),
);

export const candleStreamAtom = Atom.family(
(params: typeof CandleSubscriptionParams.Type) =>
runtimeAtom.atom(
HyperliquidService.pipe(
Effect.andThen((service) =>
service.subscribeCandle({
coin: params.coin,
interval: params.interval,
}),
),
Stream.unwrap,
),
),
);
9 changes: 9 additions & 0 deletions packages/common/src/atoms/hyperliquid-atoms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Effect, Stream } from "effect";
import { HyperliquidService, runtimeAtom } from "../services";

export const midPriceAtom = runtimeAtom.atom(
HyperliquidService.pipe(
Effect.andThen((service) => service.subscribeMidPrice),
Stream.unwrap,
),
);
2 changes: 2 additions & 0 deletions packages/common/src/atoms/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
export * from "./actions-atoms";
export * from "./candle-atoms";
export * from "./close-position-atoms";
Comment thread
petar-omni marked this conversation as resolved.
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),
}));
Comment thread
petar-omni marked this conversation as resolved.
});
}),
);
4 changes: 2 additions & 2 deletions packages/common/src/atoms/order-form-atoms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,8 @@ export const orderFormAtom = Atom.family(
}),
});

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 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),
}));
Comment thread
petar-omni marked this conversation as resolved.
});
}),
),
);
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
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,
);
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>;
};
4 changes: 2 additions & 2 deletions packages/common/src/hooks/use-deposit-form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,8 @@ export const createDepositForm = (

export const DepositForm = createDepositForm(AmountField);

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

export const useDepositForm = () => {
const submit = useAtomSet(DepositForm.submit);
Expand Down
11 changes: 6 additions & 5 deletions packages/common/src/hooks/use-order-form.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Result, useAtomSet, useAtomValue } from "@effect-atom/atom-react";
import { Option, Schema } from "effect";
import { Option, Record, Schema } from "effect";
import {
calculateOrderPercentage,
calculateOrderPositionSize,
Expand Down Expand Up @@ -87,14 +87,15 @@ export const useCurrentPosition = (
) => {
const positions = useAtomValue(
positionsAtom(wallet.currentAccount.address),
).pipe(Result.getOrElse(() => []));
).pipe(Result.getOrElse(Record.empty));

const currentPosition = positions.find(
(position) => position.marketId === marketId,
const currentPosition = Record.get(positions, marketId).pipe(
Option.map((ref) => ref.value),
Option.getOrNull,
);
Comment thread
petar-omni marked this conversation as resolved.

return {
currentPosition: currentPosition ?? null,
currentPosition,
};
};

Expand Down
4 changes: 2 additions & 2 deletions packages/common/src/hooks/use-withdraw-form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,8 @@ export const createWithdrawForm = (

export const WithdrawForm = createWithdrawForm(AmountField);

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

export const useWithdrawForm = () => {
const submit = useAtomSet(WithdrawForm.submit);
Expand Down
18 changes: 7 additions & 11 deletions packages/common/src/lib/formatting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,25 +139,21 @@ export function formatCompactUsdAmount(volume: number): string {
* - Long: "TP +10%, SL -5%" or "TP Off, SL Off"
* - Short: "TP -10%, SL +5%" or "TP Off, SL Off"
*/
export function formatTPOrSLSettings(
settings: TPOrSLSettings,
side: "long" | "short" = "long",
): string {
export function formatTPOrSLSettings(settings: TPOrSLSettings) {
const tp = Option.fromNullable(settings.takeProfit.percentage).pipe(
Option.filter((percentage) => percentage !== 0),
Option.map((percentage) =>
side === "short" ? `TP -${percentage}%` : `TP +${percentage}%`,
),
Option.map((percentage) => `TP ${formatPercentage(percentage)}`),
Option.getOrElse(() => "TP Off"),
);

const sl = Option.fromNullable(settings.stopLoss.percentage).pipe(
Option.filter((percentage) => percentage !== 0),
Option.map((percentage) =>
side === "short" ? `SL +${percentage}%` : `SL -${percentage}%`,
),
Option.map((percentage) => `SL ${formatPercentage(percentage)}`),
Option.getOrElse(() => "SL Off"),
);

return `${tp}, ${sl}`;
return {
tp,
sl,
};
Comment thread
petar-omni marked this conversation as resolved.
Comment thread
petar-omni marked this conversation as resolved.
}
Loading