riftenlabs-indexer/src/rpc/tokens.rs

319 lines
9.5 KiB
Rust
Raw Normal View History

// 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 log::{info, warn};
use rocket::http::Status;
use rocket::{get, response::status::Custom, serde::json::Json, State};
use serde_json::json;
use serde_json::Value;
2025-09-05 13:53:10 +00:00
use crate::db::cauldron::tokenlist::db_utils::CachedSort;
use crate::db::cauldron::tokenlist::list_cached::{
db_list_tokens_cached, db_list_tokens_cached_by_ids, TokenListItemCached,
};
use crate::db::cauldron::tokenlist::list_tokens_volume::{db_list_tokens_by_volume, TokenListItem};
use crate::db::search::db_search_tokens_cached;
use crate::db::DB;
2025-02-14 18:02:48 +01:00
use crate::db::{self};
2025-02-17 20:49:52 +01:00
use crate::rpc::ResponseCacheInner;
use crate::timeutil::time_now;
use rayon::prelude::*;
2025-02-14 18:02:48 +01:00
use super::err::to_internal_error;
use super::ResponseCache;
macro_rules! function_name {
() => {{
fn f() {}
fn type_name_of<T>(_: T) -> &'static str {
std::any::type_name::<T>()
}
let name = type_name_of(f);
&name[..name.len() - 3] // trim trailing "::f" from the name
}};
}
2024-11-28 10:27:35 +01:00
#[get("/tokens/list_by_volume")]
pub fn list_by_volume(
db: &State<DB>,
response_cache: &State<ResponseCache>,
2024-11-28 10:27:35 +01:00
) -> Result<Json<Value>, Custom<String>> {
let thirty_days = 24 * 60 * 60 * 30;
2024-11-28 10:27:35 +01:00
let duration = thirty_days;
let limit = 250;
let cache_key = function_name!().to_owned();
let db_copy = db.inner().clone();
2025-02-17 20:49:52 +01:00
let run_query = move || -> Result<Vec<TokenListItem>, Custom<String>> {
#[allow(clippy::type_complexity)]
2025-02-14 18:02:48 +01:00
let cauldron_db = db_copy.cauldron_r.get().map_err(to_internal_error)?;
let bcmr_db = db_copy.bcmr_r.get().map_err(to_internal_error)?;
2025-02-14 18:02:48 +01:00
let list: Vec<TokenListItem> =
db_list_tokens_by_volume(&cauldron_db, &bcmr_db, duration, limit)
.map_err(to_internal_error)?;
2024-11-28 10:27:35 +01:00
Ok(list)
};
2025-02-17 20:49:52 +01:00
let (needs_update, cached_value) = {
let mut cache = response_cache.lock().unwrap();
2025-02-17 20:49:52 +01:00
match cache.get(&cache_key) {
Some(c) => {
let needs_update = !c.in_progress && time_now() > c.update_timestamp - 300;
(needs_update, c.value.clone())
}
None => {
cache.insert(
cache_key.clone(),
ResponseCacheInner {
update_timestamp: time_now(),
value: None,
in_progress: true,
},
);
(true, None)
}
}
};
2025-02-17 20:49:52 +01:00
if needs_update {
2025-07-16 09:31:14 +02:00
info!("Update for {cache_key} triggered.");
2025-02-17 20:49:52 +01:00
if let Some(entry) = response_cache.lock().unwrap().get_mut(&cache_key) {
entry.in_progress = true;
};
let cache_copy = response_cache.inner().clone();
2025-02-17 20:49:52 +01:00
std::thread::spawn(move || {
let new_entry = match run_query() {
Ok(r) => ResponseCacheInner {
value: Some(json!(r)),
in_progress: false,
update_timestamp: time_now(),
},
Err(e) => {
2025-07-16 09:31:14 +02:00
warn!("Failed to update cache for {cache_key}: {e:?}");
2025-02-17 20:49:52 +01:00
ResponseCacheInner {
value: None,
in_progress: false,
update_timestamp: time_now(),
2024-11-28 10:27:35 +01:00
}
2025-02-17 20:49:52 +01:00
}
};
cache_copy
.lock()
.unwrap()
.insert(cache_key.clone(), new_entry);
2025-07-16 09:31:14 +02:00
info!("Updated cached value for {cache_key}");
2025-02-17 20:49:52 +01:00
});
}
2025-02-17 20:49:52 +01:00
match cached_value {
Some(r) => Ok(Json(r)),
None => Err(Custom(
Status::ServiceUnavailable,
"Busy, try again in 30 seconds".to_string(),
)),
}
}
2024-10-31 10:55:29 +00:00
#[get("/tokens/search_by_volume?<search_query>")]
pub fn search_by_volume(
search_query: &str,
db: &State<DB>,
) -> Result<Json<Vec<Value>>, Custom<String>> {
let db_copy = db.inner().clone();
let run_query = move || {
#[allow(clippy::type_complexity)]
2025-02-14 18:02:48 +01:00
let bcmr_db = db_copy.bcmr_r.get().map_err(to_internal_error)?;
let crc20_db = db_copy.crc20_r.get().map_err(to_internal_error)?;
2024-10-31 10:55:29 +00:00
let list: Vec<(String, Option<String>, Option<String>, u64)> =
db::search::search_tokens_by_volume(
&db_copy.cauldron_r,
2025-02-14 18:02:48 +01:00
&bcmr_db,
&crc20_db,
2024-10-31 10:55:29 +00:00
search_query,
)
2025-07-16 09:31:14 +02:00
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {e}")))?;
2024-10-31 10:55:29 +00:00
let result: Vec<Value> = list
.into_par_iter()
.map(|(token_id, name, ticker, trade_volume)| {
json!({
"token_id": token_id,
"name": name,
"ticker": ticker,
"trade_volume": trade_volume
})
})
.collect();
Ok(Json(result))
};
run_query()
}
2025-09-05 13:53:10 +00:00
#[get("/tokens/search_cached?<q>&<limit>&<offset>&<by>&<order>")]
pub fn search_cached(
db: &State<DB>,
q: Option<String>,
limit: Option<usize>,
offset: Option<usize>,
by: Option<String>,
order: Option<String>,
) -> Result<Json<Value>, Custom<String>> {
let cauldron_db = db.cauldron_r.get().map_err(to_internal_error)?;
let bcmr_db = db.bcmr_r.get().map_err(to_internal_error)?;
let sort = parse_cached_sort(by, order);
let limit = limit.unwrap_or(250);
let offset = offset.unwrap_or(0);
let query = q.unwrap_or_default();
let items: Vec<TokenListItemCached> =
db_search_tokens_cached(&cauldron_db, &bcmr_db, &query, sort, limit, offset)
.map_err(to_internal_error)?;
Ok(Json(json!(items)))
}
fn parse_cached_sort(by: Option<String>, order: Option<String>) -> CachedSort {
let desc = matches!(order.as_deref(), Some("desc") | Some("DESC"));
match by.as_deref() {
Some("name") => {
if desc {
CachedSort::NameDesc
} else {
CachedSort::NameAsc
}
}
Some("symbol") => {
if desc {
CachedSort::SymbolDesc
} else {
CachedSort::SymbolAsc
}
}
Some("tvl") => {
if desc {
CachedSort::TvlDesc
} else {
CachedSort::TvlAsc
}
}
Some("volume") => {
if desc {
CachedSort::VolumeDesc
} else {
CachedSort::VolumeAsc
}
}
Some("change_24h_bp") => {
if desc {
CachedSort::Change24hDesc
} else {
CachedSort::Change24hAsc
}
}
Some("change_7d_bp") => {
if desc {
CachedSort::Change7dDesc
} else {
CachedSort::Change7dAsc
}
}
// USD sorts
Some("price_usd") => {
if desc {
CachedSort::PriceUsdDesc
} else {
CachedSort::PriceUsdAsc
}
}
Some("change_24h_usd_bp") => {
if desc {
CachedSort::Change24hUsdDesc
} else {
CachedSort::Change24hUsdAsc
}
}
Some("change_7d_usd_bp") => {
if desc {
CachedSort::Change7dUsdDesc
} else {
CachedSort::Change7dUsdAsc
}
}
// APY (30d) sorts — accept a few aliases
Some("apy") | Some("apy_30d") | Some("apy_30d_bp") => {
if desc {
CachedSort::Apy30dDesc
} else {
CachedSort::Apy30dAsc
}
}
// default
_ => {
if desc {
CachedSort::ScoreDesc
} else {
CachedSort::ScoreAsc
}
}
}
}
#[get("/tokens/list_cached?<limit>&<offset>&<by>&<order>")]
pub fn list_cached(
db: &State<DB>,
limit: Option<usize>,
offset: Option<usize>,
by: Option<String>,
order: Option<String>,
) -> Result<Json<Value>, Custom<String>> {
let limit = limit.unwrap_or(250);
let offset = offset.unwrap_or(0);
let cauldron_db = db.cauldron_r.get().map_err(to_internal_error)?;
let bcmr_db = db.bcmr_r.get().map_err(to_internal_error)?;
let sort = parse_cached_sort(by, order);
let items: Vec<TokenListItemCached> =
db_list_tokens_cached(&cauldron_db, &bcmr_db, limit, offset, sort)
.map_err(to_internal_error)?;
Ok(Json(json!(items)))
}
#[get("/tokens/list_cached_by_ids?<ids>&<by>&<order>")]
pub fn list_cached_by_ids(
db: &State<DB>,
ids: &str,
by: Option<String>,
order: Option<String>,
) -> Result<Json<Value>, Custom<String>> {
let cauldron_db = db.cauldron_r.get().map_err(to_internal_error)?;
let bcmr_db = db.bcmr_r.get().map_err(to_internal_error)?;
// split, trim, and normalize (lowercase is typical for hex IDs in DB)
let token_ids: Vec<String> = ids
.split(',')
.map(|s| s.trim().to_lowercase())
.filter(|s| !s.is_empty())
.collect();
if token_ids.is_empty() {
return Ok(Json(json!([])));
}
let sort = parse_cached_sort(by, order);
let items = db_list_tokens_cached_by_ids(&cauldron_db, &bcmr_db, &token_ids, sort)
.map_err(to_internal_error)?;
Ok(Json(json!(items)))
}