diff --git a/Cargo.lock b/Cargo.lock index e694862..cbf423b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -684,6 +684,12 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + [[package]] name = "either" version = "1.13.0" @@ -868,6 +874,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fragile" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" + [[package]] name = "funty" version = "2.0.0" @@ -1270,6 +1282,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "ipnet" version = "2.9.0" @@ -1283,7 +1306,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi", - "rustix", + "rustix 0.38.28", "windows-sys 0.48.0", ] @@ -1344,6 +1367,12 @@ version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +[[package]] +name = "linux-raw-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" + [[package]] name = "linux-raw-sys" version = "0.4.12" @@ -1435,6 +1464,32 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mockall" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39a6bfcc6c8c7eed5ee98b9c3e33adc726054389233e201c95dab2d41a3839d2" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ca3004c2efe9011bd4e461bd8256445052b9615405b4f7ea43fc8ca5c20898" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 2.0.89", +] + [[package]] name = "moka" version = "0.12.8" @@ -1849,6 +1904,7 @@ name = "pragma-monitoring" version = "0.1.0" dependencies = [ "arc-swap", + "async-trait", "axum", "axum-macros", "bigdecimal", @@ -1864,8 +1920,10 @@ dependencies = [ "futures", "hyper", "lazy_static", + "mockall", "moka", "num-bigint", + "num-traits", "phf", "prometheus", "reqwest", @@ -1882,6 +1940,32 @@ dependencies = [ "uuid 1.6.1", ] +[[package]] +name = "predicates" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97" +dependencies = [ + "anstyle", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8177bee8e75d6846599c6b9ff679ed51e882816914eec639944d7c9aa11931" + +[[package]] +name = "predicates-tree" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b740d195ed3166cd147c8047ec98db0e22ec019eb8eeb76d343b795304fb13" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "primitive-types" version = "0.12.2" @@ -1937,6 +2021,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "procfs" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1de8dacb0873f77e6aefc6d71e044761fcc68060290f5b1089fcdf84626bb69" +dependencies = [ + "bitflags 1.3.2", + "byteorder", + "hex", + "lazy_static", + "rustix 0.36.17", +] + [[package]] name = "prometheus" version = "0.13.3" @@ -1946,8 +2043,10 @@ dependencies = [ "cfg-if", "fnv", "lazy_static", + "libc", "memchr", "parking_lot", + "procfs", "protobuf", "thiserror 1.0.50", ] @@ -2224,6 +2323,20 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.36.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "305efbd14fde4139eb501df5f136994bb520b033fa9fbdce287507dc23b8c7ed" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.1.4", + "windows-sys 0.45.0", +] + [[package]] name = "rustix" version = "0.38.28" @@ -2233,7 +2346,7 @@ dependencies = [ "bitflags 2.4.1", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.4.12", "windows-sys 0.52.0", ] @@ -2848,7 +2961,7 @@ dependencies = [ "cfg-if", "fastrand", "redox_syscall", - "rustix", + "rustix 0.38.28", "windows-sys 0.48.0", ] @@ -2861,6 +2974,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + [[package]] name = "thiserror" version = "1.0.50" @@ -3459,6 +3578,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -3477,6 +3605,21 @@ dependencies = [ "windows-targets 0.52.0", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -3507,6 +3650,12 @@ dependencies = [ "windows_x86_64_msvc 0.52.0", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -3519,6 +3668,12 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -3531,6 +3686,12 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -3543,6 +3704,12 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -3555,6 +3722,12 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -3567,6 +3740,12 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -3579,6 +3758,12 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" diff --git a/Cargo.toml b/Cargo.toml index 705e294..d558278 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,8 +34,9 @@ hyper = "0.14.27" lazy_static = "1.4.0" moka = { version = "0.12.8", features = ["future"] } num-bigint = "0.4" +num-traits = "0.2" phf = { version = "0.11", features = ["macros"] } -prometheus = "0.13.3" +prometheus = { version = "0.13.3", features = ["process"] } reqwest = { version = "0.11.22", features = ["json"] } serde = { version = "1.0.130", features = ["derive"] } serde_json = { version = "1.0.130" } @@ -51,6 +52,8 @@ uuid = { version = "1.4", features = ["fast-rng", "v4", "serde"] } [dev-dependencies] rstest = "0.18.2" criterion = { version = "0.5", features = ["async_tokio"] } +mockall = "0.13.1" +async-trait = "0.1.68" [[bench]] name = "coingecko_benchmarks" diff --git a/prometheus/alerts.rules.yml b/prometheus/alerts.rules.yml index 39bc8e6..2e06034 100644 --- a/prometheus/alerts.rules.yml +++ b/prometheus/alerts.rules.yml @@ -108,6 +108,15 @@ groups: description: | {{ $value }} sources for {{ $labels.pair }} ({{ $labels.type }}) have deviated from our price. + - alert: LSTConversionRateTooLow + expr: lst_conversion_rate <= 1.0 + for: 5m + labels: + severity: critical + annotations: + summary: "LST conversion rate is too low" + description: "The LST conversion rate for {{ $labels.pair }} is {{ $value }} which is <= 1" + - name: API rules: - alert: TimeSinceLastUpdateTooHigh diff --git a/src/config.rs b/src/config.rs index 78ce177..85fe171 100644 --- a/src/config.rs +++ b/src/config.rs @@ -65,10 +65,10 @@ pub struct DataInfo { #[derive(Debug, Clone)] #[allow(unused)] pub struct Config { - data_info: HashMap, - publishers: HashMap, - network: Network, - indexer_url: String, + pub(crate) data_info: HashMap, + pub(crate) publishers: HashMap, + pub(crate) network: Network, + pub(crate) indexer_url: String, } /// We are using `ArcSwap` as it allow us to replace the new `Config` with diff --git a/src/constants.rs b/src/constants.rs index 307b325..6c23eff 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,7 +1,7 @@ use arc_swap::ArcSwap; use lazy_static::lazy_static; use prometheus::{opts, register_gauge_vec, register_int_gauge_vec, GaugeVec, IntGaugeVec}; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::future::Future; use std::sync::Arc; use std::time::Duration; @@ -83,6 +83,21 @@ lazy_static! { map }; + /// Stores the pairs that are long tail assets and have a conversion rate + /// to USD. + /// The conversion rate is used to convert the price of the pair to USD + /// and then compare it to the reference price. + /// The conversion rate is stored in the `LST_CONVERSION_RATE` metric. + /// The conversion rate is used to convert the price of the pair to USD + /// and then compare it to the reference price. + pub static ref LST_PAIRS: HashSet<&'static str> = { + let mut set = HashSet::new(); + set.insert("XSTRK/STRK"); + set.insert("SSTRK/STRK"); + set.insert("KSTRK/STRK"); + set + }; + /// We have a list of assets that are defined as long tail assets. /// They have lower liquidity and higher volatilty - thus, it is trickier /// to track their prices and have good alerting. @@ -271,6 +286,14 @@ lazy_static! { ), &["network", "block"] ).unwrap(); + // LST conversion rate metrics + pub static ref LST_CONVERSION_RATE: GaugeVec = register_gauge_vec!( + opts!( + "lst_conversion_rate", + "Conversion rate for LST pairs" + ), + &["network", "pair"] + ).unwrap(); } #[allow(unused)] diff --git a/src/main.rs b/src/main.rs index f680aaa..8484b01 100644 --- a/src/main.rs +++ b/src/main.rs @@ -41,7 +41,7 @@ use tokio::task::JoinHandle; use tokio::time::interval; use config::{get_config, init_long_tail_asset_configuration, periodic_config_update, DataType}; -use constants::initialize_coingecko_mappings; +use constants::{initialize_coingecko_mappings, LST_PAIRS}; use processing::common::{check_publisher_balance, data_indexers_are_synced}; use tracing::instrument; use utils::{is_long_tail_asset, log_monitoring_results, log_tasks_results}; @@ -191,15 +191,12 @@ pub(crate) async fn api_monitor(cache: Cache<(String, u64), CoinPricesDTO>) { } } -#[instrument(skip(pool, cache))] pub(crate) async fn onchain_monitor( pool: Pool>, wait_for_syncing: bool, data_type: &DataType, cache: Cache<(String, u64), CoinPricesDTO>, ) { - let monitoring_config = get_config(None).await; - let mut interval = interval(Duration::from_secs(30)); loop { @@ -210,28 +207,55 @@ pub(crate) async fn onchain_monitor( continue; } - let tasks: Vec<_> = monitoring_config - .sources(data_type.clone()) + // Get fresh config for each iteration + let monitoring_config = get_config(None).await; + + // Clone the sources map before moving into tasks + let sources_map = monitoring_config.sources(data_type.clone()); + + let tasks: Vec<_> = sources_map .iter() - .flat_map(|(pair, sources)| match data_type { - DataType::Spot => { - if is_long_tail_asset(pair) { - vec![tokio::spawn(Box::pin( - processing::spot::process_long_tail_asset( - pool.clone(), - pair.clone(), - sources.to_vec(), - ), - ))] - } else { + .flat_map(|(pair, sources)| { + let pair = pair.clone(); + let sources = sources.clone(); + let mut pair_tasks = match data_type { + DataType::Spot => { + if is_long_tail_asset(&pair) { + vec![tokio::spawn(Box::pin( + processing::spot::process_long_tail_asset( + pool.clone(), + pair.clone(), + sources.to_vec(), + ), + ))] + } else { + vec![ + tokio::spawn(Box::pin(processing::spot::process_data_by_pair( + pool.clone(), + pair.clone(), + cache.clone(), + ))), + tokio::spawn(Box::pin( + processing::spot::process_data_by_pair_and_sources( + pool.clone(), + pair.clone(), + sources.to_vec(), + cache.clone(), + ), + )), + ] + } + } + // TODO: Long tail assets aren't treated as such for Future data + DataType::Future => { vec![ - tokio::spawn(Box::pin(processing::spot::process_data_by_pair( + tokio::spawn(Box::pin(processing::future::process_data_by_pair( pool.clone(), pair.clone(), cache.clone(), ))), tokio::spawn(Box::pin( - processing::spot::process_data_by_pair_and_sources( + processing::future::process_data_by_pair_and_sources( pool.clone(), pair.clone(), sources.to_vec(), @@ -240,25 +264,19 @@ pub(crate) async fn onchain_monitor( )), ] } + }; + + // Add LST monitoring task if applicable + if LST_PAIRS.contains(pair.as_str()) { + pair_tasks.push(tokio::spawn(Box::pin(async move { + // Map the Result<(), MonitoringError> to Result + monitoring::process_lst_data_by_pair(pair) + .await + .map(|_| 0u64) + }))); } - // TODO: Long tail assets aren't treated as such for Future data - DataType::Future => { - vec![ - tokio::spawn(Box::pin(processing::future::process_data_by_pair( - pool.clone(), - pair.clone(), - cache.clone(), - ))), - tokio::spawn(Box::pin( - processing::future::process_data_by_pair_and_sources( - pool.clone(), - pair.clone(), - sources.to_vec(), - cache.clone(), - ), - )), - ] - } + + pair_tasks }) .collect(); diff --git a/src/monitoring/lst.rs b/src/monitoring/lst.rs new file mode 100644 index 0000000..9dee198 --- /dev/null +++ b/src/monitoring/lst.rs @@ -0,0 +1,65 @@ +use crate::{ + config::{get_config, DataType}, + constants::LST_CONVERSION_RATE, + error::MonitoringError, +}; +use num_traits::ToPrimitive; +use starknet::{ + core::{ + types::{BlockId, BlockTag, Felt, FunctionCall}, + utils::cairo_short_string_to_felt, + }, + macros::selector, + providers::Provider, +}; + +/// Get the decimals for a specific pair from the configuration +async fn get_pair_decimals(pair: &str) -> Result { + let config = get_config(None).await; + config + .decimals(DataType::Spot) + .get(pair) + .copied() + .ok_or_else(|| MonitoringError::Api(format!("Pair {} not found", pair))) +} + +/// Process LST data for a specific pair and update the conversion rate metric +pub async fn process_lst_data_by_pair(pair: String) -> Result<(), MonitoringError> { + let config = get_config(None).await; + let client = &config.network().provider; + let network = config.network_str(); + let field_pair = cairo_short_string_to_felt(&pair).expect("failed to convert pair id"); + let decimals = get_pair_decimals(&pair).await?; + + // Call get_data with AggregationMode::ConversionRate (2) + let data = client + .call( + FunctionCall { + contract_address: config.network().oracle_address, + entry_point_selector: selector!("get_data"), + calldata: vec![Felt::ZERO, field_pair, Felt::from(2)], + }, + BlockId::Tag(BlockTag::Latest), + ) + .await + .map_err(|e| MonitoringError::OnChain(e.to_string()))?; + + let conversion_rate = data + .first() + .ok_or(MonitoringError::OnChain( + "No data returned from contract".to_string(), + ))? + .to_bigint() + .to_f64() + .ok_or(MonitoringError::Conversion( + "Failed to convert conversion rate to f64".to_string(), + ))? + / 10u64.pow(decimals) as f64; + + // Update metric before validation + LST_CONVERSION_RATE + .with_label_values(&[network, &pair]) + .set(conversion_rate); + + Ok(()) +} diff --git a/src/monitoring/mod.rs b/src/monitoring/mod.rs index e863607..cd35542 100644 --- a/src/monitoring/mod.rs +++ b/src/monitoring/mod.rs @@ -1,10 +1,12 @@ pub mod balance; +pub mod lst; pub mod on_off_deviation; pub mod price_deviation; pub mod source_deviation; pub mod time_since_last_update; pub use balance::get_on_chain_balance; +pub use lst::process_lst_data_by_pair; pub use on_off_deviation::on_off_price_deviation; pub use price_deviation::price_deviation; pub use source_deviation::source_deviation; diff --git a/src/tests/lst_tests.rs b/src/tests/lst_tests.rs new file mode 100644 index 0000000..9e699fd --- /dev/null +++ b/src/tests/lst_tests.rs @@ -0,0 +1,98 @@ +// In lst_tests.rs + +use super::{init_test_config, set_test_config}; +use crate::{ + constants::LST_CONVERSION_RATE, error::MonitoringError, + monitoring::lst::process_lst_data_by_pair, +}; +use mockall::predicate; +use starknet::{ + core::types::{BlockId, BlockTag, Felt, StarknetError}, + providers::ProviderError, +}; + +#[tokio::test] +async fn test_lst_conversion_rate_success() { + let pair = "XSTRK/STRK".to_string(); + let mock_rate = 1.2; // Valid rate > 1.0 + let mock_decimals = 8; + let mock_rate_felt = Felt::from((mock_rate * 10f64.powi(mock_decimals)) as u64); + + let mut mock_provider = init_test_config().await; + mock_provider + .expect_call() + .with( + predicate::always(), + predicate::eq(BlockId::Tag(BlockTag::Latest)), + ) + .returning(move |_, _| Ok(vec![mock_rate_felt])); + + set_test_config(&mock_provider).await; + + let result = process_lst_data_by_pair(pair).await; + assert!(result.is_ok()); + + let metric = LST_CONVERSION_RATE + .with_label_values(&["testnet", "XSTRK/STRK"]) + .get(); + assert!((metric - mock_rate).abs() < f64::EPSILON); +} + +#[tokio::test] +async fn test_lst_conversion_rate_below_one() { + let pair = "XSTRK/STRK".to_string(); + let mock_rate = 0.9; // Invalid rate < 1.0 + let mock_decimals = 8; + let mock_rate_felt = Felt::from((mock_rate * 10f64.powi(mock_decimals)) as u64); + + let mut mock_provider = init_test_config().await; + mock_provider + .expect_call() + .with( + predicate::always(), + predicate::eq(BlockId::Tag(BlockTag::Latest)), + ) + .returning(move |_, _| Ok(vec![mock_rate_felt])); + + set_test_config(&mock_provider).await; + + let result = process_lst_data_by_pair(pair).await; + assert!(matches!( + result, + Err(MonitoringError::Price(msg)) if msg.contains("<= 1") + )); +} + +#[tokio::test] +async fn test_lst_non_lst_pair() { + let pair = "BTC/USD".to_string(); + + let mock_provider = init_test_config().await; + set_test_config(&mock_provider).await; + + let result = process_lst_data_by_pair(pair).await; + assert!(result.is_ok()); +} + +#[tokio::test] +async fn test_lst_provider_error() { + let pair = "XSTRK/STRK".to_string(); + + let mut mock_provider = init_test_config().await; + mock_provider + .expect_call() + .with( + predicate::always(), + predicate::eq(BlockId::Tag(BlockTag::Latest)), + ) + .returning(|_, _| { + Err(ProviderError::StarknetError( + StarknetError::ValidationFailure("Test error".to_string()), + )) + }); + + set_test_config(&mock_provider).await; + + let result = process_lst_data_by_pair(pair).await; + assert!(matches!(result, Err(MonitoringError::OnChain(_)))); +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs index bf7f563..5e3e184 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -1,4 +1,173 @@ +use crate::config::{Config, ConfigInput, DataInfo, DataType, NetworkName}; +use arc_swap::ArcSwap; +use async_trait::async_trait; +use mockall::automock; +use starknet::{ + core::types::{BlockId, Felt, FunctionCall}, + providers::{jsonrpc::HttpTransport, JsonRpcClient, Provider, ProviderError}, +}; +use std::collections::HashMap; +use std::sync::Arc; +use std::{env, sync::Once}; +use tokio::sync::OnceCell; + +static INIT: Once = Once::new(); + +/// Initialize test environment with mock values +pub fn init_test_env() { + INIT.call_once(|| { + env::set_var("NETWORK", "testnet"); + env::set_var("ORACLE_ADDRESS", "0x1234567890"); + env::set_var("VRF_ADDRESS", "0x9876543210"); + env::set_var("SPOT_PAIRS", "XSTRK/STRK,BTC/USD"); + env::set_var("FUTURE_PAIRS", "BTC-PERP/USD"); + env::set_var("INDEXER_SERVICE_URL", "http://localhost:8000"); + env::set_var("RPC_URL", "http://localhost:5050"); + }); +} + +#[automock] +#[async_trait] +pub trait TestProvider: Send + Sync { + #[allow(dead_code)] + async fn call( + &self, + request: FunctionCall, + block_id: BlockId, + ) -> Result, ProviderError>; +} + +// Wrapper type for providers that can be either real or mock +#[derive(Clone)] +#[allow(dead_code)] +pub enum ProviderWrapper { + Real(Arc>), + Mock(Arc), +} + +impl ProviderWrapper { + #[allow(dead_code)] + pub async fn call( + &self, + request: FunctionCall, + block_id: BlockId, + ) -> Result, ProviderError> { + match self { + ProviderWrapper::Real(provider) => provider.call(request, block_id).await, + ProviderWrapper::Mock(provider) => provider.call(request, block_id).await, + } + } +} + +// Modified Network struct to use ProviderWrapper +#[derive(Clone)] +#[allow(dead_code)] +pub struct TestNetwork { + pub name: NetworkName, + pub provider: ProviderWrapper, + pub oracle_address: Felt, + pub vrf_address: Felt, + pub publisher_registry_address: Felt, +} + +impl Clone for MockTestProvider { + fn clone(&self) -> Self { + MockTestProvider::new() + } +} + +static TEST_CONFIG: OnceCell> = OnceCell::const_new(); + +pub async fn init_test_config() -> MockTestProvider { + init_test_env(); + let mock_provider = MockTestProvider::new(); + let config = create_mock_config(mock_provider.clone()); + + TEST_CONFIG + .get_or_init(|| async { ArcSwap::new(Arc::new(config)) }) + .await; + + mock_provider +} + +// Create a mock Config directly without using JsonRpcClient +#[allow(unused)] +pub fn create_mock_config(provider: MockTestProvider) -> Config { + let mut spot_decimals = HashMap::new(); + spot_decimals.insert("XSTRK/STRK".to_string(), 8); + spot_decimals.insert("BTC/USD".to_string(), 8); + + let mut future_decimals = HashMap::new(); + future_decimals.insert("BTC-PERP/USD".to_string(), 8); + + let mut spot_sources = HashMap::new(); + spot_sources.insert( + "XSTRK/STRK".to_string(), + vec!["source1".to_string(), "source2".to_string()], + ); + spot_sources.insert( + "BTC/USD".to_string(), + vec!["source1".to_string(), "source2".to_string()], + ); + + let mut future_sources = HashMap::new(); + future_sources.insert("BTC-PERP/USD".to_string(), vec!["source1".to_string()]); + + let mut data_info = HashMap::new(); + data_info.insert( + DataType::Spot, + DataInfo { + pairs: vec!["XSTRK/STRK".to_string(), "BTC/USD".to_string()], + sources: spot_sources, + decimals: spot_decimals, + table_name: "spot_entry".to_string(), + }, + ); + + data_info.insert( + DataType::Future, + DataInfo { + pairs: vec!["BTC-PERP/USD".to_string()], + sources: future_sources, + decimals: future_decimals, + table_name: "future_entry".to_string(), + }, + ); + + let mut publishers = HashMap::new(); + publishers.insert("publisher1".to_string(), Felt::from_hex_unchecked("0x123")); + + Config { + data_info, + publishers, + network: crate::config::Network { + name: NetworkName::Testnet, + provider: Arc::new(JsonRpcClient::new(HttpTransport::new( + url::Url::parse("http://localhost:5050").unwrap(), + ))), + oracle_address: Felt::from_hex_unchecked("0x1234567890"), + vrf_address: Felt::from_hex_unchecked("0x9876543210"), + publisher_registry_address: Felt::from_hex_unchecked("0x5555"), + }, + indexer_url: "http://localhost:8000".to_string(), + } +} + +/// Helper function to force initialize config for tests +pub async fn set_test_config(mock_provider: &MockTestProvider) { + let config = create_mock_config(mock_provider.clone()); + crate::config::config_force_init(ConfigInput { + network: config.network.name.clone(), + oracle_address: config.network.oracle_address, + vrf_address: config.network.vrf_address, + spot_pairs: config.data_info[&DataType::Spot].pairs.clone(), + future_pairs: config.data_info[&DataType::Future].pairs.clone(), + }) + .await; +} + mod common; +mod lst_tests; #[cfg(test)] mod monitoring;