diff --git a/contracts/stream/src/interface.rs b/contracts/stream/src/interface.rs index d36b357..5772eda 100644 --- a/contracts/stream/src/interface.rs +++ b/contracts/stream/src/interface.rs @@ -175,6 +175,7 @@ pub trait SoroStreamInterface { fn get_stream(env: Env, stream_id: u64) -> Result; fn get_all_stream_ids(env: Env, start: u32, limit: u32) -> Vec; fn get_claimable(env: Env, stream_id: u64) -> Result; + fn get_accrued_balance(env: Env, stream_id: u64, recipient: Address) -> Result; fn is_participant(env: Env, stream_id: u64, address: Address) -> Result; fn get_streams_by_sender(env: Env, sender: Address, start: u32, limit: u32) -> Vec; fn get_streams_by_recipient(env: Env, recipient: Address, start: u32, limit: u32) -> Vec; diff --git a/contracts/stream/src/lib.rs b/contracts/stream/src/lib.rs index 5162c77..1c2c3c9 100644 --- a/contracts/stream/src/lib.rs +++ b/contracts/stream/src/lib.rs @@ -4406,6 +4406,90 @@ impl SoroStreamContract { Ok(claimable) } + pub fn get_accrued_balance(env: Env, stream_id: u64, _recipient: Address) -> Result { + let stream = load_stream(&env, stream_id).ok_or(StreamError::StreamNotFound)?; + + if stream.status != StreamStatus::Active && stream.status != StreamStatus::Paused { + return Ok(0); + } + + let now = if stream.status == StreamStatus::Paused { + stream.last_pause_time + } else { + env.ledger().timestamp() + }; + + if now >= stream.end_time && stream.total_withdrawn >= stream.deposit { + return Ok(0); + } + + if stream.milestone_release_mode { + let mut accrued: i128 = 0; + for milestone in stream.milestones.iter() { + if (now >= milestone.unlock_time && milestone.status == crate::types::MilestoneStatus::Pending) + || milestone.status == crate::types::MilestoneStatus::Released { + accrued = accrued + .checked_add(milestone.amount) + .ok_or(StreamError::Overflow)?; + } + } + return Ok(accrued.max(0)); + } + + if stream.is_step_vesting { + let tranches = load_tranches(&env, stream_id); + let mut accrued: i128 = 0; + for i in 0..tranches.len() { + let t = tranches.get(i).unwrap(); + if now >= t.unlock_time { + accrued = accrued + .checked_add(t.amount) + .ok_or(StreamError::Overflow)?; + } else { + break; + } + } + return Ok(accrued); + } + + if now < stream.cliff_time { + return Ok(0); + } + + let raw = match &stream.curve { + VestingCurve::Linear => vesting_math::compute_claimable( + stream.flow_rate, + now, + stream.cliff_time, + stream.end_time, + stream.start_time, + ) + .ok_or(StreamError::Overflow)?, + + VestingCurve::TimeDecay(decay_factor) => { + vesting_math::compute_claimable_decay( + stream.deposit, + stream.start_time, + stream.end_time, + now, + stream.cliff_time, + stream.start_time, + *decay_factor, + ) + .ok_or(StreamError::Overflow)? + } + }; + + let available = stream.deposit.saturating_sub(stream.total_withdrawn); + let accrued = raw.min(available); + + if accrued <= DUST_THRESHOLD { + return Ok(0); + } + + Ok(accrued) + } + /// Returns true if `address` is either the sender or recipient of the given stream. pub fn is_participant(env: Env, stream_id: u64, address: Address) -> Result { let stream = load_stream(&env, stream_id).ok_or(StreamError::StreamNotFound)?;