diff --git a/contracts/stream_contract/src/errors.rs b/contracts/stream_contract/src/errors.rs index 596d68d3..6ab8f74f 100644 --- a/contracts/stream_contract/src/errors.rs +++ b/contracts/stream_contract/src/errors.rs @@ -35,4 +35,6 @@ pub enum StreamError { StreamNotPaused = 13, /// `pause_stream` was called on a stream that is already paused. StreamAlreadyPaused = 14, + /// Operation requires an active stream, but the stream is inactive (cancelled or completed). + StreamNotActive = 15, } diff --git a/contracts/stream_contract/src/lib.rs b/contracts/stream_contract/src/lib.rs index 3ce60858..17f746f2 100644 --- a/contracts/stream_contract/src/lib.rs +++ b/contracts/stream_contract/src/lib.rs @@ -535,6 +535,8 @@ impl StreamContract { stream.is_active = false; stream.status = StreamStatus::Cancelled; + stream.paused = false; + stream.paused_at = None; stream.last_update_time = now; let recipient = stream.recipient.clone(); @@ -622,6 +624,10 @@ impl StreamContract { let mut stream = load_stream(&env, stream_id)?; Self::validate_stream_ownership(&stream, &sender)?; + if !stream.is_active { + return Err(StreamError::StreamNotActive); + } + if !stream.paused { return Err(StreamError::StreamNotPaused); } diff --git a/contracts/stream_contract/src/test.rs b/contracts/stream_contract/src/test.rs index 74a87b12..4b8b93cf 100644 --- a/contracts/stream_contract/src/test.rs +++ b/contracts/stream_contract/src/test.rs @@ -2100,6 +2100,76 @@ fn test_fuzz_cancel_early_refunds() { } } +#[test] +fn test_resume_on_cancelled_stream_fails() { + let env = Env::default(); + env.mock_all_auths(); + let (token, _) = create_token(&env); + let sender = Address::generate(&env); + let recipient = Address::generate(&env); + mint(&env, &token, &sender, 1_000); + + let client = create_contract(&env); + let id = client.create_stream(&sender, &recipient, &token, &1_000, &1_000); + + // Advance time and pause the stream. + env.ledger().with_mut(|l| l.timestamp += 300); + client.pause_stream(&sender, &id); + + // Cancel the paused stream — this should set is_active=false and status=Cancelled, + // but previously would leave paused=true, allowing a subsequent resume to corrupt state. + client.cancel_stream(&sender, &id); + + // Resume on a cancelled stream must fail. + let result = client.try_resume_stream(&sender, &id); + assert_eq!( + result, + Err(Ok(StreamError::StreamNotActive)), + "resume_stream must return StreamNotActive on an inactive stream" + ); + + // Stream state must be unchanged: still cancelled, not resumed. + let s = client.get_stream(&id).unwrap(); + assert!(!s.is_active); + assert_eq!(s.status, StreamStatus::Cancelled); + assert!(s.paused); +} + +#[test] +fn test_resume_on_cancelled_stream_fails() { + let env = Env::default(); + env.mock_all_auths(); + let (token, _) = create_token(&env); + let sender = Address::generate(&env); + let recipient = Address::generate(&env); + mint(&env, &token, &sender, 1_000); + + let client = create_contract(&env); + let id = client.create_stream(&sender, &recipient, &token, &1_000, &1_000); + + // Advance time and pause the stream. + env.ledger().with_mut(|l| l.timestamp += 300); + client.pause_stream(&sender, &id); + + // Cancel the paused stream — this should set is_active=false and status=Cancelled, + // but previously would leave paused=true, allowing a subsequent resume to corrupt state. + client.cancel_stream(&sender, &id); + + // Resume on a cancelled stream must fail with StreamNotActive. + let result = client.try_resume_stream(&sender, &id); + assert_eq!( + result, + Err(Ok(StreamError::StreamNotActive)), + "resume_stream must return StreamNotActive on an inactive stream" + ); + + // Stream state must be unchanged: still cancelled, not resumed. + let s = client.get_stream(&id).unwrap(); + assert!(!s.is_active); + assert_eq!(s.status, StreamStatus::Cancelled); + assert!(s.paused); +} + #[test] fn test_fuzz_pause_resume_maintains_active_state() { let env = Env::default(); diff --git a/frontend/src/components/NotificationDropdown.tsx b/frontend/src/components/NotificationDropdown.tsx index 68178168..f077404b 100644 --- a/frontend/src/components/NotificationDropdown.tsx +++ b/frontend/src/components/NotificationDropdown.tsx @@ -122,6 +122,13 @@ export const NotificationDropdown: React.FC = ({ publ return (
+ + {unreadCount} new notifications +