Skip to content

Comments

Fix: misc#19

Open
petar-omni wants to merge 12 commits intomainfrom
fix/misc
Open

Fix: misc#19
petar-omni wants to merge 12 commits intomainfrom
fix/misc

Conversation

@petar-omni
Copy link
Collaborator

@petar-omni petar-omni commented Feb 6, 2026

This pull request introduces real-time price streaming and integration with Hyperliquid WebSocket APIs, enabling live updates for market and portfolio data. It also adds a new UI component for animated price changes and refactors the codebase to support these enhancements. The most important changes are grouped below:

Hyperliquid WebSocket integration and streaming:

  • Added HyperliquidService to packages/common/src/services/hyperliquid/index.ts for real-time candle and mid-price subscriptions using WebSocket APIs.
  • Introduced midPriceAtom and candleStreamAtom for subscribing to live market data, and exposed these atoms for use throughout the application. [1] [2] [3] [4] [5] [6]

Live market and portfolio updates:

  • Added marketsBySymbolAtom and updateMarketsMidPriceAtom to update market prices in real time based on mid-price streams. [1] [2]
  • Added updatePositionsMidPriceAtom to update portfolio positions with live mark prices, improving accuracy of displayed data. [1] [2] [3]
  • Increased refresh interval for market data to one minute to reduce load and rely more on streaming updates.

UI enhancements:

  • Added new PriceFlash component to animate price changes, and corresponding CSS for up/down flashes. [1] [2] [3]

Codebase and dependency improvements:

  • Refactored position and order form hooks to use records for improved lookup performance and consistency. [1] [2] [3]
  • Updated dependencies and removed unnecessary testing libraries to streamline package configuration. [1] [2] [3] [4]
  • Refactored Vite config to use a factory function for better composability. [1] [2] [3]

Formatting and utility changes:

  • Updated formatTPOrSLSettings to return structured objects instead of strings for easier UI rendering.

These changes collectively enable robust real-time market and portfolio updates, improve UI responsiveness, and streamline the codebase for future enhancements.

Summary by CodeRabbit

  • New Features

    • Real-time mid-price updates for markets and positions
    • Price flash animation to visualize price movements
    • Enhanced chart with live candle data
    • Market sorting functionality in market selector
  • Improvements

    • Replaced static market data with dynamic Hyperliquid integration
    • Enhanced order form and deposit/withdraw state management
    • Improved market information display with funding rates
    • Streamlined development and build scripts

Copilot AI review requested due to automatic review settings February 6, 2026 23:16
@vercel
Copy link

vercel bot commented Feb 6, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
perps-dashboard Ready Ready Preview, Comment Feb 20, 2026 11:41am
perps-widget Ready Ready Preview, Comment Feb 20, 2026 11:41am

Request Review

@socket-security
Copy link

socket-security bot commented Feb 6, 2026

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds Hyperliquid WebSocket-based streaming to drive real-time market/portfolio price updates, refactors markets/positions to Record<id, AtomRef<...>> for faster lookups and reactive updates, and introduces a PriceFlash UI helper for animating price changes.

Changes:

  • Add HyperliquidService + atoms for streaming mid prices and candles; wire streaming into market/position state updates.
  • Refactor portfolio/market atoms and multiple UI callsites to use Record + AtomRef instead of arrays.
  • Add PriceFlash component + CSS animation, update UI to use it, and adjust tooling/dependency configuration (Vite config factory, package deps, scripts).

Reviewed changes

Copilot reviewed 36 out of 37 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
pnpm-workspace.yaml Adds Hyperliquid client to the shared catalog; removes testing-library entries.
pnpm-lock.yaml Lockfile updates for new deps and removed testing libs.
package.json Simplifies dev script and normalizes build script placement.
packages/common/package.json Adds Hyperliquid dependency and switches @effect/experimental to catalog spec.
packages/common/vite.config.ts Updates to use the new createCommonViteConfig() factory.
packages/common/src/vite.config.ts Refactors shared Vite config into a factory function.
packages/widget/vite.config.ts Switches to createCommonViteConfig() usage.
packages/dashboard/vite.config.ts Switches to createCommonViteConfig() usage.
packages/widget/package.json Dependency spec changes (notably devDependency removals).
packages/dashboard/package.json Switches @effect/experimental to catalog spec and removes testing-library dev deps.
packages/common/src/styles/base.css Adds keyframes/classes for price flash animations.
packages/common/src/components/molecules/price-flash.tsx New component to animate price changes by toggling CSS classes.
packages/common/src/components/index.ts Exports the new PriceFlash component.
packages/common/src/services/runtime.ts Registers HyperliquidService in the runtime layer.
packages/common/src/services/index.ts Re-exports Hyperliquid service module.
packages/common/src/services/hyperliquid/index.ts New Hyperliquid WS/HTTP service wrapper (subscriptions + snapshot).
packages/common/src/atoms/hyperliquid-atoms.ts Introduces midPriceAtom stream source.
packages/common/src/atoms/candle-atoms.ts Introduces candleStreamAtom family for candle subscriptions.
packages/common/src/atoms/index.ts Exports candle atoms from the common atoms entrypoint.
packages/common/src/atoms/markets-atoms.ts Adds marketsBySymbolAtom, increases refresh interval, adds mid-price market updates.
packages/common/src/atoms/portfolio-atoms.ts Refactors positionsAtom to a Record of AtomRefs; adds mid-price position updates.
packages/common/src/lib/formatting.ts Changes formatTPOrSLSettings to return structured { tp, sl }.
packages/common/src/hooks/use-order-form.ts Updates position lookup logic to use Record.get instead of array search.
packages/dashboard/src/components/modules/root/Preload.tsx Mounts live mid-price update atoms for markets/positions.
packages/widget/src/components/modules/Root/PreloadAtoms.tsx Mounts live mid-price updates for positions when wallet is connected.
packages/dashboard/src/components/modules/trade/market-info/index.tsx Uses PriceFlash, adjusts display fields (incl. OI/funding).
packages/dashboard/src/components/modules/trade/market-info/market-selector-popover.tsx Adds sortable headers and adjusts displayed metrics (incl. OI).
packages/dashboard/src/components/modules/trade/order-form/index.tsx Updates advanced order display; adds maker/taker fee rows.
packages/dashboard/src/components/molecules/positions/index.tsx Enhances tab labels to show counts for positions/orders when connected.
packages/dashboard/src/components/molecules/positions/positions-tab.tsx Refactors to AtomRef-based data flow and adds market selection interaction.
packages/widget/src/components/modules/Home/index.tsx Updates positions count logic for Record-based positions.
packages/widget/src/components/modules/Home/Positions/index.tsx Refactors positions aggregation/rendering for Record of AtomRefs.
packages/widget/src/components/modules/Home/Positions/position-card.tsx Refactors to accept positionRef and read via useAtomRef.
packages/widget/src/components/modules/PositionDetails/Overview/index.tsx Refactors position lookup from array .find to Record.get.
packages/widget/src/components/modules/PositionDetails/Overview/Position/index.tsx Refactors to pass positionRef and read reactively via useAtomRef.
packages/widget/src/components/modules/PositionDetails/Close/state.tsx Refactors close flow to fetch position via Record.get.
packages/widget/src/components/modules/Order/Overview/index.tsx Updates TP/SL settings display to use new structured formatter output.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

