From 18f2b9aff2b149f1d7c5aac6531b3e300c385df6 Mon Sep 17 00:00:00 2001 From: Joyerin007 Date: Sat, 29 Aug 2026 21:34:29 +0000 Subject: [PATCH 1/3] =?UTF-8?q?Fix=20resume=5Fstream=20state-corruption:?= =?UTF-8?q?=20add=20is=5Factive=20check,=20cancel=5Fstream=20clears=20paus?= =?UTF-8?q?ed,=20test=20for=20pause=E2=86=92cancel=E2=86=92resume?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contracts/stream_contract/src/errors.rs | 2 ++ contracts/stream_contract/src/lib.rs | 3 +++ contracts/stream_contract/src/test.rs | 35 +++++++++++++++++++++++++ 3 files changed, 40 insertions(+) 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..f8b912a2 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(); @@ -621,6 +623,7 @@ impl StreamContract { let mut stream = load_stream(&env, stream_id)?; Self::validate_stream_ownership(&stream, &sender)?; + Self::validate_stream_active(&stream)?; 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..73324547 100644 --- a/contracts/stream_contract/src/test.rs +++ b/contracts/stream_contract/src/test.rs @@ -2100,6 +2100,41 @@ 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_fuzz_pause_resume_maintains_active_state() { let env = Env::default(); From ded4e582581c7c54a8aa0bda6d2e31d367e9d18b Mon Sep 17 00:00:00 2001 From: Joyerin007 Date: Sat, 29 Aug 2026 21:59:38 +0000 Subject: [PATCH 2/3] =?UTF-8?q?Fix=20resume=5Fstream=20state-corruption:?= =?UTF-8?q?=20add=20is=5Factive=20check,=20cancel=5Fstream=20clears=20paus?= =?UTF-8?q?ed,=20test=20for=20pause=E2=86=92cancel=E2=86=92resume?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contracts/stream_contract/src/lib.rs | 5 +++- contracts/stream_contract/src/test.rs | 35 +++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/contracts/stream_contract/src/lib.rs b/contracts/stream_contract/src/lib.rs index f8b912a2..17f746f2 100644 --- a/contracts/stream_contract/src/lib.rs +++ b/contracts/stream_contract/src/lib.rs @@ -623,7 +623,10 @@ impl StreamContract { let mut stream = load_stream(&env, stream_id)?; Self::validate_stream_ownership(&stream, &sender)?; - Self::validate_stream_active(&stream)?; + + 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 73324547..4b8b93cf 100644 --- a/contracts/stream_contract/src/test.rs +++ b/contracts/stream_contract/src/test.rs @@ -2135,6 +2135,41 @@ fn test_resume_on_cancelled_stream_fails() { 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(); From 4285e0ddba10734c2b81d422037d72e6b65206c7 Mon Sep 17 00:00:00 2001 From: Joyerin007 Date: Sun, 30 Aug 2026 21:35:00 +0000 Subject: [PATCH 3/3] Fix: Move aria-live region outside button for proper screen reader announcements of notification count changes\n\n- Move aria-live region outside button element\n- Make live region always present (not conditional on unreadCount > 0)\n- Ensures screen readers re-announce 'N new notifications' when count increases\n\nFixes: NotificationDropdown.tsx:142-150 - badge only conveyed via button's aria-label\n\nCo-authored-by: opencode --- frontend/src/components/NotificationDropdown.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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 +