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.
-
-
-
-
-
- );
}
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 (
-
- >
+
diff --git a/frontend/src/hooks/useIncomingStreams.ts b/frontend/src/hooks/useIncomingStreams.ts
index dd10e078..f2c4c1e1 100644
--- a/frontend/src/hooks/useIncomingStreams.ts
+++ b/frontend/src/hooks/useIncomingStreams.ts
@@ -92,7 +92,7 @@ export function useWithdrawIncomingStream(
return { previousStreams, expectedWithdrawn };
},
- onSuccess: async (result, stream, _variables, context) => {
+ onSuccess: async (result, stream, context) => {
if (publicKey) {
const targetWithdrawn = context?.expectedWithdrawn ?? stream.withdrawn;
// Start polling in the background without blocking the mutation
diff --git a/frontend/vitest.config.mts b/frontend/vitest.config.mts
index 2e942715..73275a69 100644
--- a/frontend/vitest.config.mts
+++ b/frontend/vitest.config.mts
@@ -3,7 +3,8 @@ import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
- plugins: [react()],
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ plugins: [react()] as any,
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),