volume: Order.mapInput(Order.number, (ref: MarketRef) => ref.value.volume24h),
oi: Order.mapInput(
Order.number,
(ref: MarketRef) => ref.value.openInterest * ref.value.markPrice,
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

openInterest is already documented/typed as “total notional value of all open positions” (i.e., already in quote/USD). Multiplying it by markPrice here will inflate the value and break the OI sort. Use openInterest directly for both sorting and display unless the API field semantics have changed (in which case the API schema/docs should be updated too).

Suggested change
(ref: MarketRef) => ref.value.openInterest * ref.value.markPrice,
(ref: MarketRef) => ref.value.openInterest,

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm, from what i see in API responses, it returns openInterest in baseAsset size, while swagger comment says this as total notional value of all open positions - @Philippoes is comment wrong or this in fact should be in notional value?

</Text>
<Text variant="labelSmWhiteNeg" className="font-medium">
{market.makerFee ? formatRate(market.makerFee) : "-"}
{formatCompactUsdAmount(market.openInterest * market.markPrice)}
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Open interest is displayed as market.openInterest * market.markPrice, but openInterest is already a notional value per the API schema. This will overstate OI; use market.openInterest directly (or update the API schema/docs if the backend now returns contracts instead of notional).

Suggested change
{formatCompactUsdAmount(market.openInterest * market.markPrice)}
{formatCompactUsdAmount(market.openInterest)}

Copilot uses AI. Check for mistakes.
{/* Open Interest */}
<Text variant="labelSmWhiteNeg" className="font-medium">
{formatCompactUsdAmount(market.openInterest)}
{formatCompactUsdAmount(market.openInterest * market.markPrice)}
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Open interest display is being computed as market.openInterest * market.markPrice, but openInterest is already a notional value per the API schema. This will show an incorrect (overstated) OI number; display market.openInterest directly unless the backend field meaning has changed.

Suggested change
{formatCompactUsdAmount(market.openInterest * market.markPrice)}
{formatCompactUsdAmount(market.openInterest)}

Copilot uses AI. Check for mistakes.
@coderabbitai
Copy link

coderabbitai bot commented Feb 13, 2026

📝 Walkthrough

Walkthrough

This pull request integrates Hyperliquid protocol support with live market data streaming, replaces TradingView charts with lightweight-charts powered by real-time candle data, refactors form field access to use a unified API pattern, adds price change animations, and updates dependencies across the monorepo.

Changes

Cohort / File(s) Summary
Hyperliquid Service
packages/common/src/services/hyperliquid/index.ts, packages/common/src/services/index.ts
New service providing candleSnapshot, subscribeCandle, and subscribeMidPrice methods for Hyperliquid protocol interaction with WebSocket and HTTP clients.
Mid-Price Atoms
packages/common/src/atoms/hyperliquid-atoms.ts, packages/common/src/atoms/index.ts, packages/common/src/atoms/markets-atoms.ts, packages/common/src/atoms/portfolio-atoms.ts
Added midPriceAtom and atoms for updating market and position data with real-time mid-prices; records now keyed by symbol for efficient lookups.
Chart System Refactoring
packages/common/src/components/molecules/Chart/*, packages/common/src/components/molecules/candle-mapping.ts
Replaced TradingView integration with lightweight-charts; added candle data mapping, interval constants, and atom-driven chart state with history preloading.
TradingView Symbol Generation Removal
packages/common/scripts/generate-tradingview-symbols/*, packages/common/src/assets/tradingview-symbols.json, packages/common/src/services/constants.ts
Deleted TradingView symbol generation script, utility schemas, and static symbol mappings; removed TRADING_VIEW_WIDGET_SCRIPT_URL constant.
Form Field API Refactoring
packages/common/src/atoms/order-form-atoms.ts, packages/common/src/hooks/use-deposit-form.ts, packages/common/src/hooks/use-order-form.ts, packages/common/src/hooks/use-withdraw-form.ts, packages/common/src/components/molecules/limit-price-dialog.tsx, packages/dashboard/src/components/molecules/header/deposit/state.tsx, packages/dashboard/src/components/molecules/header/withdraw/state.tsx
Consolidated field value/setter retrieval using getFieldAtoms instead of separate getFieldValue and setValue calls.
Formatting & Schema Updates
packages/common/src/lib/formatting.ts, packages/common/src/services/runtime.ts
Changed formatTPOrSLSettings to return object with separate tp/sl fields; added Match-based signer selection logic and HyperliquidService to runtime.
Price Display & Animation
packages/common/src/components/molecules/price-flash.tsx, packages/common/src/components/index.ts, packages/common/src/styles/base.css
Added PriceFlash component with CSS animations for price direction indicators; exported new component from public API.
Position & Market Record-Based Lookups
packages/dashboard/src/atoms/selected-market-atom.ts, packages/dashboard/src/components/modules/trade/market-info/index.tsx, packages/dashboard/src/components/modules/trade/market-info/market-selector-popover.tsx, packages/widget/src/components/modules/Home/Positions/index.tsx, packages/widget/src/components/modules/Home/Positions/position-card.tsx, packages/widget/src/components/modules/PositionDetails/.../*
Refactored position/market data flow to use Record-based lookups and AtomRef wrappers instead of direct DTOs; added sorting to market selector.
Dashboard Trade Components
packages/dashboard/src/components/modules/trade/chart-v2.tsx, packages/dashboard/src/components/modules/trade/order-form/index.tsx, packages/dashboard/src/components/molecules/positions/index.tsx, packages/dashboard/src/components/molecules/positions/positions-tab.tsx
Added chart-v2 with TradingView widget stub, updated order form with taker/maker fees, refactored positions display to use AtomRefs.
Vite Config Refactoring
packages/common/src/vite.config.ts, packages/common/vite.config.ts, packages/dashboard/vite.config.ts, packages/widget/vite.config.ts
Converted exported commonViteConfig constant to createCommonViteConfig factory function; updated all consumers to invoke the function.
Dependencies & Workspace
package.json, packages/common/package.json, packages/dashboard/package.json, packages/widget/package.json, pnpm-workspace.yaml, tsconfig.json
Updated @effect/experimental to catalog resolution, added @nktkas/hyperliquid and lightweight-charts, removed testing-library dependencies, bumped various tooling versions, added catalog overrides.
Preload Updates
packages/dashboard/src/components/modules/root/Preload.tsx, packages/widget/src/components/modules/Root/PreloadAtoms.tsx, packages/widget/src/app.tsx
Mounted mid-price update atoms for wallet-connected state; removed TradingView widget preload script.

Sequence Diagram(s)

sequenceDiagram
    participant UI as UI Component
    participant Service as HyperliquidService
    participant Atom as Atom State
    participant WebSocket as WebSocket
    
    UI->>Atom: Subscribe to midPriceAtom
    Atom->>Service: subscribeMidPrice()
    Service->>WebSocket: Open connection
    WebSocket-->>Service: AllMidsWsEvent (streaming)
    Service-->>Atom: emit mid-price data
    Atom->>Atom: Update markets/positions markPrice
    Atom-->>UI: Re-render with new prices
    
    Note over Service,WebSocket: On unmount
    Service->>WebSocket: Close connection
Loading
sequenceDiagram
    participant User as User
    participant Chart as Chart Component
    participant Atom as chartDataAtom
    participant Service as HyperliquidService
    participant LWC as Lightweight Charts
    
    User->>Chart: Select symbol/variant
    Chart->>Atom: Request chart data
    Atom->>Service: candleSnapshot(symbol, interval)
    Service-->>Atom: Initial candles
    Atom->>LWC: Initialize chart & series
    LWC-->>Chart: Chart ready
    Chart-->>User: Display candles
    
    User->>Chart: Scroll near history start
    Chart->>Atom: loadOlderCandles()
    Atom->>Service: candleSnapshot(earlier range)
    Service-->>Atom: Historical candles
    Atom->>LWC: Update series data
    LWC-->>Chart: Extended history
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐰 A rabbit hops through Hyperliquid streams,
Real-time candles dance in chart-driven dreams,
Atoms now atomic, with prices that flash,
Records keyed smartly—no more cache crash!
The form fields unified, the widget retired,
This perps-forward leap has the team inspired! ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Fix: misc' is vague and generic, using non-descriptive terminology that does not convey meaningful information about the substantial changeset. Replace with a more descriptive title that captures the primary changes, such as 'feat: add Hyperliquid streaming and real-time price updates' or similar.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/misc

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/dashboard/src/components/modules/trade/market-info/market-selector-popover.tsx (1)

343-345: ⚠️ Potential issue | 🟡 Minor

Consider defensive handling for potential index mismatch.

Option.getOrThrow will throw if the index is out of bounds. While the virtualizer's count is based on marketData.length, there's a small window where data could change between renders (e.g., rapid search input), leaving the virtualizer with stale indices.

🛡️ Suggested defensive approach
             {rowVirtualizer.getVirtualItems().map((virtualItem) => {
-              const market = _Array
+              const marketOption = _Array
                 .get(marketData, virtualItem.index)
-                .pipe(Option.getOrThrow);
+
+              if (Option.isNone(marketOption)) return null;
+
+              const market = marketOption.value;

               return (
packages/dashboard/src/components/modules/trade/order-form/index.tsx (1)

401-414: ⚠️ Potential issue | 🟡 Minor

Only the last OrderDetailRow should set isLast.

isLast is now set on multiple rows, which will render extra bottom borders. Keep it on the final row only.

🧩 Suggested fix
         <OrderDetailRow
           label="Fees"
           value={formatAmount(calculations.fees)}
-          isLast
         />
         <OrderDetailRow
           label="Taker Fee"
           value={market.takerFee ? formatRate(market.takerFee) : "-"}
-          isLast
         />
         <OrderDetailRow
           label="Maker Fee"
           value={market.makerFee ? formatRate(market.makerFee) : "-"}
           isLast
         />
🤖 Fix all issues with AI agents
In `@packages/common/src/hooks/use-order-form.ts`:
- Around line 88-95: Result.getOrElse is being passed a non-lazy fallback
(Record.empty) causing a type mismatch; update the call where positions is
defined (the useAtomValue of positionsAtom) to pass a lazy fallback by wrapping
Record.empty in a zero-arg function (i.e., use Result.getOrElse(() =>
Record.empty)). This change affects the positions declaration using
Result.getOrElse, and no other logic (like Record.get(positions, marketId) or
currentPosition) needs modification.

In `@packages/common/src/lib/formatting.ts`:
- Around line 142-158: The doc and output are inconsistent: update
formatTPOrSLSettings to accept a side parameter (e.g., side: PositionSide) and
apply the correct sign prefix when composing TP/SL strings; inside
formatTPOrSLSettings use getTpSlPercentPrefix(side, "tp") and
getTpSlPercentPrefix(side, "sl") (or equivalent prefix helpers) and concatenate
that prefix with formatPercentage(percentage) when mapping takeProfit/stopLoss,
preserving the "TP Off"/"SL Off" fallback behavior; keep the Option/pipe logic
but replace the current unsigned formatPercentage call with prefix +
formatPercentage so short positions render signed percentages as documented.

In `@packages/common/src/services/hyperliquid/index.ts`:
- Around line 13-18: The WebSocketTransport instance created in the scoped
Effect (inside scoped: Effect.gen) is never closed; add a scope finalizer to
call transport.close() when the service scope ends. Inside the generator where
you construct const transport = new WebSocketTransport(), register a finalizer
(e.g., via Effect.addFinalizer or the equivalent used in this codebase) that
invokes transport.close() (wrapped in an Effect.sync/Effect.try if necessary) so
the transport is cleaned up when the scoped Effect completes; leave
HttpTransport and existing subscription unsubscribe finalizers unchanged.

In `@packages/dashboard/src/components/modules/trade/market-info/index.tsx`:
- Line 138: The displayed open interest is incorrectly multiplied by
market.markPrice; change the call that currently uses
formatCompactUsdAmount(market.openInterest * market.markPrice) to use the raw
USD value formatCompactUsdAmount(market.openInterest) instead, removing the
multiplication by market.markPrice so the UI uses the API's already-denominated
USD openInterest (refer to formatCompactUsdAmount and the market.openInterest /
market.markPrice symbols to locate the line).

In `@packages/widget/src/components/modules/Home/Positions/index.tsx`:
- Around line 42-49: The totals calculation uses AtomRef.value directly
(positionRefs.reduce accessing ref.value.unrealizedPnl and ref.value.margin),
which does not subscribe to updates and will not re-render when individual
position atoms change; to fix, subscribe to each position AtomRef before
reducing (e.g., call useAtomRef or the library's hook on each ref or map
positions to subscribed values) so the component re-renders on per-position
updates, or alternatively compute totals from a derived/computed atom that
itself subscribes to the underlying position atoms; update the code around
positionRefs / positions to obtain subscribed position values rather than
reading ref.value directly.
🧹 Nitpick comments (7)
packages/dashboard/src/components/modules/trade/market-info/market-selector-popover.tsx (1)

171-188: Consider adding aria-sort for improved accessibility.

The sortable header buttons work well visually. For screen reader users, adding an aria-sort attribute would communicate the current sort state more explicitly.

♿ Optional accessibility improvement
   return (
     <button
       type="button"
       onClick={() => onToggle(column)}
       className="flex items-center gap-1 hover:text-white/80 transition-colors"
+      aria-sort={direction === "asc" ? "ascending" : direction === "desc" ? "descending" : undefined}
     >
packages/common/src/styles/base.css (1)

26-52: Add a reduced‑motion fallback for price flash animations.

A short override helps users who prefer reduced motion avoid flashing effects.

♿ Suggested CSS override
+@media (prefers-reduced-motion: reduce) {
+  .price-flash-up,
+  .price-flash-down {
+    animation: none;
+  }
+}
packages/common/src/services/hyperliquid/index.ts (1)

26-28: Consider using Effect.mapError for error transformation.

Using Effect.catchAll with an error class constructor works, but Effect.mapError is more idiomatic for simple error type transformations since it clearly signals the intent is to transform the error rather than handle and recover from it.

♻️ Proposed refactor
       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 })),
+          Effect.mapError((cause) => new GetCandleSnapshotError({ cause })),
         );
packages/common/src/atoms/portfolio-atoms.ts (1)

116-137: Consider adding a brief comment clarifying the double .value access.

Line 128 uses marketRef.value.value.id which is correct but may confuse readers:

  • First .value unwraps the Option from Record.get
  • Second .value accesses the AtomRef's current value

A brief inline comment would improve maintainability.

📝 Suggested documentation
         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);
+          // marketRef.value is the AtomRef, .value.id accesses the market's id
+          const positionRef = Record.get(positions, marketRef.value.value.id);
           if (positionRef._tag === "None") return;
packages/dashboard/src/components/modules/trade/chart-v2.tsx (2)

230-233: Consider showing an error state when TradingView library fails to load.

When createChartWidget returns null (TradingView not loaded), the code sets isLoading to false, leaving the user with an empty chart area. Consider showing an error or retry state instead.

💡 Suggested improvement
+  const [error, setError] = useState<string | null>(null);
+
   useEffect(() => {
     const container = containerRef.current;
     if (!container) return;
 
     // Clean up a previous widget instance
     if (widgetRef.current) {
       widgetRef.current.remove?.();
       widgetRef.current = null;
     }
 
     setIsLoading(true);
+    setError(null);
 
     const widget = createChartWidget(container, symbol);
     widgetRef.current = widget;
 
     if (widget) {
       widget.onChartReady?.(() => {
         setIsLoading(false);
       });
     } else {
       // TradingView library not loaded yet – keep loading state
       setIsLoading(false);
+      setError("Chart library not available");
     }

249-258: Consider extracting the inline loading overlay to reuse ChartLoading.

The loading overlay (lines 249-258) duplicates the ChartLoading component defined at lines 173-182. Consider reusing it for consistency.

♻️ Suggested refactor
       {isLoading && (
-        <div className="absolute inset-0 bg-black flex items-center justify-center z-10 transition-opacity duration-200">
-          <div className="flex flex-col items-center gap-3">
-            <div className="w-8 h-8 border-2 border-white/20 border-t-white rounded-full animate-spin" />
-            <Text as="span" variant="inherit" className="text-white/70 text-sm">
-              Loading chart...
-            </Text>
-          </div>
-        </div>
+        <div className="absolute inset-0 z-10">
+          <ChartLoading />
+        </div>
       )}
packages/dashboard/src/components/molecules/positions/positions-tab.tsx (1)

151-153: Accessing positionRef.value outside of useAtomRef is safe here but warrants a note.

Line 152 accesses positionRef.value.marketId directly for filtering, which doesn't subscribe to updates. This is acceptable since marketId is immutable for a position, but differs from the reactive pattern used in PositionRow.

Comment on lines 88 to 95
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,
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find packages/common -name "use-order-form.ts" -type f

Repository: stakekit/perps-widget

Length of output: 108


🏁 Script executed:

cd packages/common && head -100 src/hooks/use-order-form.ts | tail -30

Repository: stakekit/perps-widget

Length of output: 715


🏁 Script executed:

cat -n packages/common/src/hooks/use-order-form.ts | sed -n '80,100p'

Repository: stakekit/perps-widget

Length of output: 626


🏁 Script executed:

rg "Result\.getOrElse" --type ts --max-count 10 -B 2 -A 2

Repository: stakekit/perps-widget

Length of output: 30585


🏁 Script executed:

rg "Record\.empty" --type ts -B 1 -A 1 | head -30

Repository: stakekit/perps-widget

Length of output: 1708


🏁 Script executed:

rg "getOrElse" --type ts -B 2 -A 5 packages/common/src/hooks/use-deposit-form.ts | head -40

Repository: stakekit/perps-widget

Length of output: 1214


🏁 Script executed:

find . -name "*.d.ts" -o -name "*.ts" | xargs grep -l "export.*getOrElse" | head -5

Repository: stakekit/perps-widget

Length of output: 47


🏁 Script executed:

rg "Record\.empty" --type ts -B 1 -A 1 | grep -E "(const|function|export)" | head -20

Repository: stakekit/perps-widget

Length of output: 497


🌐 Web query:

effect-js Result.getOrElse API signature lazy

💡 Result:

In the effect package there isn’t a Result module with Result.getOrElse; the getOrElse you’re likely looking for is on Option (and a few other data types like Context).

Option.getOrElse (lazy fallback)

Signature:

declare const getOrElse: {
  <B>(onNone: LazyArg<B>): <A>(self: Option<A>) => A | B
  <A, B>(self: Option<A>, onNone: LazyArg<B>): A | B
}

onNone is a LazyArg<B> (i.e. () => B), so it’s evaluated only if the Option is None (lazy). [1]

Related: Context.getOrElse (also lazy)

declare const getOrElse: {
  <S, I, B>(tag: Tag<I, S>, orElse: LazyArg<B>): <Services>(self: Context<Services>) => S | B
  <Services, S, I, B>(self: Context<Services>, tag: Tag<I, S>, orElse: LazyArg<B>): S | B
}
``` [2]

**Sources:** [1] [2]

---

🏁 Script executed:

```shell
rg "import.*Result" packages/common/src/hooks/use-order-form.ts -A 3

Repository: stakekit/perps-widget

Length of output: 226


🏁 Script executed:

rg "from.*effect" packages/common/src/hooks/use-order-form.ts | head -10

Repository: stakekit/perps-widget

Length of output: 189


🌐 Web query:

effect-js Result module getOrElse API documentation

💡 Result:

In @eser/functions (functions.results), getOrElse extracts the success value from a Result, or returns a fallback when it’s a failure.

Signature (API docs):

  • results.getOrElse<T, E>(result: Result<T, E>, fallback: T | ((error: E) => T)): T [1]

Behavior:

  • If result is Ok, returns the contained value.
  • If result is Fail, returns:
    • the provided fallback value, or
    • the return value of fallback(error) if you pass a function. [1]

Example:

import * as functions from "@eser/functions";

const { results } = functions;

const a = results.ok(123);
const b = results.fail("nope");

results.getOrElse(a, 0);                 // 123
results.getOrElse(b, 0);                 // 0
results.getOrElse(b, (e) => `err:${e}`); // "err:nope"
``` [2]

**Docs location:** `@eser/functions` → `results.getOrElse` in the JSR “all symbols” page. [1]

**Sources:** [1] [2]

---

🏁 Script executed:

```shell
rg "Result\.getOrElse" --type ts -B 5 | head -80

Repository: stakekit/perps-widget

Length of output: 5684


🏁 Script executed:

find . -name "package.json" -exec grep -l "@effect-atom" {} \;

Repository: stakekit/perps-widget

Length of output: 160


🏁 Script executed:

rg "Result" node_modules/@effect-atom --type ts -A 5 2>/dev/null | head -100

Repository: stakekit/perps-widget

Length of output: 47


🏁 Script executed:

cat package.json | grep -A 5 -B 5 "@effect-atom"

Repository: stakekit/perps-widget

Length of output: 47


🌐 Web query:

@effect-atom/atom-react Result.getOrElse API signature documentation

💡 Result:

In @effect-atom/atom-react, Result is re-exported from @effect-atom/atom/Result, so you use it as Result.getOrElse. [1]

Result.getOrElse signature

export declare const getOrElse: {
  <B>(orElse: LazyArg<B>): <A, E>(self: Result<A, E>) => A | B
  <A, E, B>(self: Result<A, E>, orElse: LazyArg<B>): A | B
}

[2]

So it supports both:

  • Data-last (curried): Result.getOrElse(() => fallback)(result)
  • Data-first: Result.getOrElse(result, () => fallback)

[2]

Sources: @effect-atom/atom-react index re-exports Result [1], and the getOrElse API is documented in @effect-atom/atom’s Result.ts reference [2].

[1] (tim-smart.github.io)
[2] (tim-smart.github.io)

Citations:


🏁 Script executed:

rg "Record\.empty\(" --type ts packages/common/src/hooks/use-deposit-form.ts -B 2 -A 2

Repository: stakekit/perps-widget

Length of output: 226


Wrap Record.empty in a lazy function for Result.getOrElse.

Line 90: Result.getOrElse requires a lazy fallback (LazyArg<B>, i.e., () => B). Pass () => Record.empty instead of Record.empty.

🔧 Corrected fallback
-  ).pipe(Result.getOrElse(Record.empty));
+  ).pipe(Result.getOrElse(() => Record.empty));
🤖 Prompt for AI Agents
In `@packages/common/src/hooks/use-order-form.ts` around lines 88 - 95,
Result.getOrElse is being passed a non-lazy fallback (Record.empty) causing a
type mismatch; update the call where positions is defined (the useAtomValue of
positionsAtom) to pass a lazy fallback by wrapping Record.empty in a zero-arg
function (i.e., use Result.getOrElse(() => Record.empty)). This change affects
the positions declaration using Result.getOrElse, and no other logic (like
Record.get(positions, marketId) or currentPosition) needs modification.

Comment on lines +142 to +158
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,
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all call sites of formatTPOrSLSettings
echo "=== Call sites of formatTPOrSLSettings ==="
rg -n "formatTPOrSLSettings" --type ts --type tsx -B 2 -A 2

echo -e "\n=== TPOrSLSettings type definition ==="
rg -n "TPOrSLSettings" --type ts --type tsx -B 5 -A 10

echo -e "\n=== Where takeProfit/stopLoss percentages are assigned ==="
rg -n "takeProfit\.percentage\s*=|stopLoss\.percentage\s*=" --type ts --type tsx -B 3 -A 3

Repository: stakekit/perps-widget

Length of output: 308


🏁 Script executed:

#!/bin/bash
# Find all call sites of formatTPOrSLSettings
echo "=== Call sites of formatTPOrSLSettings ==="
rg "formatTPOrSLSettings" -B 2 -A 2

echo -e "\n=== TPOrSLSettings type definition ==="
rg "interface TPOrSLSettings|type TPOrSLSettings" -B 2 -A 15

echo -e "\n=== takeProfit and stopLoss usage patterns ==="
rg "takeProfit\.percentage|stopLoss\.percentage" -B 3 -A 3

Repository: stakekit/perps-widget

Length of output: 11322


🏁 Script executed:

#!/bin/bash
# Find where TPOrSLConfiguration percentage is set or calculated
echo "=== Where percentage is assigned in TP/SL context ==="
rg "percentage\s*:" -B 5 -A 2 | grep -A 7 -B 7 "takeProfit\|stopLoss"

echo -e "\n=== getTPOrSLConfigurationFromPosition or similar functions ==="
rg "getTPOrSLConfiguration|getTpSlOrders|useEditSLTP" -B 2 -A 15

echo -e "\n=== Look for position side handling with TP/SL ==="
rg "isLong|isShort" -B 3 -A 3 | grep -A 5 -B 5 "tp\|sl\|takeProfit\|stopLoss"

Repository: stakekit/perps-widget

Length of output: 33682


🏁 Script executed:

#!/bin/bash
# Look at the implementation of getTPOrSLConfigurationFromPosition
rg -A 40 "export function getTPOrSLConfigurationFromPosition" packages/common/src/lib/math.ts

Repository: stakekit/perps-widget

Length of output: 1544


🏁 Script executed:

#!/bin/bash
# Look at calcTpSlPercentFromTriggerPrice implementation
rg -A 30 "function calcTpSlPercentFromTriggerPrice" packages/common/src/lib/math.ts

Repository: stakekit/perps-widget

Length of output: 47


🏁 Script executed:

#!/bin/bash
# Search for calcTpSlPercentFromTriggerPrice
rg "calcTpSlPercentFromTriggerPrice" -B 2 -A 25

Repository: stakekit/perps-widget

Length of output: 7385


🏁 Script executed:

#!/bin/bash
# Look at getTpSlPercentPrefix and how formatPercentage uses the percentage
rg "getTpSlPercentPrefix" -B 2 -A 15

echo -e "\n=== Check formatPercentage function ==="
rg "function formatPercentage|const formatPercentage" -A 10 packages/common/src/lib/formatting.ts

Repository: stakekit/perps-widget

Length of output: 6455


🏁 Script executed:

#!/bin/bash
# Get the full function with doc comment
rg -B 10 "export function formatTPOrSLSettings" packages/common/src/lib/formatting.ts

Repository: stakekit/perps-widget

Length of output: 316


Doc comment is misleading and sign handling is broken for short positions.

The function's documentation promises side-specific signed output ("Long: TP +10%, SL -5%" / "Short: TP -10%, SL +5%"), but the implementation cannot deliver this. formatTPOrSLSettings has no side parameter and simply outputs unsigned percentages via formatPercentage(percentage), which appends "%" without any sign. Since TPOrSLSettings.takeProfit.percentage and stopLoss.percentage are unsigned values, short positions will display incorrect signs (e.g., "TP 10%" instead of "TP -10%").

Either:

  1. Update the doc comment to reflect the actual unsigned output, or
  2. Add a side parameter and apply the correct prefix using getTpSlPercentPrefix() to match the documented behavior.
🤖 Prompt for AI Agents
In `@packages/common/src/lib/formatting.ts` around lines 142 - 158, The doc and
output are inconsistent: update formatTPOrSLSettings to accept a side parameter
(e.g., side: PositionSide) and apply the correct sign prefix when composing
TP/SL strings; inside formatTPOrSLSettings use getTpSlPercentPrefix(side, "tp")
and getTpSlPercentPrefix(side, "sl") (or equivalent prefix helpers) and
concatenate that prefix with formatPercentage(percentage) when mapping
takeProfit/stopLoss, preserving the "TP Off"/"SL Off" fallback behavior; keep
the Option/pipe logic but replace the current unsigned formatPercentage call
with prefix + formatPercentage so short positions render signed percentages as
documented.

Comment on lines +13 to +18
scoped: Effect.gen(function* () {
const transport = new WebSocketTransport();
const client = new SubscriptionClient({ transport });

const httpTransport = new HttpTransport();
const infoClient = new InfoClient({ transport: httpTransport });
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's locate and examine the actual file
find . -path "*packages/common/src/services/hyperliquid/index.ts" -type f | head -5

Repository: stakekit/perps-widget

Length of output: 116


🌐 Web query:

@nktkas/hyperliquid WebSocketTransport close cleanup

💡 Result:

For @nktkas/hyperliquid, the intended cleanup is:

  1. Unsubscribe your subscriptions (removes event listeners; sends unsubscribe when the socket is open):
const sub = await subs.allMids((ev) => {});
await sub.unsubscribe();

The subscription object returned by subscribe(...) is { unsubscribe, failureSignal }. The manager removes the listener immediately and, if it was the last listener for that subscription, it also sends an unsubscribe request (when connected). [1]

  1. Close the shared WebSocketTransport (stops keep-alive and closes the underlying reconnecting socket):
await transport.close(); // optionally: await transport.close(abortSignal)

WebSocketTransport.close() attaches close/error listeners, calls this.socket.close(), and resolves when it’s actually closed. [2]

  1. Keep-alive timer is cleared automatically on close/error
    The transport’s keep-alive interval is started on "open" and clearInterval(...) runs on "close" / "error", so calling transport.close() is sufficient to stop the ping loop. [2]

If your goal is “close = full teardown (don’t keep subscriptions around for resubscribe)”: set resubscribe = false before closing (or ensure the socket is “terminated”); on "close"/"error", the subscription manager will then iterate subscriptions and call each stored unsubscribe() to drop listeners and internal state. [1][2]

Sources: WebSocketTransport close() + keep-alive logic [2], subscription manager unsubscribe/close behavior [1].


🌐 Web query:

@nktkas/hyperliquid library documentation

💡 Result:

Official docs for @nktkas/hyperliquid (TypeScript/JS Hyperliquid SDK):

  • GitBook documentation (start here): has Introduction, Installation, Quick Start, and guides/examples. [1][2]
  • GitHub repo README: overview + quick example + project structure/source. [3]
  • Package registries (install/versions):
    • npm package page (Node/package-manager install, changelog/versions). [4]
    • JSR package page (Deno/Bun/Workers/browser-friendly distribution details). [5]

Install (npm): npm i @nktkas/hyperliquid`` [2][4]

Quick usage pattern: InfoClient for read-only API calls, ExchangeClient for trading actions, with HttpTransport / WebSocketTransport. [1][3]

Sources: [1] [2] [3] [4] [5]


🏁 Script executed:

# Now let's read the actual file to see the full context
cat -n packages/common/src/services/hyperliquid/index.ts

Repository: stakekit/perps-widget

Length of output: 3282


🏁 Script executed:

# Also check if there are any other files in the hyperliquid service directory
ls -la packages/common/src/services/hyperliquid/

Repository: stakekit/perps-widget

Length of output: 233


Add cleanup for WebSocketTransport when the service scope ends.

The WebSocketTransport created at line 14 is never closed, which leaves the WebSocket connection open and prevents the keep-alive timer from stopping. When the service scope ends, this transport must be cleaned up by calling transport.close().

The subscriptions already have finalizers to call unsubscribe(), but transport cleanup must happen at the service scope level. Note that HttpTransport is for stateless HTTP requests and does not require explicit cleanup.

♻️ Proposed fix
    scoped: Effect.gen(function* () {
      const transport = new WebSocketTransport();
      const client = new SubscriptionClient({ transport });

      const httpTransport = new HttpTransport();
      const infoClient = new InfoClient({ transport: httpTransport });

+      yield* Effect.addFinalizer(() =>
+        Effect.promise(() => transport.close()),
+      );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
scoped: Effect.gen(function* () {
const transport = new WebSocketTransport();
const client = new SubscriptionClient({ transport });
const httpTransport = new HttpTransport();
const infoClient = new InfoClient({ transport: httpTransport });
scoped: Effect.gen(function* () {
const transport = new WebSocketTransport();
const client = new SubscriptionClient({ transport });
const httpTransport = new HttpTransport();
const infoClient = new InfoClient({ transport: httpTransport });
yield* Effect.addFinalizer(() =>
Effect.promise(() => transport.close()),
);
🤖 Prompt for AI Agents
In `@packages/common/src/services/hyperliquid/index.ts` around lines 13 - 18, The
WebSocketTransport instance created in the scoped Effect (inside scoped:
Effect.gen) is never closed; add a scope finalizer to call transport.close()
when the service scope ends. Inside the generator where you construct const
transport = new WebSocketTransport(), register a finalizer (e.g., via
Effect.addFinalizer or the equivalent used in this codebase) that invokes
transport.close() (wrapped in an Effect.sync/Effect.try if necessary) so the
transport is cleaned up when the scoped Effect completes; leave HttpTransport
and existing subscription unsubscribe finalizers unchanged.

</Text>
<Text variant="labelSmWhiteNeg" className="font-medium">
{market.makerFee ? formatRate(market.makerFee) : "-"}
{formatCompactUsdAmount(market.openInterest * market.markPrice)}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

rg -n "openInterest" --type=ts --type=tsx -C3 | head -100

Repository: stakekit/perps-widget

Length of output: 92


🏁 Script executed:

# Also find type definitions for market object
rg -n "interface.*[Mm]arket|type.*[Mm]arket" --type=ts --type=tsx | head -20

Repository: stakekit/perps-widget

Length of output: 92


🏁 Script executed:

# Search for openInterest in the specific file
cat -n packages/dashboard/src/components/modules/trade/market-info/index.tsx | sed -n '130,150p'

Repository: stakekit/perps-widget

Length of output: 845


🏁 Script executed:

rg -n "openInterest" -C3 | head -100

Repository: stakekit/perps-widget

Length of output: 4436


🏁 Script executed:

# Find type definitions for Market
rg -n "interface Market|type Market" -C5 | head -50

Repository: stakekit/perps-widget

Length of output: 3728


🏁 Script executed:

# Search for where market data comes from
rg -n "market\." packages/dashboard/src/components/modules/trade/market-info/index.tsx | head -20

Repository: stakekit/perps-widget

Length of output: 817


🏁 Script executed:

# Check api-schemas.ts for full context
cat -n packages/common/src/services/api-client/api-schemas.ts | sed -n '165,185p'

Repository: stakekit/perps-widget

Length of output: 626


🏁 Script executed:

# Search for any API response examples or test data
rg -n "openInterest.*:" -B2 -A2 | head -50

Repository: stakekit/perps-widget

Length of output: 905


🏁 Script executed:

# Check if openInterest gets transformed anywhere
rg -n "openInterest\s*=" -C2 | head -40

Repository: stakekit/perps-widget

Length of output: 47


Remove the multiplication by markPrice from the openInterest calculation.

According to the API schema, openInterest is defined as "total notional value of all open positions" — it's already denominated in USD, not base asset units. Multiplying it by markPrice produces an incorrect result. Use formatCompactUsdAmount(market.openInterest) instead, consistent with how it's handled in packages/widget/src/components/modules/PositionDetails/Overview/overview-tab-content.tsx.

🤖 Prompt for AI Agents
In `@packages/dashboard/src/components/modules/trade/market-info/index.tsx` at
line 138, The displayed open interest is incorrectly multiplied by
market.markPrice; change the call that currently uses
formatCompactUsdAmount(market.openInterest * market.markPrice) to use the raw
USD value formatCompactUsdAmount(market.openInterest) instead, removing the
multiplication by market.markPrice so the UI uses the API's already-denominated
USD openInterest (refer to formatCompactUsdAmount and the market.openInterest /
market.markPrice symbols to locate the line).

Comment on lines +42 to 49
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,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the actual file and context around the flagged lines
cat -n packages/widget/src/components/modules/Home/Positions/index.tsx | head -100

Repository: stakekit/perps-widget

Length of output: 4126


🏁 Script executed:

# Check the position-card.tsx mentioned in the review to see the useAtomRef pattern
find . -name "position-card.tsx" -o -name "position-card.ts" 2>/dev/null

Repository: stakekit/perps-widget

Length of output: 138


🏁 Script executed:

# Search for .value usage patterns on Ref objects throughout the codebase
rg "\.value\." --type=ts --type=tsx -B 2 -A 2 | head -50

Repository: stakekit/perps-widget

Length of output: 92


🌐 Web query:

@effect-atom/atom-react AtomRef synchronous value property documentation API

💡 Result:

In @effect-atom/atom-react, an AtomRef is a synchronous ref-like object whose current snapshot is exposed via the value property (it’s part of AtomRef.ReadonlyRef<A>):

  • ReadonlyRef<A> includes:

    • readonly value: A (synchronous current value)
    • subscribe(f): () => void
    • map(f): ReadonlyRef<B>
      [1]
  • AtomRef<A> extends ReadonlyRef<A> and adds:

    • prop(key): AtomRef<A[K]>
    • set(value): AtomRef<A>
    • update(f): AtomRef<A>
      [1]

If you’re in React, you typically don’t read ref.value directly in render (it won’t subscribe/re-render). Instead use the hook:

  • useAtomRef(ref: AtomRef.ReadonlyRef<A>): A (subscribes and returns the current value for rendering) [2]

Sources
[1] AtomRef API (ReadonlyRef.value, AtomRef interface): https://tim-smart.github.io/effect-atom/atom/AtomRef.ts.html
[2] React hook useAtomRef signature: https://tim-smart.github.io/effect-atom/atom-react/Hooks.ts.html


Confirm: Direct .value access won't re-render when individual positions update.

The .value property on AtomRef is synchronous and valid, but the @effect-atom/atom-react library documentation explicitly states it does not subscribe to updates and won't trigger re-renders in React. In lines 42-49, accessing ref.value.unrealizedPnl and ref.value.margin directly means the totals will only recalculate when the entire positionsResult atom changes—not when individual position atoms update via streaming or other mechanisms.

If position values change mid-trade (e.g., real-time price updates), the displayed totals will remain stale until positionsResult itself is refreshed. This may be acceptable if totals are meant to be atomic snapshots, but if they should reflect live position changes, consider whether useAtomRef() on positions should be introduced at a higher level, or if the current batched-update behavior is intentional.

🤖 Prompt for AI Agents
In `@packages/widget/src/components/modules/Home/Positions/index.tsx` around lines
42 - 49, The totals calculation uses AtomRef.value directly (positionRefs.reduce
accessing ref.value.unrealizedPnl and ref.value.margin), which does not
subscribe to updates and will not re-render when individual position atoms
change; to fix, subscribe to each position AtomRef before reducing (e.g., call
useAtomRef or the library's hook on each ref or map positions to subscribed
values) so the component re-renders on per-position updates, or alternatively
compute totals from a derived/computed atom that itself subscribes to the
underlying position atoms; update the code around positionRefs / positions to
obtain subscribed position values rather than reading ref.value directly.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
packages/common/src/services/hyperliquid/index.ts (1)

48-60: Consider bounded capacity for the mid-price broadcast.

Stream.broadcastDynamic({ capacity: "unbounded" }) can accumulate messages without limit if consumers lag behind. For high-frequency mid-price updates, a bounded capacity with a dropping strategy (e.g., { capacity: 16, strategy: "dropping" }) may prevent unbounded memory growth while still delivering timely prices.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/common/src/services/hyperliquid/index.ts` around lines 48 - 60,
subscribeMidPrice currently uses Stream.broadcastDynamic({ capacity: "unbounded"
}) which can lead to unbounded memory growth when consumers lag; change the
broadcast configuration in the subscribeMidPrice Stream to use a bounded
capacity and dropping strategy (for example, Stream.broadcastDynamic({ capacity:
16, strategy: "dropping" })) so slow consumers won’t cause unbounded buffering;
update the call site where subscribeMidPrice is defined to pass the bounded
config to Stream.broadcastDynamic and ensure any tests/consumers tolerate
dropped mid-price events.
packages/common/src/components/molecules/Chart/constants.ts (1)

28-31: Consider dynamic precision for the price formatter.

The maximumFractionDigits: 2 may truncate precision for low-value assets (e.g., 0.000012340.00). The formatAmount function already has smart precision logic when custom digits aren't specified—removing the maximumFractionDigits override would enable adaptive formatting based on price magnitude.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/common/src/components/molecules/Chart/constants.ts` around lines 28
- 31, The priceFormatter in localization currently forces maximumFractionDigits:
2 which can truncate low-value asset prices; remove the explicit
maximumFractionDigits override so formatAmount can use its built-in adaptive
precision logic. Update the localization.priceFormatter to call
formatAmount(price, { symbol: null }) (i.e., drop maximumFractionDigits) so
formatAmount can determine precision dynamically.
packages/common/src/components/molecules/Chart/index.tsx (1)

65-68: Potential repeated setContainer calls on re-renders.

The ref callback runs on every render. Since setContainer(Result.success(el)) is called each time with the same element, this may trigger unnecessary atom updates. Consider guarding against redundant updates:

♻️ Suggested improvement
         ref={(el) => {
           if (!el) return;
+          if (Result.isSuccess(containerResult) && containerResult.value === el) return;
           setContainer(Result.success(el));
         }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/common/src/components/molecules/Chart/index.tsx` around lines 65 -
68, The ref callback in the Chart component currently calls
setContainer(Result.success(el)) on every render which can cause redundant atom
updates; change the ref handler to first read the current container (or use the
atom's getter) and only call setContainer(Result.success(el)) when el is
non-null and not equal to the previously stored element (e.g., when container is
not a success or container.value !== el), so you avoid setting the same Result
repeatedly from the ref callback.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/common/src/components/molecules/Chart/state.ts`:
- Line 82: chartDataAtom is reading interval once with
ctx.get(chartIntervalAtom) but ChartDataParams keys exclude interval so changing
the toggle doesn't recreate the atom and the chart keeps old candles; fix by
including interval in the atom's identity or subscribing to changes: either add
an interval field to ChartDataParams and use it in the atom key (so
chartDataAtom is keyed by symbol, variant, and interval), or replace
ctx.get(chartIntervalAtom) with ctx.subscribe(chartIntervalAtom) inside the
chartDataAtom initializer so the atom re-runs when chartIntervalAtom changes;
update all references to ChartDataParams and the chartDataAtom initializer
accordingly.

---

Duplicate comments:
In `@packages/common/src/services/hyperliquid/index.ts`:
- Around line 13-18: The WebSocketTransport created inside the scoped Effect (in
scoped: Effect.gen(function* () { ... })) is never closed; add a finalizer that
calls transport.close() when the service scope ends. Inside the Effect.gen block
after creating const transport = new WebSocketTransport(), register a scope
finalizer (using the environment/scope helper available in your Effect
framework) to invoke transport.close() so the SubscriptionClient/WebSocket is
cleaned up when the scoped service is released.

---

Nitpick comments:
In `@packages/common/src/components/molecules/Chart/constants.ts`:
- Around line 28-31: The priceFormatter in localization currently forces
maximumFractionDigits: 2 which can truncate low-value asset prices; remove the
explicit maximumFractionDigits override so formatAmount can use its built-in
adaptive precision logic. Update the localization.priceFormatter to call
formatAmount(price, { symbol: null }) (i.e., drop maximumFractionDigits) so
formatAmount can determine precision dynamically.

In `@packages/common/src/components/molecules/Chart/index.tsx`:
- Around line 65-68: The ref callback in the Chart component currently calls
setContainer(Result.success(el)) on every render which can cause redundant atom
updates; change the ref handler to first read the current container (or use the
atom's getter) and only call setContainer(Result.success(el)) when el is
non-null and not equal to the previously stored element (e.g., when container is
not a success or container.value !== el), so you avoid setting the same Result
repeatedly from the ref callback.

In `@packages/common/src/services/hyperliquid/index.ts`:
- Around line 48-60: subscribeMidPrice currently uses Stream.broadcastDynamic({
capacity: "unbounded" }) which can lead to unbounded memory growth when
consumers lag; change the broadcast configuration in the subscribeMidPrice
Stream to use a bounded capacity and dropping strategy (for example,
Stream.broadcastDynamic({ capacity: 16, strategy: "dropping" })) so slow
consumers won’t cause unbounded buffering; update the call site where
subscribeMidPrice is defined to pass the bounded config to
Stream.broadcastDynamic and ensure any tests/consumers tolerate dropped
mid-price events.

const { chart, series } = yield* ctx.result(initChartAtom);
const timeScale = chart.timeScale();

const interval = ctx.get(chartIntervalAtom);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the state.ts file around line 82
cd packages/common/src/components/molecules/Chart && wc -l state.ts && sed -n '70,100p' state.ts

Repository: stakekit/perps-widget

Length of output: 1019


🏁 Script executed:

# Check ChartDataParams definition
rg -n "ChartDataParams|interface.*Params" packages/common/src/components/molecules/Chart/state.ts -B 2 -A 5

Repository: stakekit/perps-widget

Length of output: 1034


🏁 Script executed:

# Check how chartIntervalAtom is used and if there are dependency mechanisms
rg -n "chartIntervalAtom|subscribe" packages/common/src/components/molecules/Chart/state.ts -B 2 -A 2

Repository: stakekit/perps-widget

Length of output: 876


🏁 Script executed:

# Look for atom re-run or dependency patterns across the file
rg -n "ctx\.subscribe|ctx\.get\(" packages/common/src/components/molecules/Chart/state.ts -B 1 -A 1

Repository: stakekit/perps-widget

Length of output: 128


🏁 Script executed:

# Look for how chartIntervalAtom is updated (setInterval or similar patterns)
rg -rn "chartIntervalAtom\s*=" packages/common/src/components/molecules/Chart/ -A 2 -B 2

Repository: stakekit/perps-widget

Length of output: 451


🏁 Script executed:

# Check the full file to understand the atom lifecycle and any re-initialization
sed -n '142,160p' packages/common/src/components/molecules/Chart/state.ts

Repository: stakekit/perps-widget

Length of output: 557


🏁 Script executed:

# Search for usage of chartDataAtom and interval changes in Chart components
rg -rn "chartDataAtom|setInterval" packages/common/src/components/molecules/Chart/ --type ts

Repository: stakekit/perps-widget

Length of output: 543


🏁 Script executed:

# Check if there's a hook or effect that handles interval changes
rg -rn "useAtom|useAtomValue|interval" packages/common/src/components/molecules/Chart/ --type ts | head -40

Repository: stakekit/perps-widget

Length of output: 1716


🏁 Script executed:

# Let's check the index.tsx to see how chartDataAtom is used when interval changes
sed -n '1,80p' packages/common/src/components/molecules/Chart/index.tsx | grep -A 20 -B 5 "chartData\|setInterval"

Repository: stakekit/perps-widget

Length of output: 1703


🏁 Script executed:

# Check if there's any re-initialization logic or dependency tracking in the file
rg -n "addFinalizer|ctx.subscribe|ctx.get" packages/common/src/components/molecules/Chart/state.ts -B 1 -A 1

Repository: stakekit/perps-widget

Length of output: 303


🏁 Script executed:

# Let's see the complete chartDataAtom definition to understand its full lifecycle
sed -n '73,155p' packages/common/src/components/molecules/Chart/state.ts

Repository: stakekit/perps-widget

Length of output: 2582


Interval changes don't update the chart—old interval data persists.

ctx.get(chartIntervalAtom) reads the interval once at atom initialization. Since chartDataAtom is keyed only by symbol and variant (not interval), changing the interval via the toggle won't re-initialize the atom. The chart will continue showing data and subscribing to candles for the old interval.

Fix by adding interval to ChartDataParams so each interval gets its own atom instance, or use ctx.subscribe(chartIntervalAtom) to re-run the atom when the interval changes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/common/src/components/molecules/Chart/state.ts` at line 82,
chartDataAtom is reading interval once with ctx.get(chartIntervalAtom) but
ChartDataParams keys exclude interval so changing the toggle doesn't recreate
the atom and the chart keeps old candles; fix by including interval in the
atom's identity or subscribing to changes: either add an interval field to
ChartDataParams and use it in the atom key (so chartDataAtom is keyed by symbol,
variant, and interval), or replace ctx.get(chartIntervalAtom) with
ctx.subscribe(chartIntervalAtom) inside the chartDataAtom initializer so the
atom re-runs when chartIntervalAtom changes; update all references to
ChartDataParams and the chartDataAtom initializer accordingly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant