diff --git a/src/components/common/BondingCurveChart.tsx b/src/components/common/BondingCurveChart.tsx index 419500a6..18bbf883 100644 --- a/src/components/common/BondingCurveChart.tsx +++ b/src/components/common/BondingCurveChart.tsx @@ -138,6 +138,52 @@ const BondingCurveChart: React.FC = ({ const displayHeight = typeof height === 'number' ? `${height}px` : height; const displayWidth = typeof width === 'number' ? `${width}px` : width; + const renderTwapTooltip = ({ + active, + payload, + label, + }: { + active?: boolean; + payload?: Array<{ + dataKey?: string; + value?: number | string; + color?: string; + }>; + label?: string | number; + }) => { + if (!active || !payload || payload.length === 0) return null; + + const spotEntry = payload.find(entry => entry.dataKey === 'priceXLM'); + const twapEntry = payload.find(entry => entry.dataKey === 'twapPriceXLM'); + const spotValue = Number(spotEntry?.value ?? 0); + const twapValue = Number(twapEntry?.value ?? 0); + const deviation = + spotValue > 0 ? ((twapValue - spotValue) / spotValue) * 100 : 0; + + return ( +
+
+ {twapLabel} +
+
+
Spot: {spotValue.toFixed(4)} XLM
+
TWAP: {twapValue.toFixed(4)} XLM
+
= 0 ? 'text-amber-300' : 'text-emerald-300' + } + > + Deviation: {deviation >= 0 ? '+' : ''} + {deviation.toFixed(2)}% +
+ {label !== undefined && ( +
Supply: {label}
+ )} +
+
+ ); + }; + return (
diff --git a/src/hooks/useKeyTwap.ts b/src/hooks/useKeyTwap.ts index 6dd8f504..c6f2b8d7 100644 --- a/src/hooks/useKeyTwap.ts +++ b/src/hooks/useKeyTwap.ts @@ -2,12 +2,15 @@ import { useQuery } from '@tanstack/react-query'; import { queryKeys } from '@/lib/queryKeys'; import { courseService } from '@/services/course.service'; -export function useKeyTwap(keyId: string) { +export type KeyTwapWindow = '1h' | '24h'; + +export function useKeyTwap(keyId: string, window: KeyTwapWindow = '24h') { return useQuery({ - queryKey: queryKeys.creators.twap(keyId), - queryFn: () => courseService.getKeyTwap(keyId), + queryKey: queryKeys.creators.twap(keyId, window), + queryFn: () => courseService.getKeyTwap(keyId, window), enabled: !!keyId, staleTime: 60_000, + refetchInterval: 60_000, retry: false, }); } diff --git a/src/pages/CreatorDetailPage.tsx b/src/pages/CreatorDetailPage.tsx index 2d6aa9d6..3975fc9c 100644 --- a/src/pages/CreatorDetailPage.tsx +++ b/src/pages/CreatorDetailPage.tsx @@ -766,6 +766,51 @@ function CreatorDetailPageContent() { )}
+ {isTwapLoading ? ( +
+ + +
+ ) : twapPrice != null ? ( +
+
+ {formatDisplayKeyPrice(twapPrice)} +
+ {twapDelta != null && ( + + {twapDelta < 0 ? '▼' : '▲'}{' '} + {formatDisplayKeyPrice(Math.abs(twapDelta))} vs + spot + + )} + {twapDeviationPercent != null && ( +
+ {twapDeviationPercent >= 0 ? '+' : ''} + {twapDeviationPercent.toFixed(2)}% from spot +
+ )} +
+ ) : ( +
+ TWAP unavailable +
+ )}
) : null} diff --git a/src/services/course.service.ts b/src/services/course.service.ts index 1313be2f..59edb3e6 100644 --- a/src/services/course.service.ts +++ b/src/services/course.service.ts @@ -307,7 +307,10 @@ export interface PerformanceBond { } export type CourseSortOption = - 'volume_desc' | 'price_asc' | 'price_desc' | 'newest'; + | 'volume_desc' + | 'price_asc' + | 'price_desc' + | 'newest'; export interface GetCoursesParams { page?: number; @@ -362,10 +365,12 @@ export interface KeyHoldersPage { nextCursor: string | null; } +export type KeyTwapWindow = '1h' | '24h'; + export interface KeyTwap { - /** 24-hour time-weighted average price in stroops. */ + /** Time-weighted average price in stroops for the requested window. */ priceStroops: number | null; - window?: string; + window?: KeyTwapWindow; } export interface KeyBuybackInfo { @@ -546,7 +551,10 @@ class CourseService extends BaseApiService { } // Get the time-weighted average price - GET /keys/:keyId/twap - async getKeyTwap(keyId: string, window = '24h'): Promise { + async getKeyTwap( + keyId: string, + window: KeyTwapWindow = '24h' + ): Promise { try { const response = await this.api.get>( `/keys/${keyId}/twap`,