Add PriceFetcher API to fetch prices of multiple currencies
This commit is contained in:
@@ -23,6 +23,11 @@ class CoinGeckoPriceFetcher : PriceFetcher {
|
|||||||
"https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=\(currency.identifier.lowercased())&precision=18"
|
"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? {
|
func convertBTC(toCurrency currency: Locale.Currency) async throws -> Decimal? {
|
||||||
do {
|
do {
|
||||||
guard let urlComponents = URLComponents(string: urlString(toCurrency: currency)), let url = urlComponents.url else {
|
guard let urlComponents = URLComponents(string: urlString(toCurrency: currency)), let url = urlComponents.url else {
|
||||||
@@ -45,4 +50,43 @@ class CoinGeckoPriceFetcher : PriceFetcher {
|
|||||||
return nil
|
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 [:]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,11 +20,22 @@ private struct CoinbasePrice: Codable {
|
|||||||
let currency: String
|
let currency: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct CoinbaseExchangeRatesResponse: Codable {
|
||||||
|
let data: CoinbaseExchangeRatesResponseData
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct CoinbaseExchangeRatesResponseData: Codable {
|
||||||
|
let currency: String
|
||||||
|
let rates: [String: String]
|
||||||
|
}
|
||||||
|
|
||||||
class CoinbasePriceFetcher : PriceFetcher {
|
class CoinbasePriceFetcher : PriceFetcher {
|
||||||
func urlString(toCurrency currency: Locale.Currency) -> String {
|
func urlString(toCurrency currency: Locale.Currency) -> String {
|
||||||
"https://api.coinbase.com/v2/prices/BTC-\(currency.identifier)/spot"
|
"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? {
|
func convertBTC(toCurrency currency: Locale.Currency) async throws -> Decimal? {
|
||||||
do {
|
do {
|
||||||
guard let urlComponents = URLComponents(string: urlString(toCurrency: currency)), let url = urlComponents.url else {
|
guard let urlComponents = URLComponents(string: urlString(toCurrency: currency)), let url = urlComponents.url else {
|
||||||
@@ -49,4 +60,48 @@ class CoinbasePriceFetcher : PriceFetcher {
|
|||||||
return nil
|
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 [:]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,19 @@ import Foundation
|
|||||||
/// Fake price fetcher that returns a randomized price. Useful for development testing without requiring a network call.
|
/// Fake price fetcher that returns a randomized price. Useful for development testing without requiring a network call.
|
||||||
class FakePriceFetcher: PriceFetcher {
|
class FakePriceFetcher: PriceFetcher {
|
||||||
func convertBTC(toCurrency currency: Locale.Currency) async throws -> Decimal? {
|
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))
|
Decimal(Double.random(in: 10000...100000))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,19 @@ import Foundation
|
|||||||
|
|
||||||
/// Fake price fetcher that returns a randomized price. Useful for development testing without requiring a network call.
|
/// Fake price fetcher that returns a randomized price. Useful for development testing without requiring a network call.
|
||||||
class ManualPriceFetcher: PriceFetcher {
|
class ManualPriceFetcher: PriceFetcher {
|
||||||
var price: Decimal = Decimal(1)
|
var prices: [Locale.Currency: Decimal] = [:]
|
||||||
|
|
||||||
func convertBTC(toCurrency currency: Locale.Currency) async throws -> 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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,4 +12,5 @@ import Foundation
|
|||||||
|
|
||||||
protocol PriceFetcher {
|
protocol PriceFetcher {
|
||||||
func convertBTC(toCurrency currency: Locale.Currency) async throws -> Decimal?
|
func convertBTC(toCurrency currency: Locale.Currency) async throws -> Decimal?
|
||||||
|
func convertBTC(toCurrencies currencies: [Locale.Currency]) async throws -> [Locale.Currency: Decimal]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,4 +42,8 @@ class PriceFetcherDelegator: PriceFetcher {
|
|||||||
func convertBTC(toCurrency currency: Locale.Currency) async throws -> Decimal? {
|
func convertBTC(toCurrency currency: Locale.Currency) async throws -> Decimal? {
|
||||||
return try await delegate.convertBTC(toCurrency: currency)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user