Conversation
Show open interest in USD by applying mark price and remove the redundant USDC suffix.
Use Effect Order module with tri-state cycling (desc → asc → none) for all market table columns via sortable headers with direction icons.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
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+AtomRefinstead of arrays. - Add
PriceFlashcomponent + 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, |
There was a problem hiding this comment.
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).
| (ref: MarketRef) => ref.value.openInterest * ref.value.markPrice, | |
| (ref: MarketRef) => ref.value.openInterest, |
There was a problem hiding this comment.
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)} |
There was a problem hiding this comment.
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).
| {formatCompactUsdAmount(market.openInterest * market.markPrice)} | |
| {formatCompactUsdAmount(market.openInterest)} |
| {/* Open Interest */} | ||
| <Text variant="labelSmWhiteNeg" className="font-medium"> | ||
| {formatCompactUsdAmount(market.openInterest)} | ||
| {formatCompactUsdAmount(market.openInterest * market.markPrice)} |
There was a problem hiding this comment.
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.
| {formatCompactUsdAmount(market.openInterest * market.markPrice)} | |
| {formatCompactUsdAmount(market.openInterest)} |
…et on markets change
📝 WalkthroughWalkthroughThis 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
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Tip Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord. Comment |
There was a problem hiding this comment.
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 | 🟡 MinorConsider defensive handling for potential index mismatch.
Option.getOrThrowwill throw if the index is out of bounds. While the virtualizer's count is based onmarketData.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 | 🟡 MinorOnly the last OrderDetailRow should set
isLast.
isLastis 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 addingaria-sortfor improved accessibility.The sortable header buttons work well visually. For screen reader users, adding an
aria-sortattribute 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 usingEffect.mapErrorfor error transformation.Using
Effect.catchAllwith an error class constructor works, butEffect.mapErroris 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.valueaccess.Line 128 uses
marketRef.value.value.idwhich is correct but may confuse readers:
- First
.valueunwraps theOptionfromRecord.get- Second
.valueaccesses theAtomRef's current valueA 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
createChartWidgetreturnsnull(TradingView not loaded), the code setsisLoadingtofalse, 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 reuseChartLoading.The loading overlay (lines 249-258) duplicates the
ChartLoadingcomponent 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: AccessingpositionRef.valueoutside ofuseAtomRefis safe here but warrants a note.Line 152 accesses
positionRef.value.marketIddirectly for filtering, which doesn't subscribe to updates. This is acceptable sincemarketIdis immutable for a position, but differs from the reactive pattern used inPositionRow.
| 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, | ||
| ); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find packages/common -name "use-order-form.ts" -type fRepository: stakekit/perps-widget
Length of output: 108
🏁 Script executed:
cd packages/common && head -100 src/hooks/use-order-form.ts | tail -30Repository: 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 2Repository: stakekit/perps-widget
Length of output: 30585
🏁 Script executed:
rg "Record\.empty" --type ts -B 1 -A 1 | head -30Repository: 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 -40Repository: stakekit/perps-widget
Length of output: 1214
🏁 Script executed:
find . -name "*.d.ts" -o -name "*.ts" | xargs grep -l "export.*getOrElse" | head -5Repository: stakekit/perps-widget
Length of output: 47
🏁 Script executed:
rg "Record\.empty" --type ts -B 1 -A 1 | grep -E "(const|function|export)" | head -20Repository: 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 3Repository: stakekit/perps-widget
Length of output: 226
🏁 Script executed:
rg "from.*effect" packages/common/src/hooks/use-order-form.ts | head -10Repository: 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
resultisOk, returns the containedvalue. - If
resultisFail, 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 -80Repository: 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 -100Repository: 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:
- 1: https://tim-smart.github.io/effect-atom/atom-react/index.ts.html?utm_source=openai
- 2: https://tim-smart.github.io/effect-atom/atom/Result.ts.html
🏁 Script executed:
rg "Record\.empty\(" --type ts packages/common/src/hooks/use-deposit-form.ts -B 2 -A 2Repository: 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.
| 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, | ||
| }; |
There was a problem hiding this comment.
🧩 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 3Repository: 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 3Repository: 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.tsRepository: 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.tsRepository: stakekit/perps-widget
Length of output: 47
🏁 Script executed:
#!/bin/bash
# Search for calcTpSlPercentFromTriggerPrice
rg "calcTpSlPercentFromTriggerPrice" -B 2 -A 25Repository: 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.tsRepository: 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.tsRepository: 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:
- Update the doc comment to reflect the actual unsigned output, or
- Add a
sideparameter and apply the correct prefix usinggetTpSlPercentPrefix()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.
| scoped: Effect.gen(function* () { | ||
| const transport = new WebSocketTransport(); | ||
| const client = new SubscriptionClient({ transport }); | ||
|
|
||
| const httpTransport = new HttpTransport(); | ||
| const infoClient = new InfoClient({ transport: httpTransport }); |
There was a problem hiding this comment.
🧩 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 -5Repository: stakekit/perps-widget
Length of output: 116
🌐 Web query:
@nktkas/hyperliquid WebSocketTransport close cleanup
💡 Result:
For @nktkas/hyperliquid, the intended cleanup is:
- Unsubscribe your subscriptions (removes event listeners; sends
unsubscribewhen 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]
- 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]
- Keep-alive timer is cleared automatically on close/error
The transport’s keep-alive interval is started on"open"andclearInterval(...)runs on"close"/"error", so callingtransport.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.tsRepository: 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.
| 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)} |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
rg -n "openInterest" --type=ts --type=tsx -C3 | head -100Repository: 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 -20Repository: 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 -100Repository: stakekit/perps-widget
Length of output: 4436
🏁 Script executed:
# Find type definitions for Market
rg -n "interface Market|type Market" -C5 | head -50Repository: 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 -20Repository: 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 -50Repository: stakekit/perps-widget
Length of output: 905
🏁 Script executed:
# Check if openInterest gets transformed anywhere
rg -n "openInterest\s*=" -C2 | head -40Repository: 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).
| 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, |
There was a problem hiding this comment.
🧩 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 -100Repository: 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/nullRepository: 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 -50Repository: 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): () => voidmap(f): ReadonlyRef<B>
[1]
-
AtomRef<A>extendsReadonlyRef<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.
There was a problem hiding this comment.
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: 2may truncate precision for low-value assets (e.g.,0.00001234→0.00). TheformatAmountfunction already has smart precision logic when custom digits aren't specified—removing themaximumFractionDigitsoverride 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 repeatedsetContainercalls 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); |
There was a problem hiding this comment.
🧩 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.tsRepository: 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 5Repository: 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 2Repository: 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 1Repository: 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 2Repository: 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.tsRepository: 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 tsRepository: 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 -40Repository: 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 1Repository: 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.tsRepository: 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.
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:
HyperliquidServicetopackages/common/src/services/hyperliquid/index.tsfor real-time candle and mid-price subscriptions using WebSocket APIs.midPriceAtomandcandleStreamAtomfor 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:
marketsBySymbolAtomandupdateMarketsMidPriceAtomto update market prices in real time based on mid-price streams. [1] [2]updatePositionsMidPriceAtomto update portfolio positions with live mark prices, improving accuracy of displayed data. [1] [2] [3]UI enhancements:
PriceFlashcomponent to animate price changes, and corresponding CSS for up/down flashes. [1] [2] [3]Codebase and dependency improvements:
Formatting and utility changes:
formatTPOrSLSettingsto 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
Improvements