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
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";
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),
}));
});
}),
);
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
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>;
};
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,
);

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

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,
};
}
Loading