riftenlabs-indexer/src/db/search/mod.rs

334 lines
11 KiB
Rust
Raw Normal View History

2026-01-21 12:34:59 +01:00
// Copyright (C) 2024-2026 Whiterun LLC
2024-10-31 10:55:29 +00:00
//
// 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;
2024-10-31 10:55:29 +00:00
use bitcoin_hashes::hex::FromHex;
use bitcoincash::TokenID;
use sqlx::{Row, SqlitePool};
2024-10-31 10:55:29 +00:00
type TokenBasicInfo = (String, Option<String>, Option<String>);
type TokenVolumeInfo = (String, Option<String>, Option<String>, u64);
use crate::bcmr::parsedbcmr::ParsedBCMR;
2026-02-01 13:23:57 +01:00
use crate::db::blob::{blob_to_display_hex, display_hex_to_blob};
2025-09-05 13:53:10 +00:00
use crate::db::cauldron::tokenlist::db_utils::order_clause;
use crate::db::cauldron::tokenlist::db_utils::CachedSort;
use crate::db::cauldron::tokenlist::list_cached::TokenListItemCached;
async fn search_bcmr(bcmr_pool: &SqlitePool, search_query: &str) -> Result<Vec<TokenBasicInfo>> {
2024-10-31 10:55:29 +00:00
let is_full_hex_token_id = TokenID::from_hex(search_query).is_ok();
2024-10-31 10:55:29 +00:00
let sql = if is_full_hex_token_id {
2026-02-01 13:23:57 +01:00
"SELECT token_id, name, symbol
2024-10-31 10:55:29 +00:00
FROM (
SELECT a.token_id, b.name, b.symbol, a.height,
ROW_NUMBER() OVER (PARTITION BY a.token_id ORDER BY a.height DESC) AS rn
FROM auth_chain_entry a
LEFT JOIN bcmr_data b ON a.utxo = b.utxo
) subquery
WHERE rn = 1
AND (token_id = ?1);"
} else {
2026-02-01 13:23:57 +01:00
"SELECT token_id, name, symbol
2024-10-31 10:55:29 +00:00
FROM (
SELECT a.token_id, b.name, b.symbol, a.height,
ROW_NUMBER() OVER (PARTITION BY a.token_id ORDER BY a.height DESC) AS rn
FROM auth_chain_entry a
LEFT JOIN bcmr_data b ON a.utxo = b.utxo
WHERE a.bcmr_data IS NOT NULL
) subquery
WHERE rn = 1
AND (name LIKE ?1 OR symbol LIKE ?1);"
};
let mut bcmr_data: Vec<TokenBasicInfo> = Vec::new();
if is_full_hex_token_id {
2026-02-01 13:23:57 +01:00
let token_blob = display_hex_to_blob::<TokenID>(search_query)?;
let rows = sqlx::query(sql)
.bind(token_blob)
.fetch_all(bcmr_pool)
.await?;
for row in rows {
let token_blob: Vec<u8> = row.get(0);
2026-02-01 13:23:57 +01:00
let token_id = blob_to_display_hex::<TokenID>(&token_blob)?;
let name: Option<String> = row.get(1);
let ticker: Option<String> = row.get(2);
2026-02-01 13:23:57 +01:00
bcmr_data.push((token_id, name, ticker));
}
} else {
let search_pattern = format!("%{search_query}%");
let rows = sqlx::query(sql)
.bind(search_pattern)
.fetch_all(bcmr_pool)
.await?;
for row in rows {
let token_blob: Vec<u8> = row.get(0);
2026-02-01 13:23:57 +01:00
let token_id = blob_to_display_hex::<TokenID>(&token_blob)?;
let name: Option<String> = row.get(1);
let ticker: Option<String> = row.get(2);
2026-02-01 13:23:57 +01:00
bcmr_data.push((token_id, name, ticker));
}
2024-10-31 10:55:29 +00:00
}
Ok(bcmr_data)
}
async fn search_crc20(crc20_pool: &SqlitePool, search_query: &str) -> Result<Vec<TokenBasicInfo>> {
2024-10-31 10:55:29 +00:00
let is_full_hex_token_id = TokenID::from_hex(search_query).is_ok();
let sql = if is_full_hex_token_id {
"SELECT token_id, name, symbol FROM crc20 WHERE token_id = ?1;"
2024-10-31 10:55:29 +00:00
} else {
"SELECT token_id, name, symbol FROM crc20 WHERE name LIKE ?1 OR symbol LIKE ?1;"
2024-10-31 10:55:29 +00:00
};
2024-10-31 10:55:29 +00:00
let mut crc20_data: Vec<TokenBasicInfo> = Vec::new();
if is_full_hex_token_id {
2026-02-01 13:23:57 +01:00
let token_blob = display_hex_to_blob::<TokenID>(search_query)?;
let rows = sqlx::query(sql)
.bind(token_blob)
.fetch_all(crc20_pool)
.await?;
for row in rows {
let token_blob: Vec<u8> = row.get(0);
2026-02-01 13:23:57 +01:00
let token_id = blob_to_display_hex::<TokenID>(&token_blob)?;
let name: Option<String> = row.get(1);
let ticker: Option<String> = row.get(2);
2026-02-01 13:23:57 +01:00
crc20_data.push((token_id, name, ticker));
}
} else {
let search_pattern = format!("%{search_query}%");
let rows = sqlx::query(sql)
.bind(search_pattern)
.fetch_all(crc20_pool)
.await?;
for row in rows {
let token_blob: Vec<u8> = row.get(0);
2026-02-01 13:23:57 +01:00
let token_id = blob_to_display_hex::<TokenID>(&token_blob)?;
let name: Option<String> = row.get(1);
let ticker: Option<String> = row.get(2);
2026-02-01 13:23:57 +01:00
crc20_data.push((token_id, name, ticker));
}
2024-10-31 10:55:29 +00:00
}
Ok(crc20_data)
}
async fn token_volume(
cauldron_pool: &SqlitePool,
2024-10-31 10:55:29 +00:00
tokens: Vec<TokenBasicInfo>,
) -> Result<Vec<TokenVolumeInfo>> {
let mut result = Vec::with_capacity(tokens.len());
for (token_id, name, ticker) in tokens {
let token_blob = display_hex_to_blob::<TokenID>(&token_id)?;
let row: (i64,) = sqlx::query_as(
"WITH TradeData AS (
SELECT
p.token_id,
ABS(phe.sats_delta) as trade_volume
FROM pool_history_entry phe
JOIN pool p ON phe.pool = p.creation_utxo
JOIN tx ON phe.txid = tx.txid
WHERE tx.effective_timestamp >= (strftime('%s', 'now') - 2592000)
AND p.token_id = ?
)
SELECT
COALESCE(SUM(trade_volume), 0) as total_trade_volume
FROM TradeData;",
)
.bind(&token_blob)
.fetch_one(cauldron_pool)
.await?;
result.push((token_id, name, ticker, row.0 as u64));
}
2024-10-31 10:55:29 +00:00
Ok(result)
}
pub async fn search_tokens_by_volume(
cauldron_pool: &SqlitePool,
bcmr_pool: &SqlitePool,
crc20_pool: &SqlitePool,
2024-10-31 10:55:29 +00:00
search_query: &str,
) -> Result<Vec<TokenVolumeInfo>> {
let mut combined_tokens = [
search_bcmr(bcmr_pool, search_query).await?,
search_crc20(crc20_pool, search_query).await?,
]
.concat();
2026-04-24 07:11:29 +02:00
combined_tokens.sort_by_key(|a| a.0.to_lowercase());
combined_tokens.dedup_by(|a, b| {
if a.0.eq_ignore_ascii_case(&b.0) {
if b.1.is_none() && a.1.is_some() {
b.1 = a.1.take();
b.2 = a.2.take();
}
true
} else {
false
}
});
let mut result = token_volume(cauldron_pool, combined_tokens).await?;
2024-10-31 10:55:29 +00:00
2026-04-24 07:11:29 +02:00
result.sort_by_key(|b| std::cmp::Reverse(b.3));
2024-10-31 10:55:29 +00:00
Ok(result)
}
2025-09-05 13:53:10 +00:00
/// Search the cached list by token_id (exact), or display_name/symbol (LIKE),
/// then sort by *whatever* using CachedSort.
///
/// - `query`:
/// - empty/whitespace => returns everything
/// - full 32-byte hex => exact token_id match
/// - otherwise => display_name LIKE or display_symbol LIKE
/// - `filter_zero_tvl`: skip rows with tvl_sats=0 (usually you want `true`)
pub async fn db_search_tokens_cached(
cauldron_pool: &SqlitePool,
2025-09-05 13:53:10 +00:00
q: &str,
sort: CachedSort,
limit: usize,
offset: usize,
) -> Result<Vec<TokenListItemCached>> {
let order_sql = order_clause(sort);
enum Filter {
None,
Id(Vec<u8>),
Pat(String),
2025-09-05 13:53:10 +00:00
}
let mut where_sql = String::new();
let filter = {
let q = q.trim();
if q.is_empty() {
Filter::None
} else if TokenID::from_hex(q).is_ok() {
where_sql.push_str("WHERE token_id = ?");
Filter::Id(display_hex_to_blob::<TokenID>(&q.to_lowercase())?)
2025-09-05 13:53:10 +00:00
} else {
where_sql.push_str(
"WHERE (display_name LIKE ? COLLATE NOCASE OR display_symbol LIKE ? COLLATE NOCASE)"
2025-09-05 13:53:10 +00:00
);
Filter::Pat(format!("%{}%", q))
}
};
if matches!(filter, Filter::None | Filter::Pat(_)) {
if where_sql.is_empty() {
where_sql.push_str("WHERE tvl_sats > 0");
} else {
where_sql.push_str(" AND tvl_sats > 0");
}
}
2025-09-05 13:53:10 +00:00
let sql = format!(
"SELECT
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, bcmr_json, bcmr_well_known_json
2025-09-05 13:53:10 +00:00
FROM cached_token_metrics
{where_sql}
{order_sql}
LIMIT ? OFFSET ?"
2025-09-05 13:53:10 +00:00
);
let rows = match &filter {
Filter::None => {
sqlx::query(&sql)
.bind(limit as i64)
.bind(offset as i64)
.fetch_all(cauldron_pool)
.await?
}
Filter::Id(blob) => {
sqlx::query(&sql)
.bind(blob)
.bind(limit as i64)
.bind(offset as i64)
.fetch_all(cauldron_pool)
.await?
}
Filter::Pat(pat) => {
sqlx::query(&sql)
.bind(pat)
.bind(pat)
.bind(limit as i64)
.bind(offset as i64)
.fetch_all(cauldron_pool)
.await?
}
};
2025-09-05 13:53:10 +00:00
let mut out = Vec::with_capacity(limit);
for row in rows {
let token_blob: Vec<u8> = row.get("token_id");
2026-02-01 13:23:57 +01:00
let token_id = blob_to_display_hex::<TokenID>(&token_blob)?;
let trade_volume: i64 = row.get("trade_volume");
let tvl_sats: i64 = row.get("tvl_sats");
let tvl_tokens: i64 = row.get("tvl_tokens");
let score: i64 = row.get("score");
let score_rank: i64 = row.get("score_rank");
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 bcmr_json_str: Option<String> = row.get("bcmr_json");
let bcmr_wk_json_str: Option<String> = row.get("bcmr_well_known_json");
let bcmr: Option<ParsedBCMR> = bcmr_json_str.and_then(|s| serde_json::from_str(&s).ok());
let bcmr_well_known: Vec<ParsedBCMR> = bcmr_wk_json_str
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default();
2025-09-05 13:53:10 +00:00
out.push(TokenListItemCached {
token_id,
trade_volume: trade_volume as u64,
tvl_sats: tvl_sats as u64,
tvl_tokens: tvl_tokens as u64,
2025-09-05 13:53:10 +00:00
bcmr,
bcmr_well_known,
trade_count: 0,
2025-09-11 07:36:06 +00:00
score,
2025-09-05 13:53:10 +00:00
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(out)
}
2024-10-31 10:55:29 +00:00
#[cfg(test)]
2025-09-11 07:36:06 +00:00
mod test;