Added new event functions:
/// Emitted when a stream is paused by the employer.
/// Includes employee address for notification purposes.
pub fn stream_paused(env: &Env, id: u64, employer: &Address, employee: &Address, paused_at: u64) {
env.events().publish((symbol_short!("paused"), id), (employer.clone(), employee.clone(), paused_at));
}
/// Emitted when a stream is resumed by the employer.
pub fn stream_resumed(env: &Env, id: u64, employer: &Address, employee: &Address, resumed_at: u64) {
env.events().publish((symbol_short!("resumed"), id), (employer.clone(), employee.clone(), resumed_at));
}Added PauseEvent struct:
/// Record of a pause/resume event for history tracking.
#[contracttype]
#[derive(Clone, Debug)]
pub struct PauseEvent {
pub stream_id: u64,
pub timestamp: u64,
pub is_pause: bool, // true for pause, false for resume
}Added storage key:
pub enum DataKey {
// ... existing keys ...
/// Pause history for a stream.
PauseHistory(u64),
// ... rest of keys ...
}Added import:
use crate::types::{DataKey, PauseEvent, Proposal, ProposalStatus, Stream, StreamStatus, ERR_OVERFLOW, ERR_BAD_NONCE};Added storage functions:
// ---------------------------------------------------------------------------
// Pause history helpers
// ---------------------------------------------------------------------------
pub fn add_pause_event(env: &Env, stream_id: u64, timestamp: u64, is_pause: bool) {
let key = DataKey::PauseHistory(stream_id);
let mut history: Vec<PauseEvent> = env.storage().persistent().get(&key).unwrap_or_else(|| Vec::new(env));
history.push_back(PauseEvent {
stream_id,
timestamp,
is_pause,
});
env.storage().persistent().set(&key, &history);
env.storage().persistent().extend_ttl(&key, TTL_THRESHOLD, TTL_EXTEND_TO);
}
pub fn get_pause_history(env: &Env, stream_id: u64) -> Vec<PauseEvent> {
let key = DataKey::PauseHistory(stream_id);
env.storage().persistent().get(&key).unwrap_or_else(|| Vec::new(env))
}Updated imports:
use storage::{
add_pause_event, apply_proposal, claimable_amount, clear_pending_admin, clear_pending_employer,
consume_admin_nonce, get_admin, get_admin_nonce, get_employee_streams, get_employer_streams,
get_fee_bps, get_fee_recipient, get_max_streams_per_employer, get_min_deposit,
get_pause_history, get_pending_admin, get_pending_employer, has_voted, index_employee_stream,
index_employer_stream, load_proposal, load_stream, mark_voted, next_id, next_proposal_id,
save_proposal, save_stream, set_admin, set_fee_bps, set_fee_recipient,
set_max_streams_per_employer, set_min_deposit, set_pending_admin, set_pending_employer,
tally_proposal,
};
use types::{
DataKey, GovParam, PauseEvent, Proposal, ProposalStatus, Stream, StreamParams, StreamStatus,
ERR_FEE_TOO_HIGH, ERR_INVALID_TOKEN, ERR_OVERFLOW, ERR_REENTRANT, ERR_STREAM_CANCELLED,
ERR_STREAM_EXHAUSTED, ERR_UNAUTHORIZED_TRANSFER, ERR_WITHDRAW_COOLDOWN, ERR_ZERO_DEPOSIT,
ERR_ZERO_RATE,
};Updated pause_stream function:
pub fn pause_stream(env: Env, employer: Address, stream_id: u64) {
employer.require_auth();
let mut stream = load_stream(&env, stream_id).expect("stream not found");
assert_eq!(stream.employer, employer, "not the employer");
assert_eq!(stream.status, StreamStatus::Active, "stream not active");
let now = env.ledger().timestamp();
stream.paused_at = now;
stream.status = StreamStatus::Paused;
save_stream(&env, &stream);
add_pause_event(&env, stream_id, now, true); // NEW: Record pause event
events::stream_paused(&env, stream_id, &employer, &stream.employee, now); // CHANGED: Use new event
}Updated resume_stream function:
pub fn resume_stream(env: Env, employer: Address, stream_id: u64) {
employer.require_auth();
let mut stream = load_stream(&env, stream_id).expect("stream not found");
assert_eq!(stream.employer, employer, "not the employer");
assert_eq!(stream.status, StreamStatus::Paused, "stream not paused");
let now = env.ledger().timestamp();
// Advance last_withdraw_time by the paused duration to exclude it while
// preserving pre-pause accrued earnings.
let paused_duration = now.saturating_sub(stream.paused_at);
stream.last_withdraw_time = stream.last_withdraw_time.saturating_add(paused_duration);
stream.paused_at = 0;
stream.status = StreamStatus::Active;
save_stream(&env, &stream);
add_pause_event(&env, stream_id, now, false); // NEW: Record resume event
events::stream_resumed(&env, stream_id, &employer, &stream.employee, now); // CHANGED: Use new event
}Added new public query function:
pub fn pause_history(env: Env, stream_id: u64) -> Vec<PauseEvent> {
get_pause_history(&env, stream_id)
}Added comprehensive tests:
test_pause_event_includes_employee: Verifies pause events are emitted with employee addresstest_pause_history_tracking: Tests basic pause/resume history recordingtest_multiple_pause_resume_cycles: Validates multiple pause/resume cycles are tracked correctlytest_resume_event_includes_employee: Verifies resume events are emitted with employee address
// Generic status change event
events::stream_status_changed(&env, stream_id, &StreamStatus::Paused);
// Topic: ("status", stream_id)
// Data: StreamStatus::Paused// Specific pause event with employee address
events::stream_paused(&env, stream_id, &employer, &stream.employee, now);
// Topic: ("paused", stream_id)
// Data: (employer_address, employee_address, paused_at_timestamp)
// Specific resume event with employee address
events::stream_resumed(&env, stream_id, &employer, &stream.employee, now);
// Topic: ("resumed", stream_id)
// Data: (employer_address, employee_address, resumed_at_timestamp)pub fn pause_history(env: Env, stream_id: u64) -> Vec<PauseEvent>Returns: A vector of PauseEvent structs containing:
stream_id: The stream identifiertimestamp: When the pause/resume occurredis_pause:truefor pause events,falsefor resume events
Usage Example:
let history = client.pause_history(&stream_id);
for event in history.iter() {
println!("Stream {} was {} at timestamp {}",
event.stream_id,
if event.is_pause { "paused" } else { "resumed" },
event.timestamp
);
}- No breaking changes: All existing functionality remains intact
- Event listeners: Services listening for
stream_status_changedevents should be updated to listen forstream_pausedandstream_resumedevents for better notification handling - Storage: New pause history data is stored separately and doesn't affect existing stream data
- Backward compatibility: Old streams without pause history will return empty vectors from
pause_history()
# Run all stream contract tests
cargo test --package paystream-stream
# Run only pause notification tests
cargo test --package paystream-stream test_pause_event_includes_employee
cargo test --package paystream-stream test_pause_history_tracking
cargo test --package paystream-stream test_multiple_pause_resume_cycles
cargo test --package paystream-stream test_resume_event_includes_employee
# Run with output to see event details
cargo test --package paystream-stream test_pause_event_includes_employee -- --nocapture