// Copyright (C) 2024-2026 Whiterun LLC // // 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 poolperiod::PoolPeriod; use rocket::{get, State}; use serde_json::Value; use crate::{ db::{cauldron::pool::get_pool_period_snapshot, DB}, rpc::err::{bad_request, db_error, ApiErrorCode, CachedApiResult}, rpc::response::{cached_ok, CACHE_AGGREGATE}, timeutil::time_now, }; use serde_json::json; pub mod apyaggregator; pub mod poolperiod; #[derive(Clone, Debug)] pub struct PoolSnapshot { pub pool_id: String, pub timestamp: u64, pub sats: u64, pub token_amount: u64, } impl PoolSnapshot { #[allow(dead_code)] // used in unit tests pub fn dummy(timestamp: u64, sats: u64, token_amount: u64) -> Self { Self { pool_id: "dummy".to_string(), timestamp, sats, token_amount, } } } /// Fetch apy for a token and/or an account within a given time interval. All variables are optional. /// A query with no variables will return the AAPY based on all users and all tokens aggregated. /// /// Status: Stable /// /// - token: The 32 byte token ID /// - pkh: Public key hash for wallet account /// - start: Unix timestamp for period start (default 30 days) /// - end: Unix timestamp for period end (default NOW) /// /// **Response Example:** /// /// ```json /// {"apy":"10.00","pools":100} /// ``` /// #[get("/pool/aggregated_apy?&&&")] pub async fn aggregate_apy( token: Option<&str>, pkh: Option<&str>, start: Option, end: Option, db: &State, ) -> CachedApiResult { let end = end.unwrap_or(time_now()); let start = start.unwrap_or(end - (3600 * 24 * 30)); // 30 days if end < start { return Err(bad_request( ApiErrorCode::InvalidTimeRange, "end time cannot be less than start time", )); } if start < 0 { return Err(bad_request( ApiErrorCode::InvalidTimeRange, "start time cannot be negative", )); } let pools: anyhow::Result> = get_pool_period_snapshot(&db.cauldron_r, token, pkh, start, end) .await .map_err(db_error)? .into_iter() .map(|(start, end)| PoolPeriod::new(start, end)) .collect(); let pools = pools.map_err(db_error)?; let pools_count = pools.len(); let apy = apyaggregator::APYAggregator::aggregate_apy(pools.into_iter(), Some(start as u64)) .map_err(db_error)?; Ok(cached_ok( json!({ "apy": apy.to_string(), "pools": pools_count, }), CACHE_AGGREGATE, )) }