Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 37 additions & 3 deletions frontends/ios/BitBoxApp/BitBoxAppWidget/WidgetDataService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ struct WidgetDataService {
return nil
}

async let currentPriceFromSimplePrice = fetchCurrentPrice(coinCode: coinCode, currency: currency)

do {
let (data, response) = try await session.data(from: url)
let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1
Expand All @@ -85,18 +87,21 @@ struct WidgetDataService {
return nil
}

guard let currentPrice = prices.last,
guard let rangeCurrentPrice = prices.last,
let firstPrice = prices.first,
currentPrice.isFinite,
rangeCurrentPrice.isFinite,
firstPrice.isFinite,
firstPrice != 0 else {
return nil
}
let simplePrice = await currentPriceFromSimplePrice
let currentPrice = simplePrice ?? rangeCurrentPrice
let chartPrices = simplePrice.map { prices + [$0] } ?? prices
let change = (currentPrice - firstPrice) / firstPrice * 100
let result = PriceData(
price: currentPrice,
change24h: change,
chartPrices: prices,
chartPrices: chartPrices,
coinCode: WidgetShared.normalizeCoinCode(coinCode),
currency: currency.uppercased()
)
Expand All @@ -107,13 +112,42 @@ struct WidgetDataService {
}
}

private func fetchCurrentPrice(coinCode: String, currency: String) async -> Double? {
guard let url = simplePriceURL(for: coinCode, currency: currency) else {
return nil
}

do {
let (data, response) = try await session.data(from: url)
let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1
guard statusCode == 200,
let decoded = try? JSONDecoder().decode([String: [String: Double]].self, from: data) else {
return nil
}
let geckoID = WidgetCoinMetadata.geckoID(for: coinCode)
let geckoCurrency = currency.lowercased()
guard let currentPrice = decoded[geckoID]?[geckoCurrency],
currentPrice.isFinite else {
return nil
}
return currentPrice
} catch {
return nil
}
}

private func chartURL(for coinCode: String, currency: String) -> URL? {
let now = Int(Date().timeIntervalSince1970)
let oneDayAgo = now - Self.chartRangeSeconds
let geckoID = WidgetCoinMetadata.geckoID(for: coinCode)
return URL(string: "https://exchangerates.shiftcrypto.io/api/v3/coins/\(geckoID)/market_chart/range?vs_currency=\(currency.lowercased())&from=\(oneDayAgo)&to=\(now)")
}

private func simplePriceURL(for coinCode: String, currency: String) -> URL? {
let geckoID = WidgetCoinMetadata.geckoID(for: coinCode)
return URL(string: "https://exchangerates.shiftcrypto.io/api/v3/simple/price?ids=\(geckoID)&vs_currencies=\(currency.lowercased())")
}

private static func makeSession() -> URLSession {
let configuration = URLSessionConfiguration.default
configuration.timeoutIntervalForRequest = 10
Expand Down
22 changes: 16 additions & 6 deletions frontends/ios/WIDGET.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ The widget shows **one coin at a time**. The user switches coins with left/right

## 3. How prices are fetched

The widget calls the **Shift Crypto CoinGecko Mirror API**:
The widget calls the **Shift Crypto CoinGecko Mirror API**. It uses the chart endpoint for historical
data:

```
https://exchangerates.shiftcrypto.io/api/v3/coins/{geckoID}/market_chart/range
Expand All @@ -55,15 +56,24 @@ https://exchangerates.shiftcrypto.io/api/v3/coins/{geckoID}/market_chart/range
&to={now}
```

It also calls the simple price endpoint for a fresher current price:

```
https://exchangerates.shiftcrypto.io/api/v3/simple/price
?ids={geckoID}
&vs_currencies={currency}
```

Where `geckoID` maps `btc` -> `bitcoin`, `ltc` -> `litecoin`, `eth` -> `ethereum`.

This returns 24 hours of price data points. From that response:
The chart response returns 24 hours of price data points. The simple price response is appended to
that series when available. From those responses:

- **Current price** = last data point.
- **24-hour change** = percentage difference between first and last data points.
- **Chart data** = all returned price points (used for the sparkline).
- **Current price** = simple price when available, otherwise the chart response's last data point.
- **24-hour change** = percentage difference between the chart response's first data point and the current price.
- **Chart data** = all returned chart price points plus the simple price when available (used for the sparkline).

The request has a **10-second timeout**.
Each request has a **10-second timeout**.

---

Expand Down