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(