diff --git a/Sources/SatsPrice/Network/CoinGeckoPriceFetcher.swift b/Sources/SatsPrice/Network/CoinGeckoPriceFetcher.swift index 3740e4e..3829297 100644 --- a/Sources/SatsPrice/Network/CoinGeckoPriceFetcher.swift +++ b/Sources/SatsPrice/Network/CoinGeckoPriceFetcher.swift @@ -23,6 +23,11 @@ class CoinGeckoPriceFetcher : PriceFetcher { "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=\(currency.identifier.lowercased())&precision=18" } + func urlString(toCurrencies currencies: [Locale.Currency]) -> String { + let currenciesString = currencies.map { $0.identifier.lowercased() }.joined(separator: ",") + return "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=\(currenciesString)&precision=18" + } + func convertBTC(toCurrency currency: Locale.Currency) async throws -> Decimal? { do { guard let urlComponents = URLComponents(string: urlString(toCurrency: currency)), let url = urlComponents.url else { @@ -45,4 +50,43 @@ class CoinGeckoPriceFetcher : PriceFetcher { return nil } } + + func convertBTC(toCurrencies currencies: [Locale.Currency]) async throws -> [Locale.Currency : Decimal] { + do { + guard !currencies.isEmpty else { + return [:] + } + + if currencies.count == 1, let currency = currencies.first { + guard let price = try await convertBTC(toCurrency: currency) else { + return [:] + } + + return [currency: price] + } + + guard let urlComponents = URLComponents(string: urlString(toCurrencies: currencies)), let url = urlComponents.url else { + return [:] + } + + let (data, _) = try await URLSession.shared.data(from: url, delegate: nil) + + let priceResponse = try JSONDecoder().decode(CoinGeckoPriceResponse.self, from: data) + + var results: [Locale.Currency : Decimal] = [:] + for currency in currencies { + if let price = priceResponse.bitcoin[currency.identifier] { +#if !SKIP + results[currency] = price +#else + results[currency] = Decimal(price) +#endif + } + } + + return results + } catch { + return [:] + } + } } diff --git a/Sources/SatsPrice/Network/CoinbasePriceFetcher.swift b/Sources/SatsPrice/Network/CoinbasePriceFetcher.swift index 5e8a019..1722d0a 100644 --- a/Sources/SatsPrice/Network/CoinbasePriceFetcher.swift +++ b/Sources/SatsPrice/Network/CoinbasePriceFetcher.swift @@ -20,11 +20,22 @@ private struct CoinbasePrice: Codable { let currency: String } +private struct CoinbaseExchangeRatesResponse: Codable { + let data: CoinbaseExchangeRatesResponseData +} + +private struct CoinbaseExchangeRatesResponseData: Codable { + let currency: String + let rates: [String: String] +} + class CoinbasePriceFetcher : PriceFetcher { func urlString(toCurrency currency: Locale.Currency) -> String { "https://api.coinbase.com/v2/prices/BTC-\(currency.identifier)/spot" } + private static let urlStringForAllCurrencies: String = "https://api.coinbase.com/v2/exchange-rates?currency=BTC" + func convertBTC(toCurrency currency: Locale.Currency) async throws -> Decimal? { do { guard let urlComponents = URLComponents(string: urlString(toCurrency: currency)), let url = urlComponents.url else { @@ -49,4 +60,48 @@ class CoinbasePriceFetcher : PriceFetcher { return nil } } + + func convertBTC(toCurrencies currencies: [Locale.Currency]) async throws -> [Locale.Currency : Decimal] { + do { + guard !currencies.isEmpty else { + return [:] + } + + if currencies.count == 1, let currency = currencies.first { + guard let price = try await convertBTC(toCurrency: currency) else { + return [:] + } + + return [currency: price] + } + + guard let urlComponents = URLComponents(string: CoinbasePriceFetcher.urlStringForAllCurrencies), let url = urlComponents.url else { + return [:] + } + + let (data, _) = try await URLSession.shared.data(from: url, delegate: nil) + + let coinbaseExchangeRatesResponse = try JSONDecoder().decode(CoinbaseExchangeRatesResponse.self, from: data) + let rates = coinbaseExchangeRatesResponse.data.rates + + guard coinbaseExchangeRatesResponse.data.currency == "BTC" else { + return [:] + } + + var results: [Locale.Currency : Decimal] = [:] + for currency in currencies { + if let price = rates[currency.identifier] { +#if !SKIP + results[currency] = Decimal(string: price) +#else + results[currency] = Decimal(price) +#endif + } + } + + return results + } catch { + return [:] + } + } } diff --git a/Sources/SatsPrice/Network/FakePriceFetcher.swift b/Sources/SatsPrice/Network/FakePriceFetcher.swift index 57637ff..d52a013 100644 --- a/Sources/SatsPrice/Network/FakePriceFetcher.swift +++ b/Sources/SatsPrice/Network/FakePriceFetcher.swift @@ -14,6 +14,19 @@ import Foundation /// Fake price fetcher that returns a randomized price. Useful for development testing without requiring a network call. class FakePriceFetcher: PriceFetcher { func convertBTC(toCurrency currency: Locale.Currency) async throws -> Decimal? { + randomPrice() + } + + func convertBTC(toCurrencies currencies: [Locale.Currency]) async throws -> [Locale.Currency : Decimal] { + guard !currencies.isEmpty else { + return [:] + } + + let prices = currencies.map { _ in randomPrice() } + return Dictionary(uniqueKeysWithValues: zip(currencies, prices)) + } + + private func randomPrice() -> Decimal { Decimal(Double.random(in: 10000...100000)) } } diff --git a/Sources/SatsPrice/Network/ManualPriceFetcher.swift b/Sources/SatsPrice/Network/ManualPriceFetcher.swift index f49c2ee..e515804 100644 --- a/Sources/SatsPrice/Network/ManualPriceFetcher.swift +++ b/Sources/SatsPrice/Network/ManualPriceFetcher.swift @@ -12,9 +12,19 @@ import Foundation /// Fake price fetcher that returns a randomized price. Useful for development testing without requiring a network call. class ManualPriceFetcher: PriceFetcher { - var price: Decimal = Decimal(1) + var prices: [Locale.Currency: Decimal] = [:] func convertBTC(toCurrency currency: Locale.Currency) async throws -> Decimal? { - return price + prices[currency] + } + + func convertBTC(toCurrencies currencies: [Locale.Currency]) async throws -> [Locale.Currency : Decimal] { + guard !currencies.isEmpty else { + return [:] + } + + let filteredCurrencies = currencies.filter { prices.keys.contains($0) } + let priceValues = filteredCurrencies.map { prices[$0, default: Decimal(0)] } + return Dictionary(uniqueKeysWithValues: zip(filteredCurrencies, priceValues)) } } diff --git a/Sources/SatsPrice/Network/PriceFetcher.swift b/Sources/SatsPrice/Network/PriceFetcher.swift index 083030f..223abe1 100644 --- a/Sources/SatsPrice/Network/PriceFetcher.swift +++ b/Sources/SatsPrice/Network/PriceFetcher.swift @@ -12,4 +12,5 @@ import Foundation protocol PriceFetcher { func convertBTC(toCurrency currency: Locale.Currency) async throws -> Decimal? + func convertBTC(toCurrencies currencies: [Locale.Currency]) async throws -> [Locale.Currency: Decimal] } diff --git a/Sources/SatsPrice/Network/PriceFetcherDelegator.swift b/Sources/SatsPrice/Network/PriceFetcherDelegator.swift index 1489f12..e2e6d12 100644 --- a/Sources/SatsPrice/Network/PriceFetcherDelegator.swift +++ b/Sources/SatsPrice/Network/PriceFetcherDelegator.swift @@ -42,4 +42,8 @@ class PriceFetcherDelegator: PriceFetcher { func convertBTC(toCurrency currency: Locale.Currency) async throws -> Decimal? { return try await delegate.convertBTC(toCurrency: currency) } + + func convertBTC(toCurrencies currencies: [Locale.Currency]) async throws -> [Locale.Currency : Decimal] { + return try await delegate.convertBTC(toCurrencies: currencies) + } }