Merge pull request #7329 from BlueWallet/swifttcp

REF: SwiftTCPClient to use Network framework and Backgroud thread
This commit is contained in:
GLaDOS 2024-11-18 09:56:50 +00:00 committed by GitHub
commit 1fc68e1b41
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 197 additions and 194 deletions

View File

@ -1592,7 +1592,7 @@ PODS:
- React-Core
- RealmJS (20.0.0):
- React
- RNCAsyncStorage (2.0.0):
- RNCAsyncStorage (2.1.0):
- React-Core
- RNCClipboard (1.15.0):
- React-Core
@ -1810,7 +1810,7 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- RNSVG (15.8.0):
- RNSVG (15.9.0):
- React-Core
- RNVectorIcons (10.2.0):
- DoubleConversion
@ -2260,7 +2260,7 @@ SPEC CHECKSUMS:
ReactCommon: 36d48f542b4010786d6b2bcee615fe5f906b7105
ReactNativeCameraKit: f058d47e0b1e55fd819bb55ee16505a2e0ca53db
RealmJS: 7947e9d9edcfb4fde3dcf9911d320a2e08cdd540
RNCAsyncStorage: 40367e8d25522dca9c3513c7b9815a184669bd97
RNCAsyncStorage: c91d753ede6dc21862c4922cd13f98f7cfde578e
RNCClipboard: dbcf25b8f666b4685c02eeb65be981d30198e505
RNCPushNotificationIOS: 6c4ca3388c7434e4a662b92e4dfeeee858e6f440
RNDefaultPreference: ee13d69e6693d193cd223d10e15e5b3c012d31ba
@ -2278,7 +2278,7 @@ SPEC CHECKSUMS:
RNReanimated: 6398ee150e1ebeda517fdd1e1b5525833a0c0ddc
RNScreens: 35bb8e81aeccf111baa0ea01a54231390dbbcfd9
RNShare: 6af59763338a7d8440035701f39be9d53cbc4d09
RNSVG: 8542aa11770b27563714bbd8494a8436385fc85f
RNSVG: bb4bfcb8ec723a6f34b074a1b7cd40ee35246fe5
RNVectorIcons: 182892e7d1a2f27b52d3c627eca5d2665a22ee28
RNWatch: 28fe1f5e0c6410d45fd20925f4796fce05522e3f
SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d

View File

