// 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 crate::db::moria::{get_active_loans, get_global_history, get_loan_history, get_stats}; use crate::db::DB; use crate::rpc::err::{bad_request, db_error, ApiErrorCode, CachedApiResult}; use crate::rpc::response::{cached_ok, CACHE_AGGREGATE}; use rocket::{get, State}; use serde_json::Value; fn parse_nfth(hex_str: &str) -> Result, (ApiErrorCode, String)> { let bytes = hex::decode(hex_str).map_err(|e| { ( ApiErrorCode::InvalidParameters, format!("Invalid nfth hex: {e}"), ) })?; if bytes.len() != 32 { return Err(( ApiErrorCode::InvalidParameters, format!("nfth must be 32 bytes (64 hex chars), got {}", bytes.len()), )); } Ok(bytes) } /// Get full loan history for a given borrower NFT hash. /// /// - borrower_hash: 64-character hex string (32-byte P2NFTH hash identifying the loan) /// /// Returns an array of all actions (borrow, repay, redeem, refinance, add_collateral) /// for this loan, sorted by timestamp. #[get("/loan//history")] pub async fn loan_history(borrower_hash: &str, db: &State) -> CachedApiResult { let hash_bytes = hex::decode(borrower_hash).map_err(|e| { bad_request( ApiErrorCode::InvalidParameters, &format!("Invalid borrower hash: {e}"), ) })?; if hash_bytes.len() != 32 { return Err(bad_request( ApiErrorCode::InvalidParameters, "Borrower hash must be 32 bytes (64 hex characters)", )); } let entries = get_loan_history(&db.moria_r, &hash_bytes) .await .map_err(db_error)?; Ok(cached_ok( serde_json::to_value(entries).unwrap(), CACHE_AGGREGATE, )) } /// Get global moria action history with pagination and optional nfth filter. /// /// - offset: Number of entries to skip (default: 0) /// - limit: Maximum entries to return (default: 50, max: 200) /// - nfth: Comma-separated list of borrower NFT hashes (64-char hex each) to filter by #[get("/history?&&")] pub async fn global_history( offset: Option, limit: Option, nfth: Option<&str>, db: &State, ) -> CachedApiResult { let offset = offset.unwrap_or(0).max(0); let limit = limit.unwrap_or(50).clamp(1, 200); let nfth_filter: Vec> = match nfth { Some(s) if !s.is_empty() => { let mut filters = Vec::new(); for hash_hex in s.split(',') { let hash_hex = hash_hex.trim(); if hash_hex.is_empty() { continue; } filters.push(parse_nfth(hash_hex).map_err(|(code, msg)| bad_request(code, &msg))?); } filters } _ => Vec::new(), }; let entries = get_global_history(&db.moria_r, &nfth_filter, offset, limit) .await .map_err(db_error)?; Ok(cached_ok( serde_json::to_value(entries).unwrap(), CACHE_AGGREGATE, )) } /// List all active (not yet repaid/redeemed) loans. #[get("/loans/active")] pub async fn active_loans(db: &State) -> CachedApiResult { let entries = get_active_loans(&db.moria_r).await.map_err(db_error)?; Ok(cached_ok( serde_json::to_value(entries).unwrap(), CACHE_AGGREGATE, )) } /// Get Moria protocol statistics. #[get("/stats")] pub async fn moria_stats(db: &State) -> CachedApiResult { let stats = get_stats(&db.moria_r).await.map_err(db_error)?; Ok(cached_ok(stats, CACHE_AGGREGATE)) }