// 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 crate::db::oracle::{get_closest, get_range, get_range_with_step}; use crate::db::DB; use crate::rpc::err::{bad_request, db_error, ApiErrorCode, CachedApiResult}; use crate::rpc::response::{cached_ok, CACHE_AGGREGATE}; use crate::timeutil::time_now; use bitcoin_hashes::hex::FromHex; use bitcoincash::TokenID; use rocket::{get, State}; use serde_json::{json, Value}; /// Get the closest oracle price for a given token and timestamp. /// /// Status: Stable /// /// - token_id: The 32 byte token ID /// - timestamp: Unix timestamp /// /// Returns `null` if no oracle data is found. #[get("/delphi/closest?&")] pub fn oracle_get_closest( token_id: Option, timestamp: Option, db: &State, ) -> CachedApiResult { let current_timestamp = timestamp.unwrap_or_else(time_now); let conn = db.oracle_r.get().map_err(db_error)?; let token_id = token_id .map(|t| { 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(db_error)?; // Return null with 200 OK if no data found - this is not an error condition Ok(cached_ok( entry.map_or(serde_json::Value::Null, |e| { serde_json::to_value(e).unwrap() }), CACHE_AGGREGATE, )) } /// Status: Deprecated #[get("/delphi/range?&&")] pub fn oracle_get_range( token_id: Option, start: Option, end: Option, db: &State, ) -> CachedApiResult> { 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(db_error)?; let token_id = token_id .map(|t| { 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(db_error)?; Ok(cached_ok( entries .into_iter() .map(|e| serde_json::to_value(e).unwrap()) .collect(), CACHE_AGGREGATE, )) } /// Get historical oracle prices for a given token. /// /// - token: The 32 byte token ID /// - start: Unix timestamp for start of period #[get("/delphi//history?&&")] pub fn oracle_get_history( token: &str, start: Option, end: Option, stepsize: Option, db: &State, ) -> CachedApiResult { let current_timestamp = time_now(); let conn = db.oracle_r.get().map_err(db_error)?; let token_id = token.parse::().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| { bad_request( ApiErrorCode::InvalidParameters, &format!("Query error: {e}"), ) })? } else { 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 = entries .into_iter() .map(|entry| { json!({ "time": entry.oracle_timestamp, "price": entry.oracle_price, "txid": entry.txid, "blockhash": entry.blockhash, "sequence": entry.oracle_sequence, }) }) .collect(); Ok(cached_ok( serde_json::Value::Array(json_entries), CACHE_AGGREGATE, )) }