diff --git a/backend/src/controllers/stream.controller.ts b/backend/src/controllers/stream.controller.ts index 5476fb6e..7e004ba0 100644 --- a/backend/src/controllers/stream.controller.ts +++ b/backend/src/controllers/stream.controller.ts @@ -178,7 +178,7 @@ export const listStreams = async (req: Request, res: Response) => { typeof limit === 'string' ? (Number.parseInt(limit, 10) || DEFAULT_STREAM_PAGE_SIZE) : DEFAULT_STREAM_PAGE_SIZE, MAX_STREAM_PAGE_SIZE ); - const parsedOffset = typeof offset === 'string' ? (Number.parseInt(offset, 10) || 0) : 0; + const parsedOffset = typeof offset === 'string' ? Math.max(0, Number.parseInt(offset, 10) || 0) : 0; // Validate sort field const validSortFields = ['createdAt', 'startTime', 'lastUpdateTime', 'depositedAmount', 'endTime']; @@ -295,7 +295,7 @@ export const getStreamEvents = async (req: Request, res: Response) => { let offset = 0; if (rawOffset && typeof rawOffset === 'string') { - offset = Number.parseInt(rawOffset, 10) || 0; + offset = Math.max(0, Number.parseInt(rawOffset, 10) || 0); } else if (rawPage && typeof rawPage === 'string' && !cursor) { const page = Number.parseInt(rawPage, 10) || 1; offset = Math.max(0, (page - 1) * limit); @@ -564,7 +564,7 @@ export const topUpStreamHandler = async (req: Request, res: Response) => { return res.status(200).json({ streamId, txHash, depositedAmount: newDeposited }); } catch (error: any) { logger.error(`[topUp] stream=${streamId} error:`, error); - return res.status(500).json({ error: error.message ?? 'Internal server error' }); + return res.status(400).json({ error: 'Failed to top up stream on chain', message: error.message ?? 'Unknown error' }); } }; diff --git a/backend/src/controllers/user.controller.ts b/backend/src/controllers/user.controller.ts index 95f6b3d3..ae31d17c 100644 --- a/backend/src/controllers/user.controller.ts +++ b/backend/src/controllers/user.controller.ts @@ -85,7 +85,7 @@ export const getUserEvents = async (req: Request, res: Response, next: NextFunct rawLimit && typeof rawLimit === 'string' ? (Number.parseInt(rawLimit, 10) || DEFAULT_EVENTS_PAGE_SIZE) : DEFAULT_EVENTS_PAGE_SIZE, MAX_EVENTS_PAGE_SIZE ); - const offset = rawOffset && typeof rawOffset === 'string' ? (Number.parseInt(rawOffset, 10) || 0) : 0; + const offset = rawOffset && typeof rawOffset === 'string' ? Math.max(0, Number.parseInt(rawOffset, 10) || 0) : 0; const whereClause = { stream: { diff --git a/backend/src/routes/v1/streams/index.ts b/backend/src/routes/v1/streams/index.ts index 77a4d834..abcdf5ab 100644 --- a/backend/src/routes/v1/streams/index.ts +++ b/backend/src/routes/v1/streams/index.ts @@ -1,16 +1,8 @@ import { Router } from 'express'; -import { requireAuth } from '../../../middleware/auth.js'; -import { withdrawHandler } from './withdraw.js'; import oldStreamRoutes from '../stream.routes.js'; - const router = Router(); // Mount the old routes first router.use('/', oldStreamRoutes); -/** - * Override/Add POST /api/v1/streams/:streamId/withdraw - */ -router.post('/:streamId/withdraw', requireAuth, withdrawHandler as any); - export default router; diff --git a/frontend/src/app/streams/create/page.tsx b/frontend/src/app/streams/create/page.tsx index f1c7cfd9..66164f65 100644 --- a/frontend/src/app/streams/create/page.tsx +++ b/frontend/src/app/streams/create/page.tsx @@ -8,263 +8,4 @@ export const metadata: Metadata = { export default function CreateStreamPage() { return ; -import React, { useState } from "react"; -import { - createStream, - toBaseUnits, - toDurationSeconds, - getTokenAddress, - toSorobanErrorMessage, - TOKEN_ADDRESSES -} from "@/lib/soroban"; -import { hasValidPrecision, validateAmountInput } from "@/utils/amount"; -import { isValidStellarPublicKey } from "@/lib/stellar"; -import { toast } from "react-hot-toast"; -import { useRouter } from "next/navigation"; -import Link from "next/link"; -import { ArrowLeft } from "lucide-react"; -import { useWallet } from "@/context/wallet-context"; - -const TOKEN_DECIMALS = 7; - -export default function CreateStreamPage() { - const { status, session } = useWallet(); - const router = useRouter(); - const [nowTimestamp] = useState(() => Date.now()); - const [loading, setLoading] = useState(false); - const [txState, setTxState] = useState<"idle" | "signing" | "submitted" | "confirming">("idle"); - const [formData, setFormData] = useState({ - recipient: "", - token: "XLM", - amount: "", - duration: "30", // days - }); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if (status !== "connected" || !session) { - toast.error("Please connect your wallet first."); - return; - } - - // Validate recipient - if (!formData.recipient.trim()) { - toast.error("Recipient address is required"); - return; - } - if (!isValidStellarPublicKey(formData.recipient)) { - toast.error("Invalid Stellar public key format"); - return; - } - - // Validate amount - const validationError = validateAmountInput(formData.amount, TOKEN_DECIMALS); - if (validationError) { - toast.error(validationError); - return; - } - - // Validate duration - const durationNum = parseFloat(formData.duration); - if (isNaN(durationNum) || durationNum <= 0) { - toast.error("Duration must be a positive number"); - return; - } - - setLoading(true); - setTxState("signing"); - - try { - const amountBigInt = toBaseUnits(formData.amount); - const durationBigInt = toDurationSeconds(formData.duration, "days"); - const tokenAddress = getTokenAddress(formData.token); - - const result = await createStream(session, { - recipient: formData.recipient, - tokenAddress, - amount: amountBigInt, - durationSeconds: durationBigInt, - }); - - if (result.success) { - setTxState("confirming"); - toast.success("Stream created successfully!"); - // Small delay to allow indexer to catch up - setTimeout(() => { - router.push("/dashboard"); - }, 2000); - } - } catch (error) { - console.error("Stream creation failed:", error); - toast.error(toSorobanErrorMessage(error)); - } finally { - setLoading(false); - setTxState("idle"); - } - }; - - const getButtonText = () => { - if (!loading) return "Start Streaming"; - switch (txState) { - case "signing": return "Confirm in Wallet..."; - case "submitted": return "Submitting to Network..."; - case "confirming": return "Finalizing Stream..."; - default: return "Processing..."; - } - }; - - // Inline validation feedback for the amount field. validateAmountInput - // returns an error message when invalid and null when valid. Only show it - // once the user has typed something — the empty case is handled on submit. - const amountError = formData.amount - ? validateAmountInput(formData.amount, TOKEN_DECIMALS) - : null; - - const recipientError = formData.recipient - ? (!isValidStellarPublicKey(formData.recipient) ? "Invalid Stellar public key format" : null) - : null; - - const durationError = formData.duration - ? (isNaN(Number(formData.duration)) || Number(formData.duration) <= 0 - ? "Duration must be a positive number" - : null) - : null; - - return ( -
- - - Back to Dashboard - - -
-

