Merge branch 'delphi-v2' into 'master'
Delphi v2 support See merge request riftenlabs/riftenlabs-indexer!84
This commit is contained in:
commit
bd91ff435f
7 changed files with 292 additions and 23 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
88
src/db/oracle/v2.rs
Normal 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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue