@@ -22,6 +22,15 @@ import {
2222const DEFAULT_STREAM_PAGE_SIZE = 20 ;
2323const MAX_STREAM_PAGE_SIZE = 100 ;
2424
25+ /**
26+ * Hard cap on the number of streams fetched per user in the summary endpoint.
27+ * Prevents unbounded DB queries when a wallet has thousands of streams.
28+ * Users who exceed this cap receive a truncated summary (counts and totals
29+ * reflect only the most recent streams) plus a `truncated` flag so the
30+ * frontend can offer a pagination or export fallback.
31+ */
32+ export const MAX_USER_STREAMS = 500 ;
33+
2534interface UserStreamSummary {
2635 address : string ;
2736 totalStreamsCreated : number ;
@@ -590,9 +599,15 @@ export const getUserStreamSummary = async (
590599
591600 pruneUserSummaryCache ( nowMs ) ;
592601
602+ // Issue #1246: cap the number of streams fetched per direction to prevent
603+ // unbounded DB queries. Power users with more than MAX_USER_STREAMS
604+ // streams receive a truncated summary (the `truncated` flag lets the
605+ // frontend offer a pagination/export fallback).
593606 const [ outgoingStreams , incomingStreams ] = await Promise . all ( [
594607 prisma . stream . findMany ( {
595608 where : { sender : address } ,
609+ orderBy : { startTime : "desc" } ,
610+ take : MAX_USER_STREAMS ,
596611 select : {
597612 streamId : true ,
598613 ratePerSecond : true ,
@@ -609,6 +624,8 @@ export const getUserStreamSummary = async (
609624 } ) ,
610625 prisma . stream . findMany ( {
611626 where : { recipient : address } ,
627+ orderBy : { startTime : "desc" } ,
628+ take : MAX_USER_STREAMS ,
612629 select : {
613630 streamId : true ,
614631 ratePerSecond : true ,
@@ -651,15 +668,20 @@ export const getUserStreamSummary = async (
651668 ( stream : any ) => stream . isActive ,
652669 ) . length ;
653670
654- const summary : UserStreamSummary = {
671+ const truncated =
672+ outgoingStreams . length >= MAX_USER_STREAMS ||
673+ incomingStreams . length >= MAX_USER_STREAMS ;
674+
675+ const summary = {
655676 address,
656677 totalStreamsCreated,
657678 totalStreamedOut,
658679 totalStreamedIn,
659680 currentClaimable : claimableInTotal . toString ( ) ,
660681 activeOutgoingCount,
661682 activeIncomingCount,
662- } ;
683+ ...( truncated ? { truncated : true } : { } ) ,
684+ } satisfies UserStreamSummary & { truncated ?: boolean } ;
663685
664686 userSummaryCache . set ( cacheKey , {
665687 value : summary ,
@@ -730,19 +752,24 @@ export const topUpStreamHandler = async (req: Request, res: Response) => {
730752
731753 const txHash = await topUpStream ( streamId , amount , callerAddress ) ;
732754
733- const newDeposited = ( BigInt ( stream . depositedAmount ) + amount ) . toString ( ) ;
734- await prisma . stream . update ( {
735- where : { streamId } ,
736- data : {
737- depositedAmount : newDeposited ,
738- lastUpdateTime : BigInt ( Math . floor ( Date . now ( ) / 1000 ) ) ,
739- } ,
740- } ) ;
755+ // Use raw SQL atomic increment to prevent concurrent top-ups from
756+ // overwriting each other's updates (Issue #1217 — read-compute-write race).
757+ // Prisma's built-in { increment } is unavailable on String-typed columns,
758+ // so we perform SET deposited_amount = deposited_amount + $1::bigint
759+ // directly in a single SQL statement.
760+ const now = BigInt ( Math . floor ( Date . now ( ) / 1000 ) ) ;
761+ await prisma . $executeRawUnsafe (
762+ `UPDATE "Stream" SET "depositedAmount" = ("depositedAmount"::bigint + $1::bigint)::text, "lastUpdateTime" = $2 WHERE "streamId" = $3` ,
763+ amount . toString ( ) ,
764+ now ,
765+ streamId ,
766+ ) ;
767+ const updatedStream = await prisma . stream . findUnique ( { where : { streamId } } ) ;
741768
742769 logger . info ( `[topUp] stream=${ streamId } amount=${ amount } txHash=${ txHash } ` ) ;
743770 return res
744771 . status ( 200 )
745- . json ( { streamId, txHash, depositedAmount : newDeposited } ) ;
772+ . json ( { streamId, txHash, depositedAmount : updatedStream ! . depositedAmount } ) ;
746773 } catch ( error : any ) {
747774 logger . error ( `[topUp] stream=${ streamId } error:` , error ) ;
748775 return res . status ( 400 ) . json ( { error : 'Failed to top up stream on chain' , message : error . message ?? 'Unknown error' } ) ;
0 commit comments