Skip to content

Commit 99cf3f2

Browse files
Use result even when results is empty in sim
1 parent b6a7294 commit 99cf3f2

File tree

1 file changed

+73
-21
lines changed
  • solana/rust/switchboard-on-demand-client/src

1 file changed

+73
-21
lines changed

solana/rust/switchboard-on-demand-client/src/crossbar.rs

Lines changed: 73 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,23 @@ use tokio::time::interval;
1616
use tokio::time::Duration;
1717
use tokio_stream::wrappers::IntervalStream;
1818

19+
fn de_decimal_opt<'de, D>(deserializer: D) -> Result<Option<Decimal>, D::Error>
20+
where
21+
D: Deserializer<'de>,
22+
{
23+
let v = serde_json::Value::deserialize(deserializer)?;
24+
match v {
25+
serde_json::Value::Null => Ok(None),
26+
serde_json::Value::String(s) => s.parse::<Decimal>().map(Some).map_err(DeError::custom),
27+
serde_json::Value::Number(n) => n
28+
.to_string()
29+
.parse::<Decimal>()
30+
.map(Some)
31+
.map_err(DeError::custom),
32+
_ => Err(DeError::custom("invalid decimal type")),
33+
}
34+
}
35+
1936
#[derive(Serialize, Deserialize)]
2037
pub struct StoreResponse {
2138
pub cid: String,
@@ -34,6 +51,7 @@ pub struct FetchSolanaUpdatesResponse {
3451
#[derive(Serialize, Deserialize)]
3552
pub struct Response {
3653
pub oracle: String,
54+
#[serde(default, deserialize_with = "de_decimal_opt")]
3755
pub result: Option<Decimal>,
3856
pub errors: String,
3957
}
@@ -43,7 +61,7 @@ pub struct SimulateSolanaFeedsResponse {
4361
pub feed: String,
4462
pub feedHash: String,
4563
pub results: Vec<Option<Decimal>>,
46-
#[serde(skip_deserializing, default)]
64+
#[serde(default, deserialize_with = "de_decimal_opt")]
4765
pub result: Option<Decimal>,
4866
}
4967

@@ -54,11 +72,13 @@ pub struct SimulateSuiFeedsResponse {
5472
// The TS endpoint returns the results as strings. You can choose to parse them into Decimal if desired.
5573
pub results: Vec<String>,
5674
// The result is already computed by the server; hence, no median calculation here.
57-
#[serde(skip_deserializing, default)]
75+
#[serde(default, deserialize_with = "de_decimal_opt")]
5876
pub result: Option<Decimal>,
5977
#[serde(default)]
78+
#[serde(deserialize_with = "de_decimal_opt")]
6079
pub stdev: Option<Decimal>,
6180
#[serde(default)]
81+
#[serde(deserialize_with = "de_decimal_opt")]
6282
pub variance: Option<Decimal>,
6383
}
6484

@@ -138,6 +158,18 @@ fn cluster_type_to_string(cluster_type: ClusterType) -> String {
138158
.to_string()
139159
}
140160

161+
fn compute_simulate_solana_result_if_missing(response: &mut SimulateSolanaFeedsResponse) {
162+
if response.result.is_some() {
163+
return;
164+
}
165+
166+
// Collect non-None decimals and compute median.
167+
let valid: Vec<Decimal> = response.results.iter().copied().flatten().collect();
168+
if !valid.is_empty() {
169+
response.result = Some(median(valid.as_slice()).expect("Failed to compute median"));
170+
}
171+
}
172+
141173
impl Default for CrossbarClient {
142174
fn default() -> Self {
143175
Self::new("https://crossbar.switchboard.xyz", false)
@@ -284,15 +316,9 @@ impl CrossbarClient {
284316
}
285317

286318
let mut responses: Vec<SimulateSolanaFeedsResponse> = serde_json::from_str(&raw)?;
287-
// Compute the median result for each response
288319
for response in responses.iter_mut() {
289-
// Collect non-None decimals
290-
let valid: Vec<Decimal> = response.results.iter().filter_map(|x| *x).collect();
291-
response.result = if valid.is_empty() {
292-
None
293-
} else {
294-
Some(median(valid.as_slice()).expect("Failed to compute median"))
295-
};
320+
// Prefer server-provided `result`; fall back to median(results) if absent.
321+
compute_simulate_solana_result_if_missing(response);
296322
}
297323
Ok(responses)
298324
}
@@ -543,16 +569,42 @@ impl CrossbarClient {
543569
#[cfg(test)]
544570
mod tests {
545571
use super::*;
546-
use std::str::FromStr;
547-
548-
#[tokio::test]
549-
async fn test_crossbar_client_default_initialization() {
550-
let key = Pubkey::from_str("D1MmZ3je8GCjLrTbWXotnZ797k6E56QkdyXyhPXZQocH").unwrap();
551-
let client = CrossbarClient::default();
552-
let resp = client
553-
.simulate_solana_feeds(ClusterType::MainnetBeta, &[key])
554-
.await
555-
.unwrap();
556-
println!("{:?}", resp);
572+
573+
#[test]
574+
fn simulate_solana_deserializes_result_string_even_if_results_empty() {
575+
let raw = r#"[{
576+
"feed":"D1MmZ3je8GCjLrTbWXotnZ797k6E56QkdyXyhPXZQocH",
577+
"feedHash":"deadbeef",
578+
"results":[],
579+
"result":"115.86634458"
580+
}]"#;
581+
582+
let mut responses: Vec<SimulateSolanaFeedsResponse> = serde_json::from_str(raw).unwrap();
583+
assert_eq!(
584+
responses[0].result,
585+
Some("115.86634458".parse::<Decimal>().unwrap())
586+
);
587+
588+
// Ensure our fallback computation doesn't overwrite a valid server result.
589+
compute_simulate_solana_result_if_missing(&mut responses[0]);
590+
assert_eq!(
591+
responses[0].result,
592+
Some("115.86634458".parse::<Decimal>().unwrap())
593+
);
594+
}
595+
596+
#[test]
597+
fn simulate_solana_computes_median_from_results_when_result_missing() {
598+
let raw = r#"[{
599+
"feed":"D1MmZ3je8GCjLrTbWXotnZ797k6E56QkdyXyhPXZQocH",
600+
"feedHash":"deadbeef",
601+
"results":[1,3,2]
602+
}]"#;
603+
604+
let mut responses: Vec<SimulateSolanaFeedsResponse> = serde_json::from_str(raw).unwrap();
605+
assert_eq!(responses[0].result, None);
606+
607+
compute_simulate_solana_result_if_missing(&mut responses[0]);
608+
assert_eq!(responses[0].result, Some("2".parse::<Decimal>().unwrap()));
557609
}
558610
}

0 commit comments

Comments
 (0)