Migrate all 32-byte hash fields from TEXT (64-char hex) to BLOB (32 bytes) for ~50% storage savings on hash columns. Changes: - Add src/db/blob.rs with ToBlob/FromBlob traits for hash types - Update all table schemas to use BLOB for hash columns - Replace .to_hex() with .to_blob() for DB inserts - Replace ::from_hex() with ::from_blob() for DB reads - Use SQL hex() function where API responses need hex strings - Bump DB_VERSION from 4 to 5 Affected tables: tx, pool, pool_history_entry, utxo_spending, utxo_funding, user_action, auth_chain_entry, bcmr_data, bcmr_failure, bcmr_well_known, crc20, crc20_candidates, delphi_entry, cached_token_metrics Fixes #1
228 lines
7.3 KiB
Rust
228 lines
7.3 KiB
Rust
// 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 rusqlite::{params_from_iter, Connection};
|
|
|
|
use crate::bcmr::parsedbcmr::ParsedBCMR;
|
|
use crate::db::bcmr::{get_token_bcmr, get_well_known_bcmr};
|
|
use crate::db::cauldron::tokenlist::db_utils::{order_clause, CachedSort};
|
|
|
|
#[derive(Serialize)]
|
|
pub struct TokenListItemCached {
|
|
pub token_id: String,
|
|
pub trade_volume: u64,
|
|
pub tvl_sats: u64,
|
|
pub tvl_tokens: u64,
|
|
pub bcmr_well_known: Vec<ParsedBCMR>,
|
|
pub bcmr: Option<ParsedBCMR>,
|
|
pub trade_count: u64,
|
|
pub score: i64,
|
|
pub score_rank: i64,
|
|
pub price_now: f64, // NOT NULL
|
|
pub price_24h: Option<f64>, // NULL → None, 0.0 → Some(0.0)
|
|
pub price_7d: Option<f64>, // "
|
|
pub change_24h_bp: Option<i64>, // "
|
|
pub change_7d_bp: Option<i64>, // "
|
|
pub display_name: Option<String>, // NULL → None, "" → Some("")
|
|
pub display_symbol: Option<String>,
|
|
pub price_now_usd: f64, // NOT NULL
|
|
pub price_24h_usd: Option<f64>,
|
|
pub price_7d_usd: Option<f64>,
|
|
pub change_24h_usd_bp: Option<i64>,
|
|
pub change_7d_usd_bp: Option<i64>,
|
|
pub apy_30d_bp: Option<i64>,
|
|
}
|
|
|
|
const TOKEN_METRICS_COLUMNS: &str = r#"
|
|
hex(token_id) as token_id,
|
|
trade_volume,
|
|
tvl_sats,
|
|
tvl_tokens,
|
|
score,
|
|
price_now,
|
|
price_24h,
|
|
price_7d,
|
|
change_24h_bp,
|
|
change_7d_bp,
|
|
display_name,
|
|
display_symbol,
|
|
price_now_usd,
|
|
price_24h_usd,
|
|
price_7d_usd,
|
|
change_24h_usd_bp,
|
|
change_7d_usd_bp,
|
|
apy_30d_bp,
|
|
score_rank
|
|
"#;
|
|
|
|
pub fn db_list_tokens_cached(
|
|
cauldron_conn: &Connection,
|
|
bcmr_conn: &Connection,
|
|
limit: usize,
|
|
offset: usize,
|
|
sort: CachedSort,
|
|
) -> Result<Vec<TokenListItemCached>> {
|
|
let order_sql = order_clause(sort);
|
|
let sql = format!(
|
|
r#"
|
|
SELECT
|
|
{cols}
|
|
FROM cached_token_metrics
|
|
WHERE tvl_sats > 0
|
|
{order_sql}
|
|
LIMIT ?1 OFFSET ?2
|
|
"#,
|
|
cols = TOKEN_METRICS_COLUMNS,
|
|
order_sql = order_sql
|
|
);
|
|
|
|
let mut stmt = cauldron_conn.prepare(&sql)?;
|
|
let mut rows = stmt.query(rusqlite::params![limit as i64, offset as i64])?;
|
|
let mut tokens = Vec::with_capacity(limit);
|
|
|
|
while let Some(row) = rows.next()? {
|
|
let token_id: String = row.get::<_, String>("token_id")?.to_lowercase();
|
|
let trade_volume: u64 = row.get("trade_volume")?;
|
|
let tvl_sats: u64 = row.get::<_, i64>("tvl_sats")? as u64;
|
|
let tvl_tokens: u64 = row.get::<_, i64>("tvl_tokens")? as u64;
|
|
let score: i64 = row.get("score")?;
|
|
let price_now: f64 = row.get("price_now")?;
|
|
let price_24h: Option<f64> = row.get("price_24h")?;
|
|
let price_7d: Option<f64> = row.get("price_7d")?;
|
|
let change_24h_bp: Option<i64> = row.get("change_24h_bp")?;
|
|
let change_7d_bp: Option<i64> = row.get("change_7d_bp")?;
|
|
let display_name: Option<String> = row.get("display_name")?;
|
|
let display_symbol: Option<String> = row.get("display_symbol")?;
|
|
let price_now_usd: f64 = row.get("price_now_usd")?;
|
|
let price_24h_usd: Option<f64> = row.get("price_24h_usd")?;
|
|
let price_7d_usd: Option<f64> = row.get("price_7d_usd")?;
|
|
let change_24h_usd_bp: Option<i64> = row.get("change_24h_usd_bp")?;
|
|
let change_7d_usd_bp: Option<i64> = row.get("change_7d_usd_bp")?;
|
|
let apy_30d_bp: Option<i64> = row.get("apy_30d_bp")?;
|
|
let score_rank: i64 = row.get("score_rank")?;
|
|
|
|
let bcmr = get_token_bcmr(bcmr_conn, &token_id)?;
|
|
let bcmr_well_known = get_well_known_bcmr(bcmr_conn, &token_id)?;
|
|
|
|
tokens.push(TokenListItemCached {
|
|
token_id,
|
|
trade_volume,
|
|
tvl_sats,
|
|
tvl_tokens,
|
|
bcmr,
|
|
bcmr_well_known,
|
|
trade_count: 0,
|
|
score,
|
|
score_rank,
|
|
price_now,
|
|
price_24h,
|
|
price_7d,
|
|
change_24h_bp,
|
|
change_7d_bp,
|
|
display_name,
|
|
display_symbol,
|
|
price_now_usd,
|
|
price_24h_usd,
|
|
price_7d_usd,
|
|
change_24h_usd_bp,
|
|
change_7d_usd_bp,
|
|
apy_30d_bp,
|
|
});
|
|
}
|
|
|
|
Ok(tokens)
|
|
}
|
|
|
|
pub fn db_list_tokens_cached_by_ids(
|
|
cauldron_conn: &Connection,
|
|
bcmr_conn: &Connection,
|
|
token_ids: &[String],
|
|
sort: CachedSort,
|
|
) -> Result<Vec<TokenListItemCached>> {
|
|
if token_ids.is_empty() {
|
|
return Ok(vec![]);
|
|
}
|
|
|
|
let order_sql = order_clause(sort);
|
|
let placeholders = vec!["?"; token_ids.len()].join(",");
|
|
|
|
let sql = format!(
|
|
r#"
|
|
SELECT
|
|
{cols}
|
|
FROM cached_token_metrics
|
|
WHERE token_id IN ({ph}) AND tvl_sats > 0
|
|
{order_sql}
|
|
"#,
|
|
cols = TOKEN_METRICS_COLUMNS,
|
|
ph = placeholders,
|
|
order_sql = order_sql
|
|
);
|
|
let token_blobs: Vec<Vec<u8>> = token_ids
|
|
.iter()
|
|
.filter_map(|s| hex::decode(s).ok())
|
|
.collect();
|
|
|
|
let mut stmt = cauldron_conn.prepare(&sql)?;
|
|
let mut rows = stmt.query(params_from_iter(token_blobs.iter()))?;
|
|
|
|
let mut tokens = Vec::with_capacity(token_ids.len());
|
|
while let Some(row) = rows.next()? {
|
|
let token_id: String = row.get::<_, String>("token_id")?.to_lowercase();
|
|
let trade_volume: u64 = row.get("trade_volume")?;
|
|
let tvl_sats: u64 = row.get::<_, i64>("tvl_sats")? as u64;
|
|
let tvl_tokens: u64 = row.get::<_, i64>("tvl_tokens")? as u64;
|
|
let score: i64 = row.get("score")?;
|
|
|
|
let price_now: f64 = row.get("price_now")?;
|
|
let price_24h: Option<f64> = row.get("price_24h")?;
|
|
let price_7d: Option<f64> = row.get("price_7d")?;
|
|
let change_24h_bp: Option<i64> = row.get("change_24h_bp")?;
|
|
let change_7d_bp: Option<i64> = row.get("change_7d_bp")?;
|
|
|
|
let display_name: Option<String> = row.get("display_name")?;
|
|
let display_symbol: Option<String> = row.get("display_symbol")?;
|
|
|
|
let price_now_usd: f64 = row.get("price_now_usd")?;
|
|
let price_24h_usd: Option<f64> = row.get("price_24h_usd")?;
|
|
let price_7d_usd: Option<f64> = row.get("price_7d_usd")?;
|
|
let change_24h_usd_bp: Option<i64> = row.get("change_24h_usd_bp")?;
|
|
let change_7d_usd_bp: Option<i64> = row.get("change_7d_usd_bp")?;
|
|
|
|
let apy_30d_bp: Option<i64> = row.get("apy_30d_bp")?;
|
|
|
|
let score_rank: i64 = row.get("score_rank")?;
|
|
|
|
let bcmr = get_token_bcmr(bcmr_conn, &token_id)?;
|
|
let bcmr_well_known = get_well_known_bcmr(bcmr_conn, &token_id)?;
|
|
|
|
tokens.push(TokenListItemCached {
|
|
token_id,
|
|
trade_volume,
|
|
tvl_sats,
|
|
tvl_tokens,
|
|
bcmr,
|
|
bcmr_well_known,
|
|
trade_count: 0,
|
|
score,
|
|
score_rank,
|
|
price_now,
|
|
price_24h,
|
|
price_7d,
|
|
change_24h_bp,
|
|
change_7d_bp,
|
|
display_name,
|
|
display_symbol,
|
|
price_now_usd,
|
|
price_24h_usd,
|
|
price_7d_usd,
|
|
change_24h_usd_bp,
|
|
change_7d_usd_bp,
|
|
apy_30d_bp,
|
|
});
|
|
}
|
|
Ok(tokens)
|
|
}
|