Merge branch '500-fixes' into 'master'

Fix incorrect 5XX responses for user input errors

See merge request riftenlabs/riftenlabs-indexer!48
This commit is contained in:
Dagur Valberg Johannsson 2026-01-21 08:06:13 +00:00
commit 42aa74fc41
13 changed files with 424 additions and 324 deletions

View file

@ -4,11 +4,12 @@
// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/agpl-3.0.html
use poolperiod::PoolPeriod;
use rocket::{get, http::Status, response::status::Custom, serde::json::Json, State};
use rocket::{get, serde::json::Json, State};
use serde_json::Value;
use crate::{
db::{cauldron::pool::get_pool_period_snapshot, DB},
rpc::err::{bad_request, db_error, ApiErrorCode, ApiResult},
timeutil::time_now,
};
use serde_json::json;
@ -59,40 +60,37 @@ pub fn aggregate_apy(
start: Option<i64>, // default 30 days before end
end: Option<i64>, // default now
db: &State<DB>,
) -> Result<Json<Value>, Custom<String>> {
) -> ApiResult<Value> {
let end = end.unwrap_or(time_now());
let start = start.unwrap_or(end - (3600 * 24 * 30)); // 30 days
if end < start {
return Err(Custom(
Status::BadRequest,
"end time cannot be less than start time".to_string(),
return Err(bad_request(
ApiErrorCode::InvalidTimeRange,
"end time cannot be less than start time",
));
}
if start < 0 {
return Err(Custom(
Status::BadRequest,
"start time cannot be negative".to_string(),
return Err(bad_request(
ApiErrorCode::InvalidTimeRange,
"start time cannot be negative",
));
}
let conn = db
.cauldron_r
.get()
.map_err(|e| Custom(Status::InternalServerError, format!("DB error: {e}")))?;
let conn = db.cauldron_r.get().map_err(db_error)?;
let pools: anyhow::Result<Vec<PoolPeriod>> =
get_pool_period_snapshot(&conn, token, pkh, start, end)
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {e}")))?
.map_err(db_error)?
.into_iter()
.map(|(start, end)| PoolPeriod::new(start, end))
.collect();
let pools = pools.map_err(|e| Custom(Status::InternalServerError, format!("Error: {e}")))?;
let pools = pools.map_err(db_error)?;
let pools_count = pools.len();
let apy = apyaggregator::APYAggregator::aggregate_apy(pools.into_iter(), Some(start as u64))
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {e}")))?;
.map_err(db_error)?;
Ok(Json(json!({
"apy": apy.to_string(),

View file

@ -6,18 +6,21 @@
use anyhow::Context;
use bitcoin_hashes::hex::{FromHex, ToHex};
use bitcoincash::TokenID;
use rocket::{get, http::Status, response::status::Custom, serde::json::Json, State};
use rocket::{get, serde::json::Json, State};
use serde_json::json;
use serde_json::Value;
use crate::db::bcmr::get_well_known_bcmr;
use crate::db::{bcmr::get_token_bcmr, DB};
use crate::rpc::err::{bad_request, db_error, ApiErrorCode, ApiResult};
/// Fetches BCMR data for token from on-chain registry.
/// Status: Stable
///
/// - category: Token ID or symbol
///
/// Returns `null` if no BCMR data is found for the token.
///
/// **Response Example:**
///
/// ```json
@ -41,53 +44,52 @@ use crate::db::{bcmr::get_token_bcmr, DB};
/// }
/// ```
#[get("/token/<category>")]
pub fn token_bcmr(category: Option<&str>, db: &State<DB>) -> Result<Json<Value>, Custom<String>> {
pub fn token_bcmr(category: Option<&str>, db: &State<DB>) -> ApiResult<Value> {
let token_id_hex = category
.context("category missing")
.map_err(|e| Custom(Status::BadRequest, format!("Error: {e}")))?;
.map_err(|e| bad_request(ApiErrorCode::MissingCategory, &format!("Error: {e}")))?;
// validate input by parsing it into TokenID
let token_id = TokenID::from_hex(token_id_hex)
.map_err(|e| Custom(Status::BadRequest, format!("Error: {e}")))?;
let conn = db
.bcmr_r
.get()
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {e}")))?;
let bcmr = get_token_bcmr(&conn, &token_id.to_hex())
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {e}")))?;
let token_id = TokenID::from_hex(token_id_hex).map_err(|e| {
bad_request(
ApiErrorCode::InvalidTokenId,
&format!("Invalid token ID: {e}"),
)
})?;
let conn = db.bcmr_r.get().map_err(db_error)?;
let bcmr = get_token_bcmr(&conn, &token_id.to_hex()).map_err(db_error)?;
// Return null (with 200 OK) if no BCMR found - this is not an error condition
Ok(Json(json!(bcmr)))
}
/// Fetches BCMR data for token from all registries (including OTR).
///
/// Return format is same as `/token/<category>` route; except it returns an array of BCMR entries.
/// Returns an empty array if no BCMR data is found.
#[get("/token/<category>/all")]
pub fn token_bcmr_all(
category: Option<&str>,
db: &State<DB>,
) -> Result<Json<Value>, Custom<String>> {
pub fn token_bcmr_all(category: Option<&str>, db: &State<DB>) -> ApiResult<Value> {
let token_id_hex = category
.context("category missing")
.map_err(|e| Custom(Status::BadRequest, format!("Error: {e}")))?;
.map_err(|e| bad_request(ApiErrorCode::MissingCategory, &format!("Error: {e}")))?;
// validate input by parsing it into TokenID
let token_id = TokenID::from_hex(token_id_hex)
.map_err(|e| Custom(Status::BadRequest, format!("Error: {e}")))?;
let conn = db
.bcmr_r
.get()
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {e}")))?;
let token_id = TokenID::from_hex(token_id_hex).map_err(|e| {
bad_request(
ApiErrorCode::InvalidTokenId,
&format!("Invalid token ID: {e}"),
)
})?;
let conn = db.bcmr_r.get().map_err(db_error)?;
let onchain_bcmr = get_token_bcmr(&conn, &token_id.to_hex())
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {e}")))?;
let onchain_bcmr = get_token_bcmr(&conn, &token_id.to_hex()).map_err(db_error)?;
let mut bcmr_entries = get_well_known_bcmr(&conn, &token_id.to_hex())
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {e}")))?;
let mut bcmr_entries = get_well_known_bcmr(&conn, &token_id.to_hex()).map_err(db_error)?;
if let Some(bcmr) = onchain_bcmr {
bcmr_entries.push(bcmr)
}
// Returns empty array if no BCMR found - this is not an error condition
Ok(Json(json!(bcmr_entries)))
}

View file

@ -4,9 +4,10 @@
// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/agpl-3.0.html
use crate::db::DB;
use crate::rpc::err::{bad_request, db_error, ApiErrorCode, ApiResult};
use crate::timeutil::time_now;
use anyhow::{bail, Result};
use rocket::{get, http::Status, response::status::Custom, serde::json::Json, State};
use rocket::{get, serde::json::Json, State};
use rusqlite::{params, Connection};
use serde::Serialize;
use serde_json::json;
@ -277,15 +278,15 @@ pub fn price_candlesticks(
end: Option<i64>,
stepsize: Option<i64>,
conn: &State<DB>,
) -> Result<Json<Value>, Custom<String>> {
) -> ApiResult<Value> {
let current_timestamp = time_now();
// Validate that the provided end timestamp is not in the future.
if let Some(end_ts) = end {
if end_ts > current_timestamp {
return Err(Custom(
Status::BadRequest,
"End timestamp cannot be in the future".to_string(),
return Err(bad_request(
ApiErrorCode::FutureTimestamp,
"End timestamp cannot be in the future",
));
}
}
@ -296,17 +297,17 @@ pub fn price_candlesticks(
let effective_stepsize = stepsize.unwrap_or(3600);
if effective_stepsize <= 0 {
return Err(Custom(
Status::BadRequest,
"stepsize must be > 0".to_string(),
return Err(bad_request(
ApiErrorCode::InvalidStepsize,
"stepsize must be > 0",
));
}
// Validate that start is before end.
if effective_start >= effective_end {
return Err(Custom(
Status::BadRequest,
"Start timestamp must be before end timestamp".to_string(),
return Err(bad_request(
ApiErrorCode::InvalidTimeRange,
"Start timestamp must be before end timestamp",
));
}
@ -314,16 +315,13 @@ pub fn price_candlesticks(
const MAX_INTERVALS: i64 = 10000;
let total_intervals = (effective_end - effective_start) / effective_stepsize;
if total_intervals > MAX_INTERVALS {
return Err(Custom(
Status::BadRequest,
format!("Too many intervals ({total_intervals} > {MAX_INTERVALS})"),
return Err(bad_request(
ApiErrorCode::TooManyIntervals,
&format!("Too many intervals ({total_intervals} > {MAX_INTERVALS})"),
));
}
let db = conn
.cauldron_r
.get()
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {e}")))?;
let db = conn.cauldron_r.get().map_err(db_error)?;
let candlesticks = candlesticks(
&db,
@ -332,7 +330,7 @@ pub fn price_candlesticks(
effective_stepsize,
token,
)
.map_err(|e| Custom(Status::BadRequest, format!("Error: {e}")))?;
.map_err(|e| bad_request(ApiErrorCode::InvalidParameters, &e.to_string()))?;
let candlesticks_json: Vec<Value> = candlesticks
.iter()
@ -544,7 +542,12 @@ mod tests {
assert_eq!(response.status(), Status::BadRequest);
let body = response.into_string().unwrap_or_default();
assert!(body.contains("End timestamp cannot be in the future"));
let json: serde_json::Value = serde_json::from_str(&body).expect("Response should be JSON");
assert_eq!(json["error"]["code"], "FUTURE_TIMESTAMP");
assert!(json["error"]["message"]
.as_str()
.unwrap()
.contains("future"));
}
#[test]
@ -568,7 +571,12 @@ mod tests {
assert_eq!(response.status(), Status::BadRequest);
let body = response.into_string().unwrap_or_default();
assert!(body.contains("Start timestamp must be before end timestamp"));
let json: serde_json::Value = serde_json::from_str(&body).expect("Response should be JSON");
assert_eq!(json["error"]["code"], "INVALID_TIME_RANGE");
assert!(json["error"]["message"]
.as_str()
.unwrap()
.contains("before end"));
}
#[test]

View file

@ -5,12 +5,16 @@
use anyhow::Result;
use rayon::prelude::*;
use rocket::{get, http::Status, response::status::Custom, serde::json::Json, State};
use rocket::{get, serde::json::Json, State};
use rusqlite::Connection;
use serde::Serialize;
use serde_json::{json, Value};
use crate::{db::DB, timeutil::time_now};
use crate::{
db::DB,
rpc::err::{bad_request, db_error, ApiErrorCode, ApiResult},
timeutil::time_now,
};
#[derive(Serialize)]
pub struct ContractCount {
@ -78,13 +82,9 @@ fn db_contract_count_by_token(db: &Connection, token_id: &str) -> Result<Contrac
/// {"active":100,"ended":10}
/// ```
#[get("/contract/count")]
pub fn contract_count_all(conn: &State<DB>) -> Result<Json<Value>, Custom<String>> {
let db = conn
.cauldron_r
.get()
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {e}")))?;
let count = db_contract_count_all(&db)
.map_err(|e| Custom(Status::BadRequest, format!("Error: {e}")))?;
pub fn contract_count_all(conn: &State<DB>) -> ApiResult<Value> {
let db = conn.cauldron_r.get().map_err(db_error)?;
let count = db_contract_count_all(&db).map_err(db_error)?;
Ok(Json(json!(count)))
}
@ -98,13 +98,9 @@ pub fn contract_count_all(conn: &State<DB>) -> Result<Json<Value>, Custom<String
/// {"active":100,"ended":10}
/// ```
#[get("/contract/count/<token>")]
pub fn contract_count_token(token: &str, conn: &State<DB>) -> Result<Json<Value>, Custom<String>> {
let db = conn
.cauldron_r
.get()
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {e}")))?;
let count = db_contract_count_by_token(&db, token)
.map_err(|e| Custom(Status::BadRequest, format!("Error: {e}")))?;
pub fn contract_count_token(token: &str, conn: &State<DB>) -> ApiResult<Value> {
let db = conn.cauldron_r.get().map_err(db_error)?;
let count = db_contract_count_by_token(&db, token).map_err(db_error)?;
Ok(Json(json!(count)))
}
@ -112,21 +108,15 @@ pub fn contract_count_token(token: &str, conn: &State<DB>) -> Result<Json<Value>
/// Status: Deprecated
/// (Needs to be split into interval rather than producing 3 fixed ones)
#[get("/contract/volume?<end>")]
pub fn contract_volume(
end: Option<i64>,
conn: &State<DB>,
) -> Result<Json<Vec<Value>>, Custom<String>> {
pub fn contract_volume(end: Option<i64>, conn: &State<DB>) -> ApiResult<Vec<Value>> {
let end_timestamp = match end {
Some(e) => e,
None => time_now(),
};
let db = conn
.cauldron_r
.get()
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {e}")))?;
let db = conn.cauldron_r.get().map_err(db_error)?;
let volume = super::contract_volume(&db, end_timestamp as u64)
.map_err(|e| Custom(Status::BadRequest, format!("Error: {e}")))?;
.map_err(|e| bad_request(ApiErrorCode::InvalidParameters, &e.to_string()))?;
let result: Vec<Value> = volume
.into_par_iter()

View file

@ -3,8 +3,113 @@
// 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 rocket::{http::Status, response::status::Custom};
use rocket::{http::Status, response::status::Custom, serde::json::Json};
use serde_json::{json, Value};
use std::fmt;
/// Standard API result type with JSON-formatted errors
pub type ApiResult<T> = Result<Json<T>, Custom<Json<Value>>>;
/// API error codes for consistent error responses
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ApiErrorCode {
// 400 Bad Request codes
InvalidTimestamp,
InvalidTokenId,
InvalidPoolId,
InvalidTxid,
InvalidParameters,
InvalidTimeRange,
InvalidStepsize,
MissingParameters,
MissingCategory,
TooManyIntervals,
FutureTimestamp,
// 404 Not Found codes
PriceNotFound,
PoolNotFound,
// 500 Internal Server Error codes
InternalError,
// 503 Service Unavailable codes
CacheWarming,
}
impl fmt::Display for ApiErrorCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
// 400 codes
Self::InvalidTimestamp => "INVALID_TIMESTAMP",
Self::InvalidTokenId => "INVALID_TOKEN_ID",
Self::InvalidPoolId => "INVALID_POOL_ID",
Self::InvalidTxid => "INVALID_TXID",
Self::InvalidParameters => "INVALID_PARAMETERS",
Self::InvalidTimeRange => "INVALID_TIME_RANGE",
Self::InvalidStepsize => "INVALID_STEPSIZE",
Self::MissingParameters => "MISSING_PARAMETERS",
Self::MissingCategory => "MISSING_CATEGORY",
Self::TooManyIntervals => "TOO_MANY_INTERVALS",
Self::FutureTimestamp => "FUTURE_TIMESTAMP",
// 404 codes
Self::PriceNotFound => "PRICE_NOT_FOUND",
Self::PoolNotFound => "POOL_NOT_FOUND",
// 500 codes
Self::InternalError => "INTERNAL_ERROR",
// 503 codes
Self::CacheWarming => "CACHE_WARMING",
};
write!(f, "{s}")
}
}
/// Create a 400 Bad Request error with JSON body
pub fn bad_request(code: ApiErrorCode, message: &str) -> Custom<Json<Value>> {
Custom(
Status::BadRequest,
Json(json!({
"error": { "code": code.to_string(), "message": message }
})),
)
}
/// Create a 404 Not Found error with JSON body
pub fn not_found(code: ApiErrorCode, message: &str) -> Custom<Json<Value>> {
Custom(
Status::NotFound,
Json(json!({
"error": { "code": code.to_string(), "message": message }
})),
)
}
/// Create a 500 Internal Server Error with JSON body
pub fn internal_error(message: &str) -> Custom<Json<Value>> {
Custom(
Status::InternalServerError,
Json(json!({
"error": { "code": ApiErrorCode::InternalError.to_string(), "message": message }
})),
)
}
/// Create a 500 Internal Server Error for database errors with JSON body
pub fn db_error<E: std::fmt::Display>(e: E) -> Custom<Json<Value>> {
internal_error(&format!("Database error: {e}"))
}
/// Create a 503 Service Unavailable error with JSON body
pub fn service_unavailable(code: ApiErrorCode, message: &str) -> Custom<Json<Value>> {
Custom(
Status::ServiceUnavailable,
Json(json!({
"error": { "code": code.to_string(), "message": message }
})),
)
}
// Legacy helper for backward compatibility during migration
pub fn to_internal_error<E: std::fmt::Display>(e: E) -> Custom<String> {
Custom(Status::InternalServerError, format!("Error: {e}"))
}

View file

@ -5,11 +5,10 @@
use crate::db::oracle::{get_closest, get_range, get_range_with_step};
use crate::db::DB;
use crate::rpc::err::{bad_request, db_error, ApiErrorCode, ApiResult};
use crate::timeutil::time_now;
use bitcoin_hashes::hex::FromHex;
use bitcoincash::TokenID;
use rocket::http::Status;
use rocket::response::status::Custom;
use rocket::serde::json::Json;
use rocket::{get, State};
use serde_json::{json, Value};
@ -20,25 +19,28 @@ use serde_json::{json, Value};
///
/// - token_id: The 32 byte token ID
/// - timestamp: Unix timestamp
///
/// Returns `null` if no oracle data is found.
#[get("/delphi/closest?<token_id>&<timestamp>")]
pub fn oracle_get_closest(
token_id: Option<String>,
timestamp: Option<i64>,
db: &State<DB>,
) -> Result<Json<serde_json::Value>, Custom<String>> {
) -> ApiResult<serde_json::Value> {
let current_timestamp = timestamp.unwrap_or_else(time_now);
let conn = db
.oracle_r
.get()
.map_err(|e| Custom(Status::InternalServerError, e.to_string()))?;
let conn = db.oracle_r.get().map_err(db_error)?;
let token_id = token_id
.map(|t| {
TokenID::from_hex(&t)
.map_err(|e| Custom(Status::BadRequest, format!("Invalid token ID: {e}")))
TokenID::from_hex(&t).map_err(|e| {
bad_request(
ApiErrorCode::InvalidTokenId,
&format!("Invalid token ID: {e}"),
)
})
})
.transpose()?;
let entry = get_closest(&conn, &token_id, current_timestamp)
.map_err(|e| Custom(Status::InternalServerError, e.to_string()))?;
let entry = get_closest(&conn, &token_id, current_timestamp).map_err(db_error)?;
// Return null with 200 OK if no data found - this is not an error condition
Ok(Json(entry.map_or(serde_json::Value::Null, |e| {
serde_json::to_value(e).unwrap()
})))
@ -51,21 +53,21 @@ pub fn oracle_get_range(
start: Option<i64>,
end: Option<i64>,
db: &State<DB>,
) -> Result<Json<Vec<serde_json::Value>>, Custom<String>> {
) -> ApiResult<Vec<serde_json::Value>> {
let end_timestamp = end.unwrap_or_else(time_now);
let start_timestamp = start.unwrap_or_else(|| end_timestamp - 86400); // 1 day in seconds
let conn = db
.oracle_r
.get()
.map_err(|e| Custom(Status::InternalServerError, e.to_string()))?;
let conn = db.oracle_r.get().map_err(db_error)?;
let token_id = token_id
.map(|t| {
TokenID::from_hex(&t)
.map_err(|e| Custom(Status::BadRequest, format!("Invalid token ID: {e}")))
TokenID::from_hex(&t).map_err(|e| {
bad_request(
ApiErrorCode::InvalidTokenId,
&format!("Invalid token ID: {e}"),
)
})
})
.transpose()?;
let entries = get_range(&conn, &token_id, start_timestamp, end_timestamp)
.map_err(|e| Custom(Status::InternalServerError, e.to_string()))?;
let entries = get_range(&conn, &token_id, start_timestamp, end_timestamp).map_err(db_error)?;
Ok(Json(
entries
.into_iter()
@ -85,27 +87,35 @@ pub fn oracle_get_history(
end: Option<i64>,
stepsize: Option<i64>,
db: &State<DB>,
) -> Result<Json<Value>, Custom<String>> {
) -> ApiResult<Value> {
let current_timestamp = time_now();
let conn = db
.oracle_r
.get()
.map_err(|e| Custom(Status::InternalServerError, format!("DB error: {e}")))?;
let conn = db.oracle_r.get().map_err(db_error)?;
let token_id = token
.parse::<TokenID>()
.map_err(|e| Custom(Status::BadRequest, format!("Invalid token ID: {e}")))?;
let token_id = token.parse::<TokenID>().map_err(|e| {
bad_request(
ApiErrorCode::InvalidTokenId,
&format!("Invalid token ID: {e}"),
)
})?;
let start_ts = start.unwrap_or(current_timestamp - 30 * 24 * 3600); // default: 30 days ago
let end_ts = end.unwrap_or(current_timestamp);
let entries = if let Some(step) = stepsize {
get_range_with_step(&conn, &Some(token_id), start_ts, end_ts, step)
.map_err(|e| Custom(Status::BadRequest, format!("Query error: {e}")))?
get_range_with_step(&conn, &Some(token_id), start_ts, end_ts, step).map_err(|e| {
bad_request(
ApiErrorCode::InvalidParameters,
&format!("Query error: {e}"),
)
})?
} else {
get_range(&conn, &Some(token_id), start_ts, end_ts)
.map_err(|e| Custom(Status::BadRequest, format!("Query error: {e}")))?
get_range(&conn, &Some(token_id), start_ts, end_ts).map_err(|e| {
bad_request(
ApiErrorCode::InvalidParameters,
&format!("Query error: {e}"),
)
})?
};
let json_entries: Vec<Value> = entries

View file

@ -15,13 +15,14 @@ use crate::{
DB,
},
def::PoolID,
rpc::err::{bad_request, db_error, not_found, ApiErrorCode, ApiResult},
timeutil::time_now,
};
use anyhow::Result;
use bitcoin_hashes::hex::{FromHex, ToHex};
use bitcoincash::Txid;
use riftenlabs_defi::chainutil::compute_outpoint_hash;
use rocket::{get, http::Status, response::status::Custom, serde::json::Json, State};
use rocket::{get, serde::json::Json, State};
use rusqlite::{params, Connection};
use serde_json::{json, Value};
@ -151,14 +152,10 @@ fn pools_by_apy(connection: &Connection) -> Result<Vec<PoolYield>> {
/// Status: Deprecated
#[get("/pool/list_by_apy")]
pub fn list_pools_by_apy(conn: &State<DB>) -> Result<Json<Value>, Custom<String>> {
let db = conn
.cauldron_r
.get()
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {e}")))?;
pub fn list_pools_by_apy(conn: &State<DB>) -> ApiResult<Value> {
let db = conn.cauldron_r.get().map_err(db_error)?;
let pools: Vec<PoolYield> =
pools_by_apy(&db).map_err(|e| Custom(Status::BadRequest, format!("Error: {e}")))?;
let pools: Vec<PoolYield> = pools_by_apy(&db).map_err(db_error)?;
Ok(Json(json!({
"pools": json!(pools)
@ -242,14 +239,14 @@ pub fn list_active_pools(
token: Option<&str>,
pkh: Option<&str>,
conn: &State<DB>,
) -> Result<Json<Value>, Custom<String>> {
let db = conn
.cauldron_r
.get()
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {e}")))?;
) -> ApiResult<Value> {
let db = conn.cauldron_r.get().map_err(db_error)?;
if token.is_none() && pkh.is_none() {
return Err(Custom(Status::BadRequest, "Provide token or pkh".into()));
return Err(bad_request(
ApiErrorCode::MissingParameters,
"Provide token or pkh",
));
}
let mut active_pool_list = ActivePoolList::default();
@ -265,7 +262,7 @@ pub fn list_active_pools(
timestamp_gte: None,
},
)
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {e}")))?;
.map_err(db_error)?;
Ok(Json(json!({
"active": active_pool_list.active,
@ -273,26 +270,28 @@ pub fn list_active_pools(
}
#[get("/pool/history/<pool_id>?<start>")]
pub fn pool_history(
pool_id: &str,
start: Option<u64>,
conn: &State<DB>,
) -> Result<Json<Value>, Custom<String>> {
let db = conn
.cauldron_r
.get()
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {e}")))?;
pub fn pool_history(pool_id: &str, start: Option<u64>, conn: &State<DB>) -> ApiResult<Value> {
let db = conn.cauldron_r.get().map_err(db_error)?;
let start = start.unwrap_or(time_now() as u64 - (30 * 3600 * 24) /* 30 days ago */);
let pool_id = PoolID::from_hex(pool_id)
.map_err(|e| Custom(Status::BadRequest, format!("Invalid pool ID: {e}")))?;
let pool_id = PoolID::from_hex(pool_id).map_err(|e| {
bad_request(
ApiErrorCode::InvalidPoolId,
&format!("Invalid pool ID: {e}"),
)
})?;
let (token_id, owner_pkh) = db_pool_get_details(&db, &pool_id)
.map_err(|e| Custom(Status::BadRequest, format!("Error: {e}")))?;
let (token_id, owner_pkh) = db_pool_get_details(&db, &pool_id).map_err(|e| {
let err_msg = e.to_string();
if err_msg.contains("not found") || err_msg.contains("no rows") {
not_found(ApiErrorCode::PoolNotFound, &format!("Pool not found: {e}"))
} else {
db_error(e)
}
})?;
let history = db_pool_history(&db, &pool_id, start)
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {e}")))?;
let history = db_pool_history(&db, &pool_id, start).map_err(db_error)?;
Ok(Json(json!({
"history": history,
@ -313,34 +312,26 @@ pub fn pool_history(
/// }
/// ```
#[get("/pool/id_from_utxo?<txid>&<n>")]
pub fn pool_id_from_utxo(
txid: &str,
n: u32,
conn: &State<DB>,
) -> Result<Json<Value>, Custom<String>> {
let db = conn
.cauldron_r
.get()
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {e}")))?;
pub fn pool_id_from_utxo(txid: &str, n: u32, conn: &State<DB>) -> ApiResult<Value> {
let db = conn.cauldron_r.get().map_err(db_error)?;
// Parse txid from hex
let txid = Txid::from_hex(txid)
.map_err(|e| Custom(Status::BadRequest, format!("Invalid txid: {e}")))?;
.map_err(|e| bad_request(ApiErrorCode::InvalidTxid, &format!("Invalid txid: {e}")))?;
// Compute outpoint hash from txid and input position
let utxo_hash = compute_outpoint_hash(&txid, n);
// Query pool_history_entry table
let pool_id = db_pool_id_from_utxo(&db, &utxo_hash)
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {e}")))?;
let pool_id = db_pool_id_from_utxo(&db, &utxo_hash).map_err(db_error)?;
match pool_id {
Some(pool_id) => Ok(Json(json!({
"pool_id": pool_id
}))),
None => Err(Custom(
Status::NotFound,
format!(
None => Err(not_found(
ApiErrorCode::PoolNotFound,
&format!(
"No pool found for UTXO: txid={}, input_pos={}",
txid.to_hex(),
n

View file

@ -6,7 +6,7 @@
use anyhow::{bail, Context, Result};
use bitcoin_hashes::hex::{FromHex, ToHex};
use bitcoincash::TokenID;
use rocket::{get, http::Status, response::status::Custom, serde::json::Json, State};
use rocket::{get, serde::json::Json, State};
use rusqlite::{params, Connection};
use rust_decimal::prelude::*;
use serde_json::{json, Value};
@ -18,7 +18,10 @@ use crate::{
},
DB,
},
rpc::tvl::get_token_tvl,
rpc::{
err::{bad_request, db_error, not_found, ApiErrorCode, ApiResult},
tvl::get_token_tvl,
},
timeutil::time_now,
};
@ -87,10 +90,27 @@ impl PriceInterval {
}
}
/// Error returned when no price data is available for a token
#[derive(Debug)]
pub struct NoPriceData;
impl std::fmt::Display for NoPriceData {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "No price data available")
}
}
impl std::error::Error for NoPriceData {}
// Get the current price of a given token
fn current_price(db: &Connection, token_id: &str) -> Result<f64> {
let (sats, tokens) = get_token_tvl(db, None, token_id)?;
// No TVL means no price data - return specific error
if tokens == 0 {
return Err(NoPriceData.into());
}
let sum_sats = Decimal::from_u64(sats).context("overflow")?;
let sum_tokens = Decimal::from_u64(tokens).context("overflow")?;
@ -268,42 +288,43 @@ pub fn price_at_or_before_2(
/// }
/// ```
#[get("/price/<token>/at/<timestamp>")]
pub fn price_at(
token: &str,
timestamp: &str,
conn: &State<DB>,
) -> Result<Json<Value>, Custom<String>> {
pub fn price_at(token: &str, timestamp: &str, conn: &State<DB>) -> ApiResult<Value> {
let timestamp: i64 = timestamp.parse().map_err(|_| {
Custom(
Status::BadRequest,
"Invalid timestamp format. Must be a valid number.".to_string(),
bad_request(
ApiErrorCode::InvalidTimestamp,
"Invalid timestamp format. Must be a valid number.",
)
})?;
let db = conn
.cauldron_r
.get()
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {e}")))?;
let db = conn.cauldron_r.get().map_err(db_error)?;
let current_time = time_now();
if timestamp > current_time {
return Err(Custom(
Status::BadRequest,
"Timestamp is in the future".to_string(),
return Err(bad_request(
ApiErrorCode::FutureTimestamp,
"Timestamp is in the future",
));
}
let token =
TokenID::from_hex(token).map_err(|e| Custom(Status::BadRequest, format!("Error: {e}")))?;
let token = TokenID::from_hex(token).map_err(|e| {
bad_request(
ApiErrorCode::InvalidTokenId,
&format!("Invalid token ID: {e}"),
)
})?;
match price_at_or_before_2(&db, timestamp, &token) {
Ok((latest_timestamp, price)) => Ok(Json(json!({
"timestamp": latest_timestamp,
"price": price
}))),
Err(e) => Err(Custom(
Status::InternalServerError,
format!("Error fetching price: {e}"),
)),
Err(e) => {
let err_msg = e.to_string();
if err_msg.contains("No price data found") {
Err(not_found(ApiErrorCode::PriceNotFound, &err_msg))
} else {
Err(db_error(e))
}
}
}
}
@ -320,14 +341,19 @@ pub fn price_at(
/// ```
///
#[get("/price/<token>/current")]
pub fn price_current(token: &str, conn: &State<DB>) -> Result<Json<Value>, Custom<String>> {
let db = conn
.cauldron_r
.get()
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {e}")))?;
pub fn price_current(token: &str, conn: &State<DB>) -> ApiResult<Value> {
let db = conn.cauldron_r.get().map_err(db_error)?;
let price = current_price(&db, token)
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {e}")))?;
let price = current_price(&db, token).map_err(|e| {
if e.downcast_ref::<NoPriceData>().is_some() {
not_found(
ApiErrorCode::PriceNotFound,
"No price data available for this token",
)
} else {
db_error(e)
}
})?;
Ok(Json(json!({
"price": price,
@ -376,13 +402,10 @@ pub fn price_history(
end: Option<i64>,
stepsize: Option<i64>,
conn: &State<DB>,
) -> Result<Json<Value>, Custom<String>> {
) -> ApiResult<Value> {
let current_timestamp = time_now();
let db = conn
.cauldron_r
.get()
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {e}")))?;
let db = conn.cauldron_r.get().map_err(db_error)?;
let history = historic_price(
&db,
@ -391,7 +414,7 @@ pub fn price_history(
stepsize.unwrap_or(3600 /* 1 hour */),
token,
)
.map_err(|e| Custom(Status::BadRequest, format!("Error: {e}")))?;
.map_err(|e| bad_request(ApiErrorCode::InvalidParameters, &e.to_string()))?;
let history_json: Vec<Value> = history
.iter()
@ -768,6 +791,11 @@ mod tests {
.get(format!("/cauldron/price/{bad_token_id}/at/{timestamp}"))
.dispatch();
assert_eq!(response.status(), Status::BadRequest);
// Verify JSON error format
let body = response.into_string().unwrap();
let json: serde_json::Value = serde_json::from_str(&body).expect("Response should be JSON");
assert!(json["error"]["code"].as_str().is_some());
}
#[test]
@ -789,7 +817,12 @@ mod tests {
assert_eq!(response.status(), Status::BadRequest);
let body = response.into_string().unwrap();
assert!(body.contains("Timestamp is in the future"));
let json: serde_json::Value = serde_json::from_str(&body).expect("Response should be JSON");
assert_eq!(json["error"]["code"], "FUTURE_TIMESTAMP");
assert!(json["error"]["message"]
.as_str()
.unwrap()
.contains("future"));
}
#[test]
@ -812,7 +845,12 @@ mod tests {
assert_eq!(response.status(), Status::BadRequest);
let body = response.into_string().unwrap();
assert!(body.contains("Invalid timestamp format"));
let json: serde_json::Value = serde_json::from_str(&body).expect("Response should be JSON");
assert_eq!(json["error"]["code"], "INVALID_TIMESTAMP");
assert!(json["error"]["message"]
.as_str()
.unwrap()
.contains("Invalid timestamp format"));
}
#[test]
fn test_price_with_high_tokens_and_low_sats() {
@ -851,6 +889,10 @@ mod tests {
.dispatch();
// Expect that we could not handle the calculation/conversion.
assert_eq!(response.status(), Status::InternalServerError);
// This returns 404 NOT_FOUND since no valid price can be computed
assert_eq!(response.status(), Status::NotFound);
let body = response.into_string().unwrap();
let json: serde_json::Value = serde_json::from_str(&body).expect("Response should be JSON");
assert!(json["error"]["code"].as_str().is_some());
}
}

View file

@ -6,8 +6,9 @@
use bitcoin_hashes::hex::{FromHex, ToHex};
use bitcoincash::TokenID;
use log::{info, warn};
use rocket::http::Status;
use rocket::{get, response::status::Custom, serde::json::Json, State};
use rocket::response::status::Custom;
use rocket::serde::json::Json;
use rocket::{get, State};
use serde_json::json;
use serde_json::Value;
@ -25,7 +26,10 @@ use crate::rpc::ResponseCacheInner;
use crate::timeutil::time_now;
use rayon::prelude::*;
use super::err::to_internal_error;
use super::err::{
bad_request, db_error, not_found, service_unavailable, to_internal_error, ApiErrorCode,
ApiResult,
};
use super::ResponseCache;
macro_rules! function_name {
@ -40,10 +44,7 @@ macro_rules! function_name {
}
#[get("/tokens/list_by_volume")]
pub fn list_by_volume(
db: &State<DB>,
response_cache: &State<ResponseCache>,
) -> Result<Json<Value>, Custom<String>> {
pub fn list_by_volume(db: &State<DB>, response_cache: &State<ResponseCache>) -> ApiResult<Value> {
let thirty_days = 24 * 60 * 60 * 30;
let duration = thirty_days;
let limit = 250;
@ -115,23 +116,20 @@ pub fn list_by_volume(
match cached_value {
Some(r) => Ok(Json(r)),
None => Err(Custom(
Status::ServiceUnavailable,
"Busy, try again in 30 seconds".to_string(),
None => Err(service_unavailable(
ApiErrorCode::CacheWarming,
"Busy, try again in 30 seconds",
)),
}
}
#[get("/tokens/search_by_volume?<search_query>")]
pub fn search_by_volume(
search_query: &str,
db: &State<DB>,
) -> Result<Json<Vec<Value>>, Custom<String>> {
pub fn search_by_volume(search_query: &str, db: &State<DB>) -> ApiResult<Vec<Value>> {
let db_copy = db.inner().clone();
let run_query = move || {
#[allow(clippy::type_complexity)]
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)?;
let bcmr_db = db_copy.bcmr_r.get().map_err(db_error)?;
let crc20_db = db_copy.crc20_r.get().map_err(db_error)?;
let list: Vec<(String, Option<String>, Option<String>, u64)> =
db::search::search_tokens_by_volume(
@ -140,7 +138,7 @@ pub fn search_by_volume(
&crc20_db,
search_query,
)
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {e}")))?;
.map_err(db_error)?;
let result: Vec<Value> = list
.into_par_iter()
@ -166,9 +164,9 @@ pub fn search_cached(
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)?;
) -> ApiResult<Value> {
let cauldron_db = db.cauldron_r.get().map_err(db_error)?;
let bcmr_db = db.bcmr_r.get().map_err(db_error)?;
let sort = parse_cached_sort(by, order);
let limit = limit.unwrap_or(250);
@ -177,7 +175,7 @@ pub fn search_cached(
let items: Vec<TokenListItemCached> =
db_search_tokens_cached(&cauldron_db, &bcmr_db, &query, sort, limit, offset)
.map_err(to_internal_error)?;
.map_err(db_error)?;
Ok(Json(json!(items)))
}
@ -278,17 +276,16 @@ pub fn list_cached(
offset: Option<usize>,
by: Option<String>,
order: Option<String>,
) -> Result<Json<Value>, Custom<String>> {
) -> ApiResult<Value> {
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 cauldron_db = db.cauldron_r.get().map_err(db_error)?;
let bcmr_db = db.bcmr_r.get().map_err(db_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)?;
db_list_tokens_cached(&cauldron_db, &bcmr_db, limit, offset, sort).map_err(db_error)?;
Ok(Json(json!(items)))
}
@ -299,9 +296,9 @@ pub fn list_cached_by_ids(
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)?;
) -> ApiResult<Value> {
let cauldron_db = db.cauldron_r.get().map_err(db_error)?;
let bcmr_db = db.bcmr_r.get().map_err(db_error)?;
// split, trim, and normalize (lowercase is typical for hex IDs in DB)
let token_ids: Vec<String> = ids
@ -315,30 +312,28 @@ pub fn list_cached_by_ids(
}
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)?;
let items =
db_list_tokens_cached_by_ids(&cauldron_db, &bcmr_db, &token_ids, sort).map_err(db_error)?;
Ok(Json(json!(items)))
}
#[get("/token/<token>/first_pool")]
pub fn first_pool_creation(token: &str, dbp: &State<DB>) -> Result<Json<Value>, Custom<String>> {
let token = TokenID::from_hex(token)
.map_err(|e| Custom(Status::BadRequest, format!("Invalid token id: {e}")))?;
pub fn first_pool_creation(token: &str, dbp: &State<DB>) -> ApiResult<Value> {
let token = TokenID::from_hex(token).map_err(|e| {
bad_request(
ApiErrorCode::InvalidTokenId,
&format!("Invalid token id: {e}"),
)
})?;
let token_hex = token.to_hex();
let conn = dbp
.cauldron_r
.get()
.map_err(|e| Custom(Status::InternalServerError, format!("DB error: {e}")))?;
let conn = dbp.cauldron_r.get().map_err(db_error)?;
match db_first_pool_creation_row(&conn, &token_hex) {
Ok(Some((creation_utxo, txid, timestamp, block_height))) => {
// Opportunistically cache the timestamp forever
let cw = dbp
.cauldron_w
.get()
.map_err(|e| Custom(Status::InternalServerError, format!("DB error: {e}")))?;
let cw = dbp.cauldron_w.get().map_err(db_error)?;
if let Err(e) = cache_first_pool_ts_if_empty(&cw, &token_hex, timestamp) {
// Non-fatal: return the data
log::warn!("Failed to cache first_pool_ts for {token_hex}: {e}");
@ -352,7 +347,7 @@ pub fn first_pool_creation(token: &str, dbp: &State<DB>) -> Result<Json<Value>,
"block_height": block_height
})))
}
Ok(None) => Err(Custom(Status::NotFound, "No pools for token".into())),
Err(e) => Err(Custom(Status::InternalServerError, format!("Error: {e}"))),
Ok(None) => Err(not_found(ApiErrorCode::PoolNotFound, "No pools for token")),
Err(e) => Err(db_error(e)),
}
}

View file

@ -7,7 +7,7 @@ use std::collections::HashMap;
use anyhow::Result;
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use rocket::{get, http::Status, response::status::Custom, serde::json::Json, State};
use rocket::{get, serde::json::Json, State};
use rusqlite::Connection;
use serde_json::{json, Value};
@ -17,6 +17,7 @@ use crate::db::{
},
DB,
};
use crate::rpc::err::{db_error, ApiResult};
#[derive(Default)]
pub struct TvlByTokenVisitor {
@ -149,14 +150,11 @@ pub fn get_token_tvl(
/// use valuelocked with optional parameters
/// used by defilama; fix adapter before removing
#[get("/tvl/<time>")]
pub fn deprecated_tvl(time: usize, conn: &State<DB>) -> Result<Json<Vec<Value>>, Custom<String>> {
let db = conn
.cauldron_r
.get()
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {e}")))?;
pub fn deprecated_tvl(time: usize, conn: &State<DB>) -> ApiResult<Vec<Value>> {
let db = conn.cauldron_r.get().map_err(db_error)?;
let tvl: HashMap<String, (u64, u64)> = deprecated_get_all_token_tvl(&db, time)
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {e}")))?;
let tvl: HashMap<String, (u64, u64)> =
deprecated_get_all_token_tvl(&db, time).map_err(db_error)?;
let result: Vec<Value> = tvl
.into_par_iter()
@ -185,19 +183,10 @@ pub fn deprecated_tvl(time: usize, conn: &State<DB>) -> Result<Json<Vec<Value>>,
/// }
/// ```
#[get("/valuelocked?<time>")]
pub fn valuelocked_all(
time: Option<usize>,
conn: &State<DB>,
) -> Result<Json<Value>, Custom<String>> {
let db = conn.cauldron_r.get().map_err(|_| {
Custom(
Status::InternalServerError,
"Failed to get DB connection".into(),
)
})?;
pub fn valuelocked_all(time: Option<usize>, conn: &State<DB>) -> ApiResult<Value> {
let db = conn.cauldron_r.get().map_err(db_error)?;
let sats: u64 = get_total_sats_tvl(&db, time)
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {e}")))?;
let sats: u64 = get_total_sats_tvl(&db, time).map_err(db_error)?;
Ok(Json(json!({
"satoshis": sats
@ -220,20 +209,10 @@ pub fn valuelocked_all(
/// }
/// ```
#[get("/valuelocked/<token>?<time>")]
pub fn valuelocked_token(
token: &str,
time: Option<usize>,
conn: &State<DB>,
) -> Result<Json<Value>, Custom<String>> {
let db = conn.cauldron_r.get().map_err(|_| {
Custom(
Status::InternalServerError,
"Failed to get DB connection".into(),
)
})?;
pub fn valuelocked_token(token: &str, time: Option<usize>, conn: &State<DB>) -> ApiResult<Value> {
let db = conn.cauldron_r.get().map_err(db_error)?;
let (sats, token_amount) = get_token_tvl(&db, time, token)
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {e}")))?;
let (sats, token_amount) = get_token_tvl(&db, time, token).map_err(db_error)?;
Ok(Json(json!({
"token_amount": token_amount,

View file

@ -5,11 +5,12 @@
use bitcoin_hashes::hex::FromHex;
use bitcoincash::TokenID;
use rocket::{get, http::Status, response::status::Custom, serde::json::Json, State};
use rocket::{get, serde::json::Json, State};
use serde_json::json;
use serde_json::Value;
use crate::db::DB;
use crate::rpc::err::{bad_request, db_error, ApiErrorCode, ApiResult};
#[get("/tx/latest?<limit>&<offset>&<token>")]
pub fn tx_latest(
@ -17,13 +18,8 @@ pub fn tx_latest(
offset: Option<usize>,
token: Option<&str>,
conn: &State<DB>,
) -> Result<Json<Value>, Custom<String>> {
let db = conn.cauldron_r.get().map_err(|_| {
Custom(
Status::InternalServerError,
"Failed to get DB connection".into(),
)
})?;
) -> ApiResult<Value> {
let db = conn.cauldron_r.get().map_err(db_error)?;
let limit = limit.unwrap_or(100).min(10000);
let offset = offset.unwrap_or(0);
@ -32,12 +28,16 @@ pub fn tx_latest(
None => None,
Some(tokenhex) => match TokenID::from_hex(tokenhex) {
Ok(token) => Some(token),
Err(_) => return Err(Custom(Status::BadRequest, "Invalid token ID".into())),
Err(_) => {
return Err(bad_request(
ApiErrorCode::InvalidTokenId,
"Invalid token ID",
))
}
},
};
let txs = crate::db::cauldron::tx::latest(&db, limit, offset, token)
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {e}")))?;
let txs = crate::db::cauldron::tx::latest(&db, limit, offset, token).map_err(db_error)?;
let txs_json: Vec<Value> = txs
.into_iter()

View file

@ -4,23 +4,18 @@
// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/agpl-3.0.html
use rocket::get;
use rocket::{http::Status, response::status::Custom, serde::json::Json, State};
use rocket::{serde::json::Json, State};
use serde_json::json;
use serde_json::Value;
use crate::db::{cauldron::user::get_unique_per_month_accumilating, DB};
use crate::rpc::err::{db_error, ApiResult};
#[get("/user/unique_addresses")]
pub fn unique_addresses(conn: &State<DB>) -> Result<Json<Value>, Custom<String>> {
let db = conn.cauldron_r.get().map_err(|_| {
Custom(
Status::InternalServerError,
"Failed to get DB connection".into(),
)
})?;
pub fn unique_addresses(conn: &State<DB>) -> ApiResult<Value> {
let db = conn.cauldron_r.get().map_err(db_error)?;
let users = get_unique_per_month_accumilating(&db)
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {e}")))?;
let users = get_unique_per_month_accumilating(&db).map_err(db_error)?;
Ok(Json(json!(users)))
}

View file

@ -5,10 +5,9 @@
use crate::db::cauldron::pool::{get_token_volume_sats, get_total_volume_sats};
use crate::db::DB;
use crate::rpc::err::{db_error, ApiResult};
use crate::timeutil::time_now;
use rocket::get;
use rocket::http::Status;
use rocket::response::status::Custom;
use rocket::serde::json::{json, Json, Value};
use rocket::State;
@ -28,17 +27,8 @@ use rocket::State;
/// }
/// ```
#[get("/volume?<start>&<end>")]
pub fn volume_all(
start: Option<usize>,
end: Option<usize>,
conn: &State<DB>,
) -> Result<Json<Value>, Custom<String>> {
let db = conn.cauldron_r.get().map_err(|_| {
Custom(
Status::InternalServerError,
"Failed to get DB connection".into(),
)
})?;
pub fn volume_all(start: Option<usize>, end: Option<usize>, conn: &State<DB>) -> ApiResult<Value> {
let db = conn.cauldron_r.get().map_err(db_error)?;
let end_timestamp = end.unwrap_or_else(|| time_now() as usize);
@ -47,7 +37,7 @@ pub fn volume_all(
});
let total_volume = get_total_volume_sats(&db, start_timestamp as u64, end_timestamp as u64)
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {e}")))?;
.map_err(db_error)?;
Ok(Json(json!({
"total_volume_sats": total_volume,
@ -80,13 +70,8 @@ pub fn volume_token(
start: Option<usize>,
end: Option<usize>,
conn: &State<DB>,
) -> Result<Json<Value>, Custom<String>> {
let db = conn.cauldron_r.get().map_err(|_| {
Custom(
Status::InternalServerError,
"Failed to get DB connection".into(),
)
})?;
) -> ApiResult<Value> {
let db = conn.cauldron_r.get().map_err(db_error)?;
let end_timestamp = end.unwrap_or_else(|| time_now() as usize);
@ -96,7 +81,7 @@ pub fn volume_token(
let (sats_volume, token_volume) =
get_token_volume_sats(&db, start_timestamp as u64, end_timestamp as u64, token)
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {e}")))?;
.map_err(db_error)?;
Ok(Json(json!({
"volume_sats": sats_volume,