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(