From 804857d6d27f4f00e9b52a6068b548c25919ed73 Mon Sep 17 00:00:00 2001
From: Thompson <140930314+Godbrand0@users.noreply.github.com>
Date: Mon, 29 Jun 2026 18:20:17 -0400
Subject: [PATCH 1/8] Fix issues 811, 813, 816
---
backend/src/controllers/stream.controller.ts | 6 +++---
backend/src/controllers/user.controller.ts | 2 +-
backend/src/routes/v1/streams/index.ts | 7 -------
3 files changed, 4 insertions(+), 11 deletions(-)
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..0de558df 100644
--- a/backend/src/routes/v1/streams/index.ts
+++ b/backend/src/routes/v1/streams/index.ts
@@ -1,16 +1,9 @@
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;
From 56d194dd5dfaacc41e3ee480d2f8d89a1125a079 Mon Sep 17 00:00:00 2001
From: Thompson <140930314+Godbrand0@users.noreply.github.com>
Date: Mon, 29 Jun 2026 18:25:11 -0400
Subject: [PATCH 2/8] Remove unused requireAuth import in streams index
---
backend/src/routes/v1/streams/index.ts | 1 -
1 file changed, 1 deletion(-)
diff --git a/backend/src/routes/v1/streams/index.ts b/backend/src/routes/v1/streams/index.ts
index 0de558df..abcdf5ab 100644
--- a/backend/src/routes/v1/streams/index.ts
+++ b/backend/src/routes/v1/streams/index.ts
@@ -1,5 +1,4 @@
import { Router } from 'express';
-import { requireAuth } from '../../../middleware/auth.js';
import oldStreamRoutes from '../stream.routes.js';
const router = Router();
From 358e36f9aed9a76e4449834437c20aed6a8f9da7 Mon Sep 17 00:00:00 2001
From: Thompson <140930314+Godbrand0@users.noreply.github.com>
Date: Mon, 29 Jun 2026 18:28:26 -0400
Subject: [PATCH 3/8] Fix frontend syntax errors in CreateStreamPage and
SkeletonCard
---
frontend/src/app/streams/create/page.tsx | 259 ------------------
.../components/dashboard/dashboard-view.tsx | 3 +-
2 files changed, 1 insertion(+), 261 deletions(-)
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..ed9d0135 100644
--- a/frontend/src/components/dashboard/dashboard-view.tsx
+++ b/frontend/src/components/dashboard/dashboard-view.tsx
@@ -111,11 +111,10 @@ function SkeletonCard({ className = "" }: { className?: string }) {
>
{/* shimmer sweep */}
-
+
);
}
From ca743c021bfa9496af22a3bf7515e2dc58918b81 Mon Sep 17 00:00:00 2001
From: Thompson <140930314+Godbrand0@users.noreply.github.com>
Date: Mon, 29 Jun 2026 18:32:29 -0400
Subject: [PATCH 4/8] Fix Skeleton type error by wrapping in a div
---
frontend/src/components/dashboard/dashboard-view.tsx | 8 +++-----
1 file changed, 3 insertions(+), 5 deletions(-)
diff --git a/frontend/src/components/dashboard/dashboard-view.tsx b/frontend/src/components/dashboard/dashboard-view.tsx
index ed9d0135..977feb6c 100644
--- a/frontend/src/components/dashboard/dashboard-view.tsx
+++ b/frontend/src/components/dashboard/dashboard-view.tsx
@@ -108,13 +108,11 @@ const SIDEBAR_ITEMS: SidebarItem[] = [
/** Shimmer card used as a placeholder while data loads */
function SkeletonCard({ className = "" }: { className?: string }) {
return (
-
+
+
{/* shimmer sweep */}
-
+
);
}
From 9fa0f1f7b42e2a15fe7cd5ef21817fc3763509bd Mon Sep 17 00:00:00 2001
From: Thompson <140930314+Godbrand0@users.noreply.github.com>
Date: Mon, 29 Jun 2026 18:35:47 -0400
Subject: [PATCH 5/8] Fix onSuccess arguments in useIncomingStreams
---
frontend/src/hooks/useIncomingStreams.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
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
From 93d69221edd48eab3185e6e1c90709f88ebcb74a Mon Sep 17 00:00:00 2001
From: Thompson <140930314+Godbrand0@users.noreply.github.com>
Date: Mon, 29 Jun 2026 18:42:33 -0400
Subject: [PATCH 6/8] Fix TypeScript type mismatch in vitest.config.mts
---
frontend/vitest.config.mts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/frontend/vitest.config.mts b/frontend/vitest.config.mts
index 2e942715..fb1a316f 100644
--- a/frontend/vitest.config.mts
+++ b/frontend/vitest.config.mts
@@ -1,9 +1,10 @@
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import path from 'path';
+import type { PluginOption } from 'vite';
export default defineConfig({
- plugins: [react()],
+ plugins: [react()] as PluginOption[],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
From e86320b422228534184df0d91236b3bf5501d656 Mon Sep 17 00:00:00 2001
From: Thompson <140930314+Godbrand0@users.noreply.github.com>
Date: Mon, 29 Jun 2026 18:45:38 -0400
Subject: [PATCH 7/8] Fix Vite PluginOption type mismatch by casting to any
---
frontend/vitest.config.mts | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/frontend/vitest.config.mts b/frontend/vitest.config.mts
index fb1a316f..febdbf70 100644
--- a/frontend/vitest.config.mts
+++ b/frontend/vitest.config.mts
@@ -1,10 +1,9 @@
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import path from 'path';
-import type { PluginOption } from 'vite';
export default defineConfig({
- plugins: [react()] as PluginOption[],
+ plugins: [react()] as any,
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
From 3124100e8d0f80e20462d28d8d47539ff54840f6 Mon Sep 17 00:00:00 2001
From: Thompson <140930314+Godbrand0@users.noreply.github.com>
Date: Mon, 29 Jun 2026 18:47:47 -0400
Subject: [PATCH 8/8] Disable eslint no-explicit-any for vitest.config.mts
plugins array
---
frontend/vitest.config.mts | 1 +
1 file changed, 1 insertion(+)
diff --git a/frontend/vitest.config.mts b/frontend/vitest.config.mts
index febdbf70..73275a69 100644
--- a/frontend/vitest.config.mts
+++ b/frontend/vitest.config.mts
@@ -3,6 +3,7 @@ import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
plugins: [react()] as any,
resolve: {
alias: {