diff --git a/Example/IntegrationTests/Auth/AuthTests.swift b/Example/IntegrationTests/Auth/AuthTests.swift index 276e14e84..ab831899f 100644 --- a/Example/IntegrationTests/Auth/AuthTests.swift +++ b/Example/IntegrationTests/Auth/AuthTests.swift @@ -40,6 +40,7 @@ final class AuthTests: XCTestCase { keyValueStorage: keyValueStorage, keychainStorage: keychain, socketFactory: DefaultSocketFactory(), + networkMonitor: NetworkMonitor(), logger: logger) let networkingClient = NetworkingClientFactory.create( diff --git a/Example/IntegrationTests/Chat/ChatTests.swift b/Example/IntegrationTests/Chat/ChatTests.swift index ff09c65f1..747bd29a3 100644 --- a/Example/IntegrationTests/Chat/ChatTests.swift +++ b/Example/IntegrationTests/Chat/ChatTests.swift @@ -56,6 +56,7 @@ final class ChatTests: XCTestCase { keyValueStorage: keyValueStorage, keychainStorage: keychain, socketFactory: DefaultSocketFactory(), + networkMonitor: NetworkMonitor(), logger: logger) let networkingInteractor = NetworkingClientFactory.create( diff --git a/Example/IntegrationTests/Pairing/PairingTests.swift b/Example/IntegrationTests/Pairing/PairingTests.swift index 6bc02014c..d9e83de6e 100644 --- a/Example/IntegrationTests/Pairing/PairingTests.swift +++ b/Example/IntegrationTests/Pairing/PairingTests.swift @@ -34,6 +34,7 @@ final class PairingTests: XCTestCase { keyValueStorage: RuntimeKeyValueStorage(), keychainStorage: keychain, socketFactory: DefaultSocketFactory(), + networkMonitor: NetworkMonitor(), logger: logger) let networkingClient = NetworkingClientFactory.create( diff --git a/Example/IntegrationTests/Push/NotifyTests.swift b/Example/IntegrationTests/Push/NotifyTests.swift index 5644fcd89..26400548c 100644 --- a/Example/IntegrationTests/Push/NotifyTests.swift +++ b/Example/IntegrationTests/Push/NotifyTests.swift @@ -45,6 +45,7 @@ final class NotifyTests: XCTestCase { keyValueStorage: keyValueStorage, keychainStorage: keychain, socketFactory: DefaultSocketFactory(), + networkMonitor: NetworkMonitor(), logger: relayLogger) let networkingClient = NetworkingClientFactory.create( diff --git a/Example/IntegrationTests/Sign/SignClientTests.swift b/Example/IntegrationTests/Sign/SignClientTests.swift index 28202a224..9e0fc6867 100644 --- a/Example/IntegrationTests/Sign/SignClientTests.swift +++ b/Example/IntegrationTests/Sign/SignClientTests.swift @@ -26,6 +26,7 @@ final class SignClientTests: XCTestCase { keyValueStorage: keyValueStorage, keychainStorage: keychain, socketFactory: DefaultSocketFactory(), + networkMonitor: NetworkMonitor(), logger: logger ) diff --git a/Example/IntegrationTests/Sync/SyncTests.swift b/Example/IntegrationTests/Sync/SyncTests.swift index adcfdc532..5e19c1345 100644 --- a/Example/IntegrationTests/Sync/SyncTests.swift +++ b/Example/IntegrationTests/Sync/SyncTests.swift @@ -63,6 +63,7 @@ final class SyncTests: XCTestCase { keyValueStorage: RuntimeKeyValueStorage(), keychainStorage: keychain, socketFactory: DefaultSocketFactory(), + networkMonitor: NetworkMonitor(), logger: logger) let networkingInteractor = NetworkingClientFactory.create( relayClient: relayClient, diff --git a/Example/IntegrationTests/XPlatform/Web3Wallet/XPlatformW3WTests.swift b/Example/IntegrationTests/XPlatform/Web3Wallet/XPlatformW3WTests.swift index 3d794d18a..c2f18b4c1 100644 --- a/Example/IntegrationTests/XPlatform/Web3Wallet/XPlatformW3WTests.swift +++ b/Example/IntegrationTests/XPlatform/Web3Wallet/XPlatformW3WTests.swift @@ -33,6 +33,7 @@ final class XPlatformW3WTests: XCTestCase { keyValueStorage: keyValueStorage, keychainStorage: keychain, socketFactory: DefaultSocketFactory(), + networkMonitor: NetworkMonitor(), logger: relayLogger ) diff --git a/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift b/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift index 119a62901..3ac8d75b7 100644 --- a/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift +++ b/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift @@ -44,9 +44,11 @@ final class RelayClientEndToEndTests: XCTestCase { ) let socket = WebSocket(url: urlFactory.create(fallback: false)) let webSocketFactory = WebSocketFactoryMock(webSocket: socket) + let networkMonitor = NetworkMonitor() let dispatcher = Dispatcher( socketFactory: webSocketFactory, relayUrlFactory: urlFactory, + networkMonitor: networkMonitor, socketConnectionType: .manual, logger: logger ) @@ -57,7 +59,8 @@ final class RelayClientEndToEndTests: XCTestCase { keyValueStorage: keyValueStorage, keychainStorage: keychain, socketFactory: DefaultSocketFactory(), - socketConnectionType: .manual, + socketConnectionType: .manual, + networkMonitor: networkMonitor, logger: logger ) let clientId = try! relayClient.getClientId() diff --git a/Sources/WalletConnectNetworking/NetworkingInteractor.swift b/Sources/WalletConnectNetworking/NetworkingInteractor.swift index 078c8adf7..d086b8db8 100644 --- a/Sources/WalletConnectNetworking/NetworkingInteractor.swift +++ b/Sources/WalletConnectNetworking/NetworkingInteractor.swift @@ -30,8 +30,6 @@ public class NetworkingInteractor: NetworkInteracting { public var networkConnectionStatusPublisher: AnyPublisher public var socketConnectionStatusPublisher: AnyPublisher - private let networkMonitor: NetworkMonitoring - public init( relayClient: RelayClient, serializer: Serializing, @@ -43,8 +41,7 @@ public class NetworkingInteractor: NetworkInteracting { self.rpcHistory = rpcHistory self.logger = logger self.socketConnectionStatusPublisher = relayClient.socketConnectionStatusPublisher - self.networkMonitor = NetworkMonitor() - self.networkConnectionStatusPublisher = networkMonitor.networkConnectionStatusPublisher + self.networkConnectionStatusPublisher = relayClient.networkConnectionStatusPublisher setupRelaySubscribtion() } @@ -205,8 +202,21 @@ public class NetworkingInteractor: NetworkInteracting { public func request(_ request: RPCRequest, topic: String, protocolMethod: ProtocolMethod, envelopeType: Envelope.EnvelopeType) async throws { try rpcHistory.set(request, forTopic: topic, emmitedBy: .local) - let message = try serializer.serialize(topic: topic, encodable: request, envelopeType: envelopeType) - try await relayClient.publish(topic: topic, payload: message, tag: protocolMethod.requestConfig.tag, prompt: protocolMethod.requestConfig.prompt, ttl: protocolMethod.requestConfig.ttl) + + do { + let message = try serializer.serialize(topic: topic, encodable: request, envelopeType: envelopeType) + + try await relayClient.publish(topic: topic, + payload: message, + tag: protocolMethod.requestConfig.tag, + prompt: protocolMethod.requestConfig.prompt, + ttl: protocolMethod.requestConfig.ttl) + } catch { + if let id = request.id { + rpcHistory.delete(id: id) + } + throw error + } } public func respond(topic: String, response: RPCResponse, protocolMethod: ProtocolMethod, envelopeType: Envelope.EnvelopeType) async throws { diff --git a/Sources/WalletConnectRelay/Dispatching.swift b/Sources/WalletConnectRelay/Dispatching.swift index bf3ef2c92..7d13bc39f 100644 --- a/Sources/WalletConnectRelay/Dispatching.swift +++ b/Sources/WalletConnectRelay/Dispatching.swift @@ -3,6 +3,7 @@ import Combine protocol Dispatching { var onMessage: ((String) -> Void)? { get set } + var networkConnectionStatusPublisher: AnyPublisher { get } var socketConnectionStatusPublisher: AnyPublisher { get } func send(_ string: String, completion: @escaping (Error?) -> Void) func protectedSend(_ string: String, completion: @escaping (Error?) -> Void) @@ -17,8 +18,9 @@ final class Dispatcher: NSObject, Dispatching { var socketConnectionHandler: SocketConnectionHandler private let relayUrlFactory: RelayUrlFactory + private let networkMonitor: NetworkMonitoring private let logger: ConsoleLogging - + private let defaultTimeout: Int = 5 /// The property is used to determine whether relay.walletconnect.org will be used /// in case relay.walletconnect.com doesn't respond for some reason (most likely due to being blocked in the user's location). @@ -30,15 +32,21 @@ final class Dispatcher: NSObject, Dispatching { socketConnectionStatusPublisherSubject.eraseToAnyPublisher() } + var networkConnectionStatusPublisher: AnyPublisher { + networkMonitor.networkConnectionStatusPublisher + } + private let concurrentQueue = DispatchQueue(label: "com.walletconnect.sdk.dispatcher", attributes: .concurrent) init( socketFactory: WebSocketFactory, relayUrlFactory: RelayUrlFactory, + networkMonitor: NetworkMonitoring, socketConnectionType: SocketConnectionType, logger: ConsoleLogging ) { self.relayUrlFactory = relayUrlFactory + self.networkMonitor = networkMonitor self.logger = logger let socket = socketFactory.create(with: relayUrlFactory.create(fallback: fallback)) @@ -60,7 +68,7 @@ final class Dispatcher: NSObject, Dispatching { func send(_ string: String, completion: @escaping (Error?) -> Void) { guard socket.isConnected else { - completion(NetworkError.webSocketNotConnected) + completion(NetworkError.connectionFailed) return } socket.write(string: string) { @@ -69,20 +77,22 @@ final class Dispatcher: NSObject, Dispatching { } func protectedSend(_ string: String, completion: @escaping (Error?) -> Void) { - guard !socket.isConnected else { + guard !socket.isConnected || !networkMonitor.isConnected else { return send(string, completion: completion) } var cancellable: AnyCancellable? - cancellable = socketConnectionStatusPublisher - .filter { $0 == .connected } + cancellable = Publishers.CombineLatest(socketConnectionStatusPublisher, networkConnectionStatusPublisher) + .filter { $0.0 == .connected && $0.1 == .connected } .setFailureType(to: NetworkError.self) - .timeout(.seconds(defaultTimeout), scheduler: concurrentQueue, customError: { .webSocketNotConnected }) + .timeout(.seconds(defaultTimeout), scheduler: concurrentQueue, customError: { .connectionFailed }) .sink(receiveCompletion: { [unowned self] result in switch result { case .failure(let error): cancellable?.cancel() - self.handleFallbackIfNeeded(error: error) + if !socket.isConnected { + handleFallbackIfNeeded(error: error) + } completion(error) case .finished: break } @@ -137,7 +147,7 @@ extension Dispatcher { } private func handleFallbackIfNeeded(error: NetworkError) { - if error == .webSocketNotConnected && socket.request.url?.host == NetworkConstants.defaultUrl { + if error == .connectionFailed && socket.request.url?.host == NetworkConstants.defaultUrl { logger.debug("[WebSocket] - Fallback to \(NetworkConstants.fallbackUrl)") fallback = true socket.request.url = relayUrlFactory.create(fallback: fallback) diff --git a/Sources/WalletConnectRelay/Misc/NetworkError.swift b/Sources/WalletConnectRelay/Misc/NetworkError.swift index f31340bbd..e0a66c2c1 100644 --- a/Sources/WalletConnectRelay/Misc/NetworkError.swift +++ b/Sources/WalletConnectRelay/Misc/NetworkError.swift @@ -1,13 +1,13 @@ import Foundation enum NetworkError: Error, Equatable { - case webSocketNotConnected + case connectionFailed case sendMessageFailed(Error) case receiveMessageFailure(Error) static func == (lhs: NetworkError, rhs: NetworkError) -> Bool { switch (lhs, rhs) { - case (.webSocketNotConnected, .webSocketNotConnected): return true + case (.connectionFailed, .connectionFailed): return true case (.sendMessageFailed, .sendMessageFailed): return true case (.receiveMessageFailure, .receiveMessageFailure): return true default: return false @@ -22,8 +22,8 @@ extension NetworkError: LocalizedError { var localizedDescription: String { switch self { - case .webSocketNotConnected: - return "Web socket is not connected to any URL." + case .connectionFailed: + return "Web socket is not connected to any URL or networking connection error" case .sendMessageFailed(let error): return "Failed to send a message through the web socket: \(error)" case .receiveMessageFailure(let error): diff --git a/Sources/WalletConnectRelay/NetworkMonitoring.swift b/Sources/WalletConnectRelay/NetworkMonitoring.swift index 1d3932db5..e6c6b4477 100644 --- a/Sources/WalletConnectRelay/NetworkMonitoring.swift +++ b/Sources/WalletConnectRelay/NetworkMonitoring.swift @@ -8,6 +8,7 @@ public enum NetworkConnectionStatus { } public protocol NetworkMonitoring: AnyObject { + var isConnected: Bool { get } var networkConnectionStatusPublisher: AnyPublisher { get } } @@ -16,7 +17,11 @@ public final class NetworkMonitor: NetworkMonitoring { private let workerQueue = DispatchQueue(label: "com.walletconnect.sdk.network.monitor") private let networkConnectionStatusPublisherSubject = CurrentValueSubject(.connected) - + + public var isConnected: Bool { + return networkConnectionStatusPublisherSubject.value == .connected + } + public var networkConnectionStatusPublisher: AnyPublisher { networkConnectionStatusPublisherSubject .share() diff --git a/Sources/WalletConnectRelay/RelayClient.swift b/Sources/WalletConnectRelay/RelayClient.swift index 24cae47c0..5ff6135fd 100644 --- a/Sources/WalletConnectRelay/RelayClient.swift +++ b/Sources/WalletConnectRelay/RelayClient.swift @@ -27,6 +27,10 @@ public final class RelayClient { dispatcher.socketConnectionStatusPublisher } + public var networkConnectionStatusPublisher: AnyPublisher { + dispatcher.networkConnectionStatusPublisher + } + private let messagePublisherSubject = PassthroughSubject<(topic: String, message: String, publishedAt: Date), Never>() private let subscriptionResponsePublisherSubject = PassthroughSubject<(RPCID?, [String]), Never>() @@ -165,9 +169,9 @@ public final class RelayClient { } } - public func unsubscribe(topic: String, completion: @escaping ((Error?) -> Void)) { + public func unsubscribe(topic: String, completion: ((Error?) -> Void)?) { guard let subscriptionId = subscriptions[topic] else { - completion(Errors.subscriptionIdNotFound) + completion?(Errors.subscriptionIdNotFound) return } logger.debug("Unsubscribing from topic: \(topic)") @@ -179,12 +183,12 @@ public final class RelayClient { dispatcher.protectedSend(message) { [weak self] error in if let error = error { self?.logger.debug("Failed to unsubscribe from topic") - completion(error) + completion?(error) } else { self?.concurrentQueue.async(flags: .barrier) { self?.subscriptions[topic] = nil } - completion(nil) + completion?(nil) } } } diff --git a/Sources/WalletConnectRelay/RelayClientFactory.swift b/Sources/WalletConnectRelay/RelayClientFactory.swift index 98066e6c8..b59a50d29 100644 --- a/Sources/WalletConnectRelay/RelayClientFactory.swift +++ b/Sources/WalletConnectRelay/RelayClientFactory.swift @@ -20,6 +20,8 @@ public struct RelayClientFactory { let logger = ConsoleLogger(prefix: "🚄" ,loggingLevel: .off) + let networkMonitor = NetworkMonitor() + return RelayClientFactory.create( relayHost: relayHost, projectId: projectId, @@ -27,6 +29,7 @@ public struct RelayClientFactory { keychainStorage: keychainStorage, socketFactory: socketFactory, socketConnectionType: socketConnectionType, + networkMonitor: networkMonitor, logger: logger ) } @@ -39,6 +42,7 @@ public struct RelayClientFactory { keychainStorage: KeychainStorageProtocol, socketFactory: WebSocketFactory, socketConnectionType: SocketConnectionType = .automatic, + networkMonitor: NetworkMonitoring, logger: ConsoleLogging ) -> RelayClient { @@ -52,9 +56,11 @@ public struct RelayClientFactory { projectId: projectId, socketAuthenticator: socketAuthenticator ) + let dispatcher = Dispatcher( socketFactory: socketFactory, - relayUrlFactory: relayUrlFactory, + relayUrlFactory: relayUrlFactory, + networkMonitor: networkMonitor, socketConnectionType: socketConnectionType, logger: logger ) diff --git a/Tests/RelayerTests/DispatcherTests.swift b/Tests/RelayerTests/DispatcherTests.swift index 8d86455df..331bd640d 100644 --- a/Tests/RelayerTests/DispatcherTests.swift +++ b/Tests/RelayerTests/DispatcherTests.swift @@ -62,6 +62,7 @@ final class DispatcherTests: XCTestCase { networkMonitor = NetworkMonitoringMock() let defaults = RuntimeKeyValueStorage() let logger = ConsoleLoggerMock() + let networkMonitor = NetworkMonitoringMock() let keychainStorageMock = DispatcherKeychainStorageMock() let clientIdStorage = ClientIdStorage(defaults: defaults, keychain: keychainStorageMock, logger: logger) let socketAuthenticator = ClientIdAuthenticator(clientIdStorage: clientIdStorage) @@ -72,7 +73,8 @@ final class DispatcherTests: XCTestCase { ) sut = Dispatcher( socketFactory: webSocketFactory, - relayUrlFactory: relayUrlFactory, + relayUrlFactory: relayUrlFactory, + networkMonitor: networkMonitor, socketConnectionType: .manual, logger: ConsoleLoggerMock() ) diff --git a/Tests/RelayerTests/Helpers/Error+Extension.swift b/Tests/RelayerTests/Helpers/Error+Extension.swift index 901d2d829..76dd92672 100644 --- a/Tests/RelayerTests/Helpers/Error+Extension.swift +++ b/Tests/RelayerTests/Helpers/Error+Extension.swift @@ -24,7 +24,7 @@ extension Error { extension NetworkError { var isWebSocketError: Bool { - guard case .webSocketNotConnected = self else { return false } + guard case .connectionFailed = self else { return false } return true } diff --git a/Tests/RelayerTests/Mocks/DispatcherMock.swift b/Tests/RelayerTests/Mocks/DispatcherMock.swift index d5088bf61..869e3a0f9 100644 --- a/Tests/RelayerTests/Mocks/DispatcherMock.swift +++ b/Tests/RelayerTests/Mocks/DispatcherMock.swift @@ -4,11 +4,15 @@ import Combine @testable import WalletConnectRelay class DispatcherMock: Dispatching { + private var publishers = Set() private let socketConnectionStatusPublisherSubject = CurrentValueSubject(.disconnected) var socketConnectionStatusPublisher: AnyPublisher { return socketConnectionStatusPublisherSubject.eraseToAnyPublisher() } + var networkConnectionStatusPublisher: AnyPublisher { + return Just(.connected).eraseToAnyPublisher() + } var sent = false var lastMessage: String = "" diff --git a/Tests/RelayerTests/Mocks/NetworkMonitoringMock.swift b/Tests/RelayerTests/Mocks/NetworkMonitoringMock.swift index 1095d1677..bfbad58cf 100644 --- a/Tests/RelayerTests/Mocks/NetworkMonitoringMock.swift +++ b/Tests/RelayerTests/Mocks/NetworkMonitoringMock.swift @@ -4,6 +4,10 @@ import Combine @testable import WalletConnectRelay class NetworkMonitoringMock: NetworkMonitoring { + var isConnected: Bool { + return true + } + var networkConnectionStatusPublisher: AnyPublisher { networkConnectionStatusPublisherSubject.eraseToAnyPublisher() }