2025-09-05 13:53:10 +00:00
|
|
|
|
// 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
|
|
|
|
|
|
|
2025-09-11 07:36:06 +00:00
|
|
|
|
use anyhow::{anyhow, Result};
|
2025-09-05 13:53:10 +00:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-11 07:36:06 +00:00
|
|
|
|
// 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| {
|
2025-09-05 13:53:10 +00:00
|
|
|
|
warn!(
|
|
|
|
|
|
"aggregate_apy failed for token {} over 30d: {}",
|
|
|
|
|
|
token_id, e
|
|
|
|
|
|
);
|
2025-09-11 07:36:06 +00:00
|
|
|
|
e
|
|
|
|
|
|
})?;
|
2025-09-05 13:53:10 +00:00
|
|
|
|
|
2025-09-11 07:36:06 +00:00
|
|
|
|
// 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
|
2025-09-05 13:53:10 +00:00
|
|
|
|
.to_i64()
|
2025-09-11 07:36:06 +00:00
|
|
|
|
.ok_or_else(|| anyhow!("APY bp overflow for {}", token_id))?;
|
|
|
|
|
|
Ok(apy_bp)
|
2025-09-05 13:53:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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()))
|
|
|
|
|
|
}
|