diff --git a/compare_price.sh b/compare_price.sh new file mode 100755 index 0000000..c1b981d --- /dev/null +++ b/compare_price.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +# Compare price//at/ between a local instance and production. +# Usage: ./compare_price.sh [local_base] [prod_base] +# Defaults: localhost:8000 indexer.riften.net + +LOCAL="${1:-http://localhost:8000}" +PROD="${2:-https://indexer.riften.net}" + +TOKENS=( + "422134612b1914d7f7ff2cf9b793f5b5356a424ca195887a0458e55b38017d71" + "b38a33f750f84c5c169a6f23cb873e6e79605021585d4f3408789689ed87f366" +) + +# One timestamp per month from Jun 2024 through Mar 2026 +TIMESTAMPS=( + 1717200000 # 2024-06-01 + 1719878400 # 2024-07-02 + 1722470400 # 2024-08-01 + 1725148800 # 2024-09-01 + 1727740800 # 2024-10-01 + 1730419200 # 2024-11-01 + 1733011200 # 2024-12-01 + 1735689600 # 2025-01-01 + 1738368000 # 2025-02-01 + 1741046400 # 2025-03-04 + 1743724800 # 2025-04-04 + 1746316800 # 2025-05-04 + 1748736000 # 2025-06-01 + 1751328000 # 2025-07-01 + 1754006400 # 2025-08-01 + 1756684800 # 2025-09-01 + 1759276800 # 2025-10-01 + 1761955200 # 2025-11-01 + 1764547200 # 2025-12-01 + 1767225600 # 2026-01-01 + 1769904000 # 2026-02-01 + 1772323200 # 2026-03-01 +) + +query() { + local base="$1" token="$2" ts="$3" + curl -sf --max-time 8 "$base/cauldron/price/$token/at/$ts" 2>/dev/null \ + || echo '{"error":"no_response"}' +} + +price_of() { + echo "$1" | python3 -c " +import sys, json +d = json.load(sys.stdin) +if 'price' in d: + print(f\"{d['price']:.6f}\") +elif 'error' in d: + code = d['error'].get('code', d['error']) if isinstance(d['error'], dict) else d['error'] + print(f'ERR:{code}') +else: + print('ERR:unknown') +" +} + +print_header() { + local token="$1" + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "Token: ${token:0:16}…" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + printf "%-14s %-22s %-22s %s\n" "Date" "Local" "Prod" "Diff?" + printf "%-14s %-22s %-22s %s\n" "--------------" "----------------------" "----------------------" "------" +} + +DIVERGED=0 + +for token in "${TOKENS[@]}"; do + print_header "$token" + for ts in "${TIMESTAMPS[@]}"; do + date_label=$(python3 -c "import datetime; print(datetime.datetime.utcfromtimestamp($ts).strftime('%Y-%m-%d'))") + local_resp=$(query "$LOCAL" "$token" "$ts") + prod_resp=$(query "$PROD" "$token" "$ts") + local_price=$(price_of "$local_resp") + prod_price=$(price_of "$prod_resp") + + diff_marker="" + if [ "$local_price" != "$prod_price" ]; then + diff_marker="<-- DIFF" + DIVERGED=1 + fi + + printf "%-14s %-22s %-22s %s\n" "$date_label" "$local_price" "$prod_price" "$diff_marker" + done +done + +echo "" +if [ "$DIVERGED" -eq 1 ]; then + echo "DIVERGENCE FOUND — fix is exercised for at least one timestamp." + echo "Local returning price where prod returns ERR:PRICE_NOT_FOUND" + echo "means a withdrawn pool is correctly included in the historical query." +else + echo "No divergence found. Either:" + echo " - Neither token ever had a withdrawn pool, or" + echo " - Local is not running the fixed build, or" + echo " - Prod is already updated." +fi diff --git a/src/db/cauldron/tokenlist/metrics_cache.rs b/src/db/cauldron/tokenlist/metrics_cache.rs index 78f2c7a..0a3c072 100644 --- a/src/db/cauldron/tokenlist/metrics_cache.rs +++ b/src/db/cauldron/tokenlist/metrics_cache.rs @@ -51,7 +51,7 @@ pub async fn update_tvl_and_price_now( let sats_per_bch = Decimal::from_i64(SATS_PER_BCH).unwrap(); // USD per sat (now) - let usd_per_sat_now = usd_per_bch_at_or_before(oracle_pool, time_now()).await / sats_per_bch; + let usd_per_sat_now = usd_per_bch_at_or_before(oracle_pool, time_now()).await? / sats_per_bch; let mut batch: Vec<(&str, i64, i64, f64, f64)> = Vec::with_capacity(WRITE_CHUNK); for (token_id, (tvl_sats, tvl_tokens)) in tvl_by_token.iter() { @@ -349,9 +349,9 @@ pub async fn update_changes_score_volume_and_ranking(db: &DB) -> anyhow::Result< let sats_per_bch = Decimal::from_i64(SATS_PER_BCH).unwrap(); // oracle (USD/BCH) → USD per sat - let usd_per_sat_now = usd_per_bch_at_or_before(&db.oracle_r, now).await / sats_per_bch; - let usd_per_sat_24h = usd_per_bch_at_or_before(&db.oracle_r, ts_24h).await / sats_per_bch; - let usd_per_sat_7d = usd_per_bch_at_or_before(&db.oracle_r, ts_7d).await / sats_per_bch; + let usd_per_sat_now = usd_per_bch_at_or_before(&db.oracle_r, now).await? / sats_per_bch; + let usd_per_sat_24h = usd_per_bch_at_or_before(&db.oracle_r, ts_24h).await? / sats_per_bch; + let usd_per_sat_7d = usd_per_bch_at_or_before(&db.oracle_r, ts_7d).await? / sats_per_bch; // ========== READ PHASE: collect all token data ========== let token_rows = sqlx::query( diff --git a/src/db/cauldron/tokenlist/token_utils.rs b/src/db/cauldron/tokenlist/token_utils.rs index edb4119..66aaf70 100644 --- a/src/db/cauldron/tokenlist/token_utils.rs +++ b/src/db/cauldron/tokenlist/token_utils.rs @@ -11,8 +11,6 @@ use sqlx::SqlitePool; use crate::db::blob::display_hex_to_blob; use crate::db::cauldron::pool::{get_injections_between, get_pool_period_snapshot}; use crate::db::oracle::get_closest; -use crate::db::oracle::oracle_cash::get_oracle_cash_closest; -use crate::db::oracle::v2::v2_bchusd_token_id; use crate::rpc::apy::apyaggregator::APYAggregator; use crate::rpc::apy::poolperiod::split_at_injections; use malachite::base::num::arithmetic::traits::FloorSqrt; @@ -58,41 +56,16 @@ pub fn dec_to_f64_bounded(d: Decimal) -> f64 { pub const SATS_PER_BCH: i64 = 100_000_000; /// On-chain Delphi oracle scale: prices are stored in cents /// (e.g. $384.24 → 38424). Same unit as the oracles.cash feed. +/// Oracle prices are stored in cents (38424 = $384.24). Divide by 100 to get USD. pub const ORACLE_SCALE: i64 = 100; -/// Delphi v1 stopped publishing at this timestamp (2026-04-23 00:00:00 UTC). -const ORACLE_CUTOFF_TS: i64 = 1776902400; -/// Delphi v2 went live at this timestamp (2026-05-04 00:00:00 UTC). -const V2_DEPLOY_TS: i64 = 1777852800; -pub async fn usd_per_bch_at_or_before(oracle_pool: &SqlitePool, ts: i64) -> Decimal { - if ts < ORACLE_CUTOFF_TS { - // Delphi v1: on-chain historical data - match get_closest(oracle_pool, &None, ts).await { - Ok(Some(e)) => { - Decimal::from_i64(e.oracle_price).unwrap_or(Decimal::ZERO) - / Decimal::from_i64(ORACLE_SCALE).unwrap() - } - _ => Decimal::ZERO, - } - } else if ts < V2_DEPLOY_TS { - // oracles.cash: bridges the Apr 23 – May 4 gap - match get_oracle_cash_closest(oracle_pool, ts).await { - Ok(Some(e)) => { - Decimal::from_i64(e.oracle_price).unwrap_or(Decimal::ZERO) / Decimal::from(100) - } - _ => Decimal::ZERO, - } - } else { - // Delphi v2: on-chain data from May 4 2026 onward - let token_id = Some(v2_bchusd_token_id().clone()); - match get_closest(oracle_pool, &token_id, ts).await { - Ok(Some(e)) => { - Decimal::from_i64(e.oracle_price).unwrap_or(Decimal::ZERO) - / Decimal::from_i64(ORACLE_SCALE).unwrap() - } - _ => Decimal::ZERO, - } - } +pub async fn usd_per_bch_at_or_before(oracle_pool: &SqlitePool, ts: i64) -> anyhow::Result { + let entry = get_closest(oracle_pool, &None, ts) + .await? + .ok_or_else(|| anyhow::anyhow!("No oracle price found for timestamp {ts}"))?; + let price = Decimal::from_i64(entry.oracle_price) + .ok_or_else(|| anyhow::anyhow!("Oracle price out of Decimal range: {}", entry.oracle_price))?; + Ok(price / Decimal::from_i64(ORACLE_SCALE).unwrap()) } #[inline] diff --git a/src/db/init.rs b/src/db/init.rs index 6a0c798..0a6a47f 100644 --- a/src/db/init.rs +++ b/src/db/init.rs @@ -16,7 +16,6 @@ use crate::db::cauldron::config::check_db_version; use crate::db::cauldron::prepare_tables as cauldron_prepare_tables; use crate::db::crc20::prepare_tables as crc20_prepare_tables; use crate::db::moria::prepare_tables as moria_prepare_tables; -use crate::db::oracle::oracle_cash::prepare_oracle_cash_tables; use crate::db::oracle::prepare_tables as oracle_prepare_tables; /// Create read and write database pools for a given database path @@ -125,8 +124,6 @@ pub async fn initialize_databases(network: &str, read_slots: ReadSlots) -> Resul if !db_exists { oracle_prepare_tables(&oracle_db_write).await; } - // Always-run migration: safe on both new and existing oracle.db - prepare_oracle_cash_tables(&oracle_db_write).await; // Initialize moria lending database let (db_exists, moria_db_write, moria_db_read) = diff --git a/src/db/oracle/mod.rs b/src/db/oracle/mod.rs index c9e4931..537a2e0 100644 --- a/src/db/oracle/mod.rs +++ b/src/db/oracle/mod.rs @@ -3,7 +3,6 @@ // 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 -pub mod oracle_cash; pub mod v2; use anyhow::{bail, Result}; diff --git a/src/db/oracle/oracle_cash.rs b/src/db/oracle/oracle_cash.rs deleted file mode 100644 index a517930..0000000 --- a/src/db/oracle/oracle_cash.rs +++ /dev/null @@ -1,160 +0,0 @@ -// 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 anyhow::{bail, Result}; -use sqlx::{Row, SqlitePool}; - -#[derive(Debug, Clone, serde::Serialize)] -pub struct OracleCashPrice { - pub oracle_timestamp: i64, - /// Price in cents (same convention as the on-chain Delphi oracle) - pub oracle_price: i64, - pub message_sequence: Option, -} - -/// Creates the oracle_cash_price table if it doesn't already exist. -/// Safe to call on both new and existing oracle.db instances. -pub async fn prepare_oracle_cash_tables(pool: &SqlitePool) { - sqlx::query( - "CREATE TABLE IF NOT EXISTS oracle_cash_price ( - oracle_timestamp INTEGER NOT NULL PRIMARY KEY, - oracle_price INTEGER NOT NULL, - message_sequence INTEGER - )", - ) - .execute(pool) - .await - .expect("failed to create oracle_cash_price table"); - - sqlx::query( - "CREATE INDEX IF NOT EXISTS idx_oracle_cash_seq - ON oracle_cash_price(message_sequence) - WHERE message_sequence IS NOT NULL", - ) - .execute(pool) - .await - .expect("failed to create oracle_cash_seq index"); -} - -pub async fn upsert_oracle_cash_price( - pool: &SqlitePool, - oracle_timestamp: i64, - oracle_price: i64, - message_sequence: Option, -) -> Result<()> { - sqlx::query( - "INSERT OR REPLACE INTO oracle_cash_price - (oracle_timestamp, oracle_price, message_sequence) - VALUES (?, ?, ?)", - ) - .bind(oracle_timestamp) - .bind(oracle_price) - .bind(message_sequence) - .execute(pool) - .await - .map_err(|e| anyhow::anyhow!("failed to upsert oracle_cash_price: {}", e))?; - Ok(()) -} - -/// Returns the earliest oracle_timestamp stored, or None if the table is empty. -pub async fn get_oracle_cash_min_timestamp(pool: &SqlitePool) -> Result> { - let row: (Option,) = sqlx::query_as("SELECT MIN(oracle_timestamp) FROM oracle_cash_price") - .fetch_one(pool) - .await?; - Ok(row.0) -} - -pub async fn get_oracle_cash_closest( - pool: &SqlitePool, - timestamp: i64, -) -> Result> { - let row = sqlx::query( - "SELECT oracle_timestamp, oracle_price, message_sequence - FROM oracle_cash_price - WHERE oracle_timestamp <= ? - ORDER BY oracle_timestamp DESC - LIMIT 1", - ) - .bind(timestamp) - .fetch_optional(pool) - .await?; - - Ok(row.map(|r| OracleCashPrice { - oracle_timestamp: r.get(0), - oracle_price: r.get(1), - message_sequence: r.get(2), - })) -} - -pub async fn get_oracle_cash_range( - pool: &SqlitePool, - start: i64, - end: i64, -) -> Result> { - let rows = sqlx::query( - "SELECT oracle_timestamp, oracle_price, message_sequence - FROM oracle_cash_price - WHERE oracle_timestamp BETWEEN ? AND ? - ORDER BY oracle_timestamp ASC", - ) - .bind(start) - .bind(end) - .fetch_all(pool) - .await?; - - Ok(rows - .iter() - .map(|r| OracleCashPrice { - oracle_timestamp: r.get(0), - oracle_price: r.get(1), - message_sequence: r.get(2), - }) - .collect()) -} - -pub async fn get_oracle_cash_range_with_step( - pool: &SqlitePool, - start: i64, - end: i64, - step: i64, -) -> Result> { - let total_intervals = (end - start) / step; - const MAX_INTERVALS: i64 = 10_000; - if total_intervals > MAX_INTERVALS { - bail!( - "Too many intervals ({} > {})", - total_intervals, - MAX_INTERVALS - ); - } - - let all = get_oracle_cash_range(pool, start, end).await?; - - let mut buckets: Vec = Vec::new(); - let mut next_threshold = start + step; - let mut last_entry: Option = None; - - for entry in all { - while entry.oracle_timestamp >= next_threshold { - if let Some(e) = last_entry.take() { - buckets.push(e); - } - next_threshold += step; - } - last_entry = Some(entry); - } - - if let Some(e) = last_entry { - if buckets - .last() - .map(|b| b.oracle_timestamp != e.oracle_timestamp) - .unwrap_or(true) - { - buckets.push(e); - } - } - - Ok(buckets) -} diff --git a/src/main.rs b/src/main.rs index 5072105..3fc8e5a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -79,7 +79,6 @@ mod db; mod def; mod electrum; mod index; -mod oracle_cash; mod rpc; mod signal; mod timeutil; @@ -119,7 +118,6 @@ async fn start_program( BCMRDownloader, WellKnownDownloader, CRC20Fetcher, - oracle_cash::OracleCashFetcher, Arc, Arc, // indexing_in_progress )> { @@ -354,15 +352,11 @@ async fn start_program( spawn_token_metrics_updater(db.clone(), indexing_in_progress.clone()); - let mut oracle_cash_fetcher = oracle_cash::OracleCashFetcher::new(); - oracle_cash_fetcher.start(db.oracle_w.clone(), db.oracle_r.clone()); - Ok(( db, bcmrdownloader, wellknowndownloader, crc20fetcher, - oracle_cash_fetcher, ibd_state, indexing_in_progress, )) @@ -389,7 +383,6 @@ async fn launch() -> _ { bcmrdownloader, wellknowndownloader, crc20fetcher, - oracle_cash_fetcher, ibd_state, indexing_in_progress, ) = match start_program(config).await { @@ -581,7 +574,6 @@ async fn launch() -> _ { .manage(bcmrdownloader) .manage(wellknowndownloader) .manage(crc20fetcher) - .manage(oracle_cash_fetcher) .mount( "/cauldron/", routes![ @@ -620,8 +612,6 @@ async fn launch() -> _ { rpc::oracle::oracle_get_closest, rpc::oracle::oracle_get_range, rpc::oracle::oracle_get_history, - rpc::oracle::oracle_cash_closest, - rpc::oracle::oracle_cash_history, ], ) .mount( diff --git a/src/oracle_cash.rs b/src/oracle_cash.rs deleted file mode 100644 index faba6a8..0000000 --- a/src/oracle_cash.rs +++ /dev/null @@ -1,238 +0,0 @@ -// 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 - -//! Background task that maintains oracle_cash_price for the historical gap period -//! (Apr 23 – May 4 2026) between Delphi v1 and v2. -//! -//! Set `USE_ORACLES_CASH_LIVE_FEED = true` to re-enable continuous polling as a -//! live price fallback (e.g. if the on-chain v2 oracle becomes unavailable). - -use std::{ - sync::{ - atomic::{AtomicBool, Ordering}, - Arc, - }, - time::Duration, -}; - -use log::{info, warn}; -use serde::Deserialize; -use sqlx::SqlitePool; -use tokio::task::JoinHandle; - -use crate::db::oracle::oracle_cash::{get_oracle_cash_min_timestamp, upsert_oracle_cash_price}; -use crate::timeutil::time_now; - -const ORACLES_CASH_URL: &str = "https://oracles.generalprotocols.com"; -/// General Protocols BCH/USD oracle public key -const BCH_USD_ORACLE_PUBKEY: &str = - "02d09db08af1ff4e8453919cc866a4be427d7bfe18f2c05e5444c196fcf6fd2818"; - -/// Set to true to continuously poll oracles.cash for live price updates. -/// False: only backfill the gap period; Delphi v2 is the live source. -const USE_ORACLES_CASH_LIVE_FEED: bool = false; - -const POLL_INTERVAL: Duration = Duration::from_secs(300); // 5 minutes -const REQUEST_TIMEOUT: Duration = Duration::from_secs(20); -const BACKFILL_AGGREGATION: i64 = 3600; // 1-hour buckets -/// Backfill starts from when Delphi v1 stopped (2026-04-23 00:00:00 UTC). -const ORACLE_CUTOFF_TS: i64 = 1776902400; -/// Backfill ends when Delphi v2 went live (2026-05-04 00:00:00 UTC). -/// Only the gap between these two timestamps needs oracles.cash coverage. -const V2_DEPLOY_TS: i64 = 1777852800; - -// ── API response types ──────────────────────────────────────────────────────── - -#[derive(Deserialize, Debug)] -struct MessageMetrics { - #[serde(rename = "maxMessageSequence")] - max_message_sequence: i64, - /// Price in cents (same unit as on-chain Delphi: 38424 = $384.24) - #[serde(rename = "currentPrice")] - current_price: i64, - #[serde(rename = "maxMessageTimestamp")] - max_message_timestamp: i64, -} - -#[derive(Deserialize, Debug)] -struct OracleEntry { - #[serde(rename = "publicKey")] - public_key: String, - #[serde(rename = "messageMetrics")] - message_metrics: Option, -} - -#[derive(Deserialize, Debug)] -struct OraclesResponse { - oracles: Vec, -} - -#[derive(Deserialize, Debug)] -struct PriceGraphPoint { - #[serde(rename = "averageTimestamp")] - average_timestamp: f64, - /// Average price in cents over the aggregation window - #[serde(rename = "averagePrice")] - average_price: f64, -} - -#[derive(Deserialize, Debug)] -struct PriceGraphResponse { - #[serde(rename = "priceGraphPoints")] - price_graph_points: Vec, -} - -// ── Fetch helpers ───────────────────────────────────────────────────────────── - -/// Returns (timestamp_secs, price_cents, message_sequence) -async fn fetch_current_price(client: &reqwest::Client) -> anyhow::Result<(i64, i64, i64)> { - let url = format!("{}/api/v1/oracles", ORACLES_CASH_URL); - let resp: OraclesResponse = client - .get(&url) - .timeout(REQUEST_TIMEOUT) - .send() - .await? - .error_for_status()? - .json() - .await?; - - let oracle = resp - .oracles - .into_iter() - .find(|o| o.public_key == BCH_USD_ORACLE_PUBKEY) - .ok_or_else(|| anyhow::anyhow!("BCH/USD oracle not found in oracles.cash response"))?; - - let m = oracle - .message_metrics - .ok_or_else(|| anyhow::anyhow!("No messageMetrics for BCH/USD oracle"))?; - - Ok(( - m.max_message_timestamp, - m.current_price, - m.max_message_sequence, - )) -} - -/// Fetches hourly price history for the gap period [ORACLE_CUTOFF_TS, V2_DEPLOY_TS]. -async fn backfill_history(client: &reqwest::Client, pool: &SqlitePool) -> anyhow::Result<()> { - let url = format!( - "{}/api/v2/priceGraphPoints?publicKey={}&minMessageTimestamp={}&maxMessageTimestamp={}&aggregationPeriod={}", - ORACLES_CASH_URL, BCH_USD_ORACLE_PUBKEY, ORACLE_CUTOFF_TS, V2_DEPLOY_TS, BACKFILL_AGGREGATION - ); - - let resp: PriceGraphResponse = client - .get(&url) - .timeout(Duration::from_secs(30)) - .send() - .await? - .error_for_status()? - .json() - .await?; - - let count = resp.price_graph_points.len(); - for point in resp.price_graph_points { - let ts = point.average_timestamp as i64; - let price = point.average_price as i64; - // No real sequence available for aggregated points - upsert_oracle_cash_price(pool, ts, price, None).await?; - } - - info!("oracle_cash: backfilled {count} hourly price points"); - Ok(()) -} - -// ── Fetcher ─────────────────────────────────────────────────────────────────── - -pub struct OracleCashFetcher { - keep_running: Arc, - task: Option>, -} - -impl OracleCashFetcher { - pub fn new() -> Self { - Self { - keep_running: Arc::new(AtomicBool::new(true)), - task: None, - } - } - - pub fn start(&mut self, oracle_w: SqlitePool, oracle_r: SqlitePool) { - let keep_running = self.keep_running.clone(); - - self.task = Some(tokio::spawn(async move { - let client = reqwest::Client::new(); - - // Backfill if the table is empty or if the earliest entry is after - // ORACLE_CUTOFF_TS (meaning we're missing data from when the Delphi oracle - // stopped being updated). - let needs_backfill = match get_oracle_cash_min_timestamp(&oracle_r).await { - Ok(None) => { - info!("oracle_cash: table empty, will backfill from ORACLE_CUTOFF_TS"); - true - } - Ok(Some(min_ts)) => { - if min_ts > ORACLE_CUTOFF_TS { - info!( - "oracle_cash: earliest entry {min_ts} is after cutoff {ORACLE_CUTOFF_TS} — will backfill" - ); - true - } else { - info!( - "oracle_cash: earliest entry {min_ts} covers cutoff, skipping backfill" - ); - false - } - } - Err(e) => { - warn!("oracle_cash: could not check earliest timestamp: {e}"); - false - } - }; - - if needs_backfill { - if let Err(e) = backfill_history(&client, &oracle_w).await { - warn!("oracle_cash: backfill failed: {e}"); - } - } - - if !USE_ORACLES_CASH_LIVE_FEED { - info!("oracle_cash: live feed disabled, gap backfill complete"); - return; - } - - loop { - if !keep_running.load(Ordering::Relaxed) { - info!("oracle_cash: exiting fetch task"); - return; - } - - match fetch_current_price(&client).await { - Ok((ts, price_cents, seq)) => { - match upsert_oracle_cash_price(&oracle_w, ts, price_cents, Some(seq)).await - { - Ok(()) => info!( - "oracle_cash: stored price={price_cents} cents ts={ts} seq={seq}" - ), - Err(e) => warn!("oracle_cash: failed to store price: {e}"), - } - } - Err(e) => warn!("oracle_cash: failed to fetch current price: {e}"), - } - - tokio::time::sleep(POLL_INTERVAL).await; - } - })); - } -} - -impl Drop for OracleCashFetcher { - fn drop(&mut self) { - self.keep_running.store(false, Ordering::SeqCst); - if let Some(task) = self.task.take() { - task.abort(); - info!("oracle_cash fetch task aborted"); - } - } -} diff --git a/src/rpc/oracle.rs b/src/rpc/oracle.rs index fad4d22..706b3c2 100644 --- a/src/rpc/oracle.rs +++ b/src/rpc/oracle.rs @@ -3,9 +3,6 @@ // 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}; @@ -196,82 +193,3 @@ pub async fn oracle_get_history( )) } -/// 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, - )) -}