riftenlabs-indexer/src/rpc/apy/mod.rs

126 lines
3.8 KiB
Rust
Raw Normal View History

2026-01-21 12:34:59 +01:00
// 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::{
2026-03-06 12:08:15 +00:00
db::{
cauldron::pool::{get_pool_period_snapshot, get_pool_period_snapshot_by_pool_ids},
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
2026-03-06 12:08:15 +00:00
/// - pkh: Public key hash for a single wallet account
/// - pool: One or more pool IDs to scope the APY calculation to specific pools.
/// Use this for per-user APY: first resolve pool IDs via `/pool/active?pkh=<pkh>`,
/// then pass them here. Supports multiple wallets (collect IDs from each PKH first).
/// **Cannot be combined with token or pkh** — returns 400 if mixed.
/// - 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}
/// ```
///
2026-03-06 12:08:15 +00:00
#[get("/pool/aggregated_apy?<token>&<pkh>&<pool>&<start>&<end>")]
pub async fn aggregate_apy(
token: Option<&str>,
pkh: Option<&str>,
2026-03-06 12:08:15 +00:00
pool: Vec<&str>,
start: Option<i64>,
end: Option<i64>,
db: &State<DB>,
) -> CachedApiResult<Value> {
let end = end.unwrap_or(time_now());
let start = start.unwrap_or(end - (3600 * 24 * 30)); // 30 days
2026-03-06 12:08:15 +00:00
if !pool.is_empty() && (token.is_some() || pkh.is_some()) {
return Err(bad_request(
ApiErrorCode::InvalidParameters,
"pool cannot be combined with token or pkh; use pool alone to filter by specific pools",
));
}
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",
));
}
2026-03-06 12:08:15 +00:00
let pool_snapshots = if !pool.is_empty() {
let ids: Vec<String> = pool.into_iter().map(|s| s.to_string()).collect();
get_pool_period_snapshot_by_pool_ids(&db.cauldron_r, &ids, start, end)
.await
.map_err(db_error)?
} else {
get_pool_period_snapshot(&db.cauldron_r, token, pkh, start, end)
.await
.map_err(db_error)?
2026-03-06 12:08:15 +00:00
};
let pools: anyhow::Result<Vec<PoolPeriod>> = pool_snapshots
.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,
))
}