@ -14,60 +14,59 @@ struct APIError: LocalizedError {
extension MarketAPI {
static func fetchNextBlockFee(completion: @escaping ((MarketData?, Error?) -> Void), userElectrumSettings: UserDefaultsElectrumSettings = UserDefaultsGroup.getElectrumSettings()) {
let settings = userElectrumSettings
let portToUse = settings.sslPort ?? settings.port
let isSSLSupported = settings.sslPort != nil
static func fetchNextBlockFee(completion: @escaping ((MarketData?, Error?) -> Void), userElectrumSettings: UserDefaultsElectrumSettings = UserDefaultsGroup.getElectrumSettings()) {
Task {
let client = SwiftTCPClient()
defer {
print("Closing connection to \(userElectrumSettings.host ?? "unknown"):\(userElectrumSettings.sslPort ?? userElectrumSettings.port ?? 0).")
client.close()
}
DispatchQueue.global(qos: .background).async {
let client = SwiftTCPClient()
guard let host = userElectrumSettings.host, let portToUse = userElectrumSettings.sslPort ?? userElectrumSettings.port else {
completion(nil, APIError())
return
}
defer {
print("Closing connection to \(String(describing: settings.host)):\(String(describing: portToUse)).")
client.close()
}
let isSSLSupported = userElectrumSettings.sslPort != nil
print("Attempting to connect to \(host):\(portToUse) with SSL supported: \(isSSLSupported).")
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) {
print("Successfully connected to \(String(describing: settings.host)):\(portToUse) with SSL:\(isSSLSupported).")
} else {
print("Failed to connect to \(String(describing: settings.host)):\(portToUse) with SSL:\(isSSLSupported).")
completion(nil, APIError())
return
}
do {
let receivedData = try await client.receive()
print("Data received. Parsing...")
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
}
let message = "{\"id\": 1, \"method\": \"mempool.get_fee_histogram\", \"params\": []}\n"
guard let data = message.data(using: .utf8), client.send(data: data) else {
print("Message sending failed to \(String(describing: settings.host)):\(portToUse) with SSL supported: \(isSSLSupported).")
completion(nil, APIError())
return
}
print("Message sent successfully to \(String(describing: settings.host)):\(portToUse) with SSL:\(isSSLSupported).")
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())
}
}
}
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)
} catch {
print("Error receiving data from \(host): \(error.localizedDescription)")
completion(nil, APIError())
}
}
}
static func fetchMarketData(currency: String, completion: @escaping ((MarketData?, Error?) -> Void)) {
var marketDataEntry = MarketData(nextBlock: "...", sats: "...", price: "...", rate: 0)

View File

@ -9,9 +9,9 @@
import Foundation
struct UserDefaultsElectrumSettings {
let host: String?
let port: Int32?
let sslPort: Int32?
var host: String?
var port: UInt16?
var sslPort: UInt16?
}
let hardcodedPeers = [
@ -34,14 +34,14 @@ class UserDefaultsGroup {
return DefaultElectrumPeers.randomElement()!
}
let electrumSettingsTCPPort = suite?.string(forKey: UserDefaultsGroupKey.ElectrumSettingsTCPPort.rawValue) ?? "50001"
let electrumSettingsSSLPort = suite?.string(forKey: UserDefaultsGroupKey.ElectrumSettingsSSLPort.rawValue) ?? "443"
let electrumSettingsTCPPort = suite?.value(forKey: UserDefaultsGroupKey.ElectrumSettingsTCPPort.rawValue) ?? 50001
let electrumSettingsSSLPort = suite?.value(forKey: UserDefaultsGroupKey.ElectrumSettingsSSLPort.rawValue) ?? 443
let host = electrumSettingsHost
let sslPort = Int32(electrumSettingsSSLPort)
let port = Int32(electrumSettingsTCPPort)
let sslPort = electrumSettingsSSLPort
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 {

View File

@ -1,152 +1,156 @@
// BlueWallet
//
// Created by Marcos Rodriguez on 3/23/23.
// Copyright © 2023 BlueWallet. All rights reserved.
import Foundation
import Network
/**
`SwiftTCPClient` is a simple TCP client class that allows for establishing a TCP connection,
sending data, and receiving data over the network. It supports both plain TCP and SSL-secured connections.
The class uses `InputStream` and `OutputStream` for network communication, encapsulating the complexity of stream management and data transfer.
- Note: When using SSL, this implementation disables certificate chain validation for simplicity. This is not recommended for production code due to security risks.
## Examples
### Creating an instance and connecting to a server:
```swift
let client = SwiftTCPClient()
let success = client.connect(to: "example.com", port: 12345, useSSL: false)
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
enum SwiftTCPClientError: Error, LocalizedError {
case connectionNil
case connectionCancelled
case readTimedOut
case noDataReceived
case unknown(Error)
var errorDescription: String? {
switch self {
case .connectionNil:
return "Connection is nil."
case .connectionCancelled:
return "Connection was cancelled."
case .readTimedOut:
return "Read timed out."
case .noDataReceived:
return "No data received."
case .unknown(let error):
return error.localizedDescription
}
}
}
inputStream = read as InputStream
outputStream = write as OutputStream
class SwiftTCPClient {
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 {
// Configure SSL settings for the streams
let sslSettings: [NSString: Any] = [
kCFStreamSSLLevel as NSString: kCFStreamSocketSecurityLevelNegotiatedSSL as Any,
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)
parameters = NWParameters(tls: createTLSOptions(), tcp: .init())
} else {
parameters = NWParameters.tcp
}
inputStream?.delegate = self
outputStream?.delegate = self
inputStream?.schedule(in: .current, forMode: RunLoop.Mode.default)
outputStream?.schedule(in: .current, forMode: RunLoop.Mode.default)
inputStream?.open()
outputStream?.open()
return true
}
func send(data: Data) -> Bool {
guard let outputStream = outputStream else {
guard let nwPort = NWEndpoint.Port(rawValue: port) else {
print("Invalid port number: \(port)")
return false
}
connection = NWConnection(host: NWEndpoint.Host(host), port: nwPort, using: parameters)
connection?.start(queue: queue)
let bytesWritten = data.withUnsafeBytes { bufferPointer -> Int in
guard let baseAddress = bufferPointer.baseAddress else {
return 0
}
return outputStream.write(baseAddress.assumingMemoryBound(to: UInt8.self), maxLength: data.count)
}
let serialQueue = DispatchQueue(label: "SwiftTCPClient.connect.serialQueue")
var hasResumed = false
return bytesWritten == data.count
}
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)
do {
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
connection?.stateUpdateHandler = { [weak self] state in
guard let self = self else { return }
serialQueue.async {
if !hasResumed {
switch state {
case .ready:
self.connection?.stateUpdateHandler = nil
hasResumed = true
continuation.resume()
case .failed(let error):
self.connection?.stateUpdateHandler = nil
hasResumed = true
continuation.resume(throwing: error)
case .cancelled:
self.connection?.stateUpdateHandler = nil
hasResumed = true
continuation.resume(throwing: SwiftTCPClientError.connectionCancelled)
default:
break
}
}
}
}
buffer.deallocate()
}
case .errorOccurred:
print("Stream error occurred")
case .endEncountered:
close()
default:
break
return true
} catch {
print("Connection failed with error: \(error.localizedDescription)")
return false
}
}
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()
return tlsOptions
}
}