// 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 anyhow::Result; use rusqlite::{params_from_iter, Connection}; use crate::bcmr::parsedbcmr::ParsedBCMR; use crate::db::bcmr::{get_token_bcmr, get_well_known_bcmr}; use crate::db::cauldron::tokenlist::db_utils::{order_clause, CachedSort}; #[derive(Serialize)] pub struct TokenListItemCached { pub token_id: String, pub trade_volume: u64, pub tvl_sats: u64, pub tvl_tokens: u64, pub bcmr_well_known: Vec, pub bcmr: Option, pub trade_count: u64, pub score: i64, pub score_rank: i64, pub price_now: f64, // NOT NULL pub price_24h: Option, // NULL → None, 0.0 → Some(0.0) pub price_7d: Option, // " pub change_24h_bp: Option, // " pub change_7d_bp: Option, // " pub display_name: Option, // NULL → None, "" → Some("") pub display_symbol: Option, pub price_now_usd: f64, // NOT NULL pub price_24h_usd: Option, pub price_7d_usd: Option, pub change_24h_usd_bp: Option, pub change_7d_usd_bp: Option, pub apy_30d_bp: Option, } const TOKEN_METRICS_COLUMNS: &str = r#" hex(token_id) as token_id, trade_volume, tvl_sats, tvl_tokens, score, price_now, price_24h, price_7d, change_24h_bp, change_7d_bp, display_name, display_symbol, price_now_usd, price_24h_usd, price_7d_usd, change_24h_usd_bp, change_7d_usd_bp, apy_30d_bp, score_rank "#; pub fn db_list_tokens_cached( cauldron_conn: &Connection, bcmr_conn: &Connection, limit: usize, offset: usize, sort: CachedSort, ) -> Result> { let order_sql = order_clause(sort); let sql = format!( r#" SELECT {cols} FROM cached_token_metrics WHERE tvl_sats > 0 {order_sql} LIMIT ?1 OFFSET ?2 "#, cols = TOKEN_METRICS_COLUMNS, order_sql = order_sql ); let mut stmt = cauldron_conn.prepare(&sql)?; let mut rows = stmt.query(rusqlite::params![limit as i64, offset as i64])?; let mut tokens = Vec::with_capacity(limit); while let Some(row) = rows.next()? { let token_id: String = row.get::<_, String>("token_id")?.to_lowercase(); let trade_volume: u64 = row.get("trade_volume")?; let tvl_sats: u64 = row.get::<_, i64>("tvl_sats")? as u64; let tvl_tokens: u64 = row.get::<_, i64>("tvl_tokens")? as u64; let score: i64 = row.get("score")?; let price_now: f64 = row.get("price_now")?; let price_24h: Option = row.get("price_24h")?; let price_7d: Option = row.get("price_7d")?; let change_24h_bp: Option = row.get("change_24h_bp")?; let change_7d_bp: Option = row.get("change_7d_bp")?; let display_name: Option = row.get("display_name")?; let display_symbol: Option = row.get("display_symbol")?; let price_now_usd: f64 = row.get("price_now_usd")?; let price_24h_usd: Option = row.get("price_24h_usd")?; let price_7d_usd: Option = row.get("price_7d_usd")?; let change_24h_usd_bp: Option = row.get("change_24h_usd_bp")?; let change_7d_usd_bp: Option = row.get("change_7d_usd_bp")?; let apy_30d_bp: Option = row.get("apy_30d_bp")?; let score_rank: i64 = row.get("score_rank")?; let bcmr = get_token_bcmr(bcmr_conn, &token_id)?; let bcmr_well_known = get_well_known_bcmr(bcmr_conn, &token_id)?; tokens.push(TokenListItemCached { token_id, trade_volume, tvl_sats, tvl_tokens, bcmr, bcmr_well_known, trade_count: 0, score, score_rank, price_now, price_24h, price_7d, change_24h_bp, change_7d_bp, display_name, display_symbol, price_now_usd, price_24h_usd, price_7d_usd, change_24h_usd_bp, change_7d_usd_bp, apy_30d_bp, }); } Ok(tokens) } pub fn db_list_tokens_cached_by_ids( cauldron_conn: &Connection, bcmr_conn: &Connection, token_ids: &[String], sort: CachedSort, ) -> Result> { if token_ids.is_empty() { return Ok(vec![]); } let order_sql = order_clause(sort); let placeholders = vec!["?"; token_ids.len()].join(","); let sql = format!( r#" SELECT {cols} FROM cached_token_metrics WHERE token_id IN ({ph}) AND tvl_sats > 0 {order_sql} "#, cols = TOKEN_METRICS_COLUMNS, ph = placeholders, order_sql = order_sql ); let token_blobs: Vec> = token_ids .iter() .filter_map(|s| hex::decode(s).ok()) .collect(); let mut stmt = cauldron_conn.prepare(&sql)?; let mut rows = stmt.query(params_from_iter(token_blobs.iter()))?; let mut tokens = Vec::with_capacity(token_ids.len()); while let Some(row) = rows.next()? { let token_id: String = row.get::<_, String>("token_id")?.to_lowercase(); let trade_volume: u64 = row.get("trade_volume")?; let tvl_sats: u64 = row.get::<_, i64>("tvl_sats")? as u64; let tvl_tokens: u64 = row.get::<_, i64>("tvl_tokens")? as u64; let score: i64 = row.get("score")?; let price_now: f64 = row.get("price_now")?; let price_24h: Option = row.get("price_24h")?; let price_7d: Option = row.get("price_7d")?; let change_24h_bp: Option = row.get("change_24h_bp")?; let change_7d_bp: Option = row.get("change_7d_bp")?; let display_name: Option = row.get("display_name")?; let display_symbol: Option = row.get("display_symbol")?; let price_now_usd: f64 = row.get("price_now_usd")?; let price_24h_usd: Option = row.get("price_24h_usd")?; let price_7d_usd: Option = row.get("price_7d_usd")?; let change_24h_usd_bp: Option = row.get("change_24h_usd_bp")?; let change_7d_usd_bp: Option = row.get("change_7d_usd_bp")?; let apy_30d_bp: Option = row.get("apy_30d_bp")?; let score_rank: i64 = row.get("score_rank")?; let bcmr = get_token_bcmr(bcmr_conn, &token_id)?; let bcmr_well_known = get_well_known_bcmr(bcmr_conn, &token_id)?; tokens.push(TokenListItemCached { token_id, trade_volume, tvl_sats, tvl_tokens, bcmr, bcmr_well_known, trade_count: 0, score, score_rank, price_now, price_24h, price_7d, change_24h_bp, change_7d_bp, display_name, display_symbol, price_now_usd, price_24h_usd, price_7d_usd, change_24h_usd_bp, change_7d_usd_bp, apy_30d_bp, }); } Ok(tokens) }