mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2024-11-19 09:50:15 +01:00
REF: SwiftTCPClient to use Network framework and Backgroud thread
This commit is contained in:
parent
4307248e33
commit
e72ba878e2
@ -14,60 +14,61 @@ struct APIError: LocalizedError {
|
|||||||
|
|
||||||
extension MarketAPI {
|
extension MarketAPI {
|
||||||
|
|
||||||
static func fetchNextBlockFee(completion: @escaping ((MarketData?, Error?) -> Void), userElectrumSettings: UserDefaultsElectrumSettings = UserDefaultsGroup.getElectrumSettings()) {
|
static func fetchNextBlockFee(completion: @escaping ((MarketData?, Error?) -> Void), userElectrumSettings: UserDefaultsElectrumSettings = UserDefaultsGroup.getElectrumSettings()) {
|
||||||
let settings = userElectrumSettings
|
Task {
|
||||||
let portToUse = settings.sslPort ?? settings.port
|
let client = SwiftTCPClient()
|
||||||
let isSSLSupported = settings.sslPort != nil
|
defer {
|
||||||
|
print("Closing connection to \(userElectrumSettings.host ?? "unknown"):\(userElectrumSettings.sslPort ?? userElectrumSettings.port ?? 0).")
|
||||||
|
client.close()
|
||||||
|
}
|
||||||
|
|
||||||
DispatchQueue.global(qos: .background).async {
|
guard let host = userElectrumSettings.host, let portToUse = userElectrumSettings.sslPort ?? userElectrumSettings.port else {
|
||||||
let client = SwiftTCPClient()
|
completion(nil, APIError())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
defer {
|
let isSSLSupported = userElectrumSettings.sslPort != nil
|
||||||
print("Closing connection to \(String(describing: settings.host)):\(String(describing: portToUse)).")
|
print("Attempting to connect to \(host):\(portToUse) with SSL supported: \(isSSLSupported).")
|
||||||
client.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let host = settings.host, let portToUse = portToUse else { return }
|
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).")
|
||||||
|
completion(nil, APIError())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
print("Attempting to connect to \(String(describing: settings.host)):\(portToUse) with SSL supported: \(isSSLSupported).")
|
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
|
||||||
|
}
|
||||||
|
print("Message sent successfully to \(host):\(portToUse) with SSL: \(isSSLSupported).")
|
||||||
|
|
||||||
if client.connect(to: host, port: UInt32(portToUse), useSSL: isSSLSupported) {
|
do {
|
||||||
print("Successfully connected to \(String(describing: settings.host)):\(portToUse) with SSL:\(isSSLSupported).")
|
let receivedData = try await client.receive()
|
||||||
} else {
|
print("Data received. Parsing...")
|
||||||
print("Failed to connect to \(String(describing: settings.host)):\(portToUse) with SSL:\(isSSLSupported).")
|
guard let responseString = String(data: receivedData, encoding: .utf8),
|
||||||
completion(nil, APIError())
|
let responseData = responseString.data(using: .utf8),
|
||||||
return
|
let json = try JSONSerialization.jsonObject(with: responseData, options: .allowFragments) as? [String: AnyObject],
|
||||||
}
|
let feeHistogram = json["result"] as? [[Double]] else {
|
||||||
|
print("Failed to parse response from \(host).")
|
||||||
|
completion(nil, APIError())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let message = "{\"id\": 1, \"method\": \"mempool.get_fee_histogram\", \"params\": []}\n"
|
let fastestFee = calcEstimateFeeFromFeeHistogram(numberOfBlocks: 1, feeHistogram: feeHistogram)
|
||||||
guard let data = message.data(using: .utf8), client.send(data: data) else {
|
let marketData = MarketData(nextBlock: String(format: "%.0f", fastestFee), sats: "0", price: "0", rate: 0, dateString: "")
|
||||||
print("Message sending failed to \(String(describing: settings.host)):\(portToUse) with SSL supported: \(isSSLSupported).")
|
print("Parsed MarketData: \(marketData)")
|
||||||
completion(nil, APIError())
|
completion(marketData, nil)
|
||||||
return
|
} catch {
|
||||||
}
|
print("Error receiving data from \(host): \(error.localizedDescription)")
|
||||||
print("Message sent successfully to \(String(describing: settings.host)):\(portToUse) with SSL:\(isSSLSupported).")
|
completion(nil, APIError())
|
||||||
|
}
|
||||||
do {
|
}
|
||||||
let receivedData = try client.receive()
|
}
|
||||||
print("Data received. Parsing...")
|
|
||||||
guard let responseString = String(data: receivedData, encoding: .utf8),
|
|
||||||
let responseData = responseString.data(using: .utf8),
|
|
||||||
let json = try JSONSerialization.jsonObject(with: responseData, options: .allowFragments) as? [String: AnyObject],
|
|
||||||
let feeHistogram = json["result"] as? [[Double]] else {
|
|
||||||
print("Failed to parse response from \(String(describing: settings.host)).")
|
|
||||||
completion(nil, APIError())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let fastestFee = calcEstimateFeeFromFeeHistogram(numberOfBlocks: 1, feeHistogram: feeHistogram)
|
|
||||||
let marketData = MarketData(nextBlock: String(format: "%.0f", fastestFee), sats: "0", price: "0", rate: 0)
|
|
||||||
completion(marketData, nil) // Successfully fetched data, return it
|
|
||||||
} catch {
|
|
||||||
print("Error receiving data from \(String(describing: settings.host)): \(error.localizedDescription)")
|
|
||||||
completion(nil, APIError())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static func fetchMarketData(currency: String, completion: @escaping ((MarketData?, Error?) -> Void)) {
|
static func fetchMarketData(currency: String, completion: @escaping ((MarketData?, Error?) -> Void)) {
|
||||||
var marketDataEntry = MarketData(nextBlock: "...", sats: "...", price: "...", rate: 0)
|
var marketDataEntry = MarketData(nextBlock: "...", sats: "...", price: "...", rate: 0)
|
||||||
|
@ -9,9 +9,9 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct UserDefaultsElectrumSettings {
|
struct UserDefaultsElectrumSettings {
|
||||||
let host: String?
|
var host: String?
|
||||||
let port: Int32?
|
var port: UInt16?
|
||||||
let sslPort: Int32?
|
var sslPort: UInt16?
|
||||||
}
|
}
|
||||||
|
|
||||||
let hardcodedPeers = [
|
let hardcodedPeers = [
|
||||||
@ -34,14 +34,14 @@ class UserDefaultsGroup {
|
|||||||
return DefaultElectrumPeers.randomElement()!
|
return DefaultElectrumPeers.randomElement()!
|
||||||
}
|
}
|
||||||
|
|
||||||
let electrumSettingsTCPPort = suite?.string(forKey: UserDefaultsGroupKey.ElectrumSettingsTCPPort.rawValue) ?? "50001"
|
let electrumSettingsTCPPort = suite?.value(forKey: UserDefaultsGroupKey.ElectrumSettingsTCPPort.rawValue) ?? 50001
|
||||||
let electrumSettingsSSLPort = suite?.string(forKey: UserDefaultsGroupKey.ElectrumSettingsSSLPort.rawValue) ?? "443"
|
let electrumSettingsSSLPort = suite?.value(forKey: UserDefaultsGroupKey.ElectrumSettingsSSLPort.rawValue) ?? 443
|
||||||
|
|
||||||
let host = electrumSettingsHost
|
let host = electrumSettingsHost
|
||||||
let sslPort = Int32(electrumSettingsSSLPort)
|
let sslPort = electrumSettingsSSLPort
|
||||||
let port = Int32(electrumSettingsTCPPort)
|
let port = electrumSettingsTCPPort
|
||||||
|
|
||||||
return UserDefaultsElectrumSettings(host: host, port: port, sslPort: sslPort)
|
return UserDefaultsElectrumSettings(host: host, port: port as! UInt16, sslPort: sslPort as! UInt16)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func getAllWalletsBalance() -> Double {
|
static func getAllWalletsBalance() -> Double {
|
||||||
|
@ -1,152 +1,155 @@
|
|||||||
// BlueWallet
|
|
||||||
//
|
|
||||||
// Created by Marcos Rodriguez on 3/23/23.
|
|
||||||
// Copyright © 2023 BlueWallet. All rights reserved.
|
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Network
|
||||||
|
|
||||||
/**
|
enum SwiftTCPClientError: Error, LocalizedError {
|
||||||
`SwiftTCPClient` is a simple TCP client class that allows for establishing a TCP connection,
|
case connectionNil
|
||||||
sending data, and receiving data over the network. It supports both plain TCP and SSL-secured connections.
|
case connectionCancelled
|
||||||
|
case readTimedOut
|
||||||
|
case noDataReceived
|
||||||
|
case unknown(Error)
|
||||||
|
|
||||||
The class uses `InputStream` and `OutputStream` for network communication, encapsulating the complexity of stream management and data transfer.
|
var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
- Note: When using SSL, this implementation disables certificate chain validation for simplicity. This is not recommended for production code due to security risks.
|
case .connectionNil:
|
||||||
|
return "Connection is nil."
|
||||||
## Examples
|
case .connectionCancelled:
|
||||||
|
return "Connection was cancelled."
|
||||||
### Creating an instance and connecting to a server:
|
case .readTimedOut:
|
||||||
|
return "Read timed out."
|
||||||
```swift
|
case .noDataReceived:
|
||||||
let client = SwiftTCPClient()
|
return "No data received."
|
||||||
let success = client.connect(to: "example.com", port: 12345, useSSL: false)
|
case .unknown(let error):
|
||||||
|
return error.localizedDescription
|
||||||
if success {
|
|
||||||
print("Connected successfully.")
|
|
||||||
} else {
|
|
||||||
print("Failed to connect.")
|
|
||||||
}
|
|
||||||
**/
|
|
||||||
|
|
||||||
class SwiftTCPClient: NSObject {
|
|
||||||
private var inputStream: InputStream?
|
|
||||||
private var outputStream: OutputStream?
|
|
||||||
private let bufferSize = 1024
|
|
||||||
private var readData = Data()
|
|
||||||
private let readTimeout = 5.0 // Timeout in seconds
|
|
||||||
|
|
||||||
func connect(to host: String, port: UInt32, useSSL: Bool = false) -> Bool {
|
|
||||||
var readStream: Unmanaged<CFReadStream>?
|
|
||||||
var writeStream: Unmanaged<CFWriteStream>?
|
|
||||||
|
|
||||||
CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault, host as CFString, port, &readStream, &writeStream)
|
|
||||||
|
|
||||||
guard let read = readStream?.takeRetainedValue(), let write = writeStream?.takeRetainedValue() else {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
inputStream = read as InputStream
|
class SwiftTCPClient {
|
||||||
outputStream = write as OutputStream
|
private var connection: NWConnection?
|
||||||
|
private let queue = DispatchQueue(label: "SwiftTCPClientQueue")
|
||||||
|
private let readTimeout: TimeInterval = 5.0
|
||||||
|
|
||||||
|
func connect(to host: String, port: UInt16, useSSL: Bool = false) async -> Bool {
|
||||||
|
let parameters: NWParameters
|
||||||
if useSSL {
|
if useSSL {
|
||||||
// Configure SSL settings for the streams
|
parameters = NWParameters(tls: createTLSOptions(), tcp: .init())
|
||||||
let sslSettings: [NSString: Any] = [
|
} else {
|
||||||
kCFStreamSSLLevel as NSString: kCFStreamSocketSecurityLevelNegotiatedSSL as Any,
|
parameters = NWParameters.tcp
|
||||||
kCFStreamSSLValidatesCertificateChain as NSString: kCFBooleanFalse as Any
|
|
||||||
// Note: Disabling certificate chain validation (kCFStreamSSLValidatesCertificateChain: kCFBooleanFalse)
|
|
||||||
// is typically not recommended for production code as it introduces significant security risks.
|
|
||||||
]
|
|
||||||
inputStream?.setProperty(sslSettings, forKey: kCFStreamPropertySSLSettings as Stream.PropertyKey)
|
|
||||||
outputStream?.setProperty(sslSettings, forKey: kCFStreamPropertySSLSettings as Stream.PropertyKey)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inputStream?.delegate = self
|
connection = NWConnection(host: NWEndpoint.Host(host), port: NWEndpoint.Port(rawValue: port)!, using: parameters)
|
||||||
outputStream?.delegate = self
|
connection?.start(queue: queue)
|
||||||
|
|
||||||
inputStream?.schedule(in: .current, forMode: RunLoop.Mode.default)
|
let serialQueue = DispatchQueue(label: "SwiftTCPClient.connect.serialQueue")
|
||||||
outputStream?.schedule(in: .current, forMode: RunLoop.Mode.default)
|
var hasResumed = false
|
||||||
|
|
||||||
inputStream?.open()
|
do {
|
||||||
outputStream?.open()
|
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
|
||||||
|
connection?.stateUpdateHandler = { [weak self] state in
|
||||||
return true
|
guard let self = self else { return }
|
||||||
}
|
serialQueue.async {
|
||||||
|
if !hasResumed {
|
||||||
|
switch state {
|
||||||
func send(data: Data) -> Bool {
|
case .ready:
|
||||||
guard let outputStream = outputStream else {
|
self.connection?.stateUpdateHandler = nil
|
||||||
return false
|
hasResumed = true
|
||||||
}
|
continuation.resume()
|
||||||
|
case .failed(let error):
|
||||||
let bytesWritten = data.withUnsafeBytes { bufferPointer -> Int in
|
self.connection?.stateUpdateHandler = nil
|
||||||
guard let baseAddress = bufferPointer.baseAddress else {
|
hasResumed = true
|
||||||
return 0
|
continuation.resume(throwing: error)
|
||||||
}
|
case .cancelled:
|
||||||
return outputStream.write(baseAddress.assumingMemoryBound(to: UInt8.self), maxLength: data.count)
|
self.connection?.stateUpdateHandler = nil
|
||||||
}
|
hasResumed = true
|
||||||
|
continuation.resume(throwing: SwiftTCPClientError.connectionCancelled)
|
||||||
return bytesWritten == data.count
|
default:
|
||||||
}
|
break
|
||||||
|
}
|
||||||
func receive() throws -> Data {
|
}
|
||||||
guard let inputStream = inputStream else {
|
|
||||||
throw NSError(domain: "SwiftTCPClientError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Input stream is nil."])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the input stream is ready for reading
|
|
||||||
if inputStream.streamStatus != .open && inputStream.streamStatus != .reading {
|
|
||||||
throw NSError(domain: "SwiftTCPClientError", code: 3, userInfo: [NSLocalizedDescriptionKey: "Stream is not ready for reading."])
|
|
||||||
}
|
|
||||||
|
|
||||||
readData = Data()
|
|
||||||
|
|
||||||
// Wait for data to be available or timeout
|
|
||||||
let timeoutDate = Date().addingTimeInterval(readTimeout)
|
|
||||||
repeat {
|
|
||||||
RunLoop.current.run(mode: RunLoop.Mode.default, before: Date(timeIntervalSinceNow: 0.1))
|
|
||||||
if readData.count > 0 || Date() > timeoutDate {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
} while inputStream.streamStatus == .open || inputStream.streamStatus == .reading
|
|
||||||
|
|
||||||
if readData.count == 0 && Date() > timeoutDate {
|
|
||||||
throw NSError(domain: "SwiftTCPClientError", code: 2, userInfo: [NSLocalizedDescriptionKey: "Read timed out."])
|
|
||||||
}
|
|
||||||
|
|
||||||
return readData
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func close() {
|
|
||||||
inputStream?.close()
|
|
||||||
outputStream?.close()
|
|
||||||
inputStream?.remove(from: .current, forMode: RunLoop.Mode.default)
|
|
||||||
outputStream?.remove(from: .current, forMode: RunLoop.Mode.default)
|
|
||||||
inputStream = nil
|
|
||||||
outputStream = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension SwiftTCPClient: StreamDelegate {
|
|
||||||
func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
|
|
||||||
switch eventCode {
|
|
||||||
case .hasBytesAvailable:
|
|
||||||
if aStream == inputStream, let inputStream = inputStream {
|
|
||||||
let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: bufferSize)
|
|
||||||
while inputStream.hasBytesAvailable {
|
|
||||||
let bytesRead = inputStream.read(buffer, maxLength: bufferSize)
|
|
||||||
if bytesRead > 0 {
|
|
||||||
readData.append(buffer, count: bytesRead)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
buffer.deallocate()
|
|
||||||
}
|
}
|
||||||
case .errorOccurred:
|
return true
|
||||||
print("Stream error occurred")
|
} catch {
|
||||||
case .endEncountered:
|
print("Connection failed with error: \(error.localizedDescription)")
|
||||||
close()
|
return false
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func send(data: Data) async -> Bool {
|
||||||
|
guard let connection = connection else {
|
||||||
|
print("Send failed: No active connection.")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
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 {
|
||||||
|
continuation.resume()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
print("Send failed with error: \(error.localizedDescription)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func receive() async throws -> Data {
|
||||||
|
guard let connection = connection else {
|
||||||
|
throw SwiftTCPClientError.connectionNil
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
continuation.resume(throwing: SwiftTCPClientError.unknown(error))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if let data = data, !data.isEmpty {
|
||||||
|
continuation.resume(returning: data)
|
||||||
|
} else if isComplete {
|
||||||
|
self.close()
|
||||||
|
continuation.resume(throwing: SwiftTCPClientError.noDataReceived)
|
||||||
|
} else {
|
||||||
|
continuation.resume(throwing: SwiftTCPClientError.readTimedOut)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
group.addTask {
|
||||||
|
try await Task.sleep(nanoseconds: UInt64(self.readTimeout * 1_000_000_000))
|
||||||
|
throw SwiftTCPClientError.readTimedOut
|
||||||
|
}
|
||||||
|
|
||||||
|
if let firstResult = try await group.next() {
|
||||||
|
group.cancelAll()
|
||||||
|
return firstResult
|
||||||
|
} else {
|
||||||
|
throw SwiftTCPClientError.readTimedOut
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func close() {
|
||||||
|
connection?.cancel()
|
||||||
|
connection = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func createTLSOptions() -> NWProtocolTLS.Options {
|
||||||
|
let tlsOptions = NWProtocolTLS.Options()
|
||||||
|
sec_protocol_options_set_verify_block(tlsOptions.securityProtocolOptions, { _, _, completion in
|
||||||
|
completion(true)
|
||||||
|
}, DispatchQueue.global(qos: .background))
|
||||||
|
return tlsOptions
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user