mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2025-02-24 07:28:07 +01:00
Merge pull request #7407 from BlueWallet/marketw
FIX: Widgets do not work any longer on iOS and macOS #7380
This commit is contained in:
commit
8de33f9b86
10 changed files with 484 additions and 243 deletions
|
@ -162,7 +162,7 @@
|
|||
B4D0B2682C1DED67006B6B1B /* ReceiveMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D0B2672C1DED67006B6B1B /* ReceiveMethod.swift */; };
|
||||
B4EE583C226703320003363C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B40D4E35225841ED00428FCC /* Assets.xcassets */; };
|
||||
B4EFF73B2C3F6C5E0095D655 /* MockData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4EFF73A2C3F6C5E0095D655 /* MockData.swift */; };
|
||||
C978A716948AB7DEC5B6F677 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; };
|
||||
C978A716948AB7DEC5B6F677 /* (null) in Frameworks */ = {isa = PBXBuildFile; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
|
@ -427,7 +427,7 @@
|
|||
files = (
|
||||
782F075B5DD048449E2DECE9 /* libz.tbd in Frameworks */,
|
||||
764B49B1420D4AEB8109BF62 /* libsqlite3.0.tbd in Frameworks */,
|
||||
C978A716948AB7DEC5B6F677 /* BuildFile in Frameworks */,
|
||||
C978A716948AB7DEC5B6F677 /* (null) in Frameworks */,
|
||||
17CDA0718F42DB2CE856C872 /* libPods-BlueWallet.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -1670,7 +1670,7 @@
|
|||
"DEVELOPMENT_TEAM[sdk=macosx*]" = A7W54YZ4WU;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = Widgets/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
@ -1726,7 +1726,7 @@
|
|||
"DEVELOPMENT_TEAM[sdk=macosx*]" = A7W54YZ4WU;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = Widgets/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
|
|
@ -57,6 +57,15 @@
|
|||
},
|
||||
"Colombia (Colombian Peso)" : {
|
||||
|
||||
},
|
||||
"Configure Market Widget with ${electrumHost}" : {
|
||||
|
||||
},
|
||||
"Configure Market Widget with ${electrumHost} and show error messages: ${showErrorMessages}" : {
|
||||
|
||||
},
|
||||
"Configure the Market Widget to show the Electrum host connected to/being attempted and display error messages if data retrieval fails." : {
|
||||
|
||||
},
|
||||
"Croatia (Croatian Kuna)" : {
|
||||
|
||||
|
@ -78,6 +87,34 @@
|
|||
},
|
||||
"Denmark (Danish Krone)" : {
|
||||
|
||||
},
|
||||
"display_in_BROWSER_TITLE" : {
|
||||
"localizations" : {
|
||||
"en_US" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "View in Browser"
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Ver en el navegador"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Voir dans le navigateur"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Electrum Host" : {
|
||||
|
||||
},
|
||||
"Error" : {
|
||||
|
||||
},
|
||||
"European Union (Euro)" : {
|
||||
|
||||
|
@ -154,6 +191,15 @@
|
|||
},
|
||||
"Market Rate" : {
|
||||
|
||||
},
|
||||
"Market Widget" : {
|
||||
|
||||
},
|
||||
"Market Widget Configuration" : {
|
||||
|
||||
},
|
||||
"Market Widget is connected to ${electrumHost}" : {
|
||||
|
||||
},
|
||||
"Mexico (Mexican Peso)" : {
|
||||
|
||||
|
@ -205,6 +251,9 @@
|
|||
},
|
||||
"send" : {
|
||||
|
||||
},
|
||||
"Show Error Messages" : {
|
||||
|
||||
},
|
||||
"Singapore (Singapore Dollar)" : {
|
||||
|
||||
|
@ -283,6 +332,9 @@
|
|||
},
|
||||
"View the current price of Bitcoin." : {
|
||||
|
||||
},
|
||||
"View the Electrum host connected to/being attempted" : {
|
||||
|
||||
},
|
||||
"View your accumulated balance." : {
|
||||
|
||||
|
@ -307,28 +359,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"display_in_BROWSER_TITLE" : {
|
||||
"localizations" : {
|
||||
"en_US" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "View in Browser"
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Ver en el navegador"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Voir dans le navigateur"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"VIEW_TRANSACTION_DETAILS_TITLE" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
|
|
|
@ -14,88 +14,87 @@ struct APIError: LocalizedError {
|
|||
|
||||
extension MarketAPI {
|
||||
|
||||
static func fetchNextBlockFee(completion: @escaping ((MarketData?, Error?) -> Void), userElectrumSettings: UserDefaultsElectrumSettings = UserDefaultsGroup.getElectrumSettings(), retries: Int = 0) {
|
||||
Task.detached {
|
||||
let client = SwiftTCPClient()
|
||||
static func fetchNextBlockFee() async throws -> MarketData {
|
||||
let client = SwiftTCPClient(hosts: hardcodedPeers)
|
||||
defer {
|
||||
print("Closing connection to \(userElectrumSettings.host ?? "unknown"):\(userElectrumSettings.sslPort ?? userElectrumSettings.port ?? 0).")
|
||||
client.close()
|
||||
print("Closed SwiftTCPClient connection.")
|
||||
}
|
||||
|
||||
guard let host = userElectrumSettings.host, let portToUse = userElectrumSettings.sslPort ?? userElectrumSettings.port else {
|
||||
completion(nil, APIError())
|
||||
return
|
||||
}
|
||||
|
||||
let isSSLSupported = userElectrumSettings.sslPort != nil
|
||||
print("Attempting to connect to \(host):\(portToUse) with SSL supported: \(isSSLSupported).")
|
||||
|
||||
let connected = await client.connect(to: host, port: portToUse, useSSL: isSSLSupported)
|
||||
if connected {
|
||||
print("Successfully connected to \(host):\(portToUse) with SSL: \(isSSLSupported).")
|
||||
} else {
|
||||
print("Failed to connect to \(host):\(portToUse) with SSL: \(isSSLSupported).")
|
||||
if retries < client.maxRetries {
|
||||
print("Retrying fetchNextBlockFee (\(retries + 1)/\(client.maxRetries))...")
|
||||
fetchNextBlockFee(completion: completion, userElectrumSettings: userElectrumSettings, retries: retries + 1)
|
||||
} else {
|
||||
completion(nil, APIError())
|
||||
}
|
||||
return
|
||||
guard await client.connectToNextAvailable(validateCertificates: false) else {
|
||||
print("Failed to connect to any Electrum peer.")
|
||||
throw APIError()
|
||||
}
|
||||
|
||||
let message = "{\"id\": 1, \"method\": \"mempool.get_fee_histogram\", \"params\": []}\n"
|
||||
guard let data = message.data(using: .utf8), await client.send(data: data) else {
|
||||
print("Message sending failed to \(host):\(portToUse) with SSL supported: \(isSSLSupported).")
|
||||
completion(nil, APIError())
|
||||
return
|
||||
guard let data = message.data(using: .utf8) else {
|
||||
print("Failed to encode message to data.")
|
||||
throw APIError()
|
||||
}
|
||||
|
||||
print("Sending fee histogram request: \(message)")
|
||||
|
||||
guard await client.send(data: data) else {
|
||||
print("Failed to send fee histogram request.")
|
||||
throw APIError()
|
||||
}
|
||||
print("Message sent successfully to \(host):\(portToUse) with SSL: \(isSSLSupported).")
|
||||
|
||||
do {
|
||||
let receivedData = try await client.receive()
|
||||
print("Data received. Parsing...")
|
||||
print("Received data: \(receivedData)")
|
||||
|
||||
guard let json = try JSONSerialization.jsonObject(with: receivedData, options: .allowFragments) as? [String: AnyObject],
|
||||
let feeHistogram = json["result"] as? [[Double]] else {
|
||||
print("Failed to parse response from \(host).")
|
||||
completion(nil, APIError())
|
||||
return
|
||||
print("Invalid JSON structure in response.")
|
||||
throw APIError()
|
||||
}
|
||||
|
||||
let fastestFee = calcEstimateFeeFromFeeHistogram(numberOfBlocks: 1, feeHistogram: feeHistogram)
|
||||
let marketData = MarketData(nextBlock: String(format: "%.0f", fastestFee), sats: "0", price: "0", rate: 0, dateString: "")
|
||||
print("Parsed MarketData: \(marketData)")
|
||||
completion(marketData, nil)
|
||||
print("Calculated fastest fee: \(fastestFee)")
|
||||
return MarketData(nextBlock: String(format: "%.0f", fastestFee), sats: "0", price: "0", rate: 0, dateString: "")
|
||||
} catch {
|
||||
print("Error receiving data from \(host): \(error.localizedDescription)")
|
||||
if retries < client.maxRetries {
|
||||
print("Retrying fetchNextBlockFee (\(retries + 1)/\(client.maxRetries))...")
|
||||
fetchNextBlockFee(completion: completion, userElectrumSettings: userElectrumSettings, retries: retries + 1)
|
||||
} else {
|
||||
completion(nil, APIError())
|
||||
print("Error during fetchNextBlockFee: \(error.localizedDescription)")
|
||||
throw APIError()
|
||||
}
|
||||
}
|
||||
|
||||
static func fetchMarketData(currency: String) async throws -> MarketData {
|
||||
var marketDataEntry = MarketData(nextBlock: "...", sats: "...", price: "...", rate: 0)
|
||||
|
||||
do {
|
||||
if let priceResult = try await fetchPrice(currency: currency) {
|
||||
marketDataEntry.rate = priceResult.rateDouble
|
||||
marketDataEntry.price = priceResult.formattedRate ?? "!"
|
||||
print("Fetched price data: rateDouble=\(priceResult.rateDouble), formattedRate=\(priceResult.formattedRate ?? "nil")")
|
||||
}
|
||||
} catch {
|
||||
print("Error fetching price: \(error.localizedDescription)")
|
||||
}
|
||||
|
||||
do {
|
||||
let nextBlockData = try await fetchNextBlockFee()
|
||||
marketDataEntry.nextBlock = nextBlockData.nextBlock
|
||||
print("Fetched next block fee data: nextBlock=\(nextBlockData.nextBlock)")
|
||||
} catch {
|
||||
print("Error fetching next block fee: \(error.localizedDescription)")
|
||||
marketDataEntry.nextBlock = "!"
|
||||
}
|
||||
|
||||
marketDataEntry.sats = numberFormatter.string(from: NSNumber(value: Double(10 / marketDataEntry.rate) * 10000000)) ?? "!"
|
||||
print("Calculated sats: \(marketDataEntry.sats)")
|
||||
|
||||
return marketDataEntry
|
||||
}
|
||||
|
||||
static func fetchMarketData(currency: String, completion: @escaping (Result<MarketData, Error>) -> ()) {
|
||||
Task {
|
||||
do {
|
||||
let marketData = try await fetchMarketData(currency: currency)
|
||||
completion(.success(marketData))
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func fetchMarketData(currency: String, completion: @escaping ((MarketData?, Error?) -> Void)) {
|
||||
var marketDataEntry = MarketData(nextBlock: "...", sats: "...", price: "...", rate: 0)
|
||||
MarketAPI.fetchPrice(currency: currency, completion: { (result, error) in
|
||||
if let result = result {
|
||||
marketDataEntry.rate = result.rateDouble
|
||||
marketDataEntry.price = result.formattedRate ?? "!"
|
||||
}
|
||||
MarketAPI.fetchNextBlockFee { (marketData, error) in
|
||||
if let nextBlock = marketData?.nextBlock {
|
||||
marketDataEntry.nextBlock = nextBlock
|
||||
} else {
|
||||
marketDataEntry.nextBlock = "!"
|
||||
}
|
||||
if let rateDouble = result?.rateDouble {
|
||||
marketDataEntry.sats = numberFormatter.string(from: NSNumber(value: Double(10 / rateDouble) * 10000000)) ?? "!"
|
||||
}
|
||||
completion(marketDataEntry, nil)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,18 +14,22 @@ struct UserDefaultsElectrumSettings {
|
|||
var sslPort: UInt16?
|
||||
}
|
||||
|
||||
let hardcodedPeers = [
|
||||
let hardcodedPeers = DefaultElectrumPeers.map { settings in
|
||||
(
|
||||
host: settings.host ?? "",
|
||||
port: settings.sslPort ?? settings.port ?? 50001,
|
||||
useSSL: settings.sslPort != nil
|
||||
)
|
||||
}
|
||||
|
||||
let DefaultElectrumPeers = [
|
||||
UserDefaultsElectrumSettings(host: "mainnet.foundationdevices.com", port: 50001, sslPort: 50002),
|
||||
UserDefaultsElectrumSettings(host: "electrum.jochen-hoenicke.de", port: 50001, sslPort: 50006),
|
||||
// UserDefaultsElectrumSettings(host: "electrum.jochen-hoenicke.de", port: 50001, sslPort: 50006),
|
||||
UserDefaultsElectrumSettings(host: "electrum1.bluewallet.io", port: 50001, sslPort: 443),
|
||||
UserDefaultsElectrumSettings(host: "electrum.acinq.co", port: 50001, sslPort: 50002),
|
||||
UserDefaultsElectrumSettings(host: "electrum.bitaroo.net", port: 50001, sslPort: 50002),
|
||||
]
|
||||
|
||||
let DefaultElectrumPeers = [
|
||||
UserDefaultsElectrumSettings(host: "electrum1.bluewallet.io", port: 50001, sslPort: 443), //
|
||||
] + hardcodedPeers
|
||||
|
||||
class UserDefaultsGroup {
|
||||
static private let suite = UserDefaults(suiteName: UserDefaultsGroupKey.GroupName.rawValue)
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
|
||||
struct WidgetDataStore: Codable {
|
||||
let rate: String
|
||||
let lastUpdate: String
|
||||
|
@ -60,3 +59,4 @@ struct WidgetDataStore: Codable {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -18,68 +18,76 @@ struct MarketWidgetProvider: TimelineProvider {
|
|||
|
||||
func getSnapshot(in context: Context, completion: @escaping (MarketWidgetEntry) -> ()) {
|
||||
let entry: MarketWidgetEntry
|
||||
if (context.isPreview) {
|
||||
if context.isPreview {
|
||||
entry = MarketWidgetEntry(date: Date(), marketData: MarketData(nextBlock: "26", sats: "9 134", price: "$10 000", rate: 10000))
|
||||
} else {
|
||||
entry = MarketWidgetEntry(date: Date(), marketData: emptyMarketData)
|
||||
entry = MarketWidgetEntry(date: Date(), marketData: MarketData(nextBlock: "...", sats: "...", price: "...", rate: 0))
|
||||
}
|
||||
completion(entry)
|
||||
}
|
||||
|
||||
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
|
||||
let currentDate = Date()
|
||||
var entries: [MarketWidgetEntry] = []
|
||||
if context.isPreview {
|
||||
let entry = MarketWidgetEntry(date: Date(), marketData: MarketData(nextBlock: "26", sats: "9 134", price: "$10 000", rate: 10000))
|
||||
entries.append(entry)
|
||||
let timeline = Timeline(entries: entries, policy: .atEnd)
|
||||
completion(timeline)
|
||||
} else {
|
||||
|
||||
let marketDataEntry = MarketWidgetEntry(date: currentDate, marketData: MarketData(nextBlock: "...", sats: "...", price: "...", rate: 0))
|
||||
entries.append(marketDataEntry) // Initial placeholder entry
|
||||
|
||||
let userPreferredCurrency = Currency.getUserPreferredCurrency()
|
||||
fetchMarketDataWithRetry(currency: userPreferredCurrency, retries: 3) { (entry) in
|
||||
fetchMarketDataWithRetry(currency: userPreferredCurrency, retries: 3) { marketData in
|
||||
let entry = MarketWidgetEntry(date: Date(), marketData: marketData)
|
||||
entries.append(entry)
|
||||
let timeline = Timeline(entries: entries, policy: .atEnd)
|
||||
completion(timeline)
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchMarketDataWithRetry(currency: String, retries: Int, completion: @escaping (MarketData) -> ()) {
|
||||
var attempt = 0
|
||||
|
||||
func attemptFetch() {
|
||||
attempt += 1
|
||||
print("Attempt \(attempt) to fetch market data.")
|
||||
|
||||
MarketAPI.fetchMarketData(currency: currency) { result in
|
||||
switch result {
|
||||
case .success(let marketData):
|
||||
print("Successfully fetched market data on attempt \(attempt).")
|
||||
completion(marketData)
|
||||
case .failure(let error):
|
||||
print("Fetch market data failed (attempt \(attempt)): \(error.localizedDescription)")
|
||||
if attempt < retries {
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
|
||||
attemptFetch()
|
||||
}
|
||||
} else {
|
||||
print("Failed to fetch market data after \(retries) attempts.")
|
||||
let fallbackData = MarketData(nextBlock: "...", sats: "...", price: "...", rate: 0)
|
||||
completion(fallbackData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchMarketDataWithRetry(currency: String, retries: Int, completion: @escaping (MarketWidgetEntry) -> ()) {
|
||||
MarketAPI.fetchMarketData(currency: currency) { (result, error) in
|
||||
if let result = result {
|
||||
let entry = MarketWidgetEntry(date: Date(), marketData: result)
|
||||
MarketWidgetProvider.lastSuccessfulEntry = entry
|
||||
completion(entry)
|
||||
} else if retries > 0 {
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
|
||||
self.fetchMarketDataWithRetry(currency: currency, retries: retries - 1, completion: completion)
|
||||
}
|
||||
} else {
|
||||
if let lastEntry = MarketWidgetProvider.lastSuccessfulEntry {
|
||||
completion(lastEntry)
|
||||
} else {
|
||||
let entry = MarketWidgetEntry(date: Date(), marketData: emptyMarketData)
|
||||
completion(entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
attemptFetch()
|
||||
}
|
||||
}
|
||||
|
||||
struct MarketWidgetEntry: TimelineEntry {
|
||||
let date: Date
|
||||
let marketData: MarketData
|
||||
var marketData: MarketData
|
||||
}
|
||||
|
||||
struct MarketWidgetEntryView: View {
|
||||
var entry: MarketWidgetProvider.Entry
|
||||
var entry: MarketWidgetEntry
|
||||
|
||||
var MarketStack: some View {
|
||||
MarketView(marketData: entry.marketData).padding(EdgeInsets(top: 18, leading: 11, bottom: 18, trailing: 11))
|
||||
MarketView(marketData: entry.marketData)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(content: {
|
||||
MarketStack.background(Color.widgetBackground)
|
||||
MarketStack.containerBackground(Color.widgetBackground, for: .widget)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -88,27 +96,17 @@ struct MarketWidget: Widget {
|
|||
let kind: String = "MarketWidget"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
if #available(iOSApplicationExtension 16.0, *) {
|
||||
return StaticConfiguration(kind: kind, provider: MarketWidgetProvider()) { entry in
|
||||
StaticConfiguration(kind: kind, provider: MarketWidgetProvider()) { entry in
|
||||
MarketWidgetEntryView(entry: entry)
|
||||
}
|
||||
.configurationDisplayName("Market")
|
||||
.description("View the current market information.").supportedFamilies([.systemSmall])
|
||||
.contentMarginsDisabledIfAvailable()
|
||||
} else {
|
||||
return StaticConfiguration(kind: kind, provider: MarketWidgetProvider()) { entry in
|
||||
MarketWidgetEntryView(entry: entry)
|
||||
}
|
||||
.configurationDisplayName("Market")
|
||||
.description("View the current market information.").supportedFamilies([.systemSmall])
|
||||
.contentMarginsDisabledIfAvailable()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MarketWidget_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
MarketWidgetEntryView(entry: MarketWidgetEntry(date: Date(), marketData: MarketData(nextBlock: "26", sats: "9 134", price: "$10,000", rate: 0)))
|
||||
MarketWidgetEntryView(entry: MarketWidgetEntry(date: Date(), marketData: MarketData(nextBlock: "26", sats: "9,134", price: "$10,000", rate: 0)))
|
||||
.previewContext(WidgetPreviewContext(family: .systemSmall))
|
||||
}
|
||||
}
|
||||
|
|
56
ios/Widgets/Shared/HostManager.swift
Normal file
56
ios/Widgets/Shared/HostManager.swift
Normal file
|
@ -0,0 +1,56 @@
|
|||
import Foundation
|
||||
|
||||
actor HostManager {
|
||||
var availableHosts: [(host: String, port: UInt16, useSSL: Bool)]
|
||||
var hostFailureCounts: [String: Int] = [:]
|
||||
let maxRetriesPerHost: Int
|
||||
|
||||
init(hosts: [(host: String, port: UInt16, useSSL: Bool)], maxRetriesPerHost: Int) {
|
||||
self.availableHosts = hosts
|
||||
self.maxRetriesPerHost = maxRetriesPerHost
|
||||
print("Initialized HostManager with \(hosts.count) hosts.")
|
||||
}
|
||||
|
||||
func getNextHost() -> (host: String, port: UInt16, useSSL: Bool)? {
|
||||
guard !availableHosts.isEmpty else {
|
||||
print("No available hosts to retrieve.")
|
||||
return nil
|
||||
}
|
||||
|
||||
var attempts = availableHosts.count
|
||||
while attempts > 0 {
|
||||
let currentHost = availableHosts.removeFirst()
|
||||
if !shouldSkipHost(currentHost.host) {
|
||||
availableHosts.append(currentHost)
|
||||
print("Selected host: \(currentHost.host):\(currentHost.port) (SSL: \(currentHost.useSSL))")
|
||||
return currentHost
|
||||
} else {
|
||||
availableHosts.append(currentHost)
|
||||
attempts -= 1
|
||||
print("Host \(currentHost.host) is skipped due to max retries.")
|
||||
}
|
||||
}
|
||||
|
||||
print("All hosts have been exhausted after max retries.")
|
||||
return nil
|
||||
}
|
||||
|
||||
func shouldSkipHost(_ host: String) -> Bool {
|
||||
if let failureCount = hostFailureCounts[host], failureCount >= maxRetriesPerHost {
|
||||
print("Host \(host) has reached max retries (\(failureCount)). It will be skipped.")
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func resetFailureCount(for host: String) {
|
||||
hostFailureCounts[host] = 0
|
||||
print("Reset failure count for host \(host).")
|
||||
}
|
||||
|
||||
func incrementFailureCount(for host: String) {
|
||||
hostFailureCounts[host, default: 0] += 1
|
||||
let newCount = hostFailureCounts[host]!
|
||||
print("Incremented failure count for host \(host). New count: \(newCount)")
|
||||
}
|
||||
}
|
|
@ -24,16 +24,63 @@ enum SwiftTCPClientError: Error, LocalizedError {
|
|||
}
|
||||
}
|
||||
|
||||
actor HostManager {
|
||||
var availableHosts: [(host: String, port: UInt16, useSSL: Bool)]
|
||||
var hostFailureCounts: [String: Int] = [:]
|
||||
let maxRetriesPerHost: Int
|
||||
|
||||
init(hosts: [(host: String, port: UInt16, useSSL: Bool)], maxRetriesPerHost: Int) {
|
||||
self.availableHosts = hosts
|
||||
self.maxRetriesPerHost = maxRetriesPerHost
|
||||
}
|
||||
|
||||
func getNextHost() -> (host: String, port: UInt16, useSSL: Bool)? {
|
||||
guard !availableHosts.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
// Rotate the first host to the end
|
||||
let currentHost = availableHosts.removeFirst()
|
||||
availableHosts.append(currentHost)
|
||||
return currentHost
|
||||
}
|
||||
|
||||
func shouldSkipHost(_ host: String) -> Bool {
|
||||
if let failureCount = hostFailureCounts[host], failureCount >= maxRetriesPerHost {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func resetFailureCount(for host: String) {
|
||||
hostFailureCounts[host] = 0
|
||||
}
|
||||
|
||||
func incrementFailureCount(for host: String) {
|
||||
hostFailureCounts[host, default: 0] += 1
|
||||
}
|
||||
}
|
||||
|
||||
class SwiftTCPClient {
|
||||
private var connection: NWConnection?
|
||||
private let queue = DispatchQueue(label: "SwiftTCPClientQueue", qos: .userInitiated)
|
||||
private let readTimeout: TimeInterval = 5.0
|
||||
let maxRetries = 3
|
||||
private let hostManager: HostManager
|
||||
|
||||
init(hosts: [(host: String, port: UInt16, useSSL: Bool)] = [], maxRetriesPerHost: Int = 3) {
|
||||
self.hostManager = HostManager(hosts: hosts, maxRetriesPerHost: maxRetriesPerHost)
|
||||
}
|
||||
|
||||
func connect(to host: String, port: UInt16, useSSL: Bool = false, validateCertificates: Bool = true, retries: Int = 0) async -> Bool {
|
||||
// Skip host if it has failed too many times
|
||||
if await hostManager.shouldSkipHost(host) {
|
||||
print("Skipping host \(host) after \(hostManager.maxRetriesPerHost) retries.")
|
||||
return false
|
||||
}
|
||||
|
||||
func connect(to host: String, port: UInt16, useSSL: Bool = false, retries: Int = 0) async -> Bool {
|
||||
let parameters: NWParameters
|
||||
if useSSL {
|
||||
parameters = NWParameters(tls: createTLSOptions(), tcp: .init())
|
||||
parameters = NWParameters(tls: createTLSOptions(validateCertificates: validateCertificates), tcp: .init())
|
||||
} else {
|
||||
parameters = NWParameters.tcp
|
||||
}
|
||||
|
@ -45,45 +92,97 @@ class SwiftTCPClient {
|
|||
connection = NWConnection(host: NWEndpoint.Host(host), port: nwPort, using: parameters)
|
||||
connection?.start(queue: queue)
|
||||
|
||||
let serialQueue = DispatchQueue(label: "SwiftTCPClient.connect.serialQueue", qos: .userInitiated)
|
||||
var hasResumed = false
|
||||
print("Attempting to connect to \(host):\(port) (SSL: \(useSSL))")
|
||||
|
||||
do {
|
||||
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
connection?.stateUpdateHandler = { [weak self] state in
|
||||
guard let self = self else { return }
|
||||
serialQueue.async {
|
||||
if !hasResumed {
|
||||
|
||||
switch state {
|
||||
case .ready:
|
||||
print("Successfully connected to \(host):\(port)")
|
||||
self.connection?.stateUpdateHandler = nil
|
||||
hasResumed = true
|
||||
continuation.resume()
|
||||
case .failed(let error):
|
||||
if let nwError = error as? NWError, self.isTLSError(nwError) {
|
||||
print("SSL Error while connecting to \(host):\(port) - \(error.localizedDescription)")
|
||||
}
|
||||
print("Connection to \(host):\(port) failed with error: \(error.localizedDescription)")
|
||||
self.connection?.stateUpdateHandler = nil
|
||||
hasResumed = true
|
||||
continuation.resume(throwing: error)
|
||||
case .cancelled:
|
||||
print("Connection to \(host):\(port) was cancelled.")
|
||||
self.connection?.stateUpdateHandler = nil
|
||||
hasResumed = true
|
||||
continuation.resume(throwing: SwiftTCPClientError.connectionCancelled)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Reset failure count on successful connection
|
||||
await hostManager.resetFailureCount(for: host)
|
||||
return true
|
||||
} catch {
|
||||
print("Connection failed with error: \(error.localizedDescription)")
|
||||
if retries < maxRetries {
|
||||
print("Retrying connection (\(retries + 1)/\(maxRetries))...")
|
||||
return await connect(to: host, port: port, useSSL: useSSL, retries: retries + 1)
|
||||
}
|
||||
print("Connection to \(host) failed with error: \(error.localizedDescription)")
|
||||
await hostManager.incrementFailureCount(for: host)
|
||||
|
||||
if retries < maxRetries - 1 {
|
||||
print("Retrying connection to \(host) (\(retries + 1)/\(maxRetries))...")
|
||||
return await connect(to: host, port: port, useSSL: useSSL, validateCertificates: validateCertificates, retries: retries + 1)
|
||||
} else {
|
||||
print("Host \(host) failed after \(maxRetries) retries. Skipping.")
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func isTLSError(_ error: NWError) -> Bool {
|
||||
let nsError = error as NSError
|
||||
let code = nsError.code
|
||||
if #available(iOS 16.4, *) {
|
||||
switch code {
|
||||
case 20, 21, 22:
|
||||
return true
|
||||
case 1, 2, 3, 4:
|
||||
return false
|
||||
default:
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
switch code {
|
||||
case 20, 21, 22:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func connectToNextAvailable(validateCertificates: Bool = true) async -> Bool {
|
||||
while true {
|
||||
guard let currentHost = await hostManager.getNextHost() else {
|
||||
print("No available hosts to connect.")
|
||||
return false
|
||||
}
|
||||
|
||||
if await hostManager.shouldSkipHost(currentHost.host) {
|
||||
print("Skipping host \(currentHost.host) after \(hostManager.maxRetriesPerHost) retries.")
|
||||
continue
|
||||
}
|
||||
|
||||
print("Attempting to connect to next available host: \(currentHost.host):\(currentHost.port) (SSL: \(currentHost.useSSL))")
|
||||
|
||||
if await connect(to: currentHost.host, port: currentHost.port, useSSL: currentHost.useSSL, validateCertificates: validateCertificates) {
|
||||
print("Connected to host \(currentHost.host):\(currentHost.port)")
|
||||
await hostManager.resetFailureCount(for: currentHost.host)
|
||||
return true
|
||||
} else {
|
||||
print("Failed to connect to host \(currentHost.host):\(currentHost.port)")
|
||||
await hostManager.incrementFailureCount(for: currentHost.host)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func send(data: Data) async -> Bool {
|
||||
guard let connection = connection else {
|
||||
|
@ -92,12 +191,14 @@ class SwiftTCPClient {
|
|||
}
|
||||
|
||||
do {
|
||||
print("Sending data: \(data)")
|
||||
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
|
||||
connection.send(content: data, completion: .contentProcessed { error in
|
||||
if let error = error {
|
||||
print("Send error: \(error.localizedDescription)")
|
||||
continuation.resume(throwing: error)
|
||||
} else {
|
||||
print("Data sent successfully.")
|
||||
continuation.resume()
|
||||
}
|
||||
})
|
||||
|
@ -114,21 +215,27 @@ class SwiftTCPClient {
|
|||
throw SwiftTCPClientError.connectionNil
|
||||
}
|
||||
|
||||
print("Attempting to receive data...")
|
||||
|
||||
return try await withThrowingTaskGroup(of: Data.self) { group in
|
||||
group.addTask {
|
||||
return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Data, Error>) in
|
||||
connection.receive(minimumIncompleteLength: 1, maximumLength: 65536) { data, _, isComplete, error in
|
||||
if let error = error {
|
||||
print("Receive error: \(error.localizedDescription)")
|
||||
continuation.resume(throwing: SwiftTCPClientError.unknown(error))
|
||||
return
|
||||
}
|
||||
|
||||
if let data = data, !data.isEmpty {
|
||||
print("Received data: \(data)")
|
||||
continuation.resume(returning: data)
|
||||
} else if isComplete {
|
||||
print("Connection closed by peer.")
|
||||
self.close()
|
||||
continuation.resume(throwing: SwiftTCPClientError.noDataReceived)
|
||||
} else {
|
||||
print("Read timed out.")
|
||||
continuation.resume(throwing: SwiftTCPClientError.readTimedOut)
|
||||
}
|
||||
}
|
||||
|
@ -137,25 +244,35 @@ class SwiftTCPClient {
|
|||
|
||||
group.addTask {
|
||||
try await Task.sleep(nanoseconds: UInt64(self.readTimeout * 1_000_000_000))
|
||||
print("Receive operation timed out after \(self.readTimeout) seconds.")
|
||||
throw SwiftTCPClientError.readTimedOut
|
||||
}
|
||||
|
||||
if let firstResult = try await group.next() {
|
||||
group.cancelAll()
|
||||
print("Receive operation completed successfully.")
|
||||
return firstResult
|
||||
} else {
|
||||
print("Receive operation timed out.")
|
||||
throw SwiftTCPClientError.readTimedOut
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func close() {
|
||||
print("Closing connection.")
|
||||
connection?.cancel()
|
||||
connection = nil
|
||||
}
|
||||
|
||||
private func createTLSOptions() -> NWProtocolTLS.Options {
|
||||
private func createTLSOptions(validateCertificates: Bool = true) -> NWProtocolTLS.Options {
|
||||
let tlsOptions = NWProtocolTLS.Options()
|
||||
if (!validateCertificates) {
|
||||
sec_protocol_options_set_verify_block(tlsOptions.securityProtocolOptions, { _, _, completion in
|
||||
completion(true)
|
||||
}, DispatchQueue.global())
|
||||
print("SSL certificate validation is disabled.")
|
||||
}
|
||||
return tlsOptions
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ struct MarketView: View {
|
|||
HStack(alignment: .center, spacing: 0, content: {
|
||||
Text("Sats/\(Currency.getUserPreferredCurrency())").bold().lineLimit(1).font(Font.system(size:11, weight: .medium, design: .default)).foregroundColor(.textColor)
|
||||
Spacer()
|
||||
Text(marketData.sats == "..." ? "..." : marketData.sats).padding(EdgeInsets(top: 2, leading: 4, bottom: 2, trailing: 4)).lineLimit(1).minimumScaleFactor(0.1).foregroundColor(.widgetBackground).font(Font.system(size:11, weight: .semibold, design: .default)).background(Color(red: 0.97, green: 0.21, blue: 0.38)).overlay(
|
||||
Text( marketData.sats).padding(EdgeInsets(top: 2, leading: 4, bottom: 2, trailing: 4)).lineLimit(1).minimumScaleFactor(0.1).foregroundColor(.widgetBackground).font(Font.system(size:11, weight: .semibold, design: .default)).background(Color(red: 0.97, green: 0.21, blue: 0.38)).overlay(
|
||||
RoundedRectangle(cornerRadius: 4.0)
|
||||
.stroke(Color.containerRed, lineWidth: 4.0))
|
||||
})
|
||||
|
@ -38,7 +38,7 @@ struct MarketView: View {
|
|||
HStack(alignment: .center, spacing: 0, content: {
|
||||
Text("Price").bold().lineLimit(1).font(Font.system(size:11, weight: . medium, design: .default)).foregroundColor(.textColor)
|
||||
Spacer()
|
||||
Text(marketData.price == "..." ? "..." : marketData.price).padding(EdgeInsets(top: 2, leading: 4, bottom: 2, trailing: 4)).lineLimit(1).minimumScaleFactor(0.1).foregroundColor(.widgetBackground).font(Font.system(size:11, weight: .semibold, design: .default)).background(Color(red: 0.29, green: 0.86, blue: 0.73)).overlay(
|
||||
Text( marketData.price).padding(EdgeInsets(top: 2, leading: 4, bottom: 2, trailing: 4)).lineLimit(1).minimumScaleFactor(0.1).foregroundColor(.widgetBackground).font(Font.system(size:11, weight: .semibold, design: .default)).background(Color(red: 0.29, green: 0.86, blue: 0.73)).overlay(
|
||||
RoundedRectangle(cornerRadius:4.0)
|
||||
.stroke(Color.containerGreen, lineWidth: 4.0))
|
||||
})
|
||||
|
|
|
@ -11,7 +11,20 @@ import SwiftUI
|
|||
|
||||
struct WalletInformationAndMarketWidgetProvider: TimelineProvider {
|
||||
typealias Entry = WalletInformationAndMarketWidgetEntry
|
||||
static var lastSuccessfulEntries: [WalletInformationAndMarketWidgetEntry] = []
|
||||
|
||||
actor LastSuccessfulEntryStore {
|
||||
private var lastSuccessfulEntry: WalletInformationAndMarketWidgetEntry?
|
||||
|
||||
func getLastSuccessfulEntry() -> WalletInformationAndMarketWidgetEntry? {
|
||||
return lastSuccessfulEntry
|
||||
}
|
||||
|
||||
func setLastSuccessfulEntry(_ entry: WalletInformationAndMarketWidgetEntry) {
|
||||
lastSuccessfulEntry = entry
|
||||
}
|
||||
}
|
||||
|
||||
let entryStore = LastSuccessfulEntryStore()
|
||||
|
||||
func placeholder(in context: Context) -> WalletInformationAndMarketWidgetEntry {
|
||||
return WalletInformationAndMarketWidgetEntry.placeholder
|
||||
|
@ -36,24 +49,12 @@ struct WalletInformationAndMarketWidgetProvider: TimelineProvider {
|
|||
completion(timeline)
|
||||
} else {
|
||||
let userPreferredCurrency = Currency.getUserPreferredCurrency()
|
||||
let allwalletsBalance = WalletData(balance: UserDefaultsGroup.getAllWalletsBalance(), latestTransactionTime: UserDefaultsGroup.getAllWalletsLatestTransactionTime())
|
||||
let allWalletsBalance = WalletData(balance: UserDefaultsGroup.getAllWalletsBalance(), latestTransactionTime: UserDefaultsGroup.getAllWalletsLatestTransactionTime())
|
||||
|
||||
MarketAPI.fetchMarketData(currency: userPreferredCurrency) { (result, error) in
|
||||
let entry: WalletInformationAndMarketWidgetEntry
|
||||
|
||||
if let result = result {
|
||||
entry = WalletInformationAndMarketWidgetEntry(date: Date(), marketData: result, allWalletsBalance: allwalletsBalance)
|
||||
WalletInformationAndMarketWidgetProvider.lastSuccessfulEntries.append(entry)
|
||||
if WalletInformationAndMarketWidgetProvider.lastSuccessfulEntries.count > 5 {
|
||||
WalletInformationAndMarketWidgetProvider.lastSuccessfulEntries.removeFirst()
|
||||
}
|
||||
} else {
|
||||
if let lastEntry = WalletInformationAndMarketWidgetProvider.lastSuccessfulEntries.last {
|
||||
entry = lastEntry
|
||||
} else {
|
||||
entry = WalletInformationAndMarketWidgetEntry.placeholder
|
||||
}
|
||||
}
|
||||
fetchMarketDataWithRetry(currency: userPreferredCurrency, retries: 3) { marketData in
|
||||
let entry = WalletInformationAndMarketWidgetEntry(date: Date(), marketData: marketData, allWalletsBalance: allWalletsBalance)
|
||||
Task {
|
||||
await entryStore.setLastSuccessfulEntry(entry)
|
||||
entries.append(entry)
|
||||
let timeline = Timeline(entries: entries, policy: .atEnd)
|
||||
completion(timeline)
|
||||
|
@ -62,6 +63,42 @@ struct WalletInformationAndMarketWidgetProvider: TimelineProvider {
|
|||
}
|
||||
}
|
||||
|
||||
private func fetchMarketDataWithRetry(currency: String, retries: Int, completion: @escaping (MarketData) -> ()) {
|
||||
var attempt = 0
|
||||
|
||||
func attemptFetch() {
|
||||
attempt += 1
|
||||
print("Attempt \(attempt) to fetch market data.")
|
||||
|
||||
MarketAPI.fetchMarketData(currency: currency) { result in
|
||||
switch result {
|
||||
case .success(let marketData):
|
||||
print("Successfully fetched market data on attempt \(attempt).")
|
||||
completion(marketData)
|
||||
case .failure(let error):
|
||||
print("Error fetching market data: \(error.localizedDescription). Retry \(attempt)/\(retries)")
|
||||
if attempt < retries {
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
|
||||
attemptFetch()
|
||||
}
|
||||
} else {
|
||||
print("Max retries reached.")
|
||||
Task {
|
||||
if let lastEntry = await entryStore.getLastSuccessfulEntry() {
|
||||
completion(lastEntry.marketData)
|
||||
} else {
|
||||
completion(WalletInformationAndMarketWidgetEntry.placeholder.marketData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
attemptFetch()
|
||||
}
|
||||
}
|
||||
|
||||
struct WalletInformationAndMarketWidgetEntry: TimelineEntry {
|
||||
let date: Date
|
||||
let marketData: MarketData
|
||||
|
|
Loading…
Add table
Reference in a new issue