Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 88 additions & 12 deletions solana/rust/switchboard-on-demand-client/src/crossbar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,6 @@ pub struct SimulateSolanaFeedsResponse {
pub feed: String,
pub feedHash: String,
pub results: Vec<Option<Decimal>>,
#[serde(skip_deserializing, default)]
pub result: Option<Decimal>,
}

Expand All @@ -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<String>,
// The result is already computed by the server; hence, no median calculation here.
#[serde(skip_deserializing, default)]
pub result: Option<Decimal>,
#[serde(default)]
pub stdev: Option<Decimal>,
Expand Down Expand Up @@ -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<Decimal> = 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)
Expand Down Expand Up @@ -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<Decimal>.
/// 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,
Expand Down Expand Up @@ -429,15 +438,8 @@ impl CrossbarClient {
}

let mut responses: Vec<SimulateSolanaFeedsResponse> = 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<Decimal> = 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)
}
Expand Down Expand Up @@ -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<SimulateSolanaFeedsResponse> = serde_json::from_str(raw).unwrap();
assert_eq!(
responses[0].result,
Some("115.86634458".parse::<Decimal>().unwrap())
);

populate_solana_result_if_missing(&mut responses[0]);
assert_eq!(
responses[0].result,
Some("115.86634458".parse::<Decimal>().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<SimulateSolanaFeedsResponse> = serde_json::from_str(raw).unwrap();
populate_solana_result_if_missing(&mut responses[0]);
assert_eq!(responses[0].result, Some("99".parse::<Decimal>().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<SimulateSolanaFeedsResponse> = 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::<Decimal>().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<SimulateSuiFeedsResponse> = serde_json::from_str(raw).unwrap();
assert_eq!(
responses[0].result,
Some("123.45".parse::<Decimal>().unwrap())
);
assert_eq!(responses[0].stdev, Some("0.1".parse::<Decimal>().unwrap()));
assert_eq!(
responses[0].variance,
Some("0.01".parse::<Decimal>().unwrap())
);
}
}
3 changes: 1 addition & 2 deletions solana/rust/switchboard-on-demand-client/src/gateway.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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<serde_json::Value> = params
.feed_configs
Expand Down
94 changes: 82 additions & 12 deletions solana/rust/switchboard-on-demand/src/client/crossbar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ pub struct SimulateSolanaFeedsResponse {
pub feed: String,
pub feedHash: String,
pub results: Vec<Option<Decimal>>,
#[serde(skip_deserializing, default)]
pub result: Option<Decimal>,
}

Expand All @@ -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<String>,
// The result is already computed by the server; hence, no median calculation here.
#[serde(skip_deserializing, default)]
pub result: Option<Decimal>,
#[serde(default)]
pub stdev: Option<Decimal>,
Expand Down Expand Up @@ -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<Decimal> = 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)
Expand Down Expand Up @@ -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<Decimal>.
/// 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,
Expand Down Expand Up @@ -458,15 +467,8 @@ impl CrossbarClient {
}

let mut responses: Vec<SimulateSolanaFeedsResponse> = 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<Decimal> = 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)
}
Expand Down Expand Up @@ -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<SimulateSolanaFeedsResponse> = serde_json::from_str(raw).unwrap();
assert_eq!(
responses[0].result,
Some("115.86634458".parse::<Decimal>().unwrap())
);

populate_solana_result_if_missing(&mut responses[0]);
assert_eq!(
responses[0].result,
Some("115.86634458".parse::<Decimal>().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<SimulateSolanaFeedsResponse> = serde_json::from_str(raw).unwrap();
populate_solana_result_if_missing(&mut responses[0]);
assert_eq!(responses[0].result, Some("99".parse::<Decimal>().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<SimulateSolanaFeedsResponse> = 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::<Decimal>().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<SimulateSuiFeedsResponse> = serde_json::from_str(raw).unwrap();
assert_eq!(responses[0].result, Some("123.45".parse::<Decimal>().unwrap()));
assert_eq!(responses[0].stdev, Some("0.1".parse::<Decimal>().unwrap()));
assert_eq!(responses[0].variance, Some("0.01".parse::<Decimal>().unwrap()));
}
}
1 change: 0 additions & 1 deletion solana/rust/switchboard-on-demand/src/client/gateway.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<serde_json::Value> = params
.feed_configs
Expand Down
Loading