Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions contracts/stream_contract/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
6 changes: 6 additions & 0 deletions contracts/stream_contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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);
}
Expand Down
70 changes: 70 additions & 0 deletions contracts/stream_contract/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
9 changes: 8 additions & 1 deletion frontend/src/components/NotificationDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,13 @@ export const NotificationDropdown: React.FC<NotificationDropdownProps> = ({ publ

return (
<div className="relative">
<span
aria-live="polite"
aria-busy="false"
className="sr-only"
>
{unreadCount} new notifications
</span>
<button
onClick={handleDropdownOpen}
aria-label={`Notifications${unreadCount > 0 ? `, ${unreadCount} unread` : ""}`}
Expand All @@ -136,7 +143,7 @@ export const NotificationDropdown: React.FC<NotificationDropdownProps> = ({ publ
strokeLinejoin="round"
strokeWidth="2"
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
/>
/>
</svg>

{unreadCount > 0 && (
Expand Down