Skip to content
Open
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
55 changes: 41 additions & 14 deletions crates/tools/src/campaign_totals.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use anyhow::{anyhow, Result};
use std::collections::HashMap;

/// In-memory store for per-campaign donation totals, grouped by asset.
Expand All @@ -16,12 +17,16 @@ impl CampaignTotals {
}

/// Adds `amount` to the running total for `campaign_id` + `asset` and returns the new total.
#[must_use]
///
/// Returns `Err` if `amount` is non-positive or if the addition would overflow.
#[inline]
pub fn increment(&mut self, campaign_id: u64, asset: &str, amount: i128) -> i128 {
pub fn increment(&mut self, campaign_id: u64, asset: &str, amount: i128) -> Result<i128> {
if amount <= 0 {
return Err(anyhow!("CampaignTotals::increment requires positive amount; got {}", amount));
}
let entry = self.asset_totals.entry((campaign_id, asset.to_string())).or_insert(0);
*entry += amount;
*entry
*entry = entry.checked_add(amount).ok_or_else(|| anyhow!("overflow in CampaignTotals"))?;
Ok(*entry)
}

/// Returns the total for a specific `campaign_id` + `asset`, or 0 if none recorded.
Expand Down Expand Up @@ -64,34 +69,34 @@ mod tests {
#[test]
fn increments_per_asset() {
let mut totals = CampaignTotals::new();
totals.increment(1, "XLM", 500);
totals.increment(1, "XLM", 300);
totals.increment(1, "XLM", 500).unwrap();
totals.increment(1, "XLM", 300).unwrap();
assert_eq!(totals.get(1, "XLM"), 800);
}

#[test]
fn different_assets_are_independent() {
let mut totals = CampaignTotals::new();
totals.increment(1, "XLM", 100);
totals.increment(1, "USDC", 200);
totals.increment(1, "XLM", 100).unwrap();
totals.increment(1, "USDC", 200).unwrap();
assert_eq!(totals.get(1, "XLM"), 100);
assert_eq!(totals.get(1, "USDC"), 200);
}

#[test]
fn different_campaigns_are_independent() {
let mut totals = CampaignTotals::new();
totals.increment(1, "XLM", 100);
totals.increment(2, "XLM", 200);
totals.increment(1, "XLM", 100).unwrap();
totals.increment(2, "XLM", 200).unwrap();
assert_eq!(totals.get(1, "XLM"), 100);
assert_eq!(totals.get(2, "XLM"), 200);
}

#[test]
fn get_all_assets_returns_correct_map() {
let mut totals = CampaignTotals::new();
totals.increment(1, "XLM", 500);
totals.increment(1, "USDC", 300);
totals.increment(1, "XLM", 500).unwrap();
totals.increment(1, "USDC", 300).unwrap();
let map = totals.get_all_assets(1);
assert_eq!(map.get("XLM"), Some(&500));
assert_eq!(map.get("USDC"), Some(&300));
Expand All @@ -100,8 +105,30 @@ mod tests {
#[test]
fn campaign_total_aggregates_all_assets() {
let mut totals = CampaignTotals::new();
totals.increment(1, "XLM", 500);
totals.increment(1, "USDC", 300);
totals.increment(1, "XLM", 500).unwrap();
totals.increment(1, "USDC", 300).unwrap();
assert_eq!(totals.get_campaign_total(1), 800);
}

#[test]
fn rejects_negative_amount() {
let mut totals = CampaignTotals::new();
let err = totals.increment(1, "XLM", -100).unwrap_err();
assert!(err.to_string().contains("positive amount"));
}

#[test]
fn rejects_zero_amount() {
let mut totals = CampaignTotals::new();
let err = totals.increment(1, "XLM", 0).unwrap_err();
assert!(err.to_string().contains("positive amount"));
}

#[test]
fn rejects_overflow() {
let mut totals = CampaignTotals::new();
totals.increment(1, "XLM", i128::MAX).unwrap();
let err = totals.increment(1, "XLM", 1).unwrap_err();
assert!(err.to_string().contains("overflow"));
}
}