diff --git a/src/rpc/apy/mod.rs b/src/rpc/apy/mod.rs index 528dc42..b4934ba 100644 --- a/src/rpc/apy/mod.rs +++ b/src/rpc/apy/mod.rs @@ -3,15 +3,16 @@ // This software is licensed under the GNU Affero General Public License (AGPL), version 3.0 or later. // A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/agpl-3.0.html +use log::warn; use poolperiod::PoolPeriod; use rocket::{get, http::Status, response::status::Custom, serde::json::Json, State}; +use serde_json::json; use serde_json::Value; use crate::{ db::{cauldron::pool::get_pool_period_snapshot, DB}, timeutil::time_now, }; -use serde_json::json; pub mod apyaggregator; pub mod poolperiod; @@ -81,14 +82,17 @@ pub fn aggregate_apy( .get() .map_err(|e| Custom(Status::InternalServerError, format!("DB error: {e}")))?; - let pools: anyhow::Result> = - get_pool_period_snapshot(&conn, token, pkh, start, end) - .map_err(|e| Custom(Status::InternalServerError, format!("Error: {e}")))? - .into_iter() - .map(|(start, end)| PoolPeriod::new(start, end)) - .collect(); - - let pools = pools.map_err(|e| Custom(Status::InternalServerError, format!("Error: {e}")))?; + let pools: Vec = get_pool_period_snapshot(&conn, token, pkh, start, end) + .map_err(|e| Custom(Status::InternalServerError, format!("Error: {e}")))? + .into_iter() + .filter_map(|(start, end)| match PoolPeriod::new(start.clone(), end) { + Ok(period) => Some(period), + Err(e) => { + warn!("Skipping invalid pool {}: {e}", start.pool_id); + None + } + }) + .collect(); let pools_count = pools.len(); let apy = apyaggregator::APYAggregator::aggregate_apy(pools.into_iter(), Some(start as u64)) diff --git a/src/rpc/apy/poolperiod.rs b/src/rpc/apy/poolperiod.rs index 25ac798..dff71f4 100644 --- a/src/rpc/apy/poolperiod.rs +++ b/src/rpc/apy/poolperiod.rs @@ -177,4 +177,40 @@ mod tests { "expected {expected_apy} != actual {pool_apy}" ); } + + #[test] + fn test_zero_sats_rejected() { + let start = PoolSnapshot::dummy(1000, 0, 100); // zero sats + let end = PoolSnapshot::dummy(2000, 100, 100); + let result = PoolPeriod::new(start, end); + assert!(result.is_err()); + } + + #[test] + fn test_zero_token_amount_rejected() { + let start = PoolSnapshot::dummy(1000, 100, 0); // zero token_amount + let end = PoolSnapshot::dummy(2000, 100, 100); + let result = PoolPeriod::new(start, end); + assert!(result.is_err()); + } + + #[test] + fn test_large_k_no_overflow() { + // Values that would overflow u64 if multiplied directly + // 10^10 * 10^10 = 10^20 > u64::MAX (~1.8 * 10^19) + let start = PoolSnapshot::dummy(1000, 10_000_000_000, 10_000_000_000); + let end = PoolSnapshot::dummy(2000, 10_000_000_000, 10_000_000_000); + let result = PoolPeriod::new(start, end); + assert!(result.is_ok(), "Should handle large values without overflow"); + } + + #[test] + fn test_yield_calculation_valid() { + let start = PoolSnapshot::dummy(1000, 1000, 1000); + let end = PoolSnapshot::dummy(2000, 1100, 1100); // 10% increase each + let period = PoolPeriod::new(start, end).unwrap(); + let yield_result = period.pool_yield().unwrap(); + // sqrt(1.1 * 1.1) - 1 = 1.1 - 1 = 0.1 = 10% + assert!((yield_result - dec!(10)).abs() < dec!(0.01)); + } }