@@ -69,58 +69,26 @@ impl Source {
6969 let currency = currency. to_uppercase ( ) ;
7070 let url = self . url ( & currency_lc, & currency) ;
7171
72- let resp : Value = client
72+ let response = client
7373 . get ( & url)
7474 . send ( )
7575 . await
76- . map_err ( |e| anyhow ! ( "Failed to request url {url} caused by: {:?}" , e. source( ) ) ) ?
76+ . map_err ( |e| anyhow ! ( "Request failed {url}: {:?}" , e. source( ) ) ) ?;
77+
78+ let status = response. status ( ) ;
79+
80+ let resp: Value = response
7781 . json ( )
7882 . await
79- . map_err ( |e| {
80- anyhow ! (
81- "Failed to decode response body from {url}, caused by: {:?}" ,
82- e. source( )
83- )
84- } ) ?;
85-
86- let reply_members = self . reply_members ( & currency_lc, & currency) ;
83+ . map_err ( |e| anyhow ! ( "Failed to parse JSON from {url}: {:?}" , e. source( ) ) ) ?;
8784
88- let mut current = & mut resp. clone ( ) ;
89- for member in reply_members {
90- if let Ok ( pos) = member. parse :: < usize > ( ) {
91- current = current. get_mut ( pos) . ok_or ( anyhow ! (
92- "Positional member `{}` not found in {} response: {}" ,
93- member,
94- self . name( ) ,
95- resp
96- ) ) ?;
97- } else {
98- current = current. get_mut ( & member) . ok_or ( anyhow ! (
99- "Member `{}` not found in {} response: {}" ,
100- member,
101- self . name( ) ,
102- resp
103- ) ) ?;
104- }
85+ if !status. is_success ( ) {
86+ return Err ( anyhow ! ( "HTTP error {status} from {url}: body={resp}" ) ) ;
10587 }
106- let price = match current {
107- Value :: Number ( number) => number
108- . as_f64 ( )
109- . ok_or ( anyhow ! ( "Json number price could not be converted to float" ) ) ?,
110- Value :: String ( string) => string
111- . parse :: < f64 > ( )
112- . map_err ( |e| anyhow ! ( "Price string could not be converted to float: {e}" ) ) ?,
113- _ => return Err ( anyhow ! ( "Price is invalid json type" ) ) ,
114- } ;
11588
116- if price == 0.0 {
117- log:: warn!( "{} returned 0.0 as price for {}" , self . name, currency) ;
118- return Err ( anyhow ! (
119- "{} returned 0.0 as price for {}" ,
120- self . name,
121- currency
122- ) ) ;
123- }
89+ let reply_members = self . reply_members ( & currency_lc, & currency) ;
90+
91+ let price = extract_price_from_response ( & resp, & reply_members, self . name ( ) , & currency) ?;
12492
12593 log:: info!(
12694 "Fetched price in {}ms from {}: {:.2} {currency}/BTC" ,
@@ -133,6 +101,40 @@ impl Source {
133101 }
134102}
135103
104+ fn extract_price_from_response (
105+ resp : & Value ,
106+ reply_members : & [ String ] ,
107+ name : & str ,
108+ currency : & str ,
109+ ) -> Result < f64 , anyhow:: Error > {
110+ let mut current = & mut resp. clone ( ) ;
111+ for member in reply_members {
112+ if let Ok ( pos) = member. parse :: < usize > ( ) {
113+ current = current. get_mut ( pos) . ok_or ( anyhow ! (
114+ "Positional member `{member}` not found in {name} response: {resp}"
115+ ) ) ?;
116+ } else {
117+ current = current. get_mut ( member) . ok_or ( anyhow ! (
118+ "Member `{member}` not found in {name} response: {resp}"
119+ ) ) ?;
120+ }
121+ }
122+ let price = match current {
123+ Value :: Number ( number) => number
124+ . as_f64 ( )
125+ . ok_or ( anyhow ! ( "Json number price could not be converted to float" ) ) ?,
126+ Value :: String ( string) => string
127+ . parse :: < f64 > ( )
128+ . map_err ( |e| anyhow ! ( "Price string could not be converted to float: {e}" ) ) ?,
129+ _ => return Err ( anyhow ! ( "Price is invalid json type" ) ) ,
130+ } ;
131+
132+ if price == 0.0 {
133+ log:: warn!( "{name} returned 0.0 as price for {currency}" ) ;
134+ return Err ( anyhow ! ( "{name} returned 0.0 as price for {currency}" ) ) ;
135+ }
136+ Ok ( price)
137+ }
136138struct SourceHealth {
137139 source : Source ,
138140 failures : u32 ,
@@ -535,12 +537,7 @@ impl BtcPriceOracle {
535537
536538 // Give a helpful error if the source name is unknown entirely
537539 if !inner. sources . contains_key ( source_name) {
538- let available = inner
539- . sources
540- . keys ( )
541- . cloned ( )
542- . collect :: < Vec < _ > > ( )
543- . join ( ", " ) ;
540+ let available = inner. sources . keys ( ) . cloned ( ) . collect :: < Vec < _ > > ( ) . join ( ", " ) ;
544541 return Err ( anyhow ! (
545542 "Unknown source `{source_name}`. Available sources: {available}"
546543 ) ) ;
@@ -551,13 +548,12 @@ impl BtcPriceOracle {
551548 . get ( currency)
552549 . ok_or_else ( || anyhow ! ( "No rates available for `{currency}`" ) ) ?;
553550
554- let price_cache = currency_cache
555- . prices
556- . get ( source_name)
557- . ok_or_else ( || anyhow ! (
551+ let price_cache = currency_cache. prices . get ( source_name) . ok_or_else ( || {
552+ anyhow ! (
558553 "Source `{source_name}` has no data for `{currency}`. \
559554 The source may not support this currency or is currently backing off."
560- ) ) ?;
555+ )
556+ } ) ?;
561557
562558 if price_cache. timestamp + SERVE_TTL <= Instant :: now ( ) {
563559 return Err ( anyhow ! ( "Cached rate from `{source_name}` is expired" ) ) ;
@@ -577,3 +573,164 @@ fn get_median(source_results: Vec<SourceResult>) -> f64 {
577573 f64:: midpoint ( prices[ mid - 1 ] , prices[ mid] )
578574 }
579575}
576+
577+ #[ test]
578+ fn test_sources ( ) {
579+ use crate :: add_default_sources;
580+ use serde_json:: json;
581+ let mut sources = Vec :: new ( ) ;
582+ add_default_sources ( & mut sources, false ) ;
583+ let mut responses = HashMap :: new ( ) ;
584+
585+ let coingecko_price = 72732f64 ;
586+ let coingecko_response = json ! ( { "bitcoin" : { "usd" : coingecko_price} } ) ;
587+ responses. insert ( "coingecko" , ( coingecko_response, coingecko_price) ) ;
588+
589+ let kraken_price = 72745.00000 ;
590+ let kraken_reponse = json ! ( {
591+ "error" : [ ] ,
592+ "result" : {
593+ "XXBTZUSD" : {
594+ "a" : [ "72745.00000" , "1" , "1.000" ] ,
595+ "b" : [ "72744.90000" , "3" , "3.000" ] ,
596+ "c" : [ kraken_price, "0.00033610" ] ,
597+ "v" : [ "299.69911717" , "640.87872182" ] ,
598+ "p" : [ "73231.75442" , "73435.07382" ] ,
599+ "t" : [ 17317 , 39634 ] ,
600+ "l" : [ "72613.70000" , "72613.70000" ] ,
601+ "h" : [ "73960.00000" , "74070.00000" ] ,
602+ "o" : "73569.90000" ,
603+ }
604+ } ,
605+ } ) ;
606+ responses. insert ( "kraken" , ( kraken_reponse, kraken_price) ) ;
607+
608+ let blockchain_info_price = 72749.07 ;
609+ let blockchain_info_reponse = json ! ( {
610+ "ARS" : {
611+ "15m" : 1.0250388182e8 ,
612+ "last" : 1.0250388182e8 ,
613+ "buy" : 1.0250388182e8 ,
614+ "sell" : 1.0250388182e8 ,
615+ "symbol" : "ARS" ,
616+ } ,
617+ "AUD" : {
618+ "15m" : 101319.81 ,
619+ "last" : 101319.81 ,
620+ "buy" : 101319.81 ,
621+ "sell" : 101319.81 ,
622+ "symbol" : "AUD" ,
623+ } ,
624+ "BRL" : {
625+ "15m" : 366724.43 ,
626+ "last" : 366724.43 ,
627+ "buy" : 366724.43 ,
628+ "sell" : 366724.43 ,
629+ "symbol" : "BRL" ,
630+ } ,
631+ "CAD" : {
632+ "15m" : 100512.08 ,
633+ "last" : 100512.08 ,
634+ "buy" : 100512.08 ,
635+ "sell" : 100512.08 ,
636+ "symbol" : "CAD" ,
637+ } ,
638+ "USD" : {
639+ "15m" : 72749.07 ,
640+ "last" : blockchain_info_price,
641+ "buy" : 72749.07 ,
642+ "sell" : 72749.07 ,
643+ "symbol" : "USD" ,
644+ } ,
645+ } ) ;
646+ responses. insert (
647+ "blockchain.info" ,
648+ ( blockchain_info_reponse, blockchain_info_price) ,
649+ ) ;
650+
651+ let bitstamp_price = 72748.45 ;
652+ let bitstamp_reponse = json ! ( {
653+ "timestamp" : "1780301548" ,
654+ "open" : "73568.00" ,
655+ "high" : "74094.65" ,
656+ "low" : "72611.68" ,
657+ "last" : bitstamp_price,
658+ "volume" : "619.76380694" ,
659+ "vwap" : "73542.01" ,
660+ "bid" : "72748.45" ,
661+ "ask" : "72748.46" ,
662+ "side" : "1" ,
663+ "open_24" : "73769.45" ,
664+ "percent_change_24" : "-1.38" ,
665+ "market_type" : "SPOT" ,
666+ } ) ;
667+ responses. insert ( "bitstamp" , ( bitstamp_reponse, bitstamp_price) ) ;
668+
669+ let coindesk_price = 72751.6828660636 ;
670+ let coindes_response = json ! ( {
671+ "Data" : {
672+ "BTC-USD" : {
673+ "TYPE" : "266" ,
674+ "MARKET" : "cadli" ,
675+ "INSTRUMENT" : "BTC-USD" ,
676+ "CCSEQ" : 1323841393 ,
677+ "VALUE" : coindesk_price,
678+ "VALUE_FLAG" : "UP" ,
679+ "VALUE_LAST_UPDATE_TS" : 1780301570 ,
680+ "VALUE_LAST_UPDATE_TS_NS" : 94000000 ,
681+ "LAST_UPDATE_QUANTITY" : 0.105 ,
682+ "LAST_UPDATE_QUOTE_QUANTITY" : 7642.36250656015 ,
683+ "LAST_UPDATE_VOLUME_TOP_TIER" : 0 ,
684+ "LAST_UPDATE_QUOTE_VOLUME_TOP_TIER" : 0 ,
685+ "LAST_UPDATE_VOLUME_DIRECT" : 0 ,
686+ "LAST_UPDATE_CCSEQ" : 1323894738 ,
687+ "CURRENT_HOUR_VOLUME" : 1574.29740546284 ,
688+ "CURRENT_HOUR_QUOTE_VOLUME" : 114448749.088967 ,
689+ "CURRENT_HOUR_VOLUME_TOP_TIER" : 815.533688797 ,
690+ "CURRENT_HOUR_QUOTE_VOLUME_TOP_TIER" : 59271744.4993065 ,
691+ "CURRENT_YEAR_CHANGE_PERCENTAGE" : -16.8891195575271 ,
692+ "MOVING_24_HOUR_VOLUME" : 127405.003580434 ,
693+ "MOVING_24_HOUR_QUOTE_VOLUME" : 9365693546.36435 ,
694+ "MOVING_24_HOUR_VOLUME_TOP_TIER" : 60402.3558377577 ,
695+ "MOVING_24_HOUR_QUOTE_VOLUME_TOP_TIER_DIRECT" : 867716024.677109 ,
696+ "MOVING_24_HOUR_OPEN" : 73850.0780201078 ,
697+ "MOVING_7_DAY_HIGH" : 77996.1623459993 ,
698+ "MOVING_7_DAY_LOW" : 72425.5243349043 ,
699+ "MOVING_7_DAY_TOTAL_INDEX_UPDATES" : 12822192 ,
700+ "MOVING_7_DAY_CHANGE" : -4535.8043571581 ,
701+ "MOVING_365_DAY_CHANGE_PERCENTAGE" : -31.154821757090602 ,
702+ "LIFETIME_FIRST_UPDATE_TS" : 1279408140 ,
703+ "LIFETIME_QUOTE_VOLUME_TOP_TIER_DIRECT" : 4851586542024.82 ,
704+ "LIFETIME_OPEN" : 0.04951 ,
705+ "LIFETIME_LOW" : 0.01 ,
706+ "LIFETIME_LOW_TS" : 1286572500 ,
707+ "LIFETIME_TOTAL_INDEX_UPDATES" : 1329464334 ,
708+ "LIFETIME_CHANGE" : 72751.6333560636 ,
709+ "LIFETIME_CHANGE_PERCENTAGE" : 146943311.16151 ,
710+ }
711+ } ,
712+ "Err" : { } ,
713+ } ) ;
714+ responses. insert ( "coindesk" , ( coindes_response, coindesk_price) ) ;
715+
716+ let coinbase_price = 72760.125 ;
717+ let coinbase_reponse =
718+ json ! ( { "data" : { "amount" : coinbase_price, "base" : "BTC" , "currency" : "USD" } } ) ;
719+ responses. insert ( "coinbase" , ( coinbase_reponse, coinbase_price) ) ;
720+
721+ let binance_price = 72715.35000000 ;
722+ let binance_reponse = json ! ( { "symbol" : "BTCUSD" , "price" : binance_price} ) ;
723+ responses. insert ( "binance" , ( binance_reponse, binance_price) ) ;
724+
725+ for source in & sources {
726+ let ( response, price) = responses. get ( source. name ( ) ) . unwrap ( ) ;
727+ let extracted_price = extract_price_from_response (
728+ response,
729+ & source. reply_members ( "usd" , "USD" ) ,
730+ & source. name ,
731+ "USD" ,
732+ )
733+ . unwrap ( ) ;
734+ assert_eq ! ( extracted_price, * price, "Failed for {}" , source. name( ) ) ;
735+ }
736+ }
0 commit comments