diff --git a/crates/tools/src/campaign_totals.rs b/crates/tools/src/campaign_totals.rs index 5e7f95e..25800b6 100644 --- a/crates/tools/src/campaign_totals.rs +++ b/crates/tools/src/campaign_totals.rs @@ -1,3 +1,4 @@ +use anyhow::{anyhow, Result}; use std::collections::HashMap; /// In-memory store for per-campaign donation totals, grouped by asset. @@ -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 { + 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. @@ -64,16 +69,16 @@ 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); } @@ -81,8 +86,8 @@ mod tests { #[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); } @@ -90,8 +95,8 @@ mod tests { #[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)); @@ -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")); + } }