// 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 bitcoincash::TokenID; use log::warn; use sqlx::SqlitePool; use crate::db::blob::display_hex_to_blob; use crate::db::cauldron::pool::{get_injections_between, get_pool_period_snapshot}; use crate::db::oracle::get_closest; use crate::rpc::apy::apyaggregator::APYAggregator; use crate::rpc::apy::poolperiod::split_at_injections; 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); 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; pub async fn usd_per_bch_at_or_before(oracle_pool: &SqlitePool, ts: i64) -> Decimal { match get_closest(oracle_pool, &None, ts).await { Ok(Some(e)) => { Decimal::from_i64(e.oracle_price).unwrap_or(Decimal::ZERO) / Decimal::from_i64(ORACLE_SCALE).unwrap() } _ => Decimal::ZERO, } } #[inline] pub fn pct_change_bp_dec(current: Decimal, past: Decimal) -> i64 { if past.is_zero() { return 0; } let tenk = dec!(10000); 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, }; let delta_bp = match ratio_bp.checked_sub(tenk) { Some(v) => v, None => return if current >= past { i64::MAX } else { i64::MIN }, }; 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; } let sqrt_tvl: Integer = Integer::from(tvl_sats).floor_sqrt(); let score_big: Integer = Integer::from(vol_30d) * sqrt_tvl; if score_big > i64::MAX { return i64::MAX; } score_big.to_string().parse::().unwrap_or(i64::MAX) } pub async fn resolve_decimals( bcmr_pool: &SqlitePool, crc20_pool: &SqlitePool, token_id: &str, ) -> u32 { let token_blob = match display_hex_to_blob::(token_id) { Ok(b) => b, Err(_) => return 0, }; // On-chain BCMR let row: Option<(Option,)> = sqlx::query_as( 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 "#, ) .bind(&token_blob) .fetch_optional(bcmr_pool) .await .ok() .flatten(); if let Some((Some(v),)) = row { let dd = v.max(0) as u32; if dd > 0 { return dd; } } // Well-known BCMR let row: Option<(Option,)> = sqlx::query_as( "SELECT decimals FROM bcmr_well_known WHERE token_id = ?1 ORDER BY source LIMIT 1", ) .bind(&token_blob) .fetch_optional(bcmr_pool) .await .ok() .flatten(); if let Some((Some(v),)) = row { let dd = v.max(0) as u32; if dd > 0 { return dd; } } // CRC20 fallback let row: Option<(Option,)> = sqlx::query_as("SELECT decimals FROM crc20 WHERE token_id = ?1 LIMIT 1") .bind(&token_blob) .fetch_optional(crc20_pool) .await .ok() .flatten(); if let Some((Some(v),)) = row { let dd = v.max(0) as u32; if dd > 0 { return dd; } } 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 { 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 async fn apy_30d_bp_for_token( cauldron_pool: &SqlitePool, token_id: &str, now: i64, ) -> Result { let start = now - 30 * 86_400; let pairs = get_pool_period_snapshot(cauldron_pool, Some(token_id), None, start, now).await?; let pool_id_blobs: Vec> = pairs .iter() .filter_map(|(s, _)| display_hex_to_blob::(&s.pool_id).ok()) .collect(); let injections_map = get_injections_between(cauldron_pool, &pool_id_blobs, start, now).await?; 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 }) .flat_map(|(s, e)| { let injections = injections_map .get(&s.pool_id) .map(|entries| { entries .iter() .filter(|r| r.timestamp > s.timestamp && r.timestamp < e.timestamp) .collect::>() }) .unwrap_or_default(); split_at_injections(s, e, injections) }) .collect::>>()?; if periods.is_empty() { return Ok(0); } 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 })?; let hundred = dec!(100); let bp_dec = match apy_dec.checked_mul(hundred) { Some(v) => v.round(), None => { 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 async fn resolve_display_labels( bcmr_pool: &SqlitePool, crc20_pool: &SqlitePool, token_id: &str, ) -> Result<(String, String)> { let token_blob = display_hex_to_blob::(token_id)?; // On-chain BCMR let row: Option<(String, String)> = sqlx::query_as( 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 "#, ) .bind(&token_blob) .fetch_optional(bcmr_pool) .await?; if let Some((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 let row: Option<(String, String)> = sqlx::query_as( "SELECT name, symbol FROM bcmr_well_known WHERE token_id = ?1 ORDER BY source LIMIT 1", ) .bind(&token_blob) .fetch_optional(bcmr_pool) .await?; if let Some((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 row: Option<(String, String)> = sqlx::query_as("SELECT name, symbol FROM crc20 WHERE token_id = ?1 LIMIT 1") .bind(&token_blob) .fetch_optional(crc20_pool) .await?; if let Some((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)); } Ok((token_id.to_string(), String::new())) }