// 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 anyhow::Result; use log::warn; use rusqlite::Connection; use crate::db::cauldron::pool::get_pool_period_snapshot; use crate::db::oracle::get_closest; use crate::rpc::apy::apyaggregator::APYAggregator; use crate::rpc::apy::poolperiod::PoolPeriod; use rust_decimal::prelude::*; use rust_decimal::Decimal; pub const SATS_PER_BCH: i64 = 100_000_000; pub const ORACLE_SCALE: i64 = 1_000_000; // match the delphi scale pub fn usd_per_bch_at_or_before(conn: &Connection, ts: i64) -> Decimal { match get_closest(conn, &None, ts) { Ok(Some(e)) => { Decimal::from_i64(e.oracle_price).unwrap_or(Decimal::ZERO) / Decimal::from_i64(ORACLE_SCALE).unwrap() // USD/BCH } _ => Decimal::ZERO, } } #[inline] pub fn pct_change_bp_dec(current: Decimal, past: Decimal) -> i64 { if past.is_zero() { 0 } else { ((current - past) / past * Decimal::from_i32(10_000).unwrap()) .round() .to_i64() .unwrap_or(0) } } #[inline] pub fn compute_score(tvl_sats: u64, volume: u64) -> i64 { if tvl_sats == 0 || volume == 0 { return 0; } let s = (volume as f64) * (tvl_sats as f64).sqrt(); // same as BigNumber sqrt + pow(2) pattern if !s.is_finite() { return 0; } if s >= i64::MAX as f64 { i64::MAX } else { s.round() as i64 } // ROUND_HALF_UP for positives } pub fn resolve_decimals(bcmr_conn: &Connection, crc20_conn: &Connection, token_id: &str) -> u32 { // On-chain BCMR (latest by height) if let Ok(Some(v)) = bcmr_conn.query_row( r#" SELECT decimals FROM ( SELECT b.decimals, a.height, ROW_NUMBER() OVER (PARTITION BY a.token_id ORDER BY a.height DESC) rn FROM auth_chain_entry a LEFT JOIN bcmr_data b ON a.utxo = b.utxo WHERE a.bcmr_data IS NOT NULL AND a.token_id = ?1 ) s WHERE rn = 1 "#, [token_id], |r| r.get::<_, Option>(0), ) { let dd = v.max(0) as u32; if dd > 0 { return dd; // only accept positive } } // Well-known BCMR if let Ok(Some(v)) = bcmr_conn.query_row( r#"SELECT decimals FROM bcmr_well_known WHERE token_id = ?1 ORDER BY source LIMIT 1"#, [token_id], |r| r.get::<_, Option>(0), ) { let dd = v.max(0) as u32; if dd > 0 { return dd; } } // CRC20 fallback if let Ok(Some(v)) = crc20_conn.query_row( r#"SELECT decimals FROM crc20 WHERE token_id = ?1 LIMIT 1"#, [token_id], |r| r.get::<_, Option>(0), ) { let dd = v.max(0) as u32; if dd > 0 { return dd; } } // Final fallback 0 } #[inline] pub fn pow10_dec(decimals: u32) -> Decimal { // rust_decimal supports integer powers Decimal::from_i32(10).unwrap().powu(decimals as u64) } #[inline] pub fn price_from_tvl(tvl_sats: u64, tvl_tokens: u64) -> Option { if tvl_tokens == 0 { return None; } let sats = Decimal::from_u64(tvl_sats)?; let tokens = Decimal::from_u64(tvl_tokens)?; sats.checked_div(tokens) } pub fn apy_30d_bp_for_token(conn: &Connection, token_id: &str, now: i64) -> Result { let start = now - 30 * 86_400; let pairs = get_pool_period_snapshot(conn, Some(token_id), None, start, now)?; let periods = pairs .into_iter() .filter(|(s, e)| { e.timestamp > s.timestamp && s.sats > 0 && s.token_amount > 0 && e.sats > 0 && e.token_amount > 0 }) .map(|(s, e)| PoolPeriod::new(s, e)) .collect::>>()?; if periods.is_empty() { return Ok(0); } let apy_dec = match APYAggregator::aggregate_apy(periods.into_iter(), Some(start as u64)) { Ok(v) => v, // v is a PERCENT value, e.g. 5.239 = 5.239% Err(e) => { warn!( "aggregate_apy failed for token {} over 30d: {}", token_id, e ); Decimal::ZERO } }; // Percent → basis points (100 bp = 1%) Ok((apy_dec * Decimal::from_i32(100).unwrap()) .round() .to_i64() .unwrap_or(0)) } pub fn resolve_display_labels( bcmr_conn: &Connection, crc20_conn: &Connection, token_id: &str, ) -> Result<(String, String)> { // On-chain BCMR (latest by height) let mut onchain = bcmr_conn.prepare( r#" SELECT name, symbol FROM ( SELECT b.name, b.symbol, a.height, ROW_NUMBER() OVER (PARTITION BY a.token_id ORDER BY a.height DESC) rn FROM auth_chain_entry a LEFT JOIN bcmr_data b ON a.utxo = b.utxo WHERE a.bcmr_data IS NOT NULL AND a.token_id = ?1 ) s WHERE rn = 1 "#, )?; if let Ok(row) = onchain.query_row([token_id], |r| { Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)) }) { let (name, sym) = row; let dn = if !name.is_empty() { name } else if !sym.is_empty() { sym.clone() } else { token_id.to_string() }; return Ok((dn, sym)); } // Well-known BCMR (pick a deterministic row) let mut wk = bcmr_conn.prepare( r#"SELECT name, symbol FROM bcmr_well_known WHERE token_id = ?1 ORDER BY source LIMIT 1"#, )?; if let Ok(row) = wk.query_row([token_id], |r| { Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)) }) { let (name, sym) = row; let dn = if !name.is_empty() { name } else if !sym.is_empty() { sym.clone() } else { token_id.to_string() }; return Ok((dn, sym)); } // CRC20 fallback let mut crc = crc20_conn.prepare(r#"SELECT name, symbol FROM crc20 WHERE token_id = ?1 LIMIT 1"#)?; if let Ok(row) = crc.query_row([token_id], |r| { Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)) }) { let (name, sym) = row; let dn = if !name.is_empty() { name } else if !sym.is_empty() { sym.clone() } else { token_id.to_string() }; return Ok((dn, sym)); } // Last resort Ok((token_id.to_string(), String::new())) }