From 2288acd912cb84bca275eacbec3fceb38cb00164 Mon Sep 17 00:00:00 2001 From: Sakariyah Abdulhazeem Date: Fri, 28 Aug 2026 19:01:14 +0100 Subject: [PATCH 1/4] feat: display 24h key price twap --- src/hooks/useKeyTwap.ts | 13 +++++++++++++ src/lib/queryKeys.ts | 2 ++ src/pages/CreatorDetailPage.tsx | 27 +++++++++++++++++++++++++++ src/services/course.service.ts | 19 +++++++++++++++++++ 4 files changed, 61 insertions(+) create mode 100644 src/hooks/useKeyTwap.ts 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 c0f524a2..99716702 100644 --- a/src/pages/CreatorDetailPage.tsx +++ b/src/pages/CreatorDetailPage.tsx @@ -30,6 +30,9 @@ import CoCreatorSection from '@/components/creator/CoCreatorSection'; import ShareTwitterButton from '@/components/common/ShareTwitterButton'; import { usePurchaseConfetti } from '@/hooks/usePurchaseConfetti'; import { useDocumentTitle } from '@/hooks/useDocumentTitle'; +import { useKeyTwap } from '@/hooks/useKeyTwap'; +import Skeleton from '@/components/ui/skeleton'; +import { Tooltip } from '@/components/ui/tooltip'; function CreatorDetailPageContent() { usePurchaseConfetti(); @@ -66,6 +69,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( @@ -149,6 +153,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 || @@ -219,6 +226,26 @@ function CreatorDetailPageContent() { /> +
+ {isTwapLoading ? ( +
+ ) : twapPrice != null ? ( +
+
+
+ TWAP (24h) + + + +
+
{formatDisplayKeyPrice(twapPrice)}
+
+ {twapDelta != null && = 0 ? 'text-sm font-semibold text-emerald-400' : 'text-sm font-semibold text-rose-400'}>{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 9dfcad6f..bc04461d 100644 --- a/src/services/course.service.ts +++ b/src/services/course.service.ts @@ -129,6 +129,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 @@ -232,6 +238,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 { From 62f6ddd7de9804a063ec26024cdc1a6c558f60ae Mon Sep 17 00:00:00 2001 From: May_Dev Date: Sat, 29 Aug 2026 15:15:42 +0100 Subject: [PATCH 2/4] fix(TWAP): green when spot above TWAP, hide when null --- src/pages/CreatorDetailPage.tsx | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/pages/CreatorDetailPage.tsx b/src/pages/CreatorDetailPage.tsx index 99716702..6fe9706a 100644 --- a/src/pages/CreatorDetailPage.tsx +++ b/src/pages/CreatorDetailPage.tsx @@ -226,25 +226,28 @@ function CreatorDetailPageContent() { /> -
- {isTwapLoading ? ( + {isTwapLoading ? ( +
- ) : twapPrice != null ? ( +
+
+
+ ) : twapPrice != null ? ( +
- TWAP (24h) + TWAP (24h)
{formatDisplayKeyPrice(twapPrice)}
- {twapDelta != null && = 0 ? 'text-sm font-semibold text-emerald-400' : 'text-sm font-semibold text-rose-400'}>{twapDelta >= 0 ? '▲' : '▼'} {formatDisplayKeyPrice(Math.abs(twapDelta))} vs spot} + {twapDelta != null && {twapDelta < 0 ? '▼' : '▲'} {formatDisplayKeyPrice(Math.abs(twapDelta))} vs spot}
- ) : null} -
- + + ) : null} {/* Staking Rewards */} From 6ee0d0cdb70121d65efe3cb0c077474857c17f0a Mon Sep 17 00:00:00 2001 From: Auto-Fix Bot Date: Sat, 29 Aug 2026 15:18:09 +0100 Subject: [PATCH 3/4] feat: match exact TWAP tooltip copy (#827) --- src/pages/CreatorDetailPage.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pages/CreatorDetailPage.tsx b/src/pages/CreatorDetailPage.tsx index 6fe9706a..f6cdade9 100644 --- a/src/pages/CreatorDetailPage.tsx +++ b/src/pages/CreatorDetailPage.tsx @@ -230,15 +230,13 @@ function CreatorDetailPageContent() {
-
- ) : twapPrice != null ? (
TWAP (24h) - +
From e777e6ff186a20578391990e92c7294f414329ad Mon Sep 17 00:00:00 2001 From: Sakariyah Abdulhazeem Date: Thu, 3 Sep 2026 20:34:43 +0100 Subject: [PATCH 4/4] fix: restore valid simulation and slippage sources --- .../__tests__/slippageTolerance.utils.test.ts | 88 ------------------- 1 file changed, 88 deletions(-) diff --git a/src/utils/__tests__/slippageTolerance.utils.test.ts b/src/utils/__tests__/slippageTolerance.utils.test.ts index ba9bdb57..117d5dc3 100644 --- a/src/utils/__tests__/slippageTolerance.utils.test.ts +++ b/src/utils/__tests__/slippageTolerance.utils.test.ts @@ -126,91 +126,3 @@ describe('slippageTolerance.utils', () => { }); }); -import { - computeSlippagePriceBounds, - validateSlippageTolerance, - MAX_SLIPPAGE_TOLERANCE_PERCENT, -} from '@/utils/slippageTolerance.utils'; - -describe('computeSlippagePriceBounds (#877)', () => { - it('computes max_price of 100.5 for a 0.5% buy tolerance on a 100 XLM preview', () => { - const { maxPrice, minPrice } = computeSlippagePriceBounds( - 100, - 0.5, - 'buy' - ); - expect(maxPrice).toBe(100.5); - expect(minPrice).toBeNull(); - }); - - it('computes max_price of 105 for a 5% buy tolerance on a 100 XLM preview', () => { - const { maxPrice } = computeSlippagePriceBounds(100, 5, 'buy'); - expect(maxPrice).toBe(105); - }); - - it('computes min_price of 99 for a 1% sell tolerance on a 100 XLM preview', () => { - const { minPrice, maxPrice } = computeSlippagePriceBounds( - 100, - 1, - 'sell' - ); - expect(minPrice).toBe(99); - expect(maxPrice).toBeNull(); - }); - - it('sets max_price equal to the preview price for a custom 0% tolerance', () => { - const { maxPrice } = computeSlippagePriceBounds(100, 0, 'buy'); - expect(maxPrice).toBe(100); - }); - - it('sets min_price equal to the preview price for a custom 0% sell tolerance', () => { - const { minPrice } = computeSlippagePriceBounds(100, 0, 'sell'); - expect(minPrice).toBe(100); - }); - - it('does not accumulate binary floating-point drift for common percentages', () => { - // 100 * 1.005 === 100.49999999999999 in raw IEEE-754 arithmetic; - // the util must round this back to the exact expected value. - expect(computeSlippagePriceBounds(100, 0.5, 'buy').maxPrice).toBe( - 100.5 - ); - expect(computeSlippagePriceBounds(37.5, 1.5, 'buy').maxPrice).toBeCloseTo( - 38.0625, - 7 - ); - }); -}); - -describe('validateSlippageTolerance (#877)', () => { - it('accepts a custom tolerance of 0%', () => { - expect(validateSlippageTolerance(0)).toEqual({ - valid: true, - error: null, - }); - }); - - it('accepts tolerances within the valid range', () => { - expect(validateSlippageTolerance(0.5).valid).toBe(true); - expect(validateSlippageTolerance(25).valid).toBe(true); - expect(validateSlippageTolerance(MAX_SLIPPAGE_TOLERANCE_PERCENT).valid).toBe( - true - ); - }); - - it('rejects a custom tolerance above 50% with a validation error', () => { - const result = validateSlippageTolerance(51); - expect(result.valid).toBe(false); - expect(result.error).toMatch(/50%/); - }); - - it('rejects negative tolerances', () => { - const result = validateSlippageTolerance(-1); - expect(result.valid).toBe(false); - expect(result.error).toBeTruthy(); - }); - - it('rejects non-finite input', () => { - expect(validateSlippageTolerance(NaN).valid).toBe(false); - expect(validateSlippageTolerance(Infinity).valid).toBe(false); - }); -});