Merge branch 'delphi-v2' into 'master'

Delphi v2 support

See merge request riftenlabs/riftenlabs-indexer!84
This commit is contained in:
Dagur Valberg Johannsson 2026-05-04 13:27:58 +00:00
commit bd91ff435f
7 changed files with 292 additions and 23 deletions

4
Cargo.lock generated
View file

@ -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",

View file

@ -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"

View file

@ -373,14 +373,27 @@ Base URL: `{mount_display}`
def generate_example_uri(self, mount: MountInfo, route: RouteInfo) -> str:
# Default placeholder for `<token>` 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,
}

View file

@ -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:?}"
);
}
}

88
src/db/oracle/v2.rs Normal file
View file

@ -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<TokenID> = 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<TokenID> = 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"
);
}
}

View file

@ -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<Txid>, 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<HashSet<Txid>> {
let response = client.raw_call("mempool.get", [Param::Value(filter)])?;
@ -72,7 +83,8 @@ pub fn electrum_fetch_mempool(client: &Client) -> Result<(HashSet<Txid>, 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))
}

View file

@ -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?<token_id>&<timestamp>")]
pub async fn oracle_get_closest(
token_id: Option<String>,
@ -72,7 +82,7 @@ pub async fn oracle_get_closest(
))
}
/// Status: Deprecated
/// Status: Deprecated. Use `/delphi/<token>/history` instead.
#[get("/delphi/range?<token_id>&<start>&<end>")]
pub async fn oracle_get_range(
token_id: Option<String>,
@ -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/<token>/history?<start>&<end>&<stepsize>")]
pub async fn oracle_get_history(
token: &str,