// 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::oracle_cash::{ get_oracle_cash_closest, get_oracle_cash_range, get_oracle_cash_range_with_step, }; 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 of the oracle contract. Known oracle contract IDs: /// - BCH/USD: `d0d46f5cbd82188acede0d3e49c75700c19cb8331a30101f0bb6a260066ac972` /// - timestamp: Unix timestamp (optional, defaults to now) /// /// Returns `null` if no oracle data is found. /// /// **Important:** `oracle_price` is returned in **cents** (not dollars). Divide by 100 /// to get the price in USD: /// ``` /// price_usd = oracle_price / 100 /// ``` /// /// **Response Example:** /// ```json /// { /// "oracle_timestamp": 1709468902, /// "oracle_price": 64320, /// "oracle_sequence": 12345, /// "token_id": "d0d46f5cbd82188acede0d3e49c75700c19cb8331a30101f0bb6a260066ac972", /// "txid": "...", /// "blockhash": "..." /// } /// ``` /// In this example, `oracle_price` of 64320 cents = $643.20 USD. #[get("/delphi/closest?&")] pub async fn oracle_get_closest( token_id: Option, timestamp: Option, db: &State, ) -> CachedApiResult { let current_timestamp = timestamp.unwrap_or_else(time_now); 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(&db.oracle_r, &token_id, current_timestamp) .await .map_err(db_error)?; 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 async 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); 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(&db.oracle_r, &token_id, start_timestamp, end_timestamp) .await .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 of the oracle contract. Known oracle contract IDs: /// - BCH/USD: `d0d46f5cbd82188acede0d3e49c75700c19cb8331a30101f0bb6a260066ac972` /// - start: Unix timestamp for start of period /// - end: Unix timestamp for end of period (optional, defaults to now) /// - stepsize: Seconds per interval (optional) /// /// **Important:** `oracle_price` values are in **cents**. Divide by 100 to convert to USD. /// /// **Response Example:** /// ```json /// [ /// { "time": 1709468902, "price": 64320, "txid": "...", "blockhash": "...", "sequence": 12345 }, /// { "time": 1709555302, "price": 65100, "txid": "...", "blockhash": "...", "sequence": 12346 } /// ] /// ``` /// In this example, `price` values of 64320 cents = $643.20 USD. #[get("/delphi//history?&&")] pub async fn oracle_get_history( token: &str, start: Option, end: Option, stepsize: Option, db: &State, ) -> CachedApiResult { let current_timestamp = time_now(); 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); let end_ts = end.unwrap_or(current_timestamp); let entries = if let Some(step) = stepsize { get_range_with_step(&db.oracle_r, &Some(token_id), start_ts, end_ts, step) .await .map_err(|e| { bad_request( ApiErrorCode::InvalidParameters, &format!("Query error: {e}"), ) })? } else { get_range(&db.oracle_r, &Some(token_id), start_ts, end_ts) .await .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, )) } /// Get the closest BCH/USD price from the oracles.cash feed for a given timestamp. /// /// Status: Stable /// /// - timestamp: Unix timestamp in seconds (optional, defaults to now) /// /// Returns `null` if no data is available. /// /// `oracle_price` is in **cents**. Divide by 100 to get USD. #[get("/cash/closest?")] pub async fn oracle_cash_closest( timestamp: Option, db: &State, ) -> CachedApiResult { let ts = timestamp.unwrap_or_else(time_now); let entry = get_oracle_cash_closest(&db.oracle_r, ts) .await .map_err(db_error)?; Ok(cached_ok( entry.map_or(serde_json::Value::Null, |e| { serde_json::to_value(e).unwrap() }), CACHE_AGGREGATE, )) } /// Get historical BCH/USD prices from the oracles.cash feed. /// /// Status: Stable /// /// - start: Unix timestamp for start of period /// - end: Unix timestamp for end of period (optional, defaults to now) /// - stepsize: Seconds per interval (optional) /// /// `oracle_price` values are in **cents**. /// /// **Response Example:** /// ```json /// [ /// { "oracle_timestamp": 1709468902, "oracle_price": 38424, "message_sequence": 12345 }, /// { "oracle_timestamp": 1709472502, "oracle_price": 38501, "message_sequence": null } /// ] /// ``` #[get("/cash/history?&&")] pub async fn oracle_cash_history( start: Option, end: Option, stepsize: Option, db: &State, ) -> CachedApiResult { let current_timestamp = time_now(); let start_ts = start.unwrap_or(current_timestamp - 30 * 24 * 3600); let end_ts = end.unwrap_or(current_timestamp); let entries = if let Some(step) = stepsize { get_oracle_cash_range_with_step(&db.oracle_r, start_ts, end_ts, step) .await .map_err(|e| { bad_request( ApiErrorCode::InvalidParameters, &format!("Query error: {e}"), ) })? } else { get_oracle_cash_range(&db.oracle_r, start_ts, end_ts) .await .map_err(|e| { bad_request( ApiErrorCode::InvalidParameters, &format!("Query error: {e}"), ) })? }; Ok(cached_ok( serde_json::to_value(entries).unwrap(), CACHE_AGGREGATE, )) }