diff --git a/src/hooks/useKeyTwap.ts b/src/hooks/useKeyTwap.ts new file mode 100644 index 00000000..6dd8f504 --- /dev/null +++ b/src/hooks/useKeyTwap.ts @@ -0,0 +1,13 @@ +import { useQuery } from '@tanstack/react-query'; +import { queryKeys } from '@/lib/queryKeys'; +import { courseService } from '@/services/course.service'; + +export function useKeyTwap(keyId: string) { + return useQuery({ + queryKey: queryKeys.creators.twap(keyId), + queryFn: () => courseService.getKeyTwap(keyId), + enabled: !!keyId, + staleTime: 60_000, + retry: false, + }); +} diff --git a/src/lib/queryKeys.ts b/src/lib/queryKeys.ts index fa3def57..a09d09d6 100644 --- a/src/lib/queryKeys.ts +++ b/src/lib/queryKeys.ts @@ -25,6 +25,8 @@ export const queryKeys = { ['creators', creatorId, 'holders'] as const, activity: (creatorId: string) => ['creators', creatorId, 'activity'] as const, + twap: (creatorId: string) => + ['creators', creatorId, 'twap', '24h'] as const, }, wallet: { holdings: (address: string) => ['wallet', address, 'holdings'] as const, diff --git a/src/pages/CreatorDetailPage.tsx b/src/pages/CreatorDetailPage.tsx index 3edaf068..596833c8 100644 --- a/src/pages/CreatorDetailPage.tsx +++ b/src/pages/CreatorDetailPage.tsx @@ -29,6 +29,9 @@ import { useWalletHoldings } from '@/hooks/useWallet'; import CoCreatorSection from '@/components/creator/CoCreatorSection'; import ShareTwitterButton from '@/components/common/ShareTwitterButton'; import { useDocumentTitle } from '@/hooks/useDocumentTitle'; +import { useKeyTwap } from '@/hooks/useKeyTwap'; +import Skeleton from '@/components/ui/skeleton'; +import { Tooltip } from '@/components/ui/tooltip'; function CreatorDetailPageContent() { const { id } = useParams<{ id: string }>(); @@ -63,6 +66,7 @@ function CreatorDetailPageContent() { // return a per-user one. Only shown for authenticated users. const nextBuyAllowedAt = userPosition?.nextBuyAllowedAt ?? creator?.nextBuyAllowedAt ?? null; + const { data: twap, isLoading: isTwapLoading } = useKeyTwap(id || ''); // Track stale data indicator const { shouldShowBadge, handleRefetch } = useCreatorProfileStaleIndicator( @@ -146,6 +150,9 @@ function CreatorDetailPageContent() { supply: (index + 1) * 20, priceXLM: priceStroops / 10_000_000, })); + const spotPrice = resolveCreatorKeyPriceStroops(creator); + const twapPrice = twap?.priceStroops ?? null; + const twapDelta = twapPrice != null && spotPrice != null ? twapPrice - spotPrice : null; const hasRealStakingData = creator.stakingPoolBalance != null || @@ -216,6 +223,27 @@ function CreatorDetailPageContent() { /> + {isTwapLoading ? ( +
+
+
+ ) : twapPrice != null ? ( +
+
+
+
+ TWAP (24h) + + + +
+
{formatDisplayKeyPrice(twapPrice)}
+
+ {twapDelta != null && {twapDelta < 0 ? '▼' : '▲'} {formatDisplayKeyPrice(Math.abs(twapDelta))} vs spot} +
+
+ ) : null} + {/* Staking Rewards */} diff --git a/src/services/course.service.ts b/src/services/course.service.ts index be837f8e..b390a7f6 100644 --- a/src/services/course.service.ts +++ b/src/services/course.service.ts @@ -127,6 +127,12 @@ export interface KeyHoldersPage { nextCursor: string | null; } +export interface KeyTwap { + /** 24-hour time-weighted average price in stroops. */ + priceStroops: number | null; + window?: string; +} + class CourseService extends BaseApiService { private readonly PROFILE_CACHE_TTL = 30000; // 30 seconds @@ -230,6 +236,19 @@ class CourseService extends BaseApiService { } } + // Get the time-weighted average price - GET /keys/:keyId/twap + async getKeyTwap(keyId: string, window = '24h'): Promise { + try { + const response = await this.api.get>( + `/keys/${keyId}/twap`, + { params: { window } } + ); + return response.data.data; + } catch (error) { + throw this.handleError(error); + } + } + // Get enrolled courses - GET /courses/enrolled async getEnrolledCourses(): Promise { try {