@@ -12,14 +12,14 @@ use soroban_sdk::{contract, contractimpl, token, vec, Address, Env, InvokeError,
1212
1313use errors:: StreamError ;
1414use events:: {
15- FeeCollectedEvent , StreamCancelledEvent , StreamCreatedEvent , StreamToppedUpEvent ,
16- TokensWithdrawnEvent ,
15+ FeeCollectedEvent , StreamCancelledEvent , StreamCompletedEvent , StreamCreatedEvent ,
16+ StreamPausedEvent , StreamResumedEvent , StreamToppedUpEvent , TokensWithdrawnEvent ,
1717} ;
1818use storage:: {
1919 config_exists, load_config, load_stream, next_stream_id, save_config, save_stream,
2020 try_load_config, try_load_stream,
2121} ;
22- use types:: { ProtocolConfig , Stream } ;
22+ use types:: { ProtocolConfig , Stream , StreamStatus } ;
2323
2424/// Maximum allowed protocol fee: 1 000 bps = 10%.
2525const MAX_FEE_RATE_BPS : u32 = 1_000 ;
@@ -157,6 +157,9 @@ impl StreamContract {
157157 start_time,
158158 last_update_time : start_time,
159159 is_active : true ,
160+ paused : false ,
161+ paused_at : None ,
162+ status : StreamStatus :: Active ,
160163 } ,
161164 ) ;
162165
@@ -248,17 +251,15 @@ impl StreamContract {
248251
249252 /// Calculate the claimable amount for a stream at a given timestamp.
250253 ///
251- /// This helper computes how many tokens have been streamed since the last
252- /// update, capped at the remaining balance to prevent over-withdrawal.
253- ///
254- /// # Arguments
255- /// * `stream` - The stream to calculate claimable amount for
256- /// * `now` - Current ledger timestamp
257- ///
258- /// # Returns
259- /// The amount of tokens that can be claimed, never exceeding remaining balance
254+ /// Excludes any time the stream was paused. If the stream is currently
255+ /// paused, accrual stops at `paused_at`.
260256 fn calculate_claimable ( stream : & Stream , now : u64 ) -> i128 {
261- let elapsed = now. saturating_sub ( stream. last_update_time ) ;
257+ let effective_now = if stream. paused {
258+ stream. paused_at . unwrap_or ( stream. last_update_time )
259+ } else {
260+ now
261+ } ;
262+ let elapsed = effective_now. saturating_sub ( stream. last_update_time ) ;
262263
263264 let streamed = ( elapsed as i128 )
264265 . checked_mul ( stream. rate_per_second )
@@ -315,9 +316,10 @@ impl StreamContract {
315316 stream. withdrawn_amount += amount;
316317 stream. last_update_time = now;
317318
318- // Mark stream as inactive if fully drained
319+ // Mark stream as inactive and completed if fully drained
319320 if stream. withdrawn_amount >= stream. deposited_amount {
320321 stream. is_active = false ;
322+ stream. status = StreamStatus :: Completed ;
321323 }
322324 }
323325
@@ -342,8 +344,11 @@ impl StreamContract {
342344 return Err ( StreamError :: Unauthorized ) ;
343345 }
344346
345- // Validate stream is active
347+ // Validate stream is active and not paused
346348 Self :: validate_stream_active ( & stream) ?;
349+ if stream. paused {
350+ return Err ( StreamError :: StreamInactive ) ;
351+ }
347352
348353 let now = env. ledger ( ) . timestamp ( ) ;
349354 let claimable = Self :: calculate_claimable ( & stream, now) ;
@@ -355,19 +360,31 @@ impl StreamContract {
355360 // Use helper function to transfer tokens and update state
356361 Self :: transfer_and_update_stream ( & env, & mut stream, & recipient, claimable, now) ;
357362
363+ let completed = stream. status == StreamStatus :: Completed ;
358364 save_stream ( & env, stream_id, & stream) ;
359365
360- // Emit withdrawal event
361366 env. events ( ) . publish (
362367 ( Symbol :: new ( & env, "tokens_withdrawn" ) , stream_id) ,
363368 TokensWithdrawnEvent {
364369 stream_id,
365- recipient,
370+ recipient : recipient . clone ( ) ,
366371 amount : claimable,
367372 timestamp : stream. last_update_time ,
368373 } ,
369374 ) ;
370375
376+ // Emit COMPLETED event on final withdrawal
377+ if completed {
378+ env. events ( ) . publish (
379+ ( Symbol :: new ( & env, "stream_completed" ) , stream_id) ,
380+ StreamCompletedEvent {
381+ stream_id,
382+ recipient,
383+ total_withdrawn : stream. withdrawn_amount ,
384+ } ,
385+ ) ;
386+ }
387+
371388 Ok ( claimable)
372389 }
373390
@@ -413,6 +430,7 @@ impl StreamContract {
413430
414431 // Mark stream as inactive
415432 stream. is_active = false ;
433+ stream. status = StreamStatus :: Cancelled ;
416434 stream. last_update_time = now;
417435
418436 let recipient = stream. recipient . clone ( ) ;
@@ -435,13 +453,98 @@ impl StreamContract {
435453 Ok ( ( ) )
436454 }
437455
456+ /// Pause an active stream. Only the sender may pause.
457+ ///
458+ /// # Errors
459+ /// - `StreamNotFound` — no stream exists with `stream_id`.
460+ /// - `Unauthorized` — caller is not the stream's sender.
461+ /// - `StreamInactive` — stream is already inactive.
462+ pub fn pause_stream ( env : Env , sender : Address , stream_id : u64 ) -> Result < ( ) , StreamError > {
463+ sender. require_auth ( ) ;
464+
465+ let mut stream = load_stream ( & env, stream_id) ?;
466+ Self :: validate_stream_ownership ( & stream, & sender) ?;
467+ Self :: validate_stream_active ( & stream) ?;
468+
469+ if stream. paused {
470+ return Err ( StreamError :: StreamInactive ) ;
471+ }
472+
473+ let now = env. ledger ( ) . timestamp ( ) ;
474+ stream. paused = true ;
475+ stream. paused_at = Some ( now) ;
476+ stream. status = StreamStatus :: Paused ;
477+ save_stream ( & env, stream_id, & stream) ;
478+
479+ env. events ( ) . publish (
480+ ( Symbol :: new ( & env, "stream_paused" ) , stream_id) ,
481+ StreamPausedEvent { stream_id, sender, paused_at : now } ,
482+ ) ;
483+
484+ Ok ( ( ) )
485+ }
486+
487+ /// Resume a paused stream. Adjusts `end_time` by the pause duration.
488+ ///
489+ /// The `last_update_time` is advanced to `now` so that accrual resumes
490+ /// from the current moment, effectively extending the stream by the
491+ /// duration it was paused.
492+ ///
493+ /// # Errors
494+ /// - `StreamNotFound` — no stream exists with `stream_id`.
495+ /// - `Unauthorized` — caller is not the stream's sender.
496+ /// - `StreamInactive` — stream is not paused (already active or cancelled).
497+ pub fn resume_stream ( env : Env , sender : Address , stream_id : u64 ) -> Result < u64 , StreamError > {
498+ sender. require_auth ( ) ;
499+
500+ let mut stream = load_stream ( & env, stream_id) ?;
501+ Self :: validate_stream_ownership ( & stream, & sender) ?;
502+
503+ if !stream. paused {
504+ return Err ( StreamError :: StreamInactive ) ;
505+ }
506+
507+ let now = env. ledger ( ) . timestamp ( ) ;
508+ let paused_at = stream. paused_at . unwrap_or ( now) ;
509+ let pause_duration = now. saturating_sub ( paused_at) ;
510+
511+ // Advance last_update_time by pause duration so accrual resumes from now.
512+ stream. last_update_time = stream. last_update_time . saturating_add ( pause_duration) ;
513+ // new_end_time represents when the stream will fully drain from now.
514+ let remaining = stream. deposited_amount . saturating_sub ( stream. withdrawn_amount ) ;
515+ let new_end_time = if stream. rate_per_second > 0 {
516+ now + ( remaining / stream. rate_per_second ) as u64
517+ } else {
518+ now
519+ } ;
520+
521+ stream. paused = false ;
522+ stream. paused_at = None ;
523+ stream. status = StreamStatus :: Active ;
524+ save_stream ( & env, stream_id, & stream) ;
525+
526+ env. events ( ) . publish (
527+ ( Symbol :: new ( & env, "stream_resumed" ) , stream_id) ,
528+ StreamResumedEvent { stream_id, sender, new_end_time } ,
529+ ) ;
530+
531+ Ok ( new_end_time)
532+ }
533+
438534 // ─── Read-only Queries ────────────────────────────────────────────────────
439535
440536 /// Returns the stream record for `stream_id`, or `None` if it does not exist.
441537 pub fn get_stream ( env : Env , stream_id : u64 ) -> Option < Stream > {
442538 try_load_stream ( & env, stream_id)
443539 }
444540
541+ /// Returns `true` if the stream exists and has status `Completed`.
542+ pub fn is_stream_completed ( env : Env , stream_id : u64 ) -> bool {
543+ try_load_stream ( & env, stream_id)
544+ . map ( |s| s. status == StreamStatus :: Completed )
545+ . unwrap_or ( false )
546+ }
547+
445548 /// Get the current claimable amount for a stream without modifying state.
446549 ///
447550 /// This is a read-only query that calculates how many tokens the recipient
0 commit comments