@@ -16,6 +16,23 @@ use tokio::time::interval;
1616use tokio:: time:: Duration ;
1717use 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 ) ]
2037pub struct StoreResponse {
2138 pub cid : String ,
@@ -34,6 +51,7 @@ pub struct FetchSolanaUpdatesResponse {
3451#[ derive( Serialize , Deserialize ) ]
3552pub 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+
141173impl 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) ]
544570mod 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