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 Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 35 additions & 3 deletions contracts/donation-vault/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -604,7 +604,10 @@ impl DonationVault {
/// Returns the cumulative amount deposited into all streams, including top-ups.
/// Withdrawals and cancellations do not reduce this lifetime total.
pub fn total_donated(env: Env) -> i128 {
env.storage().instance().get(&DataKey::TotalDonated).unwrap_or(0)
env.storage()
.instance()
.get(&DataKey::TotalDonated)
.unwrap_or(0)
}

/// Reads back the vault admin set by `init`.
Expand Down Expand Up @@ -800,6 +803,31 @@ impl DonationVault {
.ok_or(Error::StreamNotFound)
}

/// Lists every stream id belonging to `donor`, in creation order.
///
/// Cancelled streams are still included because their records are kept
/// (callers can inspect `status`/`cancelled`). This is a read-only view.
pub fn streams_by_donor(env: Env, donor: Address) -> Vec<u64> {
let total: u64 = env
.storage()
.instance()
.get(&DataKey::NextStreamId)
.unwrap_or(0);
let mut ids = Vec::new(&env);
for id in 0..total {
if let Some(stream) = env
.storage()
.persistent()
.get::<_, Stream>(&DataKey::Stream(id))
{
if stream.donor == donor {
ids.push_back(id);
}
}
}
ids
}

/// Reads back the number of streams ever created — the exclusive upper
/// bound on valid stream ids.
///
Expand Down Expand Up @@ -1723,7 +1751,9 @@ impl DonationVault {
STREAM_BUMP_AMOUNT,
);

env.storage().instance().set(&DataKey::TotalDonated, &next_total);
env.storage()
.instance()
.set(&DataKey::TotalDonated, &next_total);

extend_instance_ttl(&env);
extend_stream_ttl(&env, stream_id);
Expand Down Expand Up @@ -2206,7 +2236,9 @@ impl DonationVault {
.checked_add(amount)
.ok_or(Error::ArithmeticOverflow)?;

env.storage().instance().set(&DataKey::TotalDonated, &next_total);
env.storage()
.instance()
.set(&DataKey::TotalDonated, &next_total);
env.storage().persistent().set(&key, &stream);
extend_instance_ttl(&env);
extend_stream_ttl(&env, stream_id);
Expand Down
48 changes: 48 additions & 0 deletions contracts/donation-vault/src/math.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ pub fn seconds_to_deplete(rate: i128, balance: i128) -> Option<u64> {
#[cfg(test)]
mod test {
use super::{accrued, seconds_to_deplete};
use proptest::prelude::*;

#[test]
fn zero_rate_accrues_nothing() {
Expand Down Expand Up @@ -305,6 +306,53 @@ mod test {
assert_eq!(seconds_to_deplete(1_000, 1), Some(1));
}

proptest! {
/// A positive balance at a positive rate always takes at least one
/// whole second (and never returns `None`) for values that fit in u64.
#[test]
fn positive_balance_never_depletes_in_zero_seconds(
rate in 1i128..1_000_000,
balance in 1i128..1_000_000_000_000,
) {
let seconds = seconds_to_deplete(rate, balance)
.expect("bounded positive rate/balance fits in u64");
prop_assert!(seconds > 0, "rate={rate} balance={balance} -> {seconds}");
}

/// More balance can never deplete sooner at the same rate.
#[test]
fn monotonic_in_balance(
rate in 1i128..1_000_000,
low in 1i128..1_000_000_000,
extra in 0i128..1_000_000_000,
) {
let high = low + extra;
let low_seconds = seconds_to_deplete(rate, low).unwrap();
let high_seconds = seconds_to_deplete(rate, high).unwrap();
prop_assert!(
low_seconds <= high_seconds,
"rate={rate} low={low} ({low_seconds}s) high={high} ({high_seconds}s)"
);
}

/// At the returned time `accrued` has paid out the whole balance,
/// and one second earlier it has not.
#[test]
fn accrued_matches_depletion_time(
rate in 1i128..1_000_000,
balance in 1i128..1_000_000_000_000,
) {
let seconds = seconds_to_deplete(rate, balance).unwrap();
prop_assert_eq!(accrued(rate, seconds, balance), balance);
if seconds > 0 {
prop_assert!(
accrued(rate, seconds - 1, balance) < balance,
"rate={rate} balance={balance} depleted a second early"
);
}
}
}

#[test]
fn huge_values_neither_overflow_nor_panic() {
assert_eq!(seconds_to_deplete(1, i128::MAX), None);
Expand Down
Loading
Loading