From 4d205cada438ed317fe6cd25d39f0550bee780f6 Mon Sep 17 00:00:00 2001 From: Dagur Valberg Johannsson Date: Mon, 4 May 2026 14:17:15 +0200 Subject: [PATCH] Delphi v2 support Same API from consumer-standpoint --- Cargo.lock | 4 +- Cargo.toml | 2 +- contrib/api_doc_generator.py | 19 ++++- src/db/oracle/mod.rs | 143 ++++++++++++++++++++++++++++++++++- src/db/oracle/v2.rs | 88 +++++++++++++++++++++ src/electrum.rs | 18 ++++- src/rpc/oracle.rs | 41 ++++++---- 7 files changed, 292 insertions(+), 23 deletions(-) create mode 100644 src/db/oracle/v2.rs diff --git a/Cargo.lock b/Cargo.lock index dd88648..696b3d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2154,9 +2154,9 @@ dependencies = [ [[package]] name = "riftenlabs-defi" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adefa28c0d92eabb6d22de714c63bd966178a509f3d100d9d9ca82765c92688d" +checksum = "98d517c00862416d7a8c2faafc23724c5bb9c39e02a7a962cd61edb83ca284bb" dependencies = [ "anyhow", "bitcoin_hashes", diff --git a/Cargo.toml b/Cargo.toml index 37e201e..0685d63 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ serde_json = "1.0.133" rocket = { version = "0.5.1", features = ["json"] } sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] } rayon = "1.10.0" -riftenlabs-defi = "0.2.0" +riftenlabs-defi = "0.3.0" rocket_cors = "0.6.0" log = "0.4" stderrlog = "0.6.0" diff --git a/contrib/api_doc_generator.py b/contrib/api_doc_generator.py index d6c4845..1a9ba43 100755 --- a/contrib/api_doc_generator.py +++ b/contrib/api_doc_generator.py @@ -373,14 +373,27 @@ Base URL: `{mount_display}` def generate_example_uri(self, mount: MountInfo, route: RouteInfo) -> str: + # Default placeholder for `` is a representative cauldron pair + # token. Routes under `/delphi/` operate on Delphi oracle contract + # categories instead, so substitute the live mainnet BCH/USD v2 + # oracle there — that's what users actually want to see linked. + is_delphi = "/delphi/" in route.path + delphi_v2_bchusd = ( + "be0d0d8324e8cda41d34b85bd203ce2482256eb337a0ad0fea82c2ddd7306c88" + ) + cauldron_token = ( + "b79bfc8246b5fc4707e7c7dedcb6619ef1ab91f494a790c20b0f4c422ed95b92" + ) + token_for_route = delphi_v2_bchusd if is_delphi else cauldron_token + example_params = { - "token": "b79bfc8246b5fc4707e7c7dedcb6619ef1ab91f494a790c20b0f4c422ed95b92", - "category": "b79bfc8246b5fc4707e7c7dedcb6619ef1ab91f494a790c20b0f4c422ed95b92", + "token": token_for_route, + "category": cauldron_token, "pkh": "36c0020dd39e7cd66c21f237dc53d384661a557f", "start": 1716537600, "end": 1716624000, "stepsize": 3600, - "token_id": "b79bfc8246b5fc4707e7c7dedcb6619ef1ab91f494a790c20b0f4c422ed95b92", + "token_id": token_for_route, "timestamp": 1716537600, } diff --git a/src/db/oracle/mod.rs b/src/db/oracle/mod.rs index 774558b..c9e4931 100644 --- a/src/db/oracle/mod.rs +++ b/src/db/oracle/mod.rs @@ -4,12 +4,14 @@ // 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}; use bitcoin_hashes::{hex::ToHex, Hash}; use bitcoincash::{BlockHash, TokenID, Transaction, Txid}; use log::debug; use riftenlabs_defi::delphi::parse_delphi_update; +use riftenlabs_defi::delphi::v2::{is_live as v2_is_live, parse_delphi_v2_update}; use sqlx::{Row, SqlitePool}; use crate::db::blob::{blob_to_display_hex, display_hex_to_blob, ToBlob}; @@ -105,6 +107,11 @@ pub async fn index_oracle( blockhash: &BlockHash, ) -> Result<()> { for tx in txs { + // v1: cashc 0.10.5 contract. Matched by the v1 redeem-script template + // in `riftenlabs_defi::delphi::v1`. v2 spends do not match it — the + // redeem-script body differs because of the new `migrate()` function + // and the three-arg constructor — so v1 and v2 detection cannot + // collide. if let Some(update) = parse_delphi_update(tx) { let entry = DelphiEntry { txid: tx.txid().to_hex(), @@ -114,7 +121,30 @@ pub async fn index_oracle( oracle_sequence: update.sequence as i64, token_id: update.token_id.to_string(), }; - debug!("inserting delphi entry: {entry:?}"); + debug!("inserting v1 delphi entry: {entry:?}"); + insert_delphi_entry(pool, &entry).await?; + continue; + } + + // v2: cashc 0.12.1 contract. Library parser is deployment-agnostic + // (matches by redeem-script template), so any v2 oracle on any + // network parses cleanly. We then apply the "live price" filter + // here at the indexer's policy boundary: reserve UTXOs and + // operator-disabled feeds (`price == 0`) must never appear as + // historical price points. + if let Some(update) = parse_delphi_v2_update(tx) { + if !v2_is_live(&update) { + continue; + } + let entry = DelphiEntry { + txid: tx.txid().to_hex(), + blockhash: blockhash.to_hex(), + oracle_timestamp: update.timestamp as i64, + oracle_price: update.price as i64, + oracle_sequence: update.sequence as i64, + token_id: update.token_id.to_string(), + }; + debug!("inserting v2 delphi entry: {entry:?}"); insert_delphi_entry(pool, &entry).await?; } } @@ -361,4 +391,115 @@ mod tests { assert!(result.is_err(), "should fail due to MAX_INTERVALS limit"); } + + /// V1 regression guard. + /// + /// Runs a real BCH/USD v1 update transaction (the same fixture used by + /// `riftenlabs-defi 0.2.0`'s upstream test) through the full + /// `index_oracle` pipeline and asserts the entry lands in the table with + /// the on-chain values. Catches a regression where adding the v2 path + /// somehow short-circuits the v1 path (e.g. by misordering the matchers + /// or making them mutually exclusive). + #[tokio::test] + async fn index_oracle_still_indexes_v1_updates() { + use bitcoin_hashes::hex::FromHex; + use bitcoincash::consensus::deserialize; + + // Same hex fixture as `riftenlabs_defi::delphi::tests::test_parse_cauldron_trade`. + // Token: BCH/USD v1, price=384.24 USD, seq=1144563, ts=1747745823. + let v1_tx_hex = "020000000334819689e4aa77c96217371931e9a6ce902cacd365bb94593a48960894f5f4a800000000fd2601101f7c2c680b771100f37611001896000040bf4ca0881b986db52e2cde564796c102b2699783331dfba8482e1f98dea683918574bf93b5d1918a942da4bf45143f097694c2c851b64be821bc3fb85f7f33f4004cd120561fd4a4dc7cde1b06f224ec70380e8b2718a04aa0ef249e27b287531e64ec392102d09db08af1ff4e8453919cc866a4be427d7bfe18f2c05e5444c196fcf6fd28185279009c635379827701409d537a54797bbbc0009d51ce0087916952ce0088c3539d00cc00c6a26900cd00c78800d100ce885279547f75815379587f77547f7581547a5c7f77547f758151cf567f77527f7581537a56807c52807e7b54807e7c54807e00cc00c6a26900cd00c78800d100ce8851d28851d151ce8852d10088c4539c7777677b519d00ce7b877768ffffffff34819689e4aa77c96217371931e9a6ce902cacd365bb94593a48960894f5f4a801000000d2004ccf20177db58be48264cb1f1acef0ef332541f45c3e4968b06dd9cb5d2c14f137bad920c1c3f1b000630136069e86c617ab6eeecf65fd22593fda5578ba9a442673435e5279009c6300ce01207f7588c0d276827760a269c0cf78587f77547f758178587f77547f7581a0697c567f75817c567f7581a069c0ccc0c6a269c0cdc0c788c0d1c0ce877777675279519c63c0cf567f77527f7581c0ccc0c67b93a269c0cdc0c788c0d1c0ce88c0d2c0cf87777777677b529d00ce01207f757b88c0cdc0c788c0d1c0ce88c0d2c0cf87776868ffffffff2c4384438366ce1a164bb730d97518c31b6e047582d02e85595cd0fbfb418c990200000064411038fdfce34da726414e8a4b12e14bc6f586a48601884d940d34943246ae14b8e4f7fdb3d973e1e0156e334f7921bde95e0e001175ae2fdad13d890d2199a1d841210245496ebbf3d90fa94a395a3b6cc6852fbc621ee4147bbac0580a59d803cae8f8ffffffff03e80300000000000045efc1c3f1b000630136069e86c617ab6eeecf65fd22593fda5578ba9a442673435e20aa203b71bed23bf7e606d2f9178d85e15170d68c45a10a9e86db520ac08e3ef41f6a8718e403000000000056ef72c96a0660a2b60b1f10301a33b89cc10057c7493e0ddece8a1882bd5c6fd4d061101f7c2c680000e803f376110018960000aa20ed67309b36424c1b3f25e08f19d8ead4eade917c476e5b16520deac63d65d4a9870d4a7600000000001976a914925e9bcecea0e9a129ede392a075da2ef03a22c188ac29230000"; + let tx_bytes = hex::decode(v1_tx_hex).unwrap(); + let tx: Transaction = deserialize(&tx_bytes).unwrap(); + + let pool = setup_test_db().await; + let blockhash = BlockHash::hash(b"v1-regression-block"); + + index_oracle(&pool, std::slice::from_ref(&tx), &blockhash) + .await + .expect("v1 indexing succeeds"); + + // Should now have exactly one entry, populated from the v1 path. + let v1_token = + TokenID::from_hex("d0d46f5cbd82188acede0d3e49c75700c19cb8331a30101f0bb6a260066ac972") + .unwrap(); + let entry = get_closest(&pool, &Some(v1_token), 1_747_745_823 + 1) + .await + .unwrap() + .expect("v1 entry was inserted"); + assert_eq!(entry.oracle_timestamp, 1_747_745_823); + assert_eq!(entry.oracle_price, 38_424); // 384.24 USD + assert_eq!(entry.oracle_sequence, 1_144_563); + assert_eq!(entry.token_id, v1_token.to_hex()); + } + + /// V2 end-to-end pipeline test. + /// + /// Runs the live mainnet v2 update tx through `index_oracle` and asserts + /// the entry lands in the table tagged with the v2 token id and the live + /// on-chain values. Complements the unit tests in `oracle::v2::tests`, + /// which exercise `parse_delphi_v2_update` in isolation. + #[tokio::test] + async fn index_oracle_indexes_v2_updates() { + use crate::db::oracle::v2::v2_bchusd_token_id; + use bitcoincash::consensus::deserialize; + + // Same fixture as `oracle::v2::tests::REAL_V2_UPDATE_TX_HEX`. txid: + // 658e0d32a87c2104506f09dbc34b256fdc19079599e8100757c8ed7640da4413 + let v2_tx_hex = "0200000003886c30d7ddc282ea0fada037b36e258224ce03d25bb8341da4cde824830d0dbe01000000fd1a01109c6ff869ee201900d4201900c4ab0000402213eb0642081c4e6a42d73b21b79a2c53e55d53c03c97957dca196167c556525662b8377237ee4ada50182e58d38d4cf16849cd3b358c806ae1e8f13ca75784004cc520d3cc28ff8686bab70580480286c7841567e0d8acaf1566ca1e303b516eff86452102d09db08af1ff4e8453919cc866a4be427d7bfe18f2c05e5444c196fcf6fd28185279009c635379827701409d537a54797bbbc0009d51ce0087916952ce0088c3539d00cc00c6a26900cd00c78800d100ce885279547f750200007e815379587f77547f7581547a5c7f77547f758151cf567f77527f7581537a56807c52807e7b54807e7c54807e51d28851d151ce8852d10088c4539c7777677b519d00ce7b877768ffffffffca3aec85613180f5c592be18e45fecf809b42d1cc78fddc1983c882f1a2a2b3601000000fd1101004d0d01203fdbd897d64c613647dce87074494968870f2f13057fd48ce67f7acaa5248a2620f5bb739dfd3d12ab4efd3b79abe7b2a852b228c145da56664235e25f9de08c45201fd673a74360ffe0c1a99714bac964dc46952e53f4a72bca0f166b87868d6a435379009c6300ce01207f7588c0d276827760a269c0cf78587f77547f758178587f77547f7581a0697c567f75817c567f7581a069c0ccc0c6a269c0cdc0c788c0d1c0ce87777777675379519c63c0cf567f77527f75817600a269016495c0ccc0c67b93a269c0cdc0c788c0d1c0ce88c0d2c0cf886d6d51675379529c6300ce01207f757b88c0cdc0c788c0d1c0ce88c0d2c0cf8777777767537a539d00ce01207f75537a877777686868ffffffff5e6bb0dbd7997c8611288cd5c3ffadb1ecfde40e167197b9c0043d3efc25a9650100000064414da34efab53991c3afcd2ced71df60eba65a3b6ff225c1658389620b800da5e2d5f526c9b9e6067ff3b4e4416f58a7442dcd19476d8a0255354288809a294437612102e42a4d6c58b38099af9e6d1942d6c14ccdd28aff5350bc942a4423cd0f0b1b0affffffff03e80300000000000045ef1fd673a74360ffe0c1a99714bac964dc46952e53f4a72bca0f166b87868d6a4320aa20bcf107bf8ccbf500f9ee6e60cc1797625faea5c96cebbd80bd0caa08c52b618a87e80300000000000056ef886c30d7ddc282ea0fada037b36e258224ce03d25bb8341da4cde824830d0dbe61109c6ff86900000100d4201900c4ab0000aa20b6414aa81c4dd593e13e36f8176d69a9eb6755b08034d0099e61acdcf9b3e1238794d10100000000001976a9147b59e6d893157df4fbb263a5639527e5b569d1bf88ac29230000"; + let tx: Transaction = deserialize(&hex::decode(v2_tx_hex).unwrap()).unwrap(); + + let pool = setup_test_db().await; + let blockhash = BlockHash::hash(b"v2-end-to-end-block"); + + index_oracle(&pool, std::slice::from_ref(&tx), &blockhash) + .await + .expect("v2 indexing succeeds"); + + let v2_token = *v2_bchusd_token_id(); + let entry = get_closest(&pool, &Some(v2_token), 1_777_889_180 + 1) + .await + .unwrap() + .expect("v2 entry was inserted"); + assert_eq!(entry.oracle_timestamp, 1_777_889_180); + assert_eq!(entry.oracle_price, 43_972); // $439.72 + assert_eq!(entry.oracle_sequence, 1_646_804); + assert_eq!(entry.token_id, v2_token.to_hex()); + } + + /// Reserve / disabled-feed filter at the indexer's policy boundary. + /// + /// The library parser is intentionally permissive (it returns reserves + /// and `price=0` updates verbatim), so this tests that `index_oracle`'s + /// `is_live` gate keeps zero-price entries out of the table. + #[tokio::test] + async fn index_oracle_skips_zero_price_v2_updates() { + use crate::db::oracle::v2::v2_bchusd_token_id; + use bitcoincash::consensus::deserialize; + + // Real v2 update tx, but with output[1]'s commitment patched to a + // zero-price commitment (timestamp/seq advance, but price=0 — the + // operator's "feed disabled" signal). This must NOT land in the + // table. + let v2_tx_hex = "0200000003886c30d7ddc282ea0fada037b36e258224ce03d25bb8341da4cde824830d0dbe01000000fd1a01109c6ff869ee201900d4201900c4ab0000402213eb0642081c4e6a42d73b21b79a2c53e55d53c03c97957dca196167c556525662b8377237ee4ada50182e58d38d4cf16849cd3b358c806ae1e8f13ca75784004cc520d3cc28ff8686bab70580480286c7841567e0d8acaf1566ca1e303b516eff86452102d09db08af1ff4e8453919cc866a4be427d7bfe18f2c05e5444c196fcf6fd28185279009c635379827701409d537a54797bbbc0009d51ce0087916952ce0088c3539d00cc00c6a26900cd00c78800d100ce885279547f750200007e815379587f77547f7581547a5c7f77547f758151cf567f77527f7581537a56807c52807e7b54807e7c54807e51d28851d151ce8852d10088c4539c7777677b519d00ce7b877768ffffffffca3aec85613180f5c592be18e45fecf809b42d1cc78fddc1983c882f1a2a2b3601000000fd1101004d0d01203fdbd897d64c613647dce87074494968870f2f13057fd48ce67f7acaa5248a2620f5bb739dfd3d12ab4efd3b79abe7b2a852b228c145da56664235e25f9de08c45201fd673a74360ffe0c1a99714bac964dc46952e53f4a72bca0f166b87868d6a435379009c6300ce01207f7588c0d276827760a269c0cf78587f77547f758178587f77547f7581a0697c567f75817c567f7581a069c0ccc0c6a269c0cdc0c788c0d1c0ce87777777675379519c63c0cf567f77527f75817600a269016495c0ccc0c67b93a269c0cdc0c788c0d1c0ce88c0d2c0cf886d6d51675379529c6300ce01207f757b88c0cdc0c788c0d1c0ce88c0d2c0cf8777777767537a539d00ce01207f75537a877777686868ffffffff5e6bb0dbd7997c8611288cd5c3ffadb1ecfde40e167197b9c0043d3efc25a9650100000064414da34efab53991c3afcd2ced71df60eba65a3b6ff225c1658389620b800da5e2d5f526c9b9e6067ff3b4e4416f58a7442dcd19476d8a0255354288809a294437612102e42a4d6c58b38099af9e6d1942d6c14ccdd28aff5350bc942a4423cd0f0b1b0affffffff03e80300000000000045ef1fd673a74360ffe0c1a99714bac964dc46952e53f4a72bca0f166b87868d6a4320aa20bcf107bf8ccbf500f9ee6e60cc1797625faea5c96cebbd80bd0caa08c52b618a87e80300000000000056ef886c30d7ddc282ea0fada037b36e258224ce03d25bb8341da4cde824830d0dbe61109c6ff86900000100d4201900c4ab0000aa20b6414aa81c4dd593e13e36f8176d69a9eb6755b08034d0099e61acdcf9b3e1238794d10100000000001976a9147b59e6d893157df4fbb263a5639527e5b569d1bf88ac29230000"; + let mut tx: Transaction = deserialize(&hex::decode(v2_tx_hex).unwrap()).unwrap(); + // Zero out the price field (bytes 12..16 of the commitment). + let token = tx.output[1].token.as_mut().expect("output[1] has token"); + for byte in &mut token.commitment[12..16] { + *byte = 0; + } + + let pool = setup_test_db().await; + let blockhash = BlockHash::hash(b"v2-zero-price-block"); + + index_oracle(&pool, std::slice::from_ref(&tx), &blockhash) + .await + .expect("indexing succeeds"); + + let v2_token = *v2_bchusd_token_id(); + let entry = get_closest(&pool, &Some(v2_token), i64::MAX).await.unwrap(); + assert!( + entry.is_none(), + "zero-price v2 update must not be persisted, got {entry:?}" + ); + } } diff --git a/src/db/oracle/v2.rs b/src/db/oracle/v2.rs new file mode 100644 index 0000000..01c7e00 --- /dev/null +++ b/src/db/oracle/v2.rs @@ -0,0 +1,88 @@ +// Copyright (C) 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 + +//! Indexer-side glue for the Delphi v2 oracle. +//! +//! The parser, redeem-script template, and `is_live` predicate live in the +//! upstream library at `riftenlabs_defi::delphi::v2` and are intentionally +//! deployment-agnostic. This module pins the **mainnet BCH/USD** deployment +//! constants — token category ids for the v1 (legacy) and v2 (current) +//! contracts — that the indexer needs to recognise live oracle output. +//! +//! Sourced from `~/libriften/packages/delphi-contract/src/v2/mainnet.ts` +//! (the `delphi-contract/v2: typed mainnet deployment constants` commit). + +use std::sync::OnceLock; + +use bitcoin_hashes::hex::FromHex; +use bitcoincash::TokenID; + +/// Token category for the deployed v2 BCH/USD Delphi contract on mainnet. +/// +/// Reference-only: the v2 parser at +/// `riftenlabs_defi::delphi::v2::parse_delphi_v2_update` is +/// deployment-agnostic (matches by redeem-script template, not token id), +/// so the indexer doesn't gate on this constant. It's exposed so callers +/// can refer to "the BCH/USD v2 oracle" symbolically. +#[allow(dead_code)] // exported for downstream consumers; not all builds use it. +pub const V2_BCHUSD_TOKEN_HEX: &str = + "be0d0d8324e8cda41d34b85bd203ce2482256eb337a0ad0fea82c2ddd7306c88"; + +/// Token category for the v1 BCH/USD Delphi contract on mainnet (legacy). +/// +/// v1 is still on-chain but is no longer being updated by the operator. +/// Exposed publicly so downstream code (CLIs, future endpoints) can query +/// the legacy feed by name without re-embedding the hex literal. +#[allow(dead_code)] // exported for downstream consumers; not all builds use it. +pub const V1_BCHUSD_TOKEN_HEX: &str = + "d0d46f5cbd82188acede0d3e49c75700c19cb8331a30101f0bb6a260066ac972"; + +/// Returns the v2 BCH/USD contract token id, parsed once on first call. +/// +/// See [`V2_BCHUSD_TOKEN_HEX`] for the rationale on why this is +/// reference-only and not used inside `index_oracle`. +#[allow(dead_code)] // exported for downstream consumers; not all builds use it. +pub fn v2_bchusd_token_id() -> &'static TokenID { + static TOKEN: OnceLock = OnceLock::new(); + TOKEN.get_or_init(|| { + TokenID::from_hex(V2_BCHUSD_TOKEN_HEX).expect("v2 BCH/USD token hex is valid") + }) +} + +/// Returns the v1 BCH/USD contract token id, parsed once on first call. +/// +/// Exported alongside [`v2_bchusd_token_id`] so callers (CLIs, future +/// endpoints, manual queries) can refer to the legacy feed symbolically. +#[allow(dead_code)] // exported for downstream consumers; not all builds use it. +pub fn v1_bchusd_token_id() -> &'static TokenID { + static TOKEN: OnceLock = OnceLock::new(); + TOKEN.get_or_init(|| { + TokenID::from_hex(V1_BCHUSD_TOKEN_HEX).expect("v1 BCH/USD token hex is valid") + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn v1_and_v2_token_constants_differ() { + // Sanity guard: if these ever become equal, something has gone very + // wrong with the deployment constants. + assert_ne!(v1_bchusd_token_id(), v2_bchusd_token_id()); + } + + #[test] + fn v2_token_matches_libriften_constant() { + // The hex must equal `DELPHI_CONTRACT_TOKEN` in + // `~/libriften/packages/delphi-contract/src/v2/mainnet.ts`. If the + // values drift, downstream packages and the indexer will disagree + // about which on-chain UTXOs count as "the v2 oracle". + assert_eq!( + V2_BCHUSD_TOKEN_HEX, + "be0d0d8324e8cda41d34b85bd203ce2482256eb337a0ad0fea82c2ddd7306c88" + ); + } +} diff --git a/src/electrum.rs b/src/electrum.rs index 57645fc..9fbd81a 100644 --- a/src/electrum.rs +++ b/src/electrum.rs @@ -10,7 +10,10 @@ use bitcoin_hashes::hex::ToHex; use bitcoincash::{consensus::deserialize, BlockHeader, Transaction, Txid}; use electrum_client_netagnostic::{Client, ElectrumApi, Param}; use log::info; -use riftenlabs_defi::{cauldron::V2_CONTRACT_TEMPLATE, delphi::DELPHI_REDEEM_SCRIPT}; +use riftenlabs_defi::{ + cauldron::V2_CONTRACT_TEMPLATE, + delphi::{v2::DELPHI_V2_REDEEM_SCRIPT_BODY, DELPHI_REDEEM_SCRIPT}, +}; use serde_json::{json, Value}; /// Fetch blockchain tip from electrum server @@ -41,9 +44,17 @@ pub fn electrum_fetch_mempool(client: &Client) -> Result<(HashSet, HashSet "operation": "union" }); - let oracle_filter = json!({ + // v1 oracle filter: matches the cashc 0.10.5 redeem-script template. + let oracle_v1_filter = json!({ "scriptsig": hex::encode(DELPHI_REDEEM_SCRIPT), }); + // v2 oracle filter: matches the cashc 0.12.1 redeem-script body. The + // body is identical across deployments regardless of the constructor + // args, so this catches v2 spends without needing to know the deployed + // token ids. + let oracle_v2_filter = json!({ + "scriptsig": hex::encode(DELPHI_V2_REDEEM_SCRIPT_BODY), + }); let fetch_txs = |filter: Value| -> Result> { let response = client.raw_call("mempool.get", [Param::Value(filter)])?; @@ -72,7 +83,8 @@ pub fn electrum_fetch_mempool(client: &Client) -> Result<(HashSet, HashSet }; let cauldron_txs = fetch_txs(cauldron_filter)?; - let oracle_txs = fetch_txs(oracle_filter)?; + let mut oracle_txs = fetch_txs(oracle_v1_filter)?; + oracle_txs.extend(fetch_txs(oracle_v2_filter)?); Ok((cauldron_txs, oracle_txs)) } diff --git a/src/rpc/oracle.rs b/src/rpc/oracle.rs index 37050c8..fad4d22 100644 --- a/src/rpc/oracle.rs +++ b/src/rpc/oracle.rs @@ -20,10 +20,20 @@ use serde_json::{json, Value}; /// /// Status: Stable /// -/// - token_id: The 32 byte token ID of the oracle contract. Known oracle contract IDs: -/// - BCH/USD: `d0d46f5cbd82188acede0d3e49c75700c19cb8331a30101f0bb6a260066ac972` +/// - token_id: The 32 byte token ID of the oracle contract. Known IDs: +/// - **BCH/USD v2 (current, live)**: `be0d0d8324e8cda41d34b85bd203ce2482256eb337a0ad0fea82c2ddd7306c88` +/// - BCH/USD v1 (legacy, no longer updated): `d0d46f5cbd82188acede0d3e49c75700c19cb8331a30101f0bb6a260066ac972` /// - timestamp: Unix timestamp (optional, defaults to now) /// +/// When `token_id` is omitted, the closest entry across **all** indexed +/// Delphi contracts is returned. After the v2 deploy this naturally resolves +/// to the most recent live oracle (v2), since v1 stopped advancing; callers +/// who want a specific contract should pass `token_id=…` explicitly. +/// +/// Reserve UTXOs and `price=0` updates (the operator's "feed disabled" +/// signal) are filtered at index time, so any non-null entry returned here +/// is a live price. +/// /// Returns `null` if no oracle data is found. /// /// **Important:** `oracle_price` is returned in **cents** (not dollars). Divide by 100 @@ -35,15 +45,15 @@ use serde_json::{json, Value}; /// **Response Example:** /// ```json /// { -/// "oracle_timestamp": 1709468902, -/// "oracle_price": 64320, -/// "oracle_sequence": 12345, -/// "token_id": "d0d46f5cbd82188acede0d3e49c75700c19cb8331a30101f0bb6a260066ac972", +/// "oracle_timestamp": 1777889180, +/// "oracle_price": 43972, +/// "oracle_sequence": 1646804, +/// "token_id": "be0d0d8324e8cda41d34b85bd203ce2482256eb337a0ad0fea82c2ddd7306c88", /// "txid": "...", /// "blockhash": "..." /// } /// ``` -/// In this example, `oracle_price` of 64320 cents = $643.20 USD. +/// In this example, `oracle_price` of 43972 cents = $439.72 USD. #[get("/delphi/closest?&")] pub async fn oracle_get_closest( token_id: Option, @@ -72,7 +82,7 @@ pub async fn oracle_get_closest( )) } -/// Status: Deprecated +/// Status: Deprecated. Use `/delphi//history` instead. #[get("/delphi/range?&&")] pub async fn oracle_get_range( token_id: Option, @@ -106,22 +116,27 @@ pub async fn oracle_get_range( /// 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` +/// - token: The 32 byte token ID of the oracle contract. Known IDs: +/// - **BCH/USD v2 (current, live)**: `be0d0d8324e8cda41d34b85bd203ce2482256eb337a0ad0fea82c2ddd7306c88` +/// - BCH/USD v1 (legacy, no longer updated): `d0d46f5cbd82188acede0d3e49c75700c19cb8331a30101f0bb6a260066ac972` /// - start: Unix timestamp for start of period /// - end: Unix timestamp for end of period (optional, defaults to now) /// - stepsize: Seconds per interval (optional) /// +/// Reserve UTXOs and `price=0` updates (the operator's "feed disabled" +/// signal) are filtered at index time, so every entry returned here is a +/// live price. +/// /// **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 } +/// { "time": 1777886180, "price": 43900, "txid": "...", "blockhash": "...", "sequence": 1646803 }, +/// { "time": 1777889180, "price": 43972, "txid": "...", "blockhash": "...", "sequence": 1646804 } /// ] /// ``` -/// In this example, `price` values of 64320 cents = $643.20 USD. +/// In this example, `price` values of 43972 cents = $439.72 USD. #[get("/delphi//history?&&")] pub async fn oracle_get_history( token: &str,