// 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 malachite::base::num::arithmetic::traits::FloorSqrt; use malachite::Integer; use rust_decimal::prelude::*; use rust_decimal::Decimal; use rust_decimal_macros::dec; /// Fallback path when Decimal scaling overflows: convert to f64 with clamped exponent. pub fn overflow_f64_fallback(base: Decimal, decimals: u32) -> f64 { let base_f = dec_to_f64_bounded(base); let exp = (decimals as i32).clamp(0, 308); // max safe for f64::powi let factor_f = 10f64.powi(exp); let p = base_f * factor_f; if p.is_finite() { p } else { f64::MAX.copysign(p) } } /// Round to a reasonable scale before conversion (tunable). pub fn dec_round(d: Decimal, scale: u32) -> Decimal { d.round_dp_with_strategy(scale, rust_decimal::RoundingStrategy::MidpointNearestEven) } /// Convert Decimal -> f64, guaranteed finite (never NaN/∞). Clamps if needed. /// Optional: enforce a floor so tiny non-zero values don't collapse to 0. pub fn dec_to_f64_bounded(d: Decimal) -> f64 { if let Some(v) = d.to_f64() { if v.is_finite() { return v; } } if d.is_sign_negative() { -f64::MAX } else { f64::MAX } } 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() { return 0; } let tenk = dec!(10000); // Compute: ((current / past) - 1) * 10_000 in a safer order: // => (current * 10_000 / past) - 10_000 let scaled_current = match current.checked_mul(tenk) { Some(v) => v, None => return if current >= past { i64::MAX } else { i64::MIN }, }; let ratio_bp = match scaled_current.checked_div(past) { Some(v) => v, None => return 0, // should not happen (past==0 handled), but be defensive }; let delta_bp = match ratio_bp.checked_sub(tenk) { Some(v) => v, None => return if current >= past { i64::MAX } else { i64::MIN }, }; // Round and convert; clamp if it wouldn't fit in i64 match delta_bp.round().to_i64() { Some(v) => v, None => { if delta_bp.is_sign_negative() { i64::MIN } else { i64::MAX } } } } #[inline] pub fn compute_score(tvl_sats: u64, vol_30d: u64) -> i64 { if tvl_sats == 0 || vol_30d == 0 { return 0; } // score = vol_30d * floor_sqrt(tvl_sats), all in big-int to avoid overflow let sqrt_tvl: Integer = Integer::from(tvl_sats).floor_sqrt(); let score_big: Integer = Integer::from(vol_30d) * sqrt_tvl; // Clamp to i64 range (non-negative by construction) if score_big > i64::MAX { return i64::MAX; } // Convert safely; fall back to MAX on unexpected parse failure match score_big.to_string().parse::() { Ok(v) => v, Err(_) => i64::MAX, } } 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 } /// Max safe exponent for 10^d that fits in `rust_decimal` (1e28 fits; 1e29 does not). pub const MAX_DECIMALS_FOR_SCALING: u32 = 28; /// Clamp a metadata decimals value to something rust_decimal can represent. #[inline] pub fn clamp_decimals(decimals: u32) -> u32 { if decimals > MAX_DECIMALS_FOR_SCALING { warn!( "Clamping decimals {} -> {} to avoid overflow in pow10_dec()", decimals, MAX_DECIMALS_FOR_SCALING ); } decimals.min(MAX_DECIMALS_FOR_SCALING) } #[inline] pub fn pow10_dec(decimals: u32) -> Decimal { // After clamping, powu is safe and exact. dec!(10).powu(clamp_decimals(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); } // v is a PERCENT value, e.g. 5.239 = 5.239% let apy_dec = APYAggregator::aggregate_apy(periods.into_iter(), Some(start as u64)).map_err(|e| { warn!( "aggregate_apy failed for token {} over 30d: {}", token_id, e ); e })?; // Percent → basis points (100 bp = 1%) let hundred = dec!(100); let bp_dec = match apy_dec.checked_mul(hundred) { Some(v) => v.round(), None => { // clamp on overflow instead of panicking return Ok(i64::MAX); } }; let apy_bp = match bp_dec.to_i64() { Some(v) => v, None if bp_dec.is_sign_negative() => i64::MIN, None => i64::MAX, }; Ok(apy_bp) } 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())) }