diff --git a/solana/rust/switchboard-on-demand-client/src/crossbar.rs b/solana/rust/switchboard-on-demand-client/src/crossbar.rs index 0097d07..ce2435a 100644 --- a/solana/rust/switchboard-on-demand-client/src/crossbar.rs +++ b/solana/rust/switchboard-on-demand-client/src/crossbar.rs @@ -119,7 +119,6 @@ pub struct SimulateSolanaFeedsResponse { pub feed: String, pub feedHash: String, pub results: Vec>, - #[serde(skip_deserializing, default)] pub result: Option, } @@ -130,7 +129,6 @@ pub struct SimulateSuiFeedsResponse { // The TS endpoint returns the results as strings. You can choose to parse them into Decimal if desired. pub results: Vec, // The result is already computed by the server; hence, no median calculation here. - #[serde(skip_deserializing, default)] pub result: Option, #[serde(default)] pub stdev: Option, @@ -227,6 +225,17 @@ fn cluster_type_to_string(cluster_type: ClusterType) -> String { .to_string() } +fn populate_solana_result_if_missing(response: &mut SimulateSolanaFeedsResponse) { + if response.result.is_some() { + return; + } + + let valid: Vec = response.results.iter().filter_map(|x| *x).collect(); + if !valid.is_empty() { + response.result = Some(median(valid.as_slice()).expect("Failed to compute median")); + } +} + impl Default for CrossbarClient { fn default() -> Self { Self::new("https://crossbar.switchboard.xyz", false) @@ -399,8 +408,8 @@ impl CrossbarClient { } /// Simulate feed responses from the crossbar gateway for Solana feeds. - /// In addition to deserializing the JSON, compute the median for each response - /// and store it in the `result` field as an Option. + /// Preserve a server-provided `result` when present, otherwise compute the + /// median from `results` and store it in the `result` field. pub async fn simulate_solana_feeds( &self, network: ClusterType, @@ -429,15 +438,8 @@ impl CrossbarClient { } let mut responses: Vec = serde_json::from_str(&raw)?; - // Compute the median result for each response for response in responses.iter_mut() { - // Collect non-None decimals - let valid: Vec = response.results.iter().filter_map(|x| *x).collect(); - response.result = if valid.is_empty() { - None - } else { - Some(median(valid.as_slice()).expect("Failed to compute median")) - }; + populate_solana_result_if_missing(response); } Ok(responses) } @@ -826,4 +828,78 @@ mod tests { let decoded = responses[0].decode_pull_ixns().unwrap(); assert!(decoded[0].data.starts_with(&CONSENSUS_DISCRIMINATOR)); } + + #[test] + fn simulate_solana_preserves_server_result_when_results_are_empty() { + let raw = r#"[{ + "feed":"D1MmZ3je8GCjLrTbWXotnZ797k6E56QkdyXyhPXZQocH", + "feedHash":"deadbeef", + "results":[], + "result":"115.86634458" + }]"#; + + let mut responses: Vec = serde_json::from_str(raw).unwrap(); + assert_eq!( + responses[0].result, + Some("115.86634458".parse::().unwrap()) + ); + + populate_solana_result_if_missing(&mut responses[0]); + assert_eq!( + responses[0].result, + Some("115.86634458".parse::().unwrap()) + ); + } + + #[test] + fn simulate_solana_does_not_overwrite_server_result_when_results_exist() { + let raw = r#"[{ + "feed":"D1MmZ3je8GCjLrTbWXotnZ797k6E56QkdyXyhPXZQocH", + "feedHash":"deadbeef", + "results":[1,3,2], + "result":"99" + }]"#; + + let mut responses: Vec = serde_json::from_str(raw).unwrap(); + populate_solana_result_if_missing(&mut responses[0]); + assert_eq!(responses[0].result, Some("99".parse::().unwrap())); + } + + #[test] + fn simulate_solana_computes_median_when_result_is_missing() { + let raw = r#"[{ + "feed":"D1MmZ3je8GCjLrTbWXotnZ797k6E56QkdyXyhPXZQocH", + "feedHash":"deadbeef", + "results":[1,3,2] + }]"#; + + let mut responses: Vec = serde_json::from_str(raw).unwrap(); + assert_eq!(responses[0].result, None); + + populate_solana_result_if_missing(&mut responses[0]); + assert_eq!(responses[0].result, Some("2".parse::().unwrap())); + } + + #[test] + fn simulate_sui_deserializes_result_stdev_and_variance_strings() { + let raw = r#"[{ + "feed":"feed-1", + "feedHash":"deadbeef", + "results":[], + "result":"123.45", + "stdev":"0.1", + "variance":"0.01" + }]"#; + + let responses: Vec = serde_json::from_str(raw).unwrap(); + assert_eq!( + responses[0].result, + Some("123.45".parse::().unwrap()) + ); + assert_eq!(responses[0].stdev, Some("0.1".parse::().unwrap())); + assert_eq!( + responses[0].variance, + Some("0.01".parse::().unwrap()) + ); + } } diff --git a/solana/rust/switchboard-on-demand-client/src/gateway.rs b/solana/rust/switchboard-on-demand-client/src/gateway.rs index d672596..41bcf97 100644 --- a/solana/rust/switchboard-on-demand-client/src/gateway.rs +++ b/solana/rust/switchboard-on-demand-client/src/gateway.rs @@ -1,6 +1,6 @@ -use protos::oracle_job::OracleJob; use base64::prelude::*; use prost::Message; +use protos::oracle_job::OracleJob; use reqwest::header::CONTENT_TYPE; use reqwest::Client; use serde::{Deserialize, Serialize}; @@ -306,7 +306,6 @@ impl Gateway { "{}/gateway/api/v1/fetch_signatures_consensus", self.gateway_url ); - println!("Fetching signatures from: {}", url); // Build feed_requests array from feed_configs let feed_requests: Vec = params .feed_configs diff --git a/solana/rust/switchboard-on-demand/src/client/crossbar.rs b/solana/rust/switchboard-on-demand/src/client/crossbar.rs index 61ca0c4..6a385a4 100644 --- a/solana/rust/switchboard-on-demand/src/client/crossbar.rs +++ b/solana/rust/switchboard-on-demand/src/client/crossbar.rs @@ -73,7 +73,6 @@ pub struct SimulateSolanaFeedsResponse { pub feed: String, pub feedHash: String, pub results: Vec>, - #[serde(skip_deserializing, default)] pub result: Option, } @@ -84,7 +83,6 @@ pub struct SimulateSuiFeedsResponse { // The TS endpoint returns the results as strings. You can choose to parse them into Decimal if desired. pub results: Vec, // The result is already computed by the server; hence, no median calculation here. - #[serde(skip_deserializing, default)] pub result: Option, #[serde(default)] pub stdev: Option, @@ -181,6 +179,17 @@ fn cluster_type_to_string(cluster_type: ClusterType) -> String { .to_string() } +fn populate_solana_result_if_missing(response: &mut SimulateSolanaFeedsResponse) { + if response.result.is_some() { + return; + } + + let valid: Vec = response.results.iter().filter_map(|x| *x).collect(); + if !valid.is_empty() { + response.result = Some(median(valid).expect("Failed to compute median")); + } +} + impl Default for CrossbarClient { fn default() -> Self { Self::new("https://crossbar.switchboard.xyz", false) @@ -423,8 +432,8 @@ impl CrossbarClient { } /// Simulate feed responses from the crossbar gateway for Solana feeds. - /// In addition to deserializing the JSON, compute the median for each response - /// and store it in the `result` field as an Option. + /// Preserve a server-provided `result` when present, otherwise compute the + /// median from `results` and store it in the `result` field. pub async fn simulate_solana_feeds( &self, network: ClusterType, @@ -458,15 +467,8 @@ impl CrossbarClient { } let mut responses: Vec = serde_json::from_str(&raw)?; - // Compute the median result for each response for response in responses.iter_mut() { - // Collect non-None decimals - let valid: Vec = response.results.iter().filter_map(|x| *x).collect(); - response.result = if valid.is_empty() { - None - } else { - Some(median(valid).expect("Failed to compute median")) - }; + populate_solana_result_if_missing(response); } Ok(responses) } @@ -901,4 +903,72 @@ mod tests { assert!(err.to_string().contains("missing field")); } + + #[test] + fn simulate_solana_preserves_server_result_when_results_are_empty() { + let raw = r#"[{ + "feed":"D1MmZ3je8GCjLrTbWXotnZ797k6E56QkdyXyhPXZQocH", + "feedHash":"deadbeef", + "results":[], + "result":"115.86634458" + }]"#; + + let mut responses: Vec = serde_json::from_str(raw).unwrap(); + assert_eq!( + responses[0].result, + Some("115.86634458".parse::().unwrap()) + ); + + populate_solana_result_if_missing(&mut responses[0]); + assert_eq!( + responses[0].result, + Some("115.86634458".parse::().unwrap()) + ); + } + + #[test] + fn simulate_solana_does_not_overwrite_server_result_when_results_exist() { + let raw = r#"[{ + "feed":"D1MmZ3je8GCjLrTbWXotnZ797k6E56QkdyXyhPXZQocH", + "feedHash":"deadbeef", + "results":[1,3,2], + "result":"99" + }]"#; + + let mut responses: Vec = serde_json::from_str(raw).unwrap(); + populate_solana_result_if_missing(&mut responses[0]); + assert_eq!(responses[0].result, Some("99".parse::().unwrap())); + } + + #[test] + fn simulate_solana_computes_median_when_result_is_missing() { + let raw = r#"[{ + "feed":"D1MmZ3je8GCjLrTbWXotnZ797k6E56QkdyXyhPXZQocH", + "feedHash":"deadbeef", + "results":[1,3,2] + }]"#; + + let mut responses: Vec = serde_json::from_str(raw).unwrap(); + assert_eq!(responses[0].result, None); + + populate_solana_result_if_missing(&mut responses[0]); + assert_eq!(responses[0].result, Some("2".parse::().unwrap())); + } + + #[test] + fn simulate_sui_deserializes_result_stdev_and_variance_strings() { + let raw = r#"[{ + "feed":"feed-1", + "feedHash":"deadbeef", + "results":[], + "result":"123.45", + "stdev":"0.1", + "variance":"0.01" + }]"#; + + let responses: Vec = serde_json::from_str(raw).unwrap(); + assert_eq!(responses[0].result, Some("123.45".parse::().unwrap())); + assert_eq!(responses[0].stdev, Some("0.1".parse::().unwrap())); + assert_eq!(responses[0].variance, Some("0.01".parse::().unwrap())); + } } diff --git a/solana/rust/switchboard-on-demand/src/client/gateway.rs b/solana/rust/switchboard-on-demand/src/client/gateway.rs index 15643aa..feb40d0 100644 --- a/solana/rust/switchboard-on-demand/src/client/gateway.rs +++ b/solana/rust/switchboard-on-demand/src/client/gateway.rs @@ -306,7 +306,6 @@ impl Gateway { "{}/gateway/api/v1/fetch_signatures_consensus", self.gateway_url ); - println!("Fetching signatures from: {}", url); // Build feed_requests array from feed_configs let feed_requests: Vec = params .feed_configs