Skip to content

Commit 4047a76

Browse files
fix(rates): add source fallback fetch path without mapping changes
1 parent c8f711d commit 4047a76

1 file changed

Lines changed: 169 additions & 17 deletions

File tree

crates/portal-rates/src/lib.rs

Lines changed: 169 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ struct FiatUnit {
6969
// country: Option<String>,
7070
}
7171

72-
#[derive(Debug, Deserialize, Clone)]
72+
#[derive(Debug, Deserialize, Clone, PartialEq, Eq, Hash)]
7373
#[cfg_attr(feature = "bindings", derive(uniffi::Enum))]
7474
enum Source {
7575
Yadio,
@@ -242,8 +242,12 @@ impl MarketAPI {
242242
}
243243
}
244244

245-
async fn fetch_price(self: Arc<Self>, unit: &FiatUnit) -> Result<Option<String>, RatesError> {
246-
let url = Self::build_url(&unit.source, &unit.end_point_key);
245+
async fn fetch_price_for_source(
246+
self: Arc<Self>,
247+
source: &Source,
248+
key: &str,
249+
) -> Result<Option<String>, RatesError> {
250+
let url = Self::build_url(source, key);
247251
let res = self
248252
.client
249253
.get(&url)
@@ -259,10 +263,61 @@ impl MarketAPI {
259263
.text()
260264
.await
261265
.map_err(|e| RatesError::HttpRequest(e.to_string()))?;
262-
let parsed = Self::parse_price_json(&text, &unit.source, &unit.end_point_key)?;
266+
let parsed = Self::parse_price_json(&text, source, key)?;
263267
Ok(Some(parsed))
264268
}
265269

270+
fn fallback_sources(primary: &Source) -> Vec<Source> {
271+
match primary {
272+
Source::Kraken => vec![Source::CoinGecko, Source::CoinDesk],
273+
Source::CoinGecko => vec![Source::Kraken, Source::CoinDesk],
274+
Source::Yadio => vec![Source::CoinGecko, Source::YadioConvert],
275+
Source::Exir => vec![Source::CoinGecko],
276+
Source::YadioConvert
277+
| Source::Coinpaprika
278+
| Source::Bitstamp
279+
| Source::Coinbase
280+
| Source::BNR
281+
| Source::CoinDesk => vec![Source::CoinGecko],
282+
}
283+
}
284+
285+
async fn resolve_price_with_fallback<F, Fut>(
286+
unit: &FiatUnit,
287+
mut fetcher: F,
288+
) -> Result<(String, Source), RatesError>
289+
where
290+
F: FnMut(&Source, &str) -> Fut,
291+
Fut: std::future::Future<Output = Result<Option<String>, RatesError>>,
292+
{
293+
let mut attempts = Vec::with_capacity(1 + Self::fallback_sources(&unit.source).len());
294+
attempts.push(unit.source.clone());
295+
attempts.extend(Self::fallback_sources(&unit.source));
296+
297+
for source in attempts {
298+
match fetcher(&source, &unit.end_point_key).await {
299+
Ok(Some(price_str)) => return Ok((price_str, source)),
300+
Ok(None) => {
301+
log::debug!(
302+
"No price from source={} for key={}",
303+
source,
304+
unit.end_point_key
305+
);
306+
}
307+
Err(e) => {
308+
log::warn!(
309+
"Price fetch failed from source={} for key={}: {}",
310+
source,
311+
unit.end_point_key,
312+
e
313+
);
314+
}
315+
}
316+
}
317+
318+
Err(RatesError::MarketDataFetchFailed)
319+
}
320+
266321
async fn fetch_market_data_internal(
267322
self: Arc<Self>,
268323
currency: &str,
@@ -274,21 +329,25 @@ impl MarketAPI {
274329
None => return Err(RatesError::UnsupportedCurrency),
275330
};
276331

277-
if let Some(price_str) = self.fetch_price(&unit).await? {
278-
if let Ok(rate) = price_str.parse::<f64>() {
279-
let data = MarketData {
280-
price: format!("{} {:.0}", unit.symbol, rate),
281-
rate: rate,
282-
source: unit.source.to_string(),
283-
};
284-
log::debug!("Market data fetched in {:?}", start.elapsed());
285-
return Ok(data);
286-
} else {
287-
return Err(RatesError::PriceParseFailed(price_str));
288-
}
332+
let (price_str, used_source) = Self::resolve_price_with_fallback(&unit, |source, key| {
333+
let api = self.clone();
334+
let source = source.clone();
335+
let key = key.to_string();
336+
async move { api.fetch_price_for_source(&source, &key).await }
337+
})
338+
.await?;
339+
340+
if let Ok(rate) = price_str.parse::<f64>() {
341+
let data = MarketData {
342+
price: format!("{} {:.0}", unit.symbol, rate),
343+
rate: rate,
344+
source: used_source.to_string(),
345+
};
346+
log::debug!("Market data fetched in {:?}", start.elapsed());
347+
return Ok(data);
289348
}
290349

291-
Err(RatesError::MarketDataFetchFailed)
350+
Err(RatesError::PriceParseFailed(price_str))
292351
}
293352
}
294353

@@ -328,6 +387,99 @@ impl MarketAPI {
328387
}
329388
}
330389

390+
#[tokio::test]
391+
async fn test_fallback_primary_success() -> Result<(), RatesError> {
392+
let unit = FiatUnit {
393+
end_point_key: "USD".to_string(),
394+
source: Source::Kraken,
395+
symbol: "$".to_string(),
396+
};
397+
398+
let attempts = std::sync::Arc::new(std::sync::Mutex::new(Vec::<Source>::new()));
399+
let attempts_closure = attempts.clone();
400+
401+
let (price, used_source) = MarketAPI::resolve_price_with_fallback(&unit, move |source, _key| {
402+
let attempts = attempts_closure.clone();
403+
let source = source.clone();
404+
async move {
405+
attempts.lock().unwrap().push(source.clone());
406+
if source == Source::Kraken {
407+
Ok(Some("60000.0".to_string()))
408+
} else {
409+
Ok(None)
410+
}
411+
}
412+
})
413+
.await?;
414+
415+
assert_eq!(price, "60000.0");
416+
assert_eq!(used_source, Source::Kraken);
417+
assert_eq!(attempts.lock().unwrap().as_slice(), &[Source::Kraken]);
418+
Ok(())
419+
}
420+
421+
#[tokio::test]
422+
async fn test_fallback_primary_fail_then_success() -> Result<(), RatesError> {
423+
let unit = FiatUnit {
424+
end_point_key: "USD".to_string(),
425+
source: Source::Kraken,
426+
symbol: "$".to_string(),
427+
};
428+
429+
let attempts = std::sync::Arc::new(std::sync::Mutex::new(Vec::<Source>::new()));
430+
let attempts_closure = attempts.clone();
431+
432+
let (price, used_source) = MarketAPI::resolve_price_with_fallback(&unit, move |source, _key| {
433+
let attempts = attempts_closure.clone();
434+
let source = source.clone();
435+
async move {
436+
attempts.lock().unwrap().push(source.clone());
437+
match source {
438+
Source::Kraken => Err(RatesError::HttpRequest("kraken down".to_string())),
439+
Source::CoinGecko => Ok(Some("61000.0".to_string())),
440+
_ => Ok(None),
441+
}
442+
}
443+
})
444+
.await?;
445+
446+
assert_eq!(price, "61000.0");
447+
assert_eq!(used_source, Source::CoinGecko);
448+
assert_eq!(
449+
attempts.lock().unwrap().as_slice(),
450+
&[Source::Kraken, Source::CoinGecko]
451+
);
452+
Ok(())
453+
}
454+
455+
#[tokio::test]
456+
async fn test_fallback_all_fail() {
457+
let unit = FiatUnit {
458+
end_point_key: "USD".to_string(),
459+
source: Source::Kraken,
460+
symbol: "$".to_string(),
461+
};
462+
463+
let attempts = std::sync::Arc::new(std::sync::Mutex::new(Vec::<Source>::new()));
464+
let attempts_closure = attempts.clone();
465+
466+
let result = MarketAPI::resolve_price_with_fallback(&unit, move |source, _key| {
467+
let attempts = attempts_closure.clone();
468+
let source = source.clone();
469+
async move {
470+
attempts.lock().unwrap().push(source);
471+
Ok(None)
472+
}
473+
})
474+
.await;
475+
476+
assert!(matches!(result, Err(RatesError::MarketDataFetchFailed)));
477+
assert_eq!(
478+
attempts.lock().unwrap().as_slice(),
479+
&[Source::Kraken, Source::CoinGecko, Source::CoinDesk]
480+
);
481+
}
482+
331483
#[tokio::test]
332484
async fn test_market_data_fetch_eur() -> Result<(), RatesError> {
333485
let api = MarketAPI::new()?;

0 commit comments

Comments
 (0)