// Copyright (C) 2024 Riften Labs AS // // 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 std::{ collections::HashMap, sync::{Arc, Mutex}, }; use anyhow::{Context, Result}; use rusqlite::{params, Connection}; use crate::{ bcmr::parsedbcmr::ParsedBCMR, db::bcmr::{get_token_bcmr, get_well_known_bcmr}, }; pub mod bcmr; pub mod contract; pub mod pool; pub mod price; pub mod tokens; pub mod tvl; pub mod user; pub type ResponseCache = Arc)>>>; #[allow(clippy::type_complexity)] pub fn list_tokens_by_volume( cauldron_conn: &Connection, bcmr_conn: &Connection, seconds: usize, limit: usize, ) -> Result< Vec<( String, u64, u64, u64, u64, u64, u64, Option, Vec, )>, > { // List token ID's by volume last n seconds let mut statement = cauldron_conn .prepare( " WITH TradeData AS ( SELECT uf1.token_id, ABS(uf1.sats - COALESCE(uf2.sats, 0)) as trade_volume FROM utxo_funding uf1 INNER JOIN utxo_funding uf2 ON uf1.spent_utxo_hash = uf2.new_utxo_hash JOIN tx ON uf1.txid = tx.txid WHERE COALESCE(tx.first_seen_timestamp, tx.mtp_timestamp) >= (strftime('%s', 'now') - ?) ), TVLData AS ( SELECT token_id, SUM(sats) as tvl_sats, SUM(token_amount) as tvl_tokens FROM utxo_funding WHERE new_utxo_hash NOT IN (SELECT spent_utxo_hash FROM utxo_spending) GROUP BY token_id ), HighestUnspentUTXO AS ( SELECT token_id, MAX(sats) as highest_sats, MAX(token_amount) as token_amount FROM utxo_funding WHERE new_utxo_hash NOT IN (SELECT spent_utxo_hash FROM utxo_spending) GROUP BY token_id ), AggregateTradeData AS ( SELECT COALESCE(td.token_id, tvl.token_id) as token_id, COALESCE(SUM(td.trade_volume), 0) as total_trade_volume, COALESCE(COUNT(td.token_id), 0) as number_of_trades, tvl.tvl_sats, tvl.tvl_tokens, hu.highest_sats, hu.token_amount FROM TVLData tvl LEFT JOIN TradeData td ON tvl.token_id = td.token_id LEFT JOIN HighestUnspentUTXO hu ON tvl.token_id = hu.token_id GROUP BY tvl.token_id ) SELECT token_id, total_trade_volume, number_of_trades, tvl_sats, tvl_tokens, highest_sats, token_amount FROM AggregateTradeData ORDER BY total_trade_volume DESC, tvl_sats DESC LIMIT ?; ", )?; let tokens: Vec<( String, u64, u64, u64, u64, u64, u64, Option, Vec, )> = statement .query_and_then([seconds, limit], |row| { let token_id: String = row.get(0)?; let trade_volume: u64 = row.get(1)?; let trade_count: u64 = row.get(2)?; let tvl_sats: u64 = row.get(3)?; let tvl_token: u64 = row.get(4)?; let best_contract_sats: u64 = row.get(5)?; let best_contracts_token: u64 = row.get(6)?; let bcmr = get_token_bcmr(bcmr_conn, &token_id)?; let bcmr_well_known = get_well_known_bcmr(bcmr_conn, &token_id)?; Ok(( token_id, trade_volume, trade_count, tvl_sats, tvl_token, best_contract_sats, best_contracts_token, bcmr, bcmr_well_known, )) }) .unwrap() .map( |row: Result<( String, u64, u64, u64, u64, u64, u64, Option, Vec, )>| row.unwrap(), ) .collect(); Ok(tokens) } fn all_time_volume(db: &Connection, end_timestamp: u64) -> Result> { let sql = " SELECT uf1.token_id, SUM(ABS(uf1.sats - COALESCE(uf2.sats, 0))) AS total_volume_sats FROM utxo_funding uf1 INNER JOIN utxo_funding uf2 ON uf1.spent_utxo_hash = uf2.new_utxo_hash JOIN tx ON uf1.txid = tx.txid WHERE COALESCE(tx.first_seen_timestamp, tx.mtp_timestamp) <= ? GROUP BY uf1.token_id"; let mut stmt = db.prepare(sql)?; let volume_iter = stmt.query_map(params![end_timestamp], |row| { Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?)) })?; let mut volumes = Vec::new(); for volume in volume_iter { volumes.push(volume?); } Ok(volumes) } fn period_volume( db: &Connection, begin_timestamp: u64, end_timestamp: u64, ) -> Result> { let sql = " SELECT uf1.token_id, SUM(ABS(uf1.sats - COALESCE(uf2.sats, 0))) AS total_volume_sats FROM utxo_funding uf1 INNER JOIN utxo_funding uf2 ON uf1.spent_utxo_hash = uf2.new_utxo_hash JOIN tx ON uf1.txid = tx.txid WHERE COALESCE(tx.first_seen_timestamp, tx.mtp_timestamp) BETWEEN ? AND ? GROUP BY uf1.token_id "; let mut stmt = db.prepare(sql)?; let volume_iter = stmt.query_map(params![begin_timestamp, end_timestamp], |row| { Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?)) })?; let mut volumes = Vec::new(); for volume in volume_iter { volumes.push(volume?); } Ok(volumes) } pub fn contract_volume( db: &Connection, end_timestamp: u64, ) -> Result> { let one_day_seconds = 86400_u64; let thirty_days_seconds = 30 * 86400_u64; let one_day_begin_timestamp = end_timestamp .checked_sub(one_day_seconds) .context("timestamp underflow")?; let thirty_days_begin_timestamp = end_timestamp .checked_sub(thirty_days_seconds) .context("timestamp underflow")?; let all_time = all_time_volume(db, end_timestamp)?; let one_day = period_volume(db, one_day_begin_timestamp, end_timestamp)?; let thirty_days = period_volume(db, thirty_days_begin_timestamp, end_timestamp)?; let mut result = HashMap::new(); for (token_id, volume) in all_time.into_iter() { result.insert(token_id, (volume, 0, 0)); } for (token_id, day_volume) in one_day.into_iter() { if let Some((_, _, one_day_volume)) = result.get_mut(&token_id) { *one_day_volume = day_volume; } else { result.insert(token_id.clone(), (0, 0, day_volume)); } } for (token_id, month_volume) in thirty_days.into_iter() { if let Some((_, thirty_day_volume, _)) = result.get_mut(&token_id) { *thirty_day_volume = month_volume; } else { result.insert(token_id.clone(), (0, month_volume, 0)); } } Ok(result) }