diff --git a/assets/fiatUnits.json b/assets/fiatUnits.json index 032d4e8b..5a8363ad 100644 --- a/assets/fiatUnits.json +++ b/assets/fiatUnits.json @@ -13,17 +13,10 @@ "symbol": "د.إ.", "country": "United Arab Emirates (UAE Dirham)" }, - "AMD": { - "endPointKey": "AMD", - "locale": "hy-AM", - "source": "CoinDesk", - "symbol": "֏", - "country": "Armenia (Armenian Dram)" - }, "ANG": { "endPointKey": "ANG", "locale": "en-SX", - "source": "YadioConvert", + "source": "Yadio", "symbol": "ƒ", "country": "Sint Maarten (Netherlands Antillean Guilder)" }, @@ -37,14 +30,14 @@ "AUD": { "endPointKey": "AUD", "locale": "en-AU", - "source": "CoinGecko", + "source": "Kraken", "symbol": "$", "country": "Australia (Australian Dollar)" }, "AWG": { "endPointKey": "AWG", "locale": "nl-AW", - "source": "CoinDesk", + "source": "Yadio", "symbol": "ƒ", "country": "Aruba (Aruban Florin)" }, @@ -65,14 +58,14 @@ "CAD": { "endPointKey": "CAD", "locale": "en-CA", - "source": "CoinGecko", + "source": "Kraken", "symbol": "$", "country": "Canada (Canadian Dollar)" }, "CHF": { "endPointKey": "CHF", "locale": "de-CH", - "source": "CoinGecko", + "source": "Kraken", "symbol": "CHF", "country": "Switzerland (Swiss Franc)" }, @@ -86,14 +79,14 @@ "CNY": { "endPointKey": "CNY", "locale": "zh-CN", - "source": "Coinbase", + "source": "CoinGecko", "symbol": "¥", "country": "China (Chinese Yuan)" }, "COP": { "endPointKey": "COP", "locale": "es-CO", - "source": "CoinDesk", + "source": "Yadio", "symbol": "$", "country": "Colombia (Colombian Peso)" }, @@ -132,13 +125,6 @@ "symbol": "HK$", "country": "Hong Kong (Hong Kong Dollar)" }, - "HRK": { - "endPointKey": "HRK", - "locale": "hr-HR", - "source": "CoinDesk", - "symbol": "HRK", - "country": "Croatia (Croatian Kuna)" - }, "HUF": { "endPointKey": "HUF", "locale": "hu-HU", @@ -163,7 +149,7 @@ "INR": { "endPointKey": "INR", "locale": "hi-IN", - "source": "coinpaprika", + "source": "CoinGecko", "symbol": "₹", "country": "India (Indian Rupee)" }, @@ -184,21 +170,21 @@ "ISK": { "endPointKey": "ISK", "locale": "is-IS", - "source": "CoinDesk", + "source": "Yadio", "symbol": "kr", "country": "Iceland (Icelandic Króna)" }, "JPY": { "endPointKey": "JPY", "locale": "ja-JP", - "source": "CoinGecko", + "source": "Kraken", "symbol": "¥", "country": "Japan (Japanese Yen)" }, "KES": { "endPointKey": "KES", "locale": "en-KE", - "source": "CoinDesk", + "source": "Yadio", "symbol": "Ksh", "country": "Kenya (Kenyan Shilling)" }, @@ -219,7 +205,7 @@ "LBP": { "endPointKey": "LBP", "locale": "ar-LB", - "source": "YadioConvert", + "source": "Yadio", "symbol": "ل.ل.", "country": "Lebanon (Lebanese Pound)" }, @@ -244,13 +230,6 @@ "symbol": "RM", "country": "Malaysia (Malaysian Ringgit)" }, - "MZN": { - "endPointKey": "MZN", - "locale": "seh-MZ", - "source": "CoinDesk", - "symbol": "MTn", - "country": "Mozambique (Mozambican Metical)" - }, "NGN": { "endPointKey": "NGN", "locale": "en-NG", @@ -275,7 +254,7 @@ "OMR": { "endPointKey": "OMR", "locale": "ar-OM", - "source": "CoinDesk", + "source": "Yadio", "symbol": "ر.ع.", "country": "Oman (Omani Rial)" }, @@ -296,28 +275,28 @@ "PYG": { "endPointKey": "PYG", "locale": "es-PY", - "source": "CoinDesk", + "source": "Yadio", "symbol": "₲", "country": "Paraguay (Paraguayan Guarani)" }, "QAR": { "endPointKey": "QAR", "locale": "ar-QA", - "source": "CoinDesk", + "source": "Yadio", "symbol": "ر.ق.", "country": "Qatar (Qatari Riyal)" }, "RON": { "endPointKey": "RON", "locale": "ro-RO", - "source": "BNR", + "source": "Yadio", "symbol": "lei", "country": "Romania (Romanian Leu)" }, "RSD": { "endPointKey": "RSD", "locale": "sr-RS", - "source": "CoinDesk", + "source": "Yadio", "symbol": "DIN", "country": "Serbia (Serbian Dinar)" }, @@ -373,7 +352,7 @@ "TZS": { "endPointKey": "TZS", "locale": "en-TZ", - "source": "CoinDesk", + "source": "Yadio", "symbol": "TSh", "country": "Tanzania (Tanzanian Shilling)" }, @@ -387,14 +366,14 @@ "UGX": { "endPointKey": "UGX", "locale": "en-UG", - "source": "CoinDesk", + "source": "Yadio", "symbol": "USh", "country": "Uganda (Ugandan Shilling)" }, "UYU": { "endPointKey": "UYU", "locale": "es-UY", - "source": "CoinDesk", + "source": "Yadio", "symbol": "$", "country": "Uruguay (Uruguayan Peso)" }, @@ -415,7 +394,7 @@ "XAF": { "endPointKey": "XAF", "locale": "fr-CF", - "source": "CoinDesk", + "source": "Yadio", "symbol": "Fr", "country": "Central African Republic (Central African Franc)" }, @@ -429,8 +408,127 @@ "GHS": { "endPointKey": "GHS", "locale": "en-GH", - "source": "CoinDesk", + "source": "Yadio", "symbol": "₵", "country": "Ghana (Ghanaian Cedi)" + }, + "VND": { + "endPointKey": "VND", + "locale": "vi-VN", + "source": "CoinGecko", + "symbol": "₫", + "country": "Vietnam (Vietnamese Dong)" + }, + "PKR": { + "endPointKey": "PKR", + "locale": "ur-PK", + "source": "CoinGecko", + "symbol": "₨", + "country": "Pakistan (Pakistani Rupee)" + }, + "BDT": { + "endPointKey": "BDT", + "locale": "bn-BD", + "source": "CoinGecko", + "symbol": "৳", + "country": "Bangladesh (Bangladeshi Taka)" + }, + "MMK": { + "endPointKey": "MMK", + "locale": "my-MM", + "source": "CoinGecko", + "symbol": "K", + "country": "Myanmar (Myanmar Kyat)" + }, + "EGP": { + "endPointKey": "EGP", + "locale": "ar-EG", + "source": "Yadio", + "symbol": "ج.م.", + "country": "Egypt (Egyptian Pound)" + }, + "MAD": { + "endPointKey": "MAD", + "locale": "ar-MA", + "source": "Yadio", + "symbol": "د.م.", + "country": "Morocco (Moroccan Dirham)" + }, + "DZD": { + "endPointKey": "DZD", + "locale": "ar-DZ", + "source": "Yadio", + "symbol": "د.ج.", + "country": "Algeria (Algerian Dinar)" + }, + "JOD": { + "endPointKey": "JOD", + "locale": "ar-JO", + "source": "Yadio", + "symbol": "د.أ.", + "country": "Jordan (Jordanian Dinar)" + }, + "PEN": { + "endPointKey": "PEN", + "locale": "es-PE", + "source": "Yadio", + "symbol": "S/", + "country": "Peru (Peruvian Sol)" + }, + "CRC": { + "endPointKey": "CRC", + "locale": "es-CR", + "source": "Yadio", + "symbol": "₡", + "country": "Costa Rica (Costa Rican Colón)" + }, + "DOP": { + "endPointKey": "DOP", + "locale": "es-DO", + "source": "Yadio", + "symbol": "RD$", + "country": "Dominican Republic (Dominican Peso)" + }, + "GTQ": { + "endPointKey": "GTQ", + "locale": "es-GT", + "source": "Yadio", + "symbol": "Q", + "country": "Guatemala (Guatemalan Quetzal)" + }, + "BOB": { + "endPointKey": "BOB", + "locale": "es-BO", + "source": "Yadio", + "symbol": "Bs.", + "country": "Bolivia (Bolivian Boliviano)" + }, + "BGN": { + "endPointKey": "BGN", + "locale": "bg-BG", + "source": "Yadio", + "symbol": "лв.", + "country": "Bulgaria (Bulgarian Lev)" + }, + "GEL": { + "endPointKey": "GEL", + "locale": "ka-GE", + "source": "CoinGecko", + "symbol": "₾", + "country": "Georgia (Georgian Lari)" + }, + "KZT": { + "endPointKey": "KZT", + "locale": "kk-KZ", + "source": "Yadio", + "symbol": "₸", + "country": "Kazakhstan (Kazakhstani Tenge)" + }, + "BYN": { + "endPointKey": "BYN", + "locale": "be-BY", + "source": "Yadio", + "symbol": "Br", + "country": "Belarus (Belarusian Ruble)" } } \ No newline at end of file diff --git a/rates/src/lib.rs b/rates/src/lib.rs index 3bf54fec..9ea5f67e 100644 --- a/rates/src/lib.rs +++ b/rates/src/lib.rs @@ -24,33 +24,18 @@ pub enum RatesError { #[error("Missing price in Yadio response")] MissingYadioPrice, - #[error("Missing rate in YadioConvert response")] - MissingYadioConvertRate, - #[error("Missing CoinGecko price")] MissingCoinGeckoPrice, #[error("Missing 'last' price")] MissingLastPrice, - #[error("Missing Coinpaprika INR price")] - MissingCoinpaprikaInrPrice, - - #[error("Missing Coinbase amount")] - MissingCoinbaseAmount, - #[error("Missing Kraken close price")] MissingKrakenClosePrice, - #[error("Missing CoinDesk field")] - MissingCoinDeskField, - #[error("Unsupported currency")] UnsupportedCurrency, - #[error("Unsupported source or malformed JSON")] - UnsupportedSource, - #[error("Failed to fetch market data")] MarketDataFetchFailed, @@ -73,31 +58,30 @@ struct FiatUnit { #[cfg_attr(feature = "bindings", derive(uniffi::Enum))] enum Source { Yadio, - YadioConvert, Exir, - #[serde(rename = "coinpaprika")] - Coinpaprika, - Bitstamp, - Coinbase, CoinGecko, - BNR, Kraken, - CoinDesk, +} + +impl Source { + /// Returns the next fallback source in the hierarchy, if any. + fn fallback(&self) -> Option { + match self { + Source::Kraken => Some(Source::CoinGecko), + Source::Yadio => Some(Source::CoinGecko), + Source::Exir => None, // Specialized for Iranian market + Source::CoinGecko => None, // Catch-all, no fallback + } + } } impl fmt::Display for Source { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Source::Yadio => write!(f, "yadio"), - Source::YadioConvert => write!(f, "yadio_convert"), Source::Exir => write!(f, "exir"), - Source::Coinpaprika => write!(f, "coinpaprika"), - Source::Bitstamp => write!(f, "bitstamp"), - Source::Coinbase => write!(f, "coinbase"), Source::CoinGecko => write!(f, "coingecko"), - Source::BNR => write!(f, "bnr"), Source::Kraken => write!(f, "kraken"), - Source::CoinDesk => write!(f, "coindesk"), } } } @@ -135,40 +119,17 @@ impl MarketAPI { fn build_url(source: &Source, key: &str) -> String { match source { Source::Yadio => format!("https://api.yadio.io/json/{}", key), - Source::YadioConvert => format!("https://api.yadio.io/convert/1/BTC/{}", key), Source::Exir => "https://api.exir.io/v1/ticker?symbol=btc-irt".to_string(), - Source::Coinpaprika => { - "https://api.coinpaprika.com/v1/tickers/btc-bitcoin?quotes=INR".to_string() - } - Source::Bitstamp => { - format!( - "https://www.bitstamp.net/api/v2/ticker/btc{}", - key.to_lowercase() - ) - } - Source::Coinbase => { - format!( - "https://api.coinbase.com/v2/prices/BTC-{}/buy", - key.to_uppercase() - ) - } Source::CoinGecko => format!( "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies={}", key.to_lowercase() ), - Source::BNR => "https://www.bnr.ro/nbrfxrates.xml".to_string(), Source::Kraken => { format!( "https://api.kraken.com/0/public/Ticker?pair=XXBTZ{}", key.to_uppercase() ) } - Source::CoinDesk => { - format!( - "https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms={}", - key.to_uppercase() - ) - } } } @@ -185,13 +146,6 @@ impl MarketAPI { .ok_or(RatesError::MissingYadioPrice)?; Ok(price.to_string()) } - Source::YadioConvert => { - let rate = v - .get("rate") - .and_then(Value::as_str) - .ok_or(RatesError::MissingYadioConvertRate)?; - Ok(rate.to_string()) - } Source::CoinGecko => { let val = v .get("bitcoin") @@ -199,29 +153,13 @@ impl MarketAPI { .ok_or(RatesError::MissingCoinGeckoPrice)?; Ok(val.to_string()) } - Source::Exir | Source::Bitstamp => { + Source::Exir => { let val = v .get("last") .and_then(Value::as_str) .ok_or(RatesError::MissingLastPrice)?; Ok(val.to_string()) } - Source::Coinpaprika => { - let val = v - .get("quotes") - .and_then(|q| q.get("INR")) - .and_then(|inr| inr.get("price")) - .ok_or(RatesError::MissingCoinpaprikaInrPrice)?; - Ok(val.to_string()) - } - Source::Coinbase => { - let val = v - .get("data") - .and_then(|d| d.get("amount")) - .and_then(Value::as_str) - .ok_or(RatesError::MissingCoinbaseAmount)?; - Ok(val.to_string()) - } Source::Kraken => { let val = v .get("result") @@ -232,18 +170,15 @@ impl MarketAPI { .ok_or(RatesError::MissingKrakenClosePrice)?; Ok(val.to_string()) } - Source::CoinDesk => { - let val = v - .get(&key.to_uppercase()) - .ok_or(RatesError::MissingCoinDeskField)?; - Ok(val.to_string()) - } - _ => Err(RatesError::UnsupportedSource), } } - async fn fetch_price(self: Arc, unit: &FiatUnit) -> Result, RatesError> { - let url = Self::build_url(&unit.source, &unit.end_point_key); + async fn fetch_price_with_source( + &self, + source: &Source, + key: &str, + ) -> Result, RatesError> { + let url = Self::build_url(source, key); let res = self .client .get(&url) @@ -259,8 +194,11 @@ impl MarketAPI { .text() .await .map_err(|e| RatesError::HttpRequest(e.to_string()))?; - let parsed = Self::parse_price_json(&text, &unit.source, &unit.end_point_key)?; - Ok(Some(parsed)) + + match Self::parse_price_json(&text, source, key) { + Ok(parsed) => Ok(Some(parsed)), + Err(_) => Ok(None), // Parsing failed, treat as source failure for fallback + } } async fn fetch_market_data_internal( @@ -274,17 +212,36 @@ impl MarketAPI { None => return Err(RatesError::UnsupportedCurrency), }; - if let Some(price_str) = self.fetch_price(&unit).await? { - if let Ok(rate) = price_str.parse::() { - let data = MarketData { - price: format!("{} {:.0}", unit.symbol, rate), - rate: rate, - source: unit.source.to_string(), - }; - log::debug!("Market data fetched in {:?}", start.elapsed()); - return Ok(data); - } else { - return Err(RatesError::PriceParseFailed(price_str)); + let mut current_source = unit.source.clone(); + + loop { + if let Some(price_str) = self + .fetch_price_with_source(¤t_source, &unit.end_point_key) + .await? + { + if let Ok(rate) = price_str.parse::() { + let data = MarketData { + price: format!("{} {:.0}", unit.symbol, rate), + rate, + source: current_source.to_string(), + }; + log::debug!("Market data fetched in {:?}", start.elapsed()); + return Ok(data); + } + } + + // Try next source in fallback chain + match current_source.fallback() { + Some(next) => { + log::debug!( + "Falling back from {} to {} for {}", + current_source, + next, + currency + ); + current_source = next; + } + None => break, } }