Create New Stream

-

- Set up a real-time payment stream to any Stellar address. -

- -
-
- - setFormData({ ...formData, recipient: e.target.value })} - required - /> - {recipientError && ( -

{recipientError}

- )} -
- -
-
- - -
-
- - { - const newValue = e.target.value; - // Only allow valid number characters and check precision - if (newValue === '' || /^\d*\.?\d*$/.test(newValue)) { - if (hasValidPrecision(newValue, TOKEN_DECIMALS)) { - setFormData({ ...formData, amount: newValue }); - } - } - }} - required - /> - {amountError && ( -

{amountError}

- )} -
-
- -
- - setFormData({ ...formData, duration: e.target.value })} - required - /> - {durationError && ( -

{durationError}

- )} -
- -
-
- Streaming Rate - - {formData.amount && formData.duration && Number(formData.duration) > 0 - ? (Number(formData.amount) / (Number(formData.duration) * 86400)).toFixed(8) - : "0.00000000"} {formData.token}/sec - -
-
- Estimated End Date - - {formData.duration && Number(formData.duration) > 0 - ? new Date(nowTimestamp + Number(formData.duration) * 86400000).toLocaleDateString() - : "—"} - -
-
- - - - {status !== "connected" && ( -

- Please connect your wallet to create a stream. -

- )} -
-
-
- ); } diff --git a/frontend/src/components/dashboard/dashboard-view.tsx b/frontend/src/components/dashboard/dashboard-view.tsx index 9247431c..977feb6c 100644 --- a/frontend/src/components/dashboard/dashboard-view.tsx +++ b/frontend/src/components/dashboard/dashboard-view.tsx @@ -108,11 +108,8 @@ const SIDEBAR_ITEMS: SidebarItem[] = [ /** Shimmer card used as a placeholder while data loads */ function SkeletonCard({ className = "" }: { className?: string }) { return ( -