Skip to content
Merged
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
1 change: 1 addition & 0 deletions contracts/stream/src/interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ pub trait SoroStreamInterface {
fn get_stream(env: Env, stream_id: u64) -> Result<Stream, StreamError>;
fn get_all_stream_ids(env: Env, start: u32, limit: u32) -> Vec<u64>;
fn get_claimable(env: Env, stream_id: u64) -> Result<i128, StreamError>;
fn get_accrued_balance(env: Env, stream_id: u64, recipient: Address) -> Result<i128, StreamError>;
fn is_participant(env: Env, stream_id: u64, address: Address) -> Result<bool, StreamError>;
fn get_streams_by_sender(env: Env, sender: Address, start: u32, limit: u32) -> Vec<Stream>;
fn get_streams_by_recipient(env: Env, recipient: Address, start: u32, limit: u32) -> Vec<Stream>;
Expand Down
84 changes: 84 additions & 0 deletions contracts/stream/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4406,6 +4406,90 @@ impl SoroStreamContract {
Ok(claimable)
}

pub fn get_accrued_balance(env: Env, stream_id: u64, _recipient: Address) -> Result<i128, StreamError> {
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<bool, StreamError> {
let stream = load_stream(&env, stream_id).ok_or(StreamError::StreamNotFound)?;
Expand Down