From 92742be219887e5f3131ee08af06082b5bc6051c Mon Sep 17 00:00:00 2001 From: Petar Todorovic Date: Fri, 6 Feb 2026 11:19:20 +0100 Subject: [PATCH 01/10] refactor: update shared vite config and deps --- packages/common/package.json | 2 - packages/common/src/vite.config.ts | 4 +- packages/common/vite.config.ts | 4 +- packages/dashboard/package.json | 2 - .../components/molecules/positions/index.tsx | 3 +- packages/dashboard/vite.config.ts | 4 +- packages/widget/package.json | 5 +- packages/widget/vite.config.ts | 4 +- pnpm-lock.yaml | 117 ------------------ pnpm-workspace.yaml | 2 - 10 files changed, 12 insertions(+), 135 deletions(-) diff --git a/packages/common/package.json b/packages/common/package.json index 7933729..36dbfc6 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -86,8 +86,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:", diff --git a/packages/common/src/vite.config.ts b/packages/common/src/vite.config.ts index 8c6e592..9a54790 100644 --- a/packages/common/src/vite.config.ts +++ b/packages/common/src/vite.config.ts @@ -26,7 +26,7 @@ export const commonPlugins = { nodePolyfills: nodePolyfills({ include: ["buffer"] }), }; -export const commonViteConfig: UserConfig = { +export const createCommonViteConfig = (): UserConfig => ({ plugins: Object.values(commonPlugins), test: { browser: { @@ -46,4 +46,4 @@ export const commonViteConfig: UserConfig = { "vite-plugin-node-polyfills/shims/process", ], }, -}; +}); diff --git a/packages/common/vite.config.ts b/packages/common/vite.config.ts index f961b21..519b49d 100644 --- a/packages/common/vite.config.ts +++ b/packages/common/vite.config.ts @@ -1,5 +1,7 @@ import { defineConfig } from "vite"; -import { commonPlugins, commonViteConfig } from "./src/vite.config"; +import { commonPlugins, createCommonViteConfig } from "./src/vite.config"; + +const commonViteConfig = createCommonViteConfig(); export default defineConfig({ plugins: [ diff --git a/packages/dashboard/package.json b/packages/dashboard/package.json index 35eb955..1f0efb2 100644 --- a/packages/dashboard/package.json +++ b/packages/dashboard/package.json @@ -43,8 +43,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:", diff --git a/packages/dashboard/src/components/molecules/positions/index.tsx b/packages/dashboard/src/components/molecules/positions/index.tsx index 8367c07..a8c0a29 100644 --- a/packages/dashboard/src/components/molecules/positions/index.tsx +++ b/packages/dashboard/src/components/molecules/positions/index.tsx @@ -8,6 +8,7 @@ import { } from "@yieldxyz/perps-common/components"; import { isWalletConnected } from "@yieldxyz/perps-common/domain"; import { cn } from "@yieldxyz/perps-common/lib"; +import { Option } from "effect"; import { OrdersTabWithWallet } from "./orders-tab"; import { PositionsTabWithWallet } from "./positions-tab"; import { TableDisconnected } from "./shared"; @@ -17,7 +18,7 @@ interface PositionsTableProps { } export function PositionsTable({ className }: PositionsTableProps) { - const wallet = useAtomValue(walletAtom).pipe(Result.getOrElse(() => null)); + const wallet = useAtomValue(walletAtom).pipe(Result.value, Option.getOrNull); const walletConnected = isWalletConnected(wallet); useAtomValue(marketsAtom); // TODO: investigate why this is needed diff --git a/packages/dashboard/vite.config.ts b/packages/dashboard/vite.config.ts index c02ea85..7748227 100644 --- a/packages/dashboard/vite.config.ts +++ b/packages/dashboard/vite.config.ts @@ -1,4 +1,4 @@ -import { commonViteConfig } from "@yieldxyz/perps-common/vite.config"; +import { createCommonViteConfig } from "@yieldxyz/perps-common/vite.config"; import { defineConfig } from "vite"; -export default defineConfig(commonViteConfig); +export default defineConfig(createCommonViteConfig()); diff --git a/packages/widget/package.json b/packages/widget/package.json index 5be4692..a17892c 100644 --- a/packages/widget/package.json +++ b/packages/widget/package.json @@ -43,8 +43,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:", @@ -60,7 +58,6 @@ "vite": "catalog:", "vite-plugin-node-polyfills": "catalog:", "vitest": "catalog:", - "vitest-browser-react": "catalog:", - "@tanstack/router-plugin": "catalog:" + "vitest-browser-react": "catalog:" } } diff --git a/packages/widget/vite.config.ts b/packages/widget/vite.config.ts index c02ea85..7748227 100644 --- a/packages/widget/vite.config.ts +++ b/packages/widget/vite.config.ts @@ -1,4 +1,4 @@ -import { commonViteConfig } from "@yieldxyz/perps-common/vite.config"; +import { createCommonViteConfig } from "@yieldxyz/perps-common/vite.config"; import { defineConfig } from "vite"; -export default defineConfig(commonViteConfig); +export default defineConfig(createCommonViteConfig()); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 52c2047..562ffd1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -72,12 +72,6 @@ catalogs: '@tanstack/router-plugin': specifier: ^1.157.8 version: 1.158.0 - '@testing-library/dom': - specifier: ^10.4.0 - version: 10.4.1 - '@testing-library/react': - specifier: ^16.3.2 - version: 16.3.2 '@tim-smart/openapi-gen': specifier: ^0.4.13 version: 0.4.13 @@ -290,12 +284,6 @@ importers: '@tanstack/router-cli': specifier: 'catalog:' version: 1.158.0 - '@testing-library/dom': - specifier: 'catalog:' - version: 10.4.1 - '@testing-library/react': - specifier: 'catalog:' - version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.11))(@types/react@19.2.11)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@tim-smart/openapi-gen': specifier: 'catalog:' version: 0.4.13(patch_hash=36720013d0f70c201ce8f233e38a7b93fc9fce74a17f9bc4293c3cf891b4fdc6) @@ -444,12 +432,6 @@ importers: '@tanstack/router-plugin': specifier: 'catalog:' version: 1.158.0(@tanstack/react-router@1.158.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)) - '@testing-library/dom': - specifier: 'catalog:' - version: 10.4.1 - '@testing-library/react': - specifier: 'catalog:' - version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.11))(@types/react@19.2.11)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@tim-smart/openapi-gen': specifier: 'catalog:' version: 0.4.13(patch_hash=36720013d0f70c201ce8f233e38a7b93fc9fce74a17f9bc4293c3cf891b4fdc6) @@ -595,15 +577,6 @@ importers: '@tanstack/router-cli': specifier: 'catalog:' version: 1.158.0 - '@tanstack/router-plugin': - specifier: 'catalog:' - version: 1.158.0(@tanstack/react-router@1.158.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)) - '@testing-library/dom': - specifier: 'catalog:' - version: 10.4.1 - '@testing-library/react': - specifier: 'catalog:' - version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.11))(@types/react@19.2.11)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@tim-smart/openapi-gen': specifier: 'catalog:' version: 0.4.13(patch_hash=36720013d0f70c201ce8f233e38a7b93fc9fce74a17f9bc4293c3cf891b4fdc6) @@ -2477,25 +2450,6 @@ packages: resolution: {integrity: sha512-cHHDnewHozgjpI+MIVp9tcib6lYEQK5MyUr0ChHpHFGBl8Xei55rohFK0I0ve/GKoHeioaK42Smd8OixPp6CTg==} engines: {node: '>=12'} - '@testing-library/dom@10.4.1': - resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} - engines: {node: '>=18'} - - '@testing-library/react@16.3.2': - resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} - engines: {node: '>=18'} - peerDependencies: - '@testing-library/dom': ^10.0.0 - '@types/react': ^18.0.0 || ^19.0.0 - '@types/react-dom': ^18.0.0 || ^19.0.0 - react: ^18.0.0 || ^19.0.0 - react-dom: ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - '@tim-smart/openapi-gen@0.4.13': resolution: {integrity: sha512-yn/4Tp6Or5JA2/VNPLKtm9ib17PZxmJGyEq+vtcmAch5qG2PAJLvuzuoAwYRBMN9GR8Cb/W5qO6eIHg0AdYL0Q==} hasBin: true @@ -2511,9 +2465,6 @@ packages: '@ton/crypto@3.3.0': resolution: {integrity: sha512-/A6CYGgA/H36OZ9BbTaGerKtzWp50rg67ZCH2oIjV1NcrBaCK9Z343M+CxedvM7Haf3f/Ee9EhxyeTp0GKMUpA==} - '@types/aria-query@5.0.4': - resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} - '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -2827,10 +2778,6 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} - ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} - ansis@4.2.0: resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} engines: {node: '>=14'} @@ -2842,9 +2789,6 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - aria-query@5.3.0: - resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} - array-ify@1.0.0: resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} @@ -3230,10 +3174,6 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} - dequal@2.0.3: - resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} - engines: {node: '>=6'} - des.js@1.1.0: resolution: {integrity: sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==} @@ -3257,9 +3197,6 @@ packages: dijkstrajs@1.0.3: resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} - dom-accessibility-api@0.5.16: - resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} - domain-browser@4.22.0: resolution: {integrity: sha512-IGBwjF7tNk3cwypFNH/7bfzBcgSCbaMOD3GsaY1AU/JRrnHnYgEM0+9kQt52iZxjNsjBtJYtao146V+f8jFZNw==} engines: {node: '>=10'} @@ -3849,10 +3786,6 @@ packages: peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 - lz-string@1.5.0: - resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} - hasBin: true - magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -4148,10 +4081,6 @@ packages: engines: {node: '>=14'} hasBin: true - pretty-format@27.5.1: - resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} @@ -4214,9 +4143,6 @@ packages: peerDependencies: react: ^19.2.4 - react-is@17.0.2: - resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} - react-refresh@0.18.0: resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} engines: {node: '>=0.10.0'} @@ -7548,27 +7474,6 @@ snapshots: '@tanstack/virtual-file-routes@1.154.7': {} - '@testing-library/dom@10.4.1': - dependencies: - '@babel/code-frame': 7.28.6 - '@babel/runtime': 7.28.6 - '@types/aria-query': 5.0.4 - aria-query: 5.3.0 - dom-accessibility-api: 0.5.16 - lz-string: 1.5.0 - picocolors: 1.1.1 - pretty-format: 27.5.1 - - '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.11))(@types/react@19.2.11)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@babel/runtime': 7.28.6 - '@testing-library/dom': 10.4.1 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.11 - '@types/react-dom': 19.2.3(@types/react@19.2.11) - '@tim-smart/openapi-gen@0.4.13(patch_hash=36720013d0f70c201ce8f233e38a7b93fc9fce74a17f9bc4293c3cf891b4fdc6)': dependencies: yaml: 2.8.2 @@ -7587,8 +7492,6 @@ snapshots: jssha: 3.2.0 tweetnacl: 1.0.3 - '@types/aria-query@5.0.4': {} - '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.29.0 @@ -8161,8 +8064,6 @@ snapshots: dependencies: color-convert: 2.0.1 - ansi-styles@5.2.0: {} - ansis@4.2.0: {} anymatch@3.1.3: @@ -8172,10 +8073,6 @@ snapshots: argparse@2.0.1: {} - aria-query@5.3.0: - dependencies: - dequal: 2.0.3 - array-ify@1.0.0: {} asn1.js@4.10.1: @@ -8633,8 +8530,6 @@ snapshots: delayed-stream@1.0.0: optional: true - dequal@2.0.3: {} - des.js@1.1.0: dependencies: inherits: 2.0.4 @@ -8656,8 +8551,6 @@ snapshots: dijkstrajs@1.0.3: {} - dom-accessibility-api@0.5.16: {} - domain-browser@4.22.0: {} dot-prop@5.3.0: @@ -9249,8 +9142,6 @@ snapshots: dependencies: react: 19.2.4 - lz-string@1.5.0: {} - magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -9594,12 +9485,6 @@ snapshots: prettier@3.8.1: {} - pretty-format@27.5.1: - dependencies: - ansi-regex: 5.0.1 - ansi-styles: 5.2.0 - react-is: 17.0.2 - process-nextick-args@2.0.1: {} process-warning@5.0.0: {} @@ -9659,8 +9544,6 @@ snapshots: react: 19.2.4 scheduler: 0.27.0 - react-is@17.0.2: {} - react-refresh@0.18.0: {} react@19.2.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 64562b7..d9ccfe5 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -26,8 +26,6 @@ catalog: '@tanstack/react-virtual': ^3.13.18 '@tanstack/router-cli': ^1.158.0 '@tanstack/router-plugin': ^1.157.8 - '@testing-library/dom': ^10.4.0 - '@testing-library/react': ^16.3.2 '@tim-smart/openapi-gen': ^0.4.13 '@types/node': ^25.2.0 '@types/react': ^19.2.11 From b10c1a05d6ecb377935fbd455fc99c4fc2cee98e Mon Sep 17 00:00:00 2001 From: Petar Todorovic Date: Fri, 6 Feb 2026 11:30:05 +0100 Subject: [PATCH 02/10] feat(dashboard): select market from positions list --- package.json | 6 ++-- .../molecules/positions/positions-tab.tsx | 34 ++++++++++++++++--- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 01ec7e0..b58535c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/dashboard/src/components/molecules/positions/positions-tab.tsx b/packages/dashboard/src/components/molecules/positions/positions-tab.tsx index e201e1f..6b9349d 100644 --- a/packages/dashboard/src/components/molecules/positions/positions-tab.tsx +++ b/packages/dashboard/src/components/molecules/positions/positions-tab.tsx @@ -1,4 +1,9 @@ -import { Result, useAtomValue } from "@effect-atom/atom-react"; +import { + type AtomRef, + Result, + useAtomSet, + useAtomValue, +} from "@effect-atom/atom-react"; import { marketsAtom, ordersAtom, @@ -30,6 +35,7 @@ import { import type { ApiSchemas, ApiTypes } from "@yieldxyz/perps-common/services"; import { Array as _Array, Option, Record } from "effect"; import { Pencil } from "lucide-react"; +import { selectedMarketAtom } from "../../../atoms/selected-market-atom"; import { ClosePositionDialog } from "./close-position-dialog"; import { PositionsTableSkeleton, @@ -40,6 +46,7 @@ import { interface PositionWithMarket { position: ApiSchemas.PositionDto; market: ApiSchemas.MarketDto; + marketRef: AtomRef.AtomRef; } interface PositionsTableContentProps { @@ -75,6 +82,7 @@ export function PositionsTabWithWallet({ Option.map((marketRef) => ({ position: p, market: marketRef.value, + marketRef, })), ), ), @@ -141,7 +149,7 @@ function PositionsTableContent({ - {positions.map(({ position, market }) => { + {positions.map(({ position, market, marketRef }) => { const marketOrders = orders.filter( (o) => o.marketId === position.marketId, ); @@ -151,6 +159,7 @@ function PositionsTableContent({ key={position.marketId} position={position} market={market} + marketRef={marketRef} orders={marketOrders} wallet={wallet} /> @@ -165,13 +174,21 @@ function PositionsTableContent({ interface PositionRowProps { position: ApiSchemas.PositionDto; market: ApiSchemas.MarketDto; + marketRef: AtomRef.AtomRef; orders: ApiTypes.OrderDto[]; wallet: WalletConnected; } -function PositionRow({ position, market, orders, wallet }: PositionRowProps) { +function PositionRow({ + position, + market, + marketRef, + orders, + wallet, +}: PositionRowProps) { const { updateLeverage } = useUpdateLeverage(); const { editTP, editSL } = useEditSLTP(); + const setSelectedMarket = useAtomSet(selectedMarketAtom); const positionActions = usePositionActions(position); const tpSlOrders = useTpSlOrders(orders); @@ -224,6 +241,9 @@ function PositionRow({ position, market, orders, wallet }: PositionRowProps) { const tpValue = tpSlOrders.takeProfit?.triggerPrice; const slValue = tpSlOrders.stopLoss?.triggerPrice; + const handleMarketSelect = () => { + setSelectedMarket(marketRef); + }; return ( @@ -235,7 +255,13 @@ function PositionRow({ position, market, orders, wallet }: PositionRowProps) { isLong ? "text-[#71e96d]" : "text-[#ff4141]", )} > - {symbol}{" "} + {" "} {positionActions.updateLeverage ? ( Date: Fri, 6 Feb 2026 11:44:32 +0100 Subject: [PATCH 03/10] refactor: simplify tp/sl formatting output --- packages/common/src/lib/formatting.ts | 18 +++++++---------- .../modules/trade/order-form/index.tsx | 20 ++++++++----------- .../modules/Order/Overview/index.tsx | 4 +++- 3 files changed, 18 insertions(+), 24 deletions(-) diff --git a/packages/common/src/lib/formatting.ts b/packages/common/src/lib/formatting.ts index eb1517d..281f563 100644 --- a/packages/common/src/lib/formatting.ts +++ b/packages/common/src/lib/formatting.ts @@ -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, + }; } diff --git a/packages/dashboard/src/components/modules/trade/order-form/index.tsx b/packages/dashboard/src/components/modules/trade/order-form/index.tsx index 8cf26d5..82382f3 100644 --- a/packages/dashboard/src/components/modules/trade/order-form/index.tsx +++ b/packages/dashboard/src/components/modules/trade/order-form/index.tsx @@ -104,7 +104,6 @@ function OrderFormDisconnected({ market.leverageRange, ); const { leverage } = useLeverage(leverageRanges); - const { tpOrSLSettings } = useTPOrSLSettings(); const maxLeverage = getMaxLeverage(leverageRanges); @@ -169,10 +168,7 @@ function OrderFormDisconnected({ )} {/* Advanced Orders */} - + {/* Order Details */} @@ -245,6 +241,8 @@ function OrderFormContent({ submit({ wallet, market, side: orderSide }); }; + const { tp, sl } = formatTPOrSLSettings(tpOrSLSettings); + return (
); From 73e6a592ada0822700e7032ae951e785bc15b023 Mon Sep 17 00:00:00 2001 From: Petar Todorovic Date: Fri, 6 Feb 2026 12:29:59 +0100 Subject: [PATCH 06/10] feat(dashboard): show funding rate and fees --- .../modules/trade/market-info/index.tsx | 28 +++++++++---------- .../modules/trade/order-form/index.tsx | 11 ++++++++ 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/packages/dashboard/src/components/modules/trade/market-info/index.tsx b/packages/dashboard/src/components/modules/trade/market-info/index.tsx index a653de4..1bd5971 100644 --- a/packages/dashboard/src/components/modules/trade/market-info/index.tsx +++ b/packages/dashboard/src/components/modules/trade/market-info/index.tsx @@ -36,6 +36,7 @@ function MarketInfoBarContent({ const setSelectedMarket = useAtomSet(selectedMarketAtom); const [isOpen, setIsOpen] = useState(false); const isPositiveChange = market.priceChangePercent24h >= 0; + const isPositiveFunding = Number(market.fundingRate) >= 0; const logo = market.baseAsset.logoURI ?? getTokenLogo(market.baseAsset.symbol); @@ -102,7 +103,7 @@ function MarketInfoBarContent({ variant="labelSmWhiteNeg" className={cn( "font-medium", - isPositiveChange ? "text-[#71e96d]" : "text-[#ff4141]", + isPositiveChange ? "text-accent-green" : "text-accent-red", )} > {isPositiveChange ? "+" : ""} @@ -131,23 +132,20 @@ function MarketInfoBarContent({
- {/* Maker Fee */} + {/* Funding Rate */}
- Maker Fee + Funding Rate - - {market.makerFee ? formatRate(market.makerFee) : "-"} - -
- - {/* Taker Fee */} -
- - Taker Fee - - - {market.takerFee ? formatRate(market.takerFee) : "-"} + + {isPositiveFunding ? "+" : ""} + {formatRate(market.fundingRate, { maximumFractionDigits: 4 })}
diff --git a/packages/dashboard/src/components/modules/trade/order-form/index.tsx b/packages/dashboard/src/components/modules/trade/order-form/index.tsx index 82382f3..da996ac 100644 --- a/packages/dashboard/src/components/modules/trade/order-form/index.tsx +++ b/packages/dashboard/src/components/modules/trade/order-form/index.tsx @@ -37,6 +37,7 @@ import { import { cn, formatAmount, + formatRate, formatTPOrSLSettings, getMaxLeverage, round, @@ -401,6 +402,16 @@ function OrderFormContent({ value={formatAmount(calculations.fees)} isLast /> + + {/* Place Order Button */} From 4e2d732d9c118c262db545b51025ac3d9fce711d Mon Sep 17 00:00:00 2001 From: Petar Todorovic Date: Fri, 6 Feb 2026 12:44:22 +0100 Subject: [PATCH 07/10] feat(dashboard): add column sorting to market selector MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use Effect Order module with tri-state cycling (desc → asc → none) for all market table columns via sortable headers with direction icons. --- .../market-info/market-selector-popover.tsx | 136 ++++++++++++++++-- 1 file changed, 128 insertions(+), 8 deletions(-) diff --git a/packages/dashboard/src/components/modules/trade/market-info/market-selector-popover.tsx b/packages/dashboard/src/components/modules/trade/market-info/market-selector-popover.tsx index a19782c..6d35913 100644 --- a/packages/dashboard/src/components/modules/trade/market-info/market-selector-popover.tsx +++ b/packages/dashboard/src/components/modules/trade/market-info/market-selector-popover.tsx @@ -17,10 +17,48 @@ import { getTokenLogo, } from "@yieldxyz/perps-common/lib"; import type { ApiTypes } from "@yieldxyz/perps-common/services"; -import { Array as _Array, Option, Record } from "effect"; -import { Search, X } from "lucide-react"; +import { Array as _Array, Option, Order, Record } from "effect"; +import { + ArrowDownUp, + ArrowDownWideNarrow, + ArrowUpNarrowWide, + Search, + X, +} from "lucide-react"; import { useRef, useState } from "react"; +type SortColumn = "symbol" | "price" | "change" | "funding" | "volume" | "oi"; + +type SortDirection = "asc" | "desc"; + +type SortState = { + readonly column: SortColumn; + readonly direction: SortDirection; +} | null; + +type MarketRef = AtomRef.AtomRef; + +const columnOrders: { + [Key in SortColumn]: Order.Order; +} = { + symbol: Order.mapInput(Order.string, (ref: MarketRef) => + ref.value.baseAsset.symbol.toLowerCase(), + ), + price: Order.mapInput(Order.number, (ref: MarketRef) => ref.value.markPrice), + change: Order.mapInput( + Order.number, + (ref: MarketRef) => ref.value.priceChangePercent24h, + ), + funding: Order.mapInput(Order.number, (ref: MarketRef) => + Number(ref.value.fundingRate), + ), + volume: Order.mapInput(Order.number, (ref: MarketRef) => ref.value.volume24h), + oi: Order.mapInput( + Order.number, + (ref: MarketRef) => ref.value.openInterest * ref.value.markPrice, + ), +}; + interface MarketSelectorContentProps { onSelect: (marketRef: AtomRef.AtomRef) => void; } @@ -116,10 +154,45 @@ function MarketRowSkeleton() { ); } +function SortableHeader({ + label, + column, + sortState, + onToggle, +}: { + label: string; + column: SortColumn; + sortState: SortState; + onToggle: (column: SortColumn) => void; +}) { + const isActive = sortState?.column === column; + const direction = isActive ? sortState.direction : null; + + return ( + + ); +} + export function MarketSelectorContent({ onSelect, }: MarketSelectorContentProps) { const [searchQuery, setSearchQuery] = useState(""); + const [sortState, setSortState] = useState(null); const parentRef = useRef(null); const markets = useAtomValue(marketsAtom); @@ -137,9 +210,26 @@ export function MarketSelectorContent({ ) : v, ), + Result.map((v) => { + if (!sortState) return v; + + const order = columnOrders[sortState.column]; + return _Array.sort( + v, + sortState.direction === "asc" ? order : Order.reverse(order), + ); + }), Result.getOrElse(() => []), ); + const toggleSort = (column: SortColumn) => { + setSortState((prev) => { + if (prev?.column !== column) return { column, direction: "desc" }; + if (prev.direction === "desc") return { column, direction: "asc" }; + return null; + }); + }; + const rowVirtualizer = useVirtualizer({ count: marketData.length, getScrollElement: () => parentRef.current, @@ -179,12 +269,42 @@ export function MarketSelectorContent({ {/* Table header */}
- Symbol - Last Price - 24H Change - 8H Funding - Volume - Open Interest + + + + + +
{/* Empty state */} From 82e3b0f3818b421c5aa616eace231aa9037329ab Mon Sep 17 00:00:00 2001 From: Petar Todorovic Date: Sat, 7 Feb 2026 00:14:55 +0100 Subject: [PATCH 08/10] feat: hyperliquid mids subscription --- packages/common/package.json | 3 +- packages/common/src/atoms/candle-atoms.ts | 30 ++++ .../common/src/atoms/hyperliquid-atoms.ts | 9 ++ packages/common/src/atoms/index.ts | 1 + packages/common/src/atoms/markets-atoms.ts | 49 ++++++- packages/common/src/atoms/portfolio-atoms.ts | 37 ++++- packages/common/src/components/index.ts | 1 + .../src/components/molecules/price-flash.tsx | 32 +++++ packages/common/src/hooks/use-order-form.ts | 11 +- .../common/src/services/hyperliquid/index.ts | 96 +++++++++++++ packages/common/src/services/index.ts | 1 + packages/common/src/services/runtime.ts | 2 + packages/common/src/styles/base.css | 28 ++++ packages/dashboard/package.json | 2 +- .../src/components/modules/root/Preload.tsx | 4 + .../modules/trade/market-info/index.tsx | 13 +- .../components/molecules/positions/index.tsx | 4 +- .../molecules/positions/positions-tab.tsx | 33 ++--- packages/widget/package.json | 2 +- .../modules/Home/Positions/index.tsx | 24 ++-- .../modules/Home/Positions/position-card.tsx | 5 +- .../src/components/modules/Home/index.tsx | 3 +- .../modules/PositionDetails/Close/state.tsx | 8 +- .../Overview/Position/index.tsx | 23 ++- .../PositionDetails/Overview/index.tsx | 8 +- .../components/modules/Root/PreloadAtoms.tsx | 2 + pnpm-lock.yaml | 132 ++++++++++++++---- pnpm-workspace.yaml | 1 + 28 files changed, 476 insertions(+), 88 deletions(-) create mode 100644 packages/common/src/atoms/candle-atoms.ts create mode 100644 packages/common/src/atoms/hyperliquid-atoms.ts create mode 100644 packages/common/src/components/molecules/price-flash.tsx create mode 100644 packages/common/src/services/hyperliquid/index.ts diff --git a/packages/common/package.json b/packages/common/package.json index 36dbfc6..cdf0d07 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -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:", diff --git a/packages/common/src/atoms/candle-atoms.ts b/packages/common/src/atoms/candle-atoms.ts new file mode 100644 index 0000000..364abf0 --- /dev/null +++ b/packages/common/src/atoms/candle-atoms.ts @@ -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, + ), + ), +); diff --git a/packages/common/src/atoms/hyperliquid-atoms.ts b/packages/common/src/atoms/hyperliquid-atoms.ts new file mode 100644 index 0000000..0c2fbc2 --- /dev/null +++ b/packages/common/src/atoms/hyperliquid-atoms.ts @@ -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, + ), +); diff --git a/packages/common/src/atoms/index.ts b/packages/common/src/atoms/index.ts index 568b037..a6b5ada 100644 --- a/packages/common/src/atoms/index.ts +++ b/packages/common/src/atoms/index.ts @@ -1,4 +1,5 @@ export * from "./actions-atoms"; +export * from "./candle-atoms"; export * from "./close-position-atoms"; export * from "./config-atom"; export * from "./edit-position-atoms"; diff --git a/packages/common/src/atoms/markets-atoms.ts b/packages/common/src/atoms/markets-atoms.ts index 7b115e0..741a4ac 100644 --- a/packages/common/src/atoms/markets-atoms.ts +++ b/packages/common/src/atoms/markets-atoms.ts @@ -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; @@ -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", ) {} @@ -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) => @@ -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), + })); + }); + }), +); diff --git a/packages/common/src/atoms/portfolio-atoms.ts b/packages/common/src/atoms/portfolio-atoms.ts index ce4cb84..411ba5a 100644 --- a/packages/common/src/atoms/portfolio-atoms.ts +++ b/packages/common/src/atoms/portfolio-atoms.ts @@ -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"; @@ -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( @@ -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), + })); + }); + }), + ), +); diff --git a/packages/common/src/components/index.ts b/packages/common/src/components/index.ts index 0b4b88a..7035ac7 100644 --- a/packages/common/src/components/index.ts +++ b/packages/common/src/components/index.ts @@ -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"; diff --git a/packages/common/src/components/molecules/price-flash.tsx b/packages/common/src/components/molecules/price-flash.tsx new file mode 100644 index 0000000..9090466 --- /dev/null +++ b/packages/common/src/components/molecules/price-flash.tsx @@ -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(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 {children}; +}; diff --git a/packages/common/src/hooks/use-order-form.ts b/packages/common/src/hooks/use-order-form.ts index 85fedb4..759f46e 100644 --- a/packages/common/src/hooks/use-order-form.ts +++ b/packages/common/src/hooks/use-order-form.ts @@ -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, @@ -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, }; }; diff --git a/packages/common/src/services/hyperliquid/index.ts b/packages/common/src/services/hyperliquid/index.ts new file mode 100644 index 0000000..a4f03de --- /dev/null +++ b/packages/common/src/services/hyperliquid/index.ts @@ -0,0 +1,96 @@ +import type { AllMidsWsEvent, CandleWsEvent } from "@nktkas/hyperliquid"; +import { + HttpTransport, + InfoClient, + SubscriptionClient, + WebSocketTransport, +} from "@nktkas/hyperliquid"; +import { Chunk, Effect, Schema, Stream } from "effect"; + +export class HyperliquidService extends Effect.Service()( + "perps/services/hyperliquid/HyperliquidService", + { + scoped: Effect.gen(function* () { + const transport = new WebSocketTransport(); + const client = new SubscriptionClient({ transport }); + + const httpTransport = new HttpTransport(); + const infoClient = new InfoClient({ transport: httpTransport }); + + const candleSnapshot = (params: { + coin: typeof CoinSchema.Type; + interval: typeof CandleIntervalSchema.Type; + startTime: number; + endTime?: number; + }) => + Effect.tryPromise(() => infoClient.candleSnapshot(params)).pipe( + Effect.catchAll((cause) => new GetCandleSnapshotError({ cause })), + ); + + const subscribeCandle = (params: { + coin: typeof CoinSchema.Type; + interval: typeof CandleIntervalSchema.Type; + }) => + Stream.asyncScoped((emit) => + Effect.gen(function* () { + const subscription = yield* Effect.promise(() => + client.candle(params, (data) => { + emit(Effect.succeed(Chunk.of(data))); + }), + ); + + yield* Effect.addFinalizer(() => + Effect.promise(() => subscription.unsubscribe()), + ); + }), + ); + + const subscribeMidPrice = Stream.asyncScoped((emit) => + Effect.gen(function* () { + const subscription = yield* Effect.promise(() => + client.allMids({}, (data) => { + emit(Effect.succeed(Chunk.of(data))); + }), + ); + + yield* Effect.addFinalizer(() => + Effect.promise(() => subscription.unsubscribe()), + ); + }), + ); + + return { + candleSnapshot, + subscribeCandle, + subscribeMidPrice, + }; + }), + }, +) {} + +export type CandleData = CandleWsEvent; + +export const CoinSchema = Schema.String; +export const CandleIntervalSchema = Schema.Literal( + "1m", + "3m", + "5m", + "15m", + "30m", + "1h", + "2h", + "4h", + "8h", + "12h", + "1d", + "3d", + "1w", + "1M", +); + +export class GetCandleSnapshotError extends Schema.TaggedError()( + "GetCandleSnapshotError", + { + cause: Schema.Unknown, + }, +) {} diff --git a/packages/common/src/services/index.ts b/packages/common/src/services/index.ts index 5b34f1b..7e9c012 100644 --- a/packages/common/src/services/index.ts +++ b/packages/common/src/services/index.ts @@ -5,6 +5,7 @@ export type * as ApiTypes from "./api-client/client-factory"; export * from "./config"; export * from "./constants"; export * from "./http-client"; +export * from "./hyperliquid"; export * from "./runtime"; export * from "./wallet/browser-signer"; export * from "./wallet/ledger-signer"; diff --git a/packages/common/src/services/runtime.ts b/packages/common/src/services/runtime.ts index b90a64e..a65234b 100644 --- a/packages/common/src/services/runtime.ts +++ b/packages/common/src/services/runtime.ts @@ -3,6 +3,7 @@ import { Cause, Effect, Layer, Logger } from "effect"; import { ApiClientService } from "./api-client"; import { ConfigService } from "./config"; import { HttpClientService } from "./http-client"; +import { HyperliquidService } from "./hyperliquid"; import { BrowserSignerLayer } from "./wallet/browser-signer"; import { LedgerSignerLayer } from "./wallet/ledger-signer"; import { isLedgerDappBrowserProvider } from "./wallet/ledger-signer/utils"; @@ -19,6 +20,7 @@ const layer = Layer.mergeAll( ApiClientService.Default, HttpClientService.Default, ConfigService.Default, + HyperliquidService.Default, Registry.layer, Logger.pretty, ).pipe( diff --git a/packages/common/src/styles/base.css b/packages/common/src/styles/base.css index a76d29a..bb458b0 100644 --- a/packages/common/src/styles/base.css +++ b/packages/common/src/styles/base.css @@ -22,3 +22,31 @@ code { @apply bg-background text-foreground; } } + +@keyframes price-flash-up { + 0%, + 30% { + color: var(--accent-green); + } + 100% { + color: inherit; + } +} + +@keyframes price-flash-down { + 0%, + 30% { + color: var(--accent-red); + } + 100% { + color: inherit; + } +} + +.price-flash-up { + animation: price-flash-up 700ms ease-out; +} + +.price-flash-down { + animation: price-flash-down 700ms ease-out; +} diff --git a/packages/dashboard/package.json b/packages/dashboard/package.json index 1f0efb2..d5db399 100644 --- a/packages/dashboard/package.json +++ b/packages/dashboard/package.json @@ -13,7 +13,7 @@ "@yieldxyz/perps-common": "workspace:*", "@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:", diff --git a/packages/dashboard/src/components/modules/root/Preload.tsx b/packages/dashboard/src/components/modules/root/Preload.tsx index 654325d..00cda45 100644 --- a/packages/dashboard/src/components/modules/root/Preload.tsx +++ b/packages/dashboard/src/components/modules/root/Preload.tsx @@ -7,6 +7,8 @@ import { providersAtom, providersBalancesAtom, refreshMarketsAtom, + updateMarketsMidPriceAtom, + updatePositionsMidPriceAtom, walletAtom, } from "@yieldxyz/perps-common/atoms"; import { @@ -25,6 +27,7 @@ const PreloadWalletConnectedAtoms = ({ useAtomMount(providersBalancesAtom(wallet.currentAccount.address)); useAtomMount(positionsAtom(wallet.currentAccount.address)); useAtomMount(ordersAtom(wallet.currentAccount.address)); + useAtomMount(updatePositionsMidPriceAtom(wallet.currentAccount.address)); return null; }; @@ -36,6 +39,7 @@ export const Preload = () => { useAtomMount(providersAtom); useAtomMount(marketsAtom); useAtomMount(refreshMarketsAtom); + useAtomMount(updateMarketsMidPriceAtom); if (Result.isSuccess(wallet) && isWalletConnected(wallet.value)) { return ; diff --git a/packages/dashboard/src/components/modules/trade/market-info/index.tsx b/packages/dashboard/src/components/modules/trade/market-info/index.tsx index 1bd5971..90fe1db 100644 --- a/packages/dashboard/src/components/modules/trade/market-info/index.tsx +++ b/packages/dashboard/src/components/modules/trade/market-info/index.tsx @@ -5,7 +5,12 @@ import { useAtomSet, useAtomValue, } from "@effect-atom/atom-react"; -import { Popover, Text, TokenIcon } from "@yieldxyz/perps-common/components"; +import { + Popover, + PriceFlash, + Text, + TokenIcon, +} from "@yieldxyz/perps-common/components"; import { cn, formatAmount, @@ -89,8 +94,10 @@ function MarketInfoBarContent({ Price - - {formatAmount(market.markPrice)} + + + {formatAmount(market.markPrice)} + diff --git a/packages/dashboard/src/components/molecules/positions/index.tsx b/packages/dashboard/src/components/molecules/positions/index.tsx index 6c9cdda..db8a4c1 100644 --- a/packages/dashboard/src/components/molecules/positions/index.tsx +++ b/packages/dashboard/src/components/molecules/positions/index.tsx @@ -16,7 +16,7 @@ import { type WalletConnected, } from "@yieldxyz/perps-common/domain"; import { cn } from "@yieldxyz/perps-common/lib"; -import { Option } from "effect"; +import { Option, Record } from "effect"; import { OrdersTabWithWallet } from "./orders-tab"; import { PositionsTabWithWallet } from "./positions-tab"; import { TableDisconnected } from "./shared"; @@ -32,7 +32,7 @@ const PositionsTabLabel = ({ wallet }: { wallet: WalletConnected }) => { return positionsResult.pipe( Result.value, - Option.map((positions) => positions.length), + Option.map((positions) => Record.size(positions)), Option.filter((count) => count > 0), Option.map((count) => `Positions (${count})`), Option.getOrElse(() => "Positions"), diff --git a/packages/dashboard/src/components/molecules/positions/positions-tab.tsx b/packages/dashboard/src/components/molecules/positions/positions-tab.tsx index 6b9349d..916d9e1 100644 --- a/packages/dashboard/src/components/molecules/positions/positions-tab.tsx +++ b/packages/dashboard/src/components/molecules/positions/positions-tab.tsx @@ -1,6 +1,7 @@ import { type AtomRef, Result, + useAtomRef, useAtomSet, useAtomValue, } from "@effect-atom/atom-react"; @@ -32,7 +33,7 @@ import { getMaxLeverage, getTPOrSLConfigurationFromPosition, } from "@yieldxyz/perps-common/lib"; -import type { ApiSchemas, ApiTypes } from "@yieldxyz/perps-common/services"; +import type { ApiTypes } from "@yieldxyz/perps-common/services"; import { Array as _Array, Option, Record } from "effect"; import { Pencil } from "lucide-react"; import { selectedMarketAtom } from "../../../atoms/selected-market-atom"; @@ -44,8 +45,7 @@ import { } from "./shared"; interface PositionWithMarket { - position: ApiSchemas.PositionDto; - market: ApiSchemas.MarketDto; + positionRef: AtomRef.AtomRef; marketRef: AtomRef.AtomRef; } @@ -77,11 +77,10 @@ export function PositionsTabWithWallet({ const positionsWithMarket = positionsResult.pipe( Result.map((positions) => - _Array.filterMap(positions, (p) => - Record.get(marketsMap, p.marketId).pipe( + _Array.filterMap(Record.values(positions), (positionRef) => + Record.get(marketsMap, positionRef.value.marketId).pipe( Option.map((marketRef) => ({ - position: p, - market: marketRef.value, + positionRef, marketRef, })), ), @@ -149,16 +148,14 @@ function PositionsTableContent({ - {positions.map(({ position, market, marketRef }) => { - const marketOrders = orders.filter( - (o) => o.marketId === position.marketId, - ); + {positions.map(({ positionRef, marketRef }) => { + const marketId = positionRef.value.marketId; + const marketOrders = orders.filter((o) => o.marketId === marketId); return ( ; marketRef: AtomRef.AtomRef; orders: ApiTypes.OrderDto[]; wallet: WalletConnected; } function PositionRow({ - position, - market, + positionRef, marketRef, orders, wallet, }: PositionRowProps) { + const position = useAtomRef(positionRef); + const market = useAtomRef(marketRef); const { updateLeverage } = useUpdateLeverage(); const { editTP, editSL } = useEditSLTP(); const setSelectedMarket = useAtomSet(selectedMarketAtom); diff --git a/packages/widget/package.json b/packages/widget/package.json index a17892c..fc2dea9 100644 --- a/packages/widget/package.json +++ b/packages/widget/package.json @@ -13,7 +13,7 @@ "@yieldxyz/perps-common": "workspace:*", "@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:", diff --git a/packages/widget/src/components/modules/Home/Positions/index.tsx b/packages/widget/src/components/modules/Home/Positions/index.tsx index b081ac2..64bae75 100644 --- a/packages/widget/src/components/modules/Home/Positions/index.tsx +++ b/packages/widget/src/components/modules/Home/Positions/index.tsx @@ -39,11 +39,15 @@ function PositionsWithWallet({ wallet }: { wallet: WalletConnected }) { balances: balancesResult, }).pipe( Result.map(({ positions, balances }) => { - const totalUnrealizedPnl = positions.reduce( - (acc, p) => acc + p.unrealizedPnl, + const positionRefs = Record.values(positions); + const totalUnrealizedPnl = positionRefs.reduce( + (acc, ref) => acc + ref.value.unrealizedPnl, + 0, + ); + const totalMargin = positionRefs.reduce( + (acc, ref) => acc + ref.value.margin, 0, ); - const totalMargin = positions.reduce((acc, p) => acc + p.margin, 0); const pnlPercent = totalMargin > 0 ? (totalUnrealizedPnl / totalMargin) * 100 : 0; @@ -86,12 +90,12 @@ function PositionsWithWallet({ wallet }: { wallet: WalletConnected }) { const positionsWithMarketAndOrders = positionsResult.pipe( Result.map((positions) => - _Array.filterMap(positions, (p) => - Record.get(marketsMap, p.marketId).pipe( + _Array.filterMap(Record.values(positions), (positionRef) => + Record.get(marketsMap, positionRef.value.marketId).pipe( Option.map((m) => ({ marketRef: m, - position: p, - orders: Record.get(ordersMap, p.marketId).pipe( + positionRef, + orders: Record.get(ordersMap, positionRef.value.marketId).pipe( Option.getOrElse(() => [] as ApiSchemas.OrderDto[]), ), })), @@ -188,10 +192,10 @@ function PositionsWithWallet({ wallet }: { wallet: WalletConnected }) { Match.when("positions", () => positionsWithMarketAndOrders.length > 0 ? ( positionsWithMarketAndOrders.map( - ({ marketRef, position, orders }) => ( + ({ marketRef, positionRef, orders }) => ( diff --git a/packages/widget/src/components/modules/Home/Positions/position-card.tsx b/packages/widget/src/components/modules/Home/Positions/position-card.tsx index 0608553..012a6af 100644 --- a/packages/widget/src/components/modules/Home/Positions/position-card.tsx +++ b/packages/widget/src/components/modules/Home/Positions/position-card.tsx @@ -18,14 +18,15 @@ import { import type { ApiSchemas } from "@yieldxyz/perps-common/services"; export function PositionCard({ - position, + positionRef, marketRef, orders, }: { - position: ApiSchemas.PositionDto; + positionRef: AtomRef.AtomRef; marketRef: AtomRef.AtomRef; orders: ApiSchemas.OrderDto[]; }) { + const position = useAtomRef(positionRef); const market = useAtomRef(marketRef); const symbol = market.baseAsset.symbol; const logo = market.baseAsset.logoURI ?? getTokenLogo(symbol); diff --git a/packages/widget/src/components/modules/Home/index.tsx b/packages/widget/src/components/modules/Home/index.tsx index 3e02708..d43d291 100644 --- a/packages/widget/src/components/modules/Home/index.tsx +++ b/packages/widget/src/components/modules/Home/index.tsx @@ -25,6 +25,7 @@ import { } from "@yieldxyz/perps-common/domain"; import { cn, formatAmount } from "@yieldxyz/perps-common/lib"; import type { ApiTypes } from "@yieldxyz/perps-common/services"; +import { Record } from "effect"; import { ChartNoAxesColumnIncreasing, ChevronRight, @@ -64,7 +65,7 @@ const PositionsTabLabel = ({ wallet }: { wallet: WalletConnected }) => { positionsAtom(wallet.currentAccount.address), ); const positionsCount = positionsResult.pipe( - Result.map((positions) => positions.length), + Result.map((positions) => Record.size(positions)), Result.getOrElse(() => 0), ); diff --git a/packages/widget/src/components/modules/PositionDetails/Close/state.tsx b/packages/widget/src/components/modules/PositionDetails/Close/state.tsx index 1c15c7b..49cdfb3 100644 --- a/packages/widget/src/components/modules/PositionDetails/Close/state.tsx +++ b/packages/widget/src/components/modules/PositionDetails/Close/state.tsx @@ -2,7 +2,7 @@ import { Atom, useAtomValue } from "@effect-atom/atom-react"; import { positionsAtom } from "@yieldxyz/perps-common/atoms"; import type { WalletAccount } from "@yieldxyz/perps-common/domain"; import { type ApiTypes, runtimeAtom } from "@yieldxyz/perps-common/services"; -import { Data, Effect } from "effect"; +import { Data, Effect, Record } from "effect"; export type { ApiTypes }; @@ -12,13 +12,13 @@ const closePositionAtom = Atom.family( Effect.fn(function* (ctx) { const positions = yield* ctx.result(positionsAtom(args.walletAddress)); - const position = positions.find((p) => p.marketId === args.marketId); + const positionRef = Record.get(positions, args.marketId); - if (!position) { + if (positionRef._tag === "None") { return yield* Effect.dieMessage("Position not found"); } - return position; + return positionRef.value.value; }), ), ); 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 9679dfd..358c2ca 100644 --- a/packages/widget/src/components/modules/PositionDetails/Overview/Position/index.tsx +++ b/packages/widget/src/components/modules/PositionDetails/Overview/Position/index.tsx @@ -1,4 +1,9 @@ -import { Result, useAtomValue } from "@effect-atom/atom-react"; +import { + type AtomRef, + Result, + useAtomRef, + useAtomValue, +} from "@effect-atom/atom-react"; import { Link, Navigate } from "@tanstack/react-router"; import { ordersAtom, @@ -35,18 +40,20 @@ import { getTPOrSLConfigurationFromPosition, } from "@yieldxyz/perps-common/lib"; import type { ApiTypes } from "@yieldxyz/perps-common/services"; +import { Option, Record } from "effect"; function PositionCardContent({ - position, + positionRef, market, wallet, orders, }: { orders: ApiTypes.OrderDto[]; - position: ApiTypes.PositionDto; + positionRef: AtomRef.AtomRef; market: ApiTypes.MarketDto; wallet: WalletConnected; }) { + const position = useAtomRef(positionRef); const { editTPResult, editTP, editSLResult, editSL } = useEditSLTP(); const { updateLeverageResult, updateLeverage } = useUpdateLeverage(); @@ -321,12 +328,14 @@ function PositionTabContentWithWallet({ Result.getOrElse(() => [] as ApiTypes.OrderDto[]), ); - const position = positionsResult.pipe( - Result.map((positions) => positions.find((p) => p.marketId === market.id)), + const positionRef = positionsResult.pipe( + Result.map((positions) => + Record.get(positions, market.id).pipe(Option.getOrUndefined), + ), Result.getOrElse(() => undefined), ); - if (!position) { + if (!positionRef) { return ( positions.find((p) => p.marketId === market.id)), + Result.map((positions) => + Record.get(positions, market.id).pipe( + Option.map((ref) => ref.value), + Option.getOrUndefined, + ), + ), Result.getOrElse(() => undefined), ); diff --git a/packages/widget/src/components/modules/Root/PreloadAtoms.tsx b/packages/widget/src/components/modules/Root/PreloadAtoms.tsx index 06dfdca..acdb73c 100644 --- a/packages/widget/src/components/modules/Root/PreloadAtoms.tsx +++ b/packages/widget/src/components/modules/Root/PreloadAtoms.tsx @@ -7,6 +7,7 @@ import { providersAtom, providersBalancesAtom, refreshMarketsAtom, + updatePositionsMidPriceAtom, walletAtom, } from "@yieldxyz/perps-common/atoms"; import { @@ -23,6 +24,7 @@ const PreloadWalletConnectedAtoms = ({ useAtomMount(providersBalancesAtom(wallet.currentAccount.address)); useAtomMount(positionsAtom(wallet.currentAccount.address)); useAtomMount(ordersAtom(wallet.currentAccount.address)); + useAtomMount(updatePositionsMidPriceAtom(wallet.currentAccount.address)); return null; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 562ffd1..9abc97e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,6 +21,9 @@ catalogs: '@effect-atom/atom-react': specifier: ^0.5.0 version: 0.5.0 + '@effect/experimental': + specifier: ^0.58.0 + version: 0.58.0 '@effect/language-service': specifier: ^0.73.0 version: 0.73.0 @@ -36,6 +39,9 @@ catalogs: '@lucas-barake/effect-form-react': specifier: ^0.18.0 version: 0.18.0 + '@nktkas/hyperliquid': + specifier: ^0.31.0 + version: 0.31.0 '@reown/appkit': specifier: ^1.8.17 version: 1.8.17 @@ -200,7 +206,7 @@ importers: specifier: 'catalog:' version: 0.5.0(@effect/experimental@0.58.0(@effect/platform@0.94.3(effect@3.19.16))(effect@3.19.16))(@effect/platform@0.94.3(effect@3.19.16))(@effect/rpc@0.73.0(@effect/platform@0.94.3(effect@3.19.16))(effect@3.19.16))(effect@3.19.16)(react@19.2.4)(scheduler@0.27.0) '@effect/experimental': - specifier: ^0.58.0 + specifier: 'catalog:' version: 0.58.0(@effect/platform@0.94.3(effect@3.19.16))(effect@3.19.16) '@effect/platform': specifier: 'catalog:' @@ -214,6 +220,9 @@ importers: '@lucas-barake/effect-form-react': specifier: 'catalog:' version: 0.18.0(@effect-atom/atom-react@0.5.0(@effect/experimental@0.58.0(@effect/platform@0.94.3(effect@3.19.16))(effect@3.19.16))(@effect/platform@0.94.3(effect@3.19.16))(@effect/rpc@0.73.0(@effect/platform@0.94.3(effect@3.19.16))(effect@3.19.16))(effect@3.19.16)(react@19.2.4)(scheduler@0.27.0))(@effect-atom/atom@0.5.0(@effect/experimental@0.58.0(@effect/platform@0.94.3(effect@3.19.16))(effect@3.19.16))(@effect/platform@0.94.3(effect@3.19.16))(@effect/rpc@0.73.0(@effect/platform@0.94.3(effect@3.19.16))(effect@3.19.16))(effect@3.19.16))(effect@3.19.16)(react@19.2.4) + '@nktkas/hyperliquid': + specifier: 'catalog:' + version: 0.31.0(typescript@5.9.3) '@reown/appkit': specifier: 'catalog:' version: 1.8.17(@types/react@19.2.11)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(utf-8-validate@5.0.10)(zod@3.25.76) @@ -310,7 +319,7 @@ importers: version: 1.0.0 jsdom: specifier: 'catalog:' - version: 28.0.0(@noble/hashes@1.8.0) + version: 28.0.0(@noble/hashes@2.0.1) openapi-filter: specifier: 'catalog:' version: 3.2.3 @@ -328,7 +337,7 @@ importers: version: 0.25.0(rollup@4.57.1)(vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)) vitest: specifier: 'catalog:' - version: 4.0.18(@types/node@25.2.0)(@vitest/browser-playwright@4.0.18)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@1.8.0))(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) + version: 4.0.18(@types/node@25.2.0)(@vitest/browser-playwright@4.0.18)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) vitest-browser-react: specifier: 'catalog:' version: 2.0.5(@types/react-dom@19.2.3(@types/react@19.2.11))(@types/react@19.2.11)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.0.18) @@ -342,7 +351,7 @@ importers: specifier: 'catalog:' version: 0.5.0(@effect/experimental@0.58.0(@effect/platform@0.94.3(effect@3.19.16))(effect@3.19.16))(@effect/platform@0.94.3(effect@3.19.16))(@effect/rpc@0.73.0(@effect/platform@0.94.3(effect@3.19.16))(effect@3.19.16))(effect@3.19.16)(react@19.2.4)(scheduler@0.27.0) '@effect/experimental': - specifier: ^0.58.0 + specifier: 'catalog:' version: 0.58.0(@effect/platform@0.94.3(effect@3.19.16))(effect@3.19.16) '@effect/platform': specifier: 'catalog:' @@ -458,7 +467,7 @@ importers: version: 1.0.0 jsdom: specifier: 'catalog:' - version: 28.0.0(@noble/hashes@1.8.0) + version: 28.0.0(@noble/hashes@2.0.1) openapi-filter: specifier: 'catalog:' version: 3.2.3 @@ -476,7 +485,7 @@ importers: version: 0.25.0(rollup@4.57.1)(vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)) vitest: specifier: 'catalog:' - version: 4.0.18(@types/node@25.2.0)(@vitest/browser-playwright@4.0.18)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@1.8.0))(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) + version: 4.0.18(@types/node@25.2.0)(@vitest/browser-playwright@4.0.18)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) vitest-browser-react: specifier: 'catalog:' version: 2.0.5(@types/react-dom@19.2.3(@types/react@19.2.11))(@types/react@19.2.11)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.0.18) @@ -490,7 +499,7 @@ importers: specifier: 'catalog:' version: 0.5.0(@effect/experimental@0.58.0(@effect/platform@0.94.3(effect@3.19.16))(effect@3.19.16))(@effect/platform@0.94.3(effect@3.19.16))(@effect/rpc@0.73.0(@effect/platform@0.94.3(effect@3.19.16))(effect@3.19.16))(effect@3.19.16)(react@19.2.4)(scheduler@0.27.0) '@effect/experimental': - specifier: ^0.58.0 + specifier: 'catalog:' version: 0.58.0(@effect/platform@0.94.3(effect@3.19.16))(effect@3.19.16) '@effect/platform': specifier: 'catalog:' @@ -603,7 +612,7 @@ importers: version: 1.0.0 jsdom: specifier: 'catalog:' - version: 28.0.0(@noble/hashes@1.8.0) + version: 28.0.0(@noble/hashes@2.0.1) openapi-filter: specifier: 'catalog:' version: 3.2.3 @@ -621,7 +630,7 @@ importers: version: 0.25.0(rollup@4.57.1)(vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)) vitest: specifier: 'catalog:' - version: 4.0.18(@types/node@25.2.0)(@vitest/browser-playwright@4.0.18)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@1.8.0))(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) + version: 4.0.18(@types/node@25.2.0)(@vitest/browser-playwright@4.0.18)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) vitest-browser-react: specifier: 'catalog:' version: 2.0.5(@types/react-dom@19.2.3(@types/react@19.2.11))(@types/react@19.2.11)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.0.18) @@ -1432,6 +1441,15 @@ packages: cpu: [x64] os: [win32] + '@nktkas/hyperliquid@0.31.0': + resolution: {integrity: sha512-yK8f0ObhiX5jrK0kCLe5DXMNaG9gbJfvCFvIjYCumbo1L/EZcjHXbhVJ24rEWnw8OkWCS9zHcncJhspZQ7MTFg==} + engines: {node: '>=20.19.0'} + hasBin: true + + '@nktkas/rews@1.2.3': + resolution: {integrity: sha512-cpfcIlkUpYlbAI1cvfCTBCajWZfUM6gWyuCJXszECKxdetsU3vURKC8Sz//MDR6RWoULk9T48eJ+0agxT7yB1w==} + engines: {node: '>=20.19.0'} + '@noble/ciphers@1.3.0': resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} engines: {node: ^14.21.3 || >=16} @@ -1448,6 +1466,10 @@ packages: resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==} engines: {node: ^14.21.3 || >=16} + '@noble/curves@2.0.1': + resolution: {integrity: sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==} + engines: {node: '>= 20.19.0'} + '@noble/hashes@1.1.5': resolution: {integrity: sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ==} @@ -1463,6 +1485,10 @@ packages: resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} + '@noble/hashes@2.0.1': + resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==} + engines: {node: '>= 20.19.0'} + '@noble/secp256k1@1.7.1': resolution: {integrity: sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==} @@ -1771,6 +1797,9 @@ packages: '@scure/base@1.2.6': resolution: {integrity: sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==} + '@scure/base@2.0.0': + resolution: {integrity: sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w==} + '@scure/bip32@1.7.0': resolution: {integrity: sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==} @@ -3810,6 +3839,14 @@ packages: resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==} engines: {node: '>=18'} + micro-eth-signer@0.18.1: + resolution: {integrity: sha512-vXKhCZxrytpl+dXR9JeaE41ZVFndi7wCKc1Jd22smOMAeDErvRcXaTxhYUf1yQxis4n2kIdv/pH7iuf+5/cj+Q==} + engines: {node: '>= 20.19.0'} + + micro-packed@0.8.0: + resolution: {integrity: sha512-AKb8znIvg9sooythbXzyFeChEY0SkW0C6iXECpy/ls0e5BtwXO45J9wD9SLzBztnS4XmF/5kwZknsq+jyynd/A==} + engines: {node: '>= 20.19.0'} + miller-rabin@4.0.1: resolution: {integrity: sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==} hasBin: true @@ -4673,6 +4710,14 @@ packages: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true + valibot@1.2.0: + resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + valtio@2.1.7: resolution: {integrity: sha512-DwJhCDpujuQuKdJ2H84VbTjEJJteaSmqsuUltsfbfdbotVfNeTE4K/qc/Wi57I9x8/2ed4JNdjEna7O6PfavRg==} engines: {node: '>=12.20.0'} @@ -5625,9 +5670,9 @@ snapshots: '@esbuild/win32-x64@0.27.2': optional: true - '@exodus/bytes@1.11.0(@noble/hashes@1.8.0)': + '@exodus/bytes@1.11.0(@noble/hashes@2.0.1)': optionalDependencies: - '@noble/hashes': 1.8.0 + '@noble/hashes': 2.0.1 '@floating-ui/core@1.7.4': dependencies: @@ -5823,6 +5868,17 @@ snapshots: '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': optional: true + '@nktkas/hyperliquid@0.31.0(typescript@5.9.3)': + dependencies: + '@nktkas/rews': 1.2.3 + '@noble/hashes': 2.0.1 + micro-eth-signer: 0.18.1 + valibot: 1.2.0(typescript@5.9.3) + transitivePeerDependencies: + - typescript + + '@nktkas/rews@1.2.3': {} + '@noble/ciphers@1.3.0': {} '@noble/curves@1.8.0': @@ -5837,6 +5893,10 @@ snapshots: dependencies: '@noble/hashes': 1.8.0 + '@noble/curves@2.0.1': + dependencies: + '@noble/hashes': 2.0.1 + '@noble/hashes@1.1.5': {} '@noble/hashes@1.4.0': @@ -5846,6 +5906,8 @@ snapshots: '@noble/hashes@1.8.0': {} + '@noble/hashes@2.0.1': {} + '@noble/secp256k1@1.7.1': {} '@parcel/watcher-android-arm64@2.5.6': @@ -6614,6 +6676,8 @@ snapshots: '@scure/base@1.2.6': {} + '@scure/base@2.0.0': {} + '@scure/bip32@1.7.0': dependencies: '@noble/curves': 1.9.7 @@ -7592,7 +7656,7 @@ snapshots: '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)) playwright: 1.58.0 tinyrainbow: 3.0.3 - vitest: 4.0.18(@types/node@25.2.0)(@vitest/browser-playwright@4.0.18)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@1.8.0))(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) + vitest: 4.0.18(@types/node@25.2.0)(@vitest/browser-playwright@4.0.18)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - bufferutil - msw @@ -7608,7 +7672,7 @@ snapshots: pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.0.3 - vitest: 4.0.18(@types/node@25.2.0)(@vitest/browser-playwright@4.0.18)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@1.8.0))(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) + vitest: 4.0.18(@types/node@25.2.0)(@vitest/browser-playwright@4.0.18)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) ws: 8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil @@ -8482,10 +8546,10 @@ snapshots: dargs@8.1.0: {} - data-urls@7.0.0(@noble/hashes@1.8.0): + data-urls@7.0.0(@noble/hashes@2.0.1): dependencies: whatwg-mimetype: 5.0.0 - whatwg-url: 16.0.0(@noble/hashes@1.8.0) + whatwg-url: 16.0.0(@noble/hashes@2.0.1) transitivePeerDependencies: - '@noble/hashes' @@ -8832,9 +8896,9 @@ snapshots: minimalistic-assert: 1.0.1 minimalistic-crypto-utils: 1.0.1 - html-encoding-sniffer@6.0.0(@noble/hashes@1.8.0): + html-encoding-sniffer@6.0.0(@noble/hashes@2.0.1): dependencies: - '@exodus/bytes': 1.11.0(@noble/hashes@1.8.0) + '@exodus/bytes': 1.11.0(@noble/hashes@2.0.1) transitivePeerDependencies: - '@noble/hashes' @@ -8995,15 +9059,15 @@ snapshots: dependencies: argparse: 2.0.1 - jsdom@28.0.0(@noble/hashes@1.8.0): + jsdom@28.0.0(@noble/hashes@2.0.1): dependencies: '@acemir/cssom': 0.9.31 '@asamuzakjp/dom-selector': 6.7.7 - '@exodus/bytes': 1.11.0(@noble/hashes@1.8.0) + '@exodus/bytes': 1.11.0(@noble/hashes@2.0.1) cssstyle: 5.3.7 - data-urls: 7.0.0(@noble/hashes@1.8.0) + data-urls: 7.0.0(@noble/hashes@2.0.1) decimal.js: 10.6.0 - html-encoding-sniffer: 6.0.0(@noble/hashes@1.8.0) + html-encoding-sniffer: 6.0.0(@noble/hashes@2.0.1) http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 is-potential-custom-element-name: 1.0.1 @@ -9015,7 +9079,7 @@ snapshots: w3c-xmlserializer: 5.0.0 webidl-conversions: 8.0.1 whatwg-mimetype: 5.0.0 - whatwg-url: 16.0.0(@noble/hashes@1.8.0) + whatwg-url: 16.0.0(@noble/hashes@2.0.1) xml-name-validator: 5.0.0 transitivePeerDependencies: - '@noble/hashes' @@ -9167,6 +9231,16 @@ snapshots: meow@13.2.0: {} + micro-eth-signer@0.18.1: + dependencies: + '@noble/curves': 2.0.1 + '@noble/hashes': 2.0.1 + micro-packed: 0.8.0 + + micro-packed@0.8.0: + dependencies: + '@scure/base': 2.0.0 + miller-rabin@4.0.1: dependencies: bn.js: 4.12.2 @@ -10062,6 +10136,10 @@ snapshots: uuid@9.0.1: {} + valibot@1.2.0(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + valtio@2.1.7(@types/react@19.2.11)(react@19.2.4): dependencies: proxy-compare: 3.0.1 @@ -10131,12 +10209,12 @@ snapshots: dependencies: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - vitest: 4.0.18(@types/node@25.2.0)(@vitest/browser-playwright@4.0.18)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@1.8.0))(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) + vitest: 4.0.18(@types/node@25.2.0)(@vitest/browser-playwright@4.0.18)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) optionalDependencies: '@types/react': 19.2.11 '@types/react-dom': 19.2.3(@types/react@19.2.11) - vitest@4.0.18(@types/node@25.2.0)(@vitest/browser-playwright@4.0.18)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@1.8.0))(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2): + vitest@4.0.18(@types/node@25.2.0)(@vitest/browser-playwright@4.0.18)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2): dependencies: '@vitest/expect': 4.0.18 '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)) @@ -10161,7 +10239,7 @@ snapshots: optionalDependencies: '@types/node': 25.2.0 '@vitest/browser-playwright': 4.0.18(bufferutil@4.1.0)(playwright@1.58.0)(utf-8-validate@5.0.10)(vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.18) - jsdom: 28.0.0(@noble/hashes@1.8.0) + jsdom: 28.0.0(@noble/hashes@2.0.1) transitivePeerDependencies: - jiti - less @@ -10213,9 +10291,9 @@ snapshots: whatwg-mimetype@5.0.0: {} - whatwg-url@16.0.0(@noble/hashes@1.8.0): + whatwg-url@16.0.0(@noble/hashes@2.0.1): dependencies: - '@exodus/bytes': 1.11.0(@noble/hashes@1.8.0) + '@exodus/bytes': 1.11.0(@noble/hashes@2.0.1) tr46: 6.0.0 webidl-conversions: 8.0.1 transitivePeerDependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d9ccfe5..311e7f3 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -33,6 +33,7 @@ catalog: '@vite-pwa/assets-generator': ^1.0.2 '@vitejs/plugin-react': ^5.1.3 '@vitest/browser-playwright': ^4.0.18 + '@nktkas/hyperliquid': ^0.31.0 babel-plugin-react-compiler: ^1.0.0 class-variance-authority: ^0.7.1 clsx: ^2.1.1 From d152918253f509cd221d2b63ce55ac26c9292794 Mon Sep 17 00:00:00 2001 From: Petar Todorovic Date: Sat, 7 Feb 2026 10:50:16 +0100 Subject: [PATCH 09/10] fix: export hyperliquid atoms and add missing router plugin dep --- packages/common/src/atoms/index.ts | 1 + packages/widget/package.json | 3 ++- pnpm-lock.yaml | 3 +++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/common/src/atoms/index.ts b/packages/common/src/atoms/index.ts index a6b5ada..cdbff87 100644 --- a/packages/common/src/atoms/index.ts +++ b/packages/common/src/atoms/index.ts @@ -3,6 +3,7 @@ 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"; diff --git a/packages/widget/package.json b/packages/widget/package.json index fc2dea9..01744be 100644 --- a/packages/widget/package.json +++ b/packages/widget/package.json @@ -58,6 +58,7 @@ "vite": "catalog:", "vite-plugin-node-polyfills": "catalog:", "vitest": "catalog:", - "vitest-browser-react": "catalog:" + "vitest-browser-react": "catalog:", + "@tanstack/router-plugin": "catalog:" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9abc97e..3993989 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -586,6 +586,9 @@ importers: '@tanstack/router-cli': specifier: 'catalog:' version: 1.158.0 + '@tanstack/router-plugin': + specifier: 'catalog:' + version: 1.158.0(@tanstack/react-router@1.158.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)) '@tim-smart/openapi-gen': specifier: 'catalog:' version: 0.4.13(patch_hash=36720013d0f70c201ce8f233e38a7b93fc9fce74a17f9bc4293c3cf891b4fdc6) From deb93c6a4baead5dcde3811bdfde7287ded6f84e Mon Sep 17 00:00:00 2001 From: Petar Todorovic Date: Mon, 9 Feb 2026 10:14:29 +0100 Subject: [PATCH 10/10] refactor: match signer selection, add managedRuntime, reset init market on markets change --- packages/common/src/services/runtime.ts | 16 +++++++++---- .../src/atoms/selected-market-atom.ts | 24 ++++++++++++------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/packages/common/src/services/runtime.ts b/packages/common/src/services/runtime.ts index a65234b..4a75791 100644 --- a/packages/common/src/services/runtime.ts +++ b/packages/common/src/services/runtime.ts @@ -1,5 +1,6 @@ import { Atom, Registry } from "@effect-atom/atom-react"; -import { Cause, Effect, Layer, Logger } from "effect"; +import { Cause, Effect, Layer, Logger, ManagedRuntime, Match } from "effect"; +import { isTruthy } from "effect/Predicate"; import { ApiClientService } from "./api-client"; import { ConfigService } from "./config"; import { HttpClientService } from "./http-client"; @@ -9,11 +10,14 @@ import { LedgerSignerLayer } from "./wallet/ledger-signer"; import { isLedgerDappBrowserProvider } from "./wallet/ledger-signer/utils"; import { WalletService } from "./wallet/wallet-service"; -const Signer = isLedgerDappBrowserProvider - ? LedgerSignerLayer.pipe(Layer.orDie) - : BrowserSignerLayer.pipe(Layer.provide(ConfigService.Default)).pipe( +const Signer = Match.value(isLedgerDappBrowserProvider).pipe( + Match.when(isTruthy, () => LedgerSignerLayer.pipe(Layer.orDie)), + Match.orElse(() => + BrowserSignerLayer.pipe(Layer.provide(ConfigService.Default)).pipe( Layer.orDie, - ); + ), + ), +); const layer = Layer.mergeAll( WalletService.Default.pipe(Layer.provide(Signer)), @@ -38,6 +42,8 @@ const atomContext = Atom.context({ memoMap }); export const runtimeAtom = atomContext(layer); +export const managedRuntime = ManagedRuntime.make(layer, memoMap); + /** * Use this instead of Atom.withReactivity to ensure the same Reactivity * service instance is used for both registering and invalidating keys. diff --git a/packages/dashboard/src/atoms/selected-market-atom.ts b/packages/dashboard/src/atoms/selected-market-atom.ts index 8271094..6e9b01f 100644 --- a/packages/dashboard/src/atoms/selected-market-atom.ts +++ b/packages/dashboard/src/atoms/selected-market-atom.ts @@ -3,16 +3,22 @@ import { MarketNotFoundError, marketsAtom } from "@yieldxyz/perps-common/atoms"; import { type ApiTypes, runtimeAtom } from "@yieldxyz/perps-common/services"; import { Array as _Array, Effect, Option, Record } from "effect"; -const initMarketAtom = runtimeAtom.atom((ctx) => - ctx.resultOnce(marketsAtom).pipe( - Effect.andThen((markets) => - Record.findFirst(markets, (m) => m.value.baseAsset.symbol === "BTC").pipe( - Option.map((v) => v[1]), - Option.orElse(() => _Array.head(Record.values(markets))), +const initMarketAtom = runtimeAtom.atom( + Effect.fn(function* (ctx) { + const markets = yield* ctx.result(marketsAtom); + + return yield* Record.findFirst( + markets, + (m) => m.value.baseAsset.symbol === "BTC", + ).pipe( + Option.map((v) => v[1]), + Option.orElse(() => _Array.head(Record.values(markets))), + Effect.catchTag( + "NoSuchElementException", + () => new MarketNotFoundError(), ), - ), - Effect.catchTag("NoSuchElementException", () => new MarketNotFoundError()), - ), + ); + }), ); export const selectedMarketAtom = Atom.writable(