riftenlabs-indexer/src/rpc/oracle.rs

180 lines
5.7 KiB
Rust
Raw Normal View History

2026-01-21 12:34:59 +01:00
// 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
2025-06-17 14:04:26 +00:00
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};
2025-06-17 14:04:26 +00:00
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?<token_id>&<timestamp>")]
pub async fn oracle_get_closest(
token_id: Option<String>,
timestamp: Option<i64>,
db: &State<DB>,
) -> CachedApiResult<serde_json::Value> {
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?<token_id>&<start>&<end>")]
pub async fn oracle_get_range(
token_id: Option<String>,
start: Option<i64>,
end: Option<i64>,
db: &State<DB>,
) -> CachedApiResult<Vec<serde_json::Value>> {
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,
))
}
2025-06-17 14:04:26 +00:00
/// 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.
2025-06-17 14:04:26 +00:00
#[get("/delphi/<token>/history?<start>&<end>&<stepsize>")]
pub async fn oracle_get_history(
2025-06-17 14:04:26 +00:00
token: &str,
start: Option<i64>,
end: Option<i64>,
stepsize: Option<i64>,
db: &State<DB>,
) -> CachedApiResult<Value> {
2025-06-17 14:04:26 +00:00
let current_timestamp = time_now();
let token_id = token.parse::<TokenID>().map_err(|e| {
bad_request(
ApiErrorCode::InvalidTokenId,
&format!("Invalid token ID: {e}"),
)
})?;
2025-06-17 14:04:26 +00:00
let start_ts = start.unwrap_or(current_timestamp - 30 * 24 * 3600);
2025-06-17 14:04:26 +00:00
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}"),
)
})?
2025-06-17 14:04:26 +00:00
} 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}"),
)
})?
2025-06-17 14:04:26 +00:00
};
let json_entries: Vec<Value> = 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,
))
2025-06-17 14:04:26 +00:00
}