From 1b0e1dadb55d9c7525484435adcddf4b8d907f6d Mon Sep 17 00:00:00 2001 From: BaseDev-Eth Date: Mon, 31 Aug 2026 07:47:34 +0100 Subject: [PATCH] fix(#387,#400,#402,#398): invert demo-history ternary, validate stream id, graceful withdrawable fallback, cap RateTicker at endTime - #387: flip fetchTransactionHistory ternary so demo data returns only when no publicKey is connected, not for real wallets - #400: validate URL segment against /^\d+$/ before BigInt() to prevent raw SyntaxError surfacing to the user - #402: load getStreamInfo first, then fetch getWithdrawable separately with independent error handling so a withdrawable failure doesn't block the entire detail page (both initial load and background refresh) - #398: add endTime prop to RateTicker and cap elapsed seconds so the live counter freezes at the stream's end instead of ticking past the contract balance --- app/stream/[id]/page.tsx | 37 ++++++++++++++++++++------------ components/stream/RateTicker.tsx | 22 +++++++++++++++---- lib/indexer.ts | 2 +- 3 files changed, 42 insertions(+), 19 deletions(-) diff --git a/app/stream/[id]/page.tsx b/app/stream/[id]/page.tsx index 29917ba..cff11c9 100644 --- a/app/stream/[id]/page.tsx +++ b/app/stream/[id]/page.tsx @@ -77,19 +77,27 @@ export default function StreamPage() { setLoading(true); setError(null); try { + if (!/^\d+$/.test(id)) { + if (isCurrent()) setError('Invalid stream ID.'); + return; + } + const addr = await getStreamAddress(publicKey, BigInt(id)); if (!isCurrent()) return; if (!addr) { setError('Stream not found.'); return; } - const [streamInfo, wAmt] = await Promise.all([ - getStreamInfo(publicKey, addr), - getWithdrawable(publicKey, addr), - ]); - + const streamInfo = await getStreamInfo(publicKey, addr); if (!isCurrent()) return; + setStreamAddress(addr); setInfo(streamInfo); - setWithdrawable(wAmt); + + try { + const wAmt = await getWithdrawable(publicKey, addr); + if (isCurrent()) setWithdrawable(wAmt); + } catch { + if (isCurrent()) setWithdrawable(0n); + } } catch (e) { if (!isCurrent()) return; setError(e instanceof Error ? e.message : 'Failed to load stream.'); @@ -109,17 +117,17 @@ export default function StreamPage() { const addr = streamAddress; const t = setInterval(async () => { try { - const [streamInfo, wAmt] = await Promise.all([ - getStreamInfo(publicKey, addr), - getWithdrawable(publicKey, addr), - ]); - if (mounted.current) { - setInfo(streamInfo); - setWithdrawable(wAmt); - } + const streamInfo = await getStreamInfo(publicKey, addr); + if (mounted.current) setInfo(streamInfo); } catch { /* keep last-good data */ } + try { + const wAmt = await getWithdrawable(publicKey, addr); + if (mounted.current) setWithdrawable(wAmt); + } catch { + /* keep last-good withdrawable */ + } }, STREAM_REFRESH_MS); return () => clearInterval(t); }, [publicKey, streamAddress]); @@ -200,6 +208,7 @@ export default function StreamPage() {

{tokenSymbol}

diff --git a/components/stream/RateTicker.tsx b/components/stream/RateTicker.tsx index 9992468..7355af7 100644 --- a/components/stream/RateTicker.tsx +++ b/components/stream/RateTicker.tsx @@ -10,13 +10,16 @@ interface RateTickerProps { startBalance: bigint; /** Decimal places to display (default: 7 for XLM) */ decimals?: number; + /** Unix timestamp when the stream ends (0 = open-ended). Ticker freezes past this. */ + endTime?: number; } /** * Live-updating balance counter. * Increments every 100ms based on ratePerSecond without any contract calls. + * Freezes at endTime so the ticker doesn't overshoot the contract balance (#398). */ -export function RateTicker({ ratePerSecond, startBalance, decimals = 7 }: RateTickerProps) { +export function RateTicker({ ratePerSecond, startBalance, decimals = 7, endTime = 0 }: RateTickerProps) { const startRef = useRef<{ ts: number; balance: bigint }>({ ts: Date.now(), balance: startBalance, @@ -30,12 +33,23 @@ export function RateTicker({ ratePerSecond, startBalance, decimals = 7 }: RateTi useEffect(() => { const id = setInterval(() => { - const elapsed = BigInt(Math.floor((Date.now() - startRef.current.ts) / 1000)); - const current = startRef.current.balance + elapsed * ratePerSecond; + const elapsedMs = Date.now() - startRef.current.ts; + let elapsedSec = BigInt(Math.floor(elapsedMs / 1000)); + + if (endTime > 0) { + const endMs = endTime * 1000; + const remainingMs = endMs - startRef.current.ts; + const remainingSec = BigInt(Math.max(0, Math.floor(remainingMs / 1000))); + if (elapsedSec > remainingSec) elapsedSec = remainingSec; + } + + if (elapsedSec < 0n) elapsedSec = 0n; + + const current = startRef.current.balance + elapsedSec * ratePerSecond; setDisplay(fromStroops(current, decimals)); }, 100); return () => clearInterval(id); - }, [ratePerSecond, decimals]); + }, [ratePerSecond, decimals, endTime]); return ( diff --git a/lib/indexer.ts b/lib/indexer.ts index 5a49890..408ccd8 100644 --- a/lib/indexer.ts +++ b/lib/indexer.ts @@ -74,7 +74,7 @@ export async function fetchTransactionHistory( return; } if (mock) { - resolve(publicKey ? DEMO_TXS : []); + resolve(publicKey ? [] : DEMO_TXS); } else { reject( new Error(