riftenlabs-indexer/src/db/cauldron/tokenlist/token_utils.rs
2025-09-11 07:36:06 +00:00

230 lines
6.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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::{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<i64>>(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<i64>>(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<i64>>(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<Decimal> {
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<i64> {
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::<Result<Vec<_>>>()?;
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%). If it won’t fit into i64, surface an error.
let bp_dec = (apy_dec * Decimal::from_i32(100).unwrap()).round();
let apy_bp = bp_dec
.to_i64()
.ok_or_else(|| anyhow!("APY bp overflow for {}", token_id))?;
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()))
}