diff --git a/Example/DApp/ClientDelegate.swift b/Example/DApp/ClientDelegate.swift index 2fecc1d15..67e410d19 100644 --- a/Example/DApp/ClientDelegate.swift +++ b/Example/DApp/ClientDelegate.swift @@ -1,4 +1,5 @@ import WalletConnect +import Relayer class ClientDelegate: WalletConnectClientDelegate { var client: WalletConnectClient @@ -13,12 +14,8 @@ class ClientDelegate: WalletConnectClientDelegate { description: "a description", url: "wallet.connect", icons: ["https://gblobscdn.gitbook.com/spaces%2F-LJJeCjcLrr53DcT1Ml7%2Favatar.png?alt=media"]) - self.client = WalletConnectClient( - metadata: metadata, - projectId: "52af113ee0c1e1a20f4995730196c13e", - isController: false, - relayHost: "relay.dev.walletconnect.com" - ) + let relayer = Relayer(relayHost: "relay.dev.walletconnect.com", projectId: "52af113ee0c1e1a20f4995730196c13e") + self.client = WalletConnectClient(metadata: metadata, isController: false, relayer: relayer) client.delegate = self } diff --git a/Example/DApp/Connect/ConnectViewController.swift b/Example/DApp/Connect/ConnectViewController.swift index 09e000a7d..8009fd9ae 100644 --- a/Example/DApp/Connect/ConnectViewController.swift +++ b/Example/DApp/Connect/ConnectViewController.swift @@ -86,7 +86,7 @@ class ConnectViewController: UIViewController, UITableViewDataSource, UITableVie func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "pairing_cell", for: indexPath) - cell.textLabel?.text = activePairings[indexPath.row].peer!.name + cell.textLabel?.text = activePairings[indexPath.row].peer?.name ?? "" return cell } diff --git a/Sources/Relayer/AppStateObserving.swift b/Sources/Relayer/AppStateObserving.swift new file mode 100644 index 000000000..777658cd0 --- /dev/null +++ b/Sources/Relayer/AppStateObserving.swift @@ -0,0 +1,48 @@ + +import Foundation +#if os(iOS) +import UIKit +#endif + +protocol AppStateObserving { + var onWillEnterForeground: (()->())? {get set} + var onWillEnterBackground: (()->())? {get set} +} + +class AppStateObserver: AppStateObserving { + @objc var onWillEnterForeground: (() -> ())? + + @objc var onWillEnterBackground: (() -> ())? + + init() { + subscribeNotificationCenter() + } + + private func subscribeNotificationCenter() { +#if os(iOS) + NotificationCenter.default.addObserver( + self, + selector: #selector(appWillEnterForeground), + name: UIApplication.willEnterForegroundNotification, + object: nil) + NotificationCenter.default.addObserver( + self, + selector: #selector(appWillEnterBackground), + name: UIApplication.willResignActiveNotification, + object: nil) +#endif + } + + @objc + private func appWillEnterBackground() { + onWillEnterBackground?() + } + + @objc + private func appWillEnterForeground() { + onWillEnterForeground?() + } + +} + + diff --git a/Sources/Relayer/Dispatching.swift b/Sources/Relayer/Dispatching.swift index 742d92154..7a863ad27 100644 --- a/Sources/Relayer/Dispatching.swift +++ b/Sources/Relayer/Dispatching.swift @@ -6,8 +6,8 @@ protocol Dispatching { var onDisconnect: (()->())? {get set} var onMessage: ((String) -> ())? {get set} func send(_ string: String, completion: @escaping (Error?)->()) - func connect() - func disconnect(closeCode: URLSessionWebSocketTask.CloseCode) + func connect() throws + func disconnect(closeCode: URLSessionWebSocketTask.CloseCode) throws } final class Dispatcher: NSObject, Dispatching { @@ -15,43 +15,36 @@ final class Dispatcher: NSObject, Dispatching { var onDisconnect: (() -> ())? var onMessage: ((String) -> ())? private var textFramesQueue = Queue() - private var networkMonitor: NetworkMonitoring - private let url: URL var socket: WebSocketSessionProtocol var socketConnectionObserver: SocketConnectionObserving + var socketConnectionHandler: SocketConnectionHandler - init(url: URL, - networkMonitor: NetworkMonitoring = NetworkMonitor(), - socket: WebSocketSessionProtocol, - socketConnectionObserver: SocketConnectionObserving) { - self.url = url - self.networkMonitor = networkMonitor + init(socket: WebSocketSessionProtocol, + socketConnectionObserver: SocketConnectionObserving, + socketConnectionHandler: SocketConnectionHandler) { self.socket = socket self.socketConnectionObserver = socketConnectionObserver + self.socketConnectionHandler = socketConnectionHandler super.init() setUpWebSocketSession() setUpSocketConnectionObserving() - setUpNetworkMonitoring() - socket.connect(on: url) } func send(_ string: String, completion: @escaping (Error?) -> Void) { if socket.isConnected { self.socket.send(string, completionHandler: completion) + //TODO - enqueue if fails } else { textFramesQueue.enqueue(string) } } - func connect() { - if !socket.isConnected { - socket.connect(on: url) - } + func connect() throws { + try socketConnectionHandler.handleConnect() } - func disconnect(closeCode: URLSessionWebSocketTask.CloseCode) { - socket.disconnect(with: closeCode) - onDisconnect?() + func disconnect(closeCode: URLSessionWebSocketTask.CloseCode) throws { + try socketConnectionHandler.handleDisconnect(closeCode: closeCode) } private func setUpWebSocketSession() { @@ -59,7 +52,7 @@ final class Dispatcher: NSObject, Dispatching { self?.onMessage?($0) } socket.onMessageError = { error in - print(error) + print("WebSocket Error \(error)") } } @@ -73,16 +66,6 @@ final class Dispatcher: NSObject, Dispatching { } } - private func setUpNetworkMonitoring() { - networkMonitor.onSatisfied = { [weak self] in - self?.connect() - } - networkMonitor.onUnsatisfied = { [weak self] in - self?.disconnect(closeCode: .goingAway) - } - networkMonitor.startMonitoring() - } - private func dequeuePendingTextFrames() { while let frame = textFramesQueue.dequeue() { send(frame) { error in @@ -93,4 +76,3 @@ final class Dispatcher: NSObject, Dispatching { } } } - diff --git a/Sources/Relayer/NetworkMonitoring.swift b/Sources/Relayer/NetworkMonitoring.swift index 99acd096b..61bbdd5ab 100644 --- a/Sources/Relayer/NetworkMonitoring.swift +++ b/Sources/Relayer/NetworkMonitoring.swift @@ -26,3 +26,4 @@ class NetworkMonitor: NetworkMonitoring { monitor.start(queue: monitorQueue) } } + diff --git a/Sources/Relayer/WakuNetworkRelay.swift b/Sources/Relayer/Relayer.swift similarity index 80% rename from Sources/Relayer/WakuNetworkRelay.swift rename to Sources/Relayer/Relayer.swift index 7477a759c..a1dc3fb94 100644 --- a/Sources/Relayer/WakuNetworkRelay.swift +++ b/Sources/Relayer/Relayer.swift @@ -4,7 +4,7 @@ import Combine import WalletConnectUtils -public final class WakuNetworkRelay { +public final class Relayer { enum RelyerError: Error { case subscriptionIdNotFound } @@ -28,7 +28,7 @@ public final class WakuNetworkRelay { requestAcknowledgePublisherSubject.eraseToAnyPublisher() } private let requestAcknowledgePublisherSubject = PassthroughSubject, Never>() - private let logger: ConsoleLogging + let logger: ConsoleLogging init(dispatcher: Dispatching, logger: ConsoleLogging, @@ -41,26 +41,44 @@ public final class WakuNetworkRelay { setUpBindings() } - public convenience init(logger: ConsoleLogging, - url: URL, - keyValueStorage: KeyValueStorage, - uniqueIdentifier: String) { + /// Instantiates Relayer + /// - Parameters: + /// - relayHost: proxy server host that your application will use to connect to Waku Network. If you register your project at `www.walletconnect.com` you can use `relay.walletconnect.com` + /// - projectId: an optional parameter used to access the public WalletConnect infrastructure. Go to `www.walletconnect.com` for info. + /// - keyValueStorage: by default WalletConnect SDK will store sequences in UserDefaults + /// - uniqueIdentifier: if your app requires more than one relayer instances you are required to call identify them + /// - socketConnectionType: socket connection type + /// - logger: logger instance + public convenience init(relayHost: String, + projectId: String, + keyValueStorage: KeyValueStorage = UserDefaults.standard, + uniqueIdentifier: String? = nil, + socketConnectionType: SocketConnectionType = .automatic, + logger: ConsoleLogging = ConsoleLogger(loggingLevel: .off)) { let socketConnectionObserver = SocketConnectionObserver() let urlSession = URLSession(configuration: .default, delegate: socketConnectionObserver, delegateQueue: OperationQueue()) - let socket = WebSocketSession(session: urlSession) - let dispatcher = Dispatcher(url: url, socket: socket, socketConnectionObserver: socketConnectionObserver) + let url = Self.makeRelayUrl(host: relayHost, projectId: projectId) + let socket = WebSocketSession(session: urlSession, url: url) + var socketConnectionHandler: SocketConnectionHandler + switch socketConnectionType { + case .automatic: + socketConnectionHandler = AutomaticSocketConnectionHandler(socket: socket) + case .manual: + socketConnectionHandler = ManualSocketConnectionHandler(socket: socket) + } + let dispatcher = Dispatcher(socket: socket, socketConnectionObserver: socketConnectionObserver, socketConnectionHandler: socketConnectionHandler) self.init(dispatcher: dispatcher, logger: logger, keyValueStorage: keyValueStorage, - uniqueIdentifier: uniqueIdentifier) + uniqueIdentifier: uniqueIdentifier ?? "") } - public func connect() { - dispatcher.connect() + public func connect() throws { + try dispatcher.connect() } - public func disconnect(closeCode: URLSessionWebSocketTask.CloseCode) { - dispatcher.disconnect(closeCode: closeCode) + public func disconnect(closeCode: URLSessionWebSocketTask.CloseCode) throws { + try dispatcher.disconnect(closeCode: closeCode) } @discardableResult public func publish(topic: String, payload: String, completion: @escaping ((Error?) -> ())) -> Int64 { @@ -192,7 +210,7 @@ public final class WakuNetworkRelay { } } - static public func makeRelayUrl(host: String, projectId: String) -> URL { + static private func makeRelayUrl(host: String, projectId: String) -> URL { var components = URLComponents() components.scheme = "wss" components.host = host diff --git a/Sources/Relayer/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift b/Sources/Relayer/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift new file mode 100644 index 000000000..947e249a3 --- /dev/null +++ b/Sources/Relayer/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift @@ -0,0 +1,77 @@ + +#if os(iOS) +import UIKit +#endif +import Foundation + +class AutomaticSocketConnectionHandler: SocketConnectionHandler { + enum Error: Swift.Error { + case manualSocketConnectionForbidden + case manualSocketDisconnectionForbidden + } + private var appStateObserver: AppStateObserving + let socket: WebSocketConnecting + private var networkMonitor: NetworkMonitoring + private let backgroundTaskRegistrar: BackgroundTaskRegistering + + init(networkMonitor: NetworkMonitoring = NetworkMonitor(), + socket: WebSocketConnecting, + appStateObserver: AppStateObserving = AppStateObserver(), + backgroundTaskRegistrar: BackgroundTaskRegistering = BackgroundTaskRegistrar()) { + self.appStateObserver = appStateObserver + self.socket = socket + self.networkMonitor = networkMonitor + self.backgroundTaskRegistrar = backgroundTaskRegistrar + setUpStateObserving() + setUpNetworkMonitoring() + socket.connect() + } + + private func setUpStateObserving() { + appStateObserver.onWillEnterBackground = { [unowned self] in + registerBackgroundTask() + } + + appStateObserver.onWillEnterForeground = { [unowned self] in + socket.connect() + } + } + + private func setUpNetworkMonitoring() { + networkMonitor.onSatisfied = { [weak self] in + self?.handleNetworkSatisfied() + } + networkMonitor.onUnsatisfied = { [weak self] in + self?.handleNetworkUnsatisfied() + } + networkMonitor.startMonitoring() + } + + func registerBackgroundTask() { + backgroundTaskRegistrar.register(name: "Finish Network Tasks") { [unowned self] in + endBackgroundTask() + } + } + + func endBackgroundTask() { + socket.disconnect(with: .normalClosure) + } + + func handleConnect() throws { + throw Error.manualSocketConnectionForbidden + } + + func handleDisconnect(closeCode: URLSessionWebSocketTask.CloseCode) throws { + throw Error.manualSocketDisconnectionForbidden + } + + func handleNetworkUnsatisfied() { + socket.disconnect(with: .goingAway) + } + + func handleNetworkSatisfied() { + if !socket.isConnected { + socket.connect() + } + } +} diff --git a/Sources/Relayer/SocketConnectionHandler/BackgroundTaskRegistering.swift b/Sources/Relayer/SocketConnectionHandler/BackgroundTaskRegistering.swift new file mode 100644 index 000000000..f36d54e01 --- /dev/null +++ b/Sources/Relayer/SocketConnectionHandler/BackgroundTaskRegistering.swift @@ -0,0 +1,26 @@ + +import Foundation +#if os(iOS) +import UIKit +#endif + +protocol BackgroundTaskRegistering { + func register(name: String, completion: @escaping ()->()) +} + +class BackgroundTaskRegistrar: BackgroundTaskRegistering { +#if os(iOS) + private var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid +#endif + + func register(name: String, completion: @escaping () -> ()) { +#if os(iOS) + backgroundTaskID = .invalid + backgroundTaskID = UIApplication.shared.beginBackgroundTask (withName: name) { [unowned self] in + UIApplication.shared.endBackgroundTask(backgroundTaskID) + backgroundTaskID = .invalid + completion() + } +#endif + } +} diff --git a/Sources/Relayer/SocketConnectionHandler/ManualSocketConnectionHandler.swift b/Sources/Relayer/SocketConnectionHandler/ManualSocketConnectionHandler.swift new file mode 100644 index 000000000..b3f2d2c8c --- /dev/null +++ b/Sources/Relayer/SocketConnectionHandler/ManualSocketConnectionHandler.swift @@ -0,0 +1,18 @@ + +import Foundation + +class ManualSocketConnectionHandler: SocketConnectionHandler { + var socket: WebSocketConnecting + + init(socket: WebSocketConnecting) { + self.socket = socket + } + + func handleConnect() throws { + socket.connect() + } + + func handleDisconnect(closeCode: URLSessionWebSocketTask.CloseCode) throws { + socket.disconnect(with: closeCode) + } +} diff --git a/Sources/Relayer/SocketConnectionHandler/SocketConnectionHandler.swift b/Sources/Relayer/SocketConnectionHandler/SocketConnectionHandler.swift new file mode 100644 index 000000000..4320b699e --- /dev/null +++ b/Sources/Relayer/SocketConnectionHandler/SocketConnectionHandler.swift @@ -0,0 +1,8 @@ + +import Foundation + +protocol SocketConnectionHandler { + var socket: WebSocketConnecting {get} + func handleConnect() throws + func handleDisconnect(closeCode: URLSessionWebSocketTask.CloseCode) throws +} diff --git a/Sources/Relayer/SocketConnectionHandler/SocketConnectionType.swift b/Sources/Relayer/SocketConnectionHandler/SocketConnectionType.swift new file mode 100644 index 000000000..bab534860 --- /dev/null +++ b/Sources/Relayer/SocketConnectionHandler/SocketConnectionType.swift @@ -0,0 +1,7 @@ + +import Foundation + +public enum SocketConnectionType { + case automatic + case manual +} diff --git a/Sources/Relayer/SocketConnectionObserving.swift b/Sources/Relayer/SocketConnectionObserving.swift index bb5ca1cfb..41f83e249 100644 --- a/Sources/Relayer/SocketConnectionObserving.swift +++ b/Sources/Relayer/SocketConnectionObserving.swift @@ -18,3 +18,6 @@ class SocketConnectionObserver: NSObject, URLSessionWebSocketDelegate, SocketCon onDisconnect?() } } + + + diff --git a/Sources/Relayer/WebSocketSession.swift b/Sources/Relayer/WebSocketSession.swift index 85c4bc7f4..7d1d83688 100644 --- a/Sources/Relayer/WebSocketSession.swift +++ b/Sources/Relayer/WebSocketSession.swift @@ -4,16 +4,19 @@ protocol WebSocketSessionProtocol { var onMessageReceived: ((String) -> ())? {get set} var onMessageError: ((Error) -> ())? {get set} var isConnected: Bool {get} - func connect(on url: URL) - func disconnect(with closeCode: URLSessionWebSocketTask.CloseCode) func send(_ message: String, completionHandler: @escaping ((Error?) -> Void)) } +protocol WebSocketConnecting { + var isConnected: Bool {get} + func connect() + func disconnect(with closeCode: URLSessionWebSocketTask.CloseCode) +} -final class WebSocketSession: NSObject, WebSocketSessionProtocol { +final class WebSocketSession: NSObject, WebSocketSessionProtocol, WebSocketConnecting { var onMessageReceived: ((String) -> ())? var onMessageError: ((Error) -> ())? - + let url: URL var isConnected: Bool { webSocketTask != nil } @@ -22,12 +25,13 @@ final class WebSocketSession: NSObject, WebSocketSessionProtocol { private var webSocketTask: URLSessionWebSocketTaskProtocol? - init(session: URLSessionProtocol) { + init(session: URLSessionProtocol, url: URL) { self.session = session + self.url = url super.init() } - func connect(on url: URL) { + func connect() { webSocketTask = session.webSocketTask(with: url) listen() webSocketTask?.resume() @@ -68,7 +72,9 @@ final class WebSocketSession: NSObject, WebSocketSessionProtocol { switch message { case .string(let text): onMessageReceived?(text) - default: + case .data(let data): + print("Transport: Unexpected type of message received: \(data)") + @unknown default: print("Transport: Unexpected type of message received") } } diff --git a/Sources/WalletConnect/Relay/NetworkRelaying.swift b/Sources/WalletConnect/Relay/NetworkRelaying.swift index 5e734874c..93f9a2b9b 100644 --- a/Sources/WalletConnect/Relay/NetworkRelaying.swift +++ b/Sources/WalletConnect/Relay/NetworkRelaying.swift @@ -2,13 +2,13 @@ import Foundation import Relayer -extension WakuNetworkRelay: NetworkRelaying {} +extension Relayer: NetworkRelaying {} protocol NetworkRelaying { var onConnect: (()->())? {get set} var onMessage: ((_ topic: String, _ message: String) -> ())? {get set} - func connect() - func disconnect(closeCode: URLSessionWebSocketTask.CloseCode) + func connect() throws + func disconnect(closeCode: URLSessionWebSocketTask.CloseCode) throws /// - returns: request id @discardableResult func publish(topic: String, payload: String, completion: @escaping ((Error?)->())) -> Int64 /// - returns: request id diff --git a/Sources/WalletConnect/Subscription/WCSubscribing.swift b/Sources/WalletConnect/Subscription/WCSubscribing.swift index afad946a2..d236d407c 100644 --- a/Sources/WalletConnect/Subscription/WCSubscribing.swift +++ b/Sources/WalletConnect/Subscription/WCSubscribing.swift @@ -31,7 +31,7 @@ class WCSubscriber: WCSubscribing { func setSubscription(topic: String) { logger.debug("Setting Subscription...") - concurrentQueue.sync { + concurrentQueue.sync(flags: .barrier) { topics.append(topic) } relay.subscribe(topic: topic) @@ -44,7 +44,7 @@ class WCSubscriber: WCSubscribing { } func removeSubscription(topic: String) { - concurrentQueue.sync { + concurrentQueue.sync(flags: .barrier) { topics.removeAll {$0 == topic} } relay.unsubscribe(topic: topic) diff --git a/Sources/WalletConnect/WalletConnectClient.swift b/Sources/WalletConnect/WalletConnectClient.swift index ddb239ff8..edfb7bc57 100644 --- a/Sources/WalletConnect/WalletConnectClient.swift +++ b/Sources/WalletConnect/WalletConnectClient.swift @@ -26,14 +26,10 @@ public final class WalletConnectClient { private let pairingEngine: PairingEngine private let sessionEngine: SessionEngine private let relay: WalletConnectRelaying - private let wakuRelay: NetworkRelaying private let crypto: Crypto private let secureStorage: SecureStorage private let pairingQueue = DispatchQueue(label: "com.walletconnect.sdk.client.pairing", qos: .userInitiated) private let history: JsonRpcHistory -#if os(iOS) - private var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid -#endif // MARK: - Initializers @@ -44,7 +40,7 @@ public final class WalletConnectClient { /// - projectId: an optional parameter used to access the public WalletConnect infrastructure. Go to `www.walletconnect.com` for info. /// - isController: the peer that controls communication permissions for allowed chains, notification types and JSON-RPC request methods. Always true for a wallet. /// - relayHost: proxy server host that your application will use to connect to Waku Network. If you register your project at `www.walletconnect.com` you can use `relay.walletconnect.com` - /// - keyValueStorage: by default WalletConnect SDK will store sequences in UserDefaults but if for some reasons you want to provide your own storage you can inject it here. + /// - keyValueStorage: by default WalletConnect SDK will store sequences in UserDefaults /// - clientName: if your app requires more than one client you are required to call them with different names to distinguish logs source and prefix storage keys. /// /// WalletConnect Client is not a singleton but once you create an instance, you should not deinitialize it. Usually only one instance of a client is required in the application. @@ -59,39 +55,47 @@ public final class WalletConnectClient { // try? keychain.deleteAll() // Use for cleanup while lifecycles are not handled yet, but FIXME whenever self.crypto = Crypto(keychain: keychain) self.secureStorage = SecureStorage(keychain: keychain) - let relayUrl = WakuNetworkRelay.makeRelayUrl(host: relayHost, projectId: projectId) - self.wakuRelay = WakuNetworkRelay(logger: logger, url: relayUrl, keyValueStorage: keyValueStorage, uniqueIdentifier: clientName ?? "") + let relayer = Relayer(relayHost: relayHost, projectId: projectId, keyValueStorage: keyValueStorage, uniqueIdentifier: clientName ?? "", logger: logger) let serializer = JSONRPCSerializer(crypto: crypto) self.history = JsonRpcHistory(logger: logger, keyValueStore: KeyValueStore(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.jsonRpcHistory(clientName: clientName ?? "_"))) - self.relay = WalletConnectRelay(networkRelayer: wakuRelay, jsonRpcSerializer: serializer, logger: logger, jsonRpcHistory: history) + self.relay = WalletConnectRelay(networkRelayer: relayer, jsonRpcSerializer: serializer, logger: logger, jsonRpcHistory: history) let pairingSequencesStore = PairingStorage(storage: SequenceStore(storage: keyValueStorage, identifier: StorageDomainIdentifiers.pairings(clientName: clientName ?? "_"))) let sessionSequencesStore = SessionStorage(storage: SequenceStore(storage: keyValueStorage, identifier: StorageDomainIdentifiers.sessions(clientName: clientName ?? "_"))) self.pairingEngine = PairingEngine(relay: relay, crypto: crypto, subscriber: WCSubscriber(relay: relay, logger: logger), sequencesStore: pairingSequencesStore, isController: isController, metadata: metadata, logger: logger) self.sessionEngine = SessionEngine(relay: relay, crypto: crypto, subscriber: WCSubscriber(relay: relay, logger: logger), sequencesStore: sessionSequencesStore, isController: isController, metadata: metadata, logger: logger) setUpEnginesCallbacks() - subscribeNotificationCenter() - registerBackgroundTask() } - func registerBackgroundTask() { -#if os(iOS) - backgroundTaskID = UIApplication.shared.beginBackgroundTask (withName: "Finish Network Tasks") { [weak self] in - self?.endBackgroundTask() - } -#endif + /// Initializes and returns newly created WalletConnect Client Instance. Establishes a network connection with the relay + /// + /// - Parameters: + /// - metadata: describes your application and will define pairing appearance in a web browser. + /// - isController: the peer that controls communication permissions for allowed chains, notification types and JSON-RPC request methods. Always true for a wallet. + /// - relayer: Relayer instance + /// - keyValueStorage: by default WalletConnect SDK will store sequences in UserDefaults but if for some reasons you want to provide your own storage you can inject it here. + /// - clientName: if your app requires more than one client you are required to call them with different names to distinguish logs source and prefix storage keys. + /// + /// WalletConnect Client is not a singleton but once you create an instance, you should not deinitialize it. Usually only one instance of a client is required in the application. + public convenience init(metadata: AppMetadata, isController: Bool, relayer: Relayer, keyValueStorage: KeyValueStorage = UserDefaults.standard, clientName: String? = nil) { + self.init(metadata: metadata, isController: isController, relayer: relayer, logger: ConsoleLogger(loggingLevel: .off), keychain: KeychainStorage(uniqueIdentifier: clientName), keyValueStorage: keyValueStorage, clientName: clientName) } - func endBackgroundTask() { -#if os(iOS) - wakuRelay.disconnect(closeCode: .goingAway) - print("Background task ended.") - UIApplication.shared.endBackgroundTask(backgroundTaskID) - backgroundTaskID = .invalid -#endif - } - deinit { - unsubscribeNotificationCenter() + init(metadata: AppMetadata, isController: Bool, relayer: Relayer, logger: ConsoleLogging, keychain: KeychainStorage, keyValueStorage: KeyValueStorage, clientName: String? = nil) { + self.metadata = metadata + self.isController = isController + self.logger = logger +// try? keychain.deleteAll() // Use for cleanup while lifecycles are not handled yet, but FIXME whenever + self.crypto = Crypto(keychain: keychain) + self.secureStorage = SecureStorage(keychain: keychain) + let serializer = JSONRPCSerializer(crypto: crypto) + self.history = JsonRpcHistory(logger: logger, keyValueStore: KeyValueStore(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.jsonRpcHistory(clientName: clientName ?? "_"))) + self.relay = WalletConnectRelay(networkRelayer: relayer, jsonRpcSerializer: serializer, logger: logger, jsonRpcHistory: history) + let pairingSequencesStore = PairingStorage(storage: SequenceStore(storage: keyValueStorage, identifier: StorageDomainIdentifiers.pairings(clientName: clientName ?? "_"))) + let sessionSequencesStore = SessionStorage(storage: SequenceStore(storage: keyValueStorage, identifier: StorageDomainIdentifiers.sessions(clientName: clientName ?? "_"))) + self.pairingEngine = PairingEngine(relay: relay, crypto: crypto, subscriber: WCSubscriber(relay: relay, logger: logger), sequencesStore: pairingSequencesStore, isController: isController, metadata: metadata, logger: logger) + self.sessionEngine = SessionEngine(relay: relay, crypto: crypto, subscriber: WCSubscriber(relay: relay, logger: logger), sequencesStore: sessionSequencesStore, isController: isController, metadata: metadata, logger: logger) + setUpEnginesCallbacks() } // MARK: - Public interface @@ -328,27 +332,4 @@ public final class WalletConnectClient { ) delegate?.didReceive(sessionProposal: sessionProposal) } - - private func subscribeNotificationCenter() { -#if os(iOS) - NotificationCenter.default.addObserver( - self, - selector: #selector(appWillEnterForeground), - name: UIApplication.willEnterForegroundNotification, - object: nil) -#endif - } - - private func unsubscribeNotificationCenter() { -#if os(iOS) - NotificationCenter.default.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil) -#endif - } - - @objc - private func appWillEnterForeground() { - wakuRelay.connect() - registerBackgroundTask() - } - } diff --git a/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift b/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift new file mode 100644 index 000000000..39c1b40f4 --- /dev/null +++ b/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift @@ -0,0 +1,66 @@ + +import Foundation +import XCTest +@testable import Relayer + +final class AutomaticSocketConnectionHandlerTests: XCTestCase { + var sut: AutomaticSocketConnectionHandler! + var webSocketSession: WebSocketSessionMock! + var networkMonitor: NetworkMonitoringMock! + var socketConnectionObserver: SocketConnectionObserverMock! + var appStateObserver: AppStateObserving! + var backgroundTaskRegistrar: BackgroundTaskRegistrarMock! + override func setUp() { + webSocketSession = WebSocketSessionMock() + networkMonitor = NetworkMonitoringMock() + appStateObserver = AppStateObserverMock() + socketConnectionObserver = SocketConnectionObserverMock() + backgroundTaskRegistrar = BackgroundTaskRegistrarMock() + sut = AutomaticSocketConnectionHandler( + networkMonitor: networkMonitor, + socket: webSocketSession, + appStateObserver: appStateObserver, + backgroundTaskRegistrar: backgroundTaskRegistrar) + } + + func testDisconnectOnConnectionLoss() { + webSocketSession.connect() + XCTAssertTrue(sut.socket.isConnected) + networkMonitor.onUnsatisfied?() + XCTAssertFalse(sut.socket.isConnected) + } + + func testConnectsOnConnectionSatisfied() { + webSocketSession.disconnect(with: .normalClosure) + XCTAssertFalse(sut.socket.isConnected) + networkMonitor.onSatisfied?() + XCTAssertTrue(sut.socket.isConnected) + } + + func testHandleConnectThrows() { + XCTAssertThrowsError(try sut.handleConnect()) + } + + func testHandleDisconnectThrows() { + XCTAssertThrowsError(try sut.handleDisconnect(closeCode: .normalClosure)) + } + + func testReconnectsOnEnterForeground() { + webSocketSession.disconnect(with: .normalClosure) + appStateObserver.onWillEnterForeground?() + XCTAssertTrue(sut.socket.isConnected) + } + + func testRegisterTaskOnEnterBackground() { + XCTAssertNil(backgroundTaskRegistrar.completion) + appStateObserver.onWillEnterBackground?() + XCTAssertNotNil(backgroundTaskRegistrar.completion) + } + + func testDisconnectOnEndBackgroundTask() { + appStateObserver.onWillEnterBackground?() + XCTAssertTrue(sut.socket.isConnected) + backgroundTaskRegistrar.completion!() + XCTAssertFalse(sut.socket.isConnected) + } +} diff --git a/Tests/RelayerTests/DispatcherTests.swift b/Tests/RelayerTests/DispatcherTests.swift index f4d969589..7d51ceeee 100644 --- a/Tests/RelayerTests/DispatcherTests.swift +++ b/Tests/RelayerTests/DispatcherTests.swift @@ -12,35 +12,21 @@ final class DispatcherTests: XCTestCase { webSocketSession = WebSocketSessionMock() networkMonitor = NetworkMonitoringMock() socketConnectionObserver = SocketConnectionObserverMock() - let url = URL(string: "ws://staging.walletconnect.org")! - sut = Dispatcher(url: url, networkMonitor: networkMonitor, socket: webSocketSession, socketConnectionObserver: socketConnectionObserver) + sut = Dispatcher(socket: webSocketSession, socketConnectionObserver: socketConnectionObserver, socketConnectionHandler: ManualSocketConnectionHandler(socket: webSocketSession)) } - - func testDisconnectOnConnectionLoss() { - XCTAssertTrue(sut.socket.isConnected) - networkMonitor.onUnsatisfied?() - XCTAssertFalse(sut.socket.isConnected) - } - - func testConnectsOnConnectionSatisfied() { - sut.disconnect(closeCode: .normalClosure) - XCTAssertFalse(sut.socket.isConnected) - networkMonitor.onSatisfied?() - XCTAssertTrue(sut.socket.isConnected) - } - + func testSendWhileConnected() { - sut.connect() + try! sut.connect() sut.send("1"){_ in} XCTAssertEqual(webSocketSession.sendCallCount, 1) } func testTextFramesSentAfterReconnectingSocket() { - sut.disconnect(closeCode: .normalClosure) + try! sut.disconnect(closeCode: .normalClosure) sut.send("1"){_ in} sut.send("2"){_ in} XCTAssertEqual(webSocketSession.sendCallCount, 0) - sut.connect() + try! sut.connect() socketConnectionObserver.onConnect?() XCTAssertEqual(webSocketSession.sendCallCount, 2) } diff --git a/Tests/RelayerTests/ManualSocketConnectionHandlerTests.swift b/Tests/RelayerTests/ManualSocketConnectionHandlerTests.swift new file mode 100644 index 000000000..f11a99b4b --- /dev/null +++ b/Tests/RelayerTests/ManualSocketConnectionHandlerTests.swift @@ -0,0 +1,29 @@ + + +import Foundation +import XCTest +@testable import Relayer + +final class ManualSocketConnectionHandlerTests: XCTestCase { + var sut: ManualSocketConnectionHandler! + var webSocketSession: WebSocketSessionMock! + var networkMonitor: NetworkMonitoringMock! + var socketConnectionObserver: SocketConnectionObserverMock! + override func setUp() { + webSocketSession = WebSocketSessionMock() + sut = ManualSocketConnectionHandler(socket: webSocketSession) + } + + func testHandleDisconnect() { + webSocketSession.connect() + XCTAssertTrue(webSocketSession.isConnected) + try! sut.handleDisconnect(closeCode: .normalClosure) + XCTAssertFalse(webSocketSession.isConnected) + } + + func testHandleConnect() { + XCTAssertFalse(webSocketSession.isConnected) + try! sut.handleConnect() + XCTAssertTrue(webSocketSession.isConnected) + } +} diff --git a/Tests/RelayerTests/Mocks/AppStateObserverMock.swift b/Tests/RelayerTests/Mocks/AppStateObserverMock.swift new file mode 100644 index 000000000..c5d363ddd --- /dev/null +++ b/Tests/RelayerTests/Mocks/AppStateObserverMock.swift @@ -0,0 +1,8 @@ + +import Foundation +@testable import Relayer + +class AppStateObserverMock: AppStateObserving { + var onWillEnterForeground: (() -> ())? + var onWillEnterBackground: (() -> ())? +} diff --git a/Tests/RelayerTests/Mocks/BackgroundTaskRegistrarMock.swift b/Tests/RelayerTests/Mocks/BackgroundTaskRegistrarMock.swift new file mode 100644 index 000000000..af86575c7 --- /dev/null +++ b/Tests/RelayerTests/Mocks/BackgroundTaskRegistrarMock.swift @@ -0,0 +1,10 @@ + +import Foundation +@testable import Relayer + +class BackgroundTaskRegistrarMock: BackgroundTaskRegistering { + var completion: (()->())? + func register(name: String, completion: @escaping () -> ()) { + self.completion = completion + } +} diff --git a/Tests/RelayerTests/Mocks/WebSocketSessionMock.swift b/Tests/RelayerTests/Mocks/WebSocketSessionMock.swift index a8f46f4d2..73534c24d 100644 --- a/Tests/RelayerTests/Mocks/WebSocketSessionMock.swift +++ b/Tests/RelayerTests/Mocks/WebSocketSessionMock.swift @@ -2,7 +2,7 @@ import Foundation @testable import Relayer -class WebSocketSessionMock: WebSocketSessionProtocol { +class WebSocketSessionMock: WebSocketSessionProtocol, WebSocketConnecting { var onConnect: (() -> ())? var onDisconnect: (() -> ())? var onMessageReceived: ((String) -> ())? @@ -10,7 +10,7 @@ class WebSocketSessionMock: WebSocketSessionProtocol { var sendCallCount: Int = 0 var isConnected: Bool = false - func connect(on url: URL) { + func connect() { isConnected = true onConnect?() } diff --git a/Tests/RelayerTests/RelayerEndToEndTests.swift b/Tests/RelayerTests/RelayerEndToEndTests.swift index 3ff516080..e87da3611 100644 --- a/Tests/RelayerTests/RelayerEndToEndTests.swift +++ b/Tests/RelayerTests/RelayerEndToEndTests.swift @@ -11,17 +11,18 @@ final class RelayerEndToEndTests: XCTestCase { let url = URL(string: "wss://staging.walletconnect.org")! private var publishers = [AnyCancellable]() - func makeRelayer() -> WakuNetworkRelay { + func makeRelayer(_ uniqueIdentifier: String = "") -> Relayer { let logger = ConsoleLogger() let socketConnectionObserver = SocketConnectionObserver() let urlSession = URLSession(configuration: .default, delegate: socketConnectionObserver, delegateQueue: OperationQueue()) - let socket = WebSocketSession(session: urlSession) - let dispatcher = Dispatcher(url: url, socket: socket, socketConnectionObserver: socketConnectionObserver) - return WakuNetworkRelay(dispatcher: dispatcher, logger: logger, keyValueStorage: RuntimeKeyValueStorage(), uniqueIdentifier: "") + let socket = WebSocketSession(session: urlSession, url: url) + let dispatcher = Dispatcher(socket: socket, socketConnectionObserver: socketConnectionObserver, socketConnectionHandler: ManualSocketConnectionHandler(socket: socket)) + return Relayer(dispatcher: dispatcher, logger: logger, keyValueStorage: RuntimeKeyValueStorage(), uniqueIdentifier: uniqueIdentifier) } func testSubscribe() { let relayer = makeRelayer() + try! relayer.connect() let subscribeExpectation = expectation(description: "subscribe call succeeds") relayer.subscribe(topic: "qwerty") { error in XCTAssertNil(error) @@ -31,8 +32,10 @@ final class RelayerEndToEndTests: XCTestCase { } func testEndToEndPayload() { - let relayA = makeRelayer() - let relayB = makeRelayer() + let relayA = makeRelayer("A") + let relayB = makeRelayer("B") + try! relayA.connect() + try! relayB.connect() let randomTopic = String.randomTopic() let payloadA = "A" diff --git a/Tests/RelayerTests/WakuRelayTests.swift b/Tests/RelayerTests/WakuRelayTests.swift index 8eca88978..9387ccda5 100644 --- a/Tests/RelayerTests/WakuRelayTests.swift +++ b/Tests/RelayerTests/WakuRelayTests.swift @@ -6,13 +6,13 @@ import XCTest @testable import Relayer class WakuRelayTests: XCTestCase { - var wakuRelay: WakuNetworkRelay! + var wakuRelay: Relayer! var dispatcher: DispatcherMock! override func setUp() { dispatcher = DispatcherMock() let logger = ConsoleLogger() - wakuRelay = WakuNetworkRelay(dispatcher: dispatcher, logger: logger, keyValueStorage: RuntimeKeyValueStorage(), uniqueIdentifier: "") + wakuRelay = Relayer(dispatcher: dispatcher, logger: logger, keyValueStorage: RuntimeKeyValueStorage(), uniqueIdentifier: "") } override func tearDown() { diff --git a/Tests/RelayerTests/WebSocketSessionTests.swift b/Tests/RelayerTests/WebSocketSessionTests.swift index 97436c340..c7163a305 100644 --- a/Tests/RelayerTests/WebSocketSessionTests.swift +++ b/Tests/RelayerTests/WebSocketSessionTests.swift @@ -7,11 +7,12 @@ final class WebSocketSessionTests: XCTestCase { var webSocketTaskMock: URLSessionWebSocketTaskMock! var sessionMock: URLSessionMock! - + var url: URL! override func setUp() { webSocketTaskMock = URLSessionWebSocketTaskMock() sessionMock = URLSessionMock(webSocketTaskMock: webSocketTaskMock) - sut = WebSocketSession(session: sessionMock) + url = URL.stub() + sut = WebSocketSession(session: sessionMock, url: url) } override func tearDown() { @@ -25,16 +26,15 @@ final class WebSocketSessionTests: XCTestCase { } func testConnect() { - let expectedURL = URL.stub() - sut.connect(on: expectedURL) + sut.connect() XCTAssertTrue(sut.isConnected) XCTAssertTrue(webSocketTaskMock.didCallResume) XCTAssertTrue(webSocketTaskMock.didCallReceive) - XCTAssertEqual(sessionMock.lastSessionTaskURL, expectedURL) + XCTAssertEqual(sessionMock.lastSessionTaskURL, url) } func testDisconnect() { - sut.connect(on: URL.stub()) + sut.connect() sut.disconnect() XCTAssertFalse(sut.isConnected) XCTAssertTrue(webSocketTaskMock.didCallCancel) @@ -43,7 +43,7 @@ final class WebSocketSessionTests: XCTestCase { func testSendMessageSuccessCallbacksNoError() { let expectedMessage = "message" - sut.connect(on: URL.stub()) + sut.connect() sut.send(expectedMessage) { error in XCTAssertNil(error) } @@ -64,7 +64,7 @@ final class WebSocketSessionTests: XCTestCase { func testSendMessageFailure() { webSocketTaskMock.sendMessageError = NSError.mock() - sut.connect(on: URL.stub()) + sut.connect() sut.send("") { error in XCTAssertNotNil(error) XCTAssert(error?.asNetworkError?.isSendMessageError == true) @@ -78,7 +78,7 @@ final class WebSocketSessionTests: XCTestCase { sut.onMessageReceived = { callbackMessage = $0 } webSocketTaskMock.receiveMessageResult = .success(.string(expectedMessage)) - sut.connect(on: URL.stub()) + sut.connect() XCTAssertEqual(callbackMessage, expectedMessage) XCTAssert(webSocketTaskMock.receiveCallsCount == 2) @@ -91,7 +91,7 @@ final class WebSocketSessionTests: XCTestCase { sut.onMessageError = { _ in didCallbackError = true } webSocketTaskMock.receiveMessageResult = .success(.data("message".data(using: .utf8)!)) - sut.connect(on: URL.stub()) + sut.connect() XCTAssertNil(callbackMessage) XCTAssertFalse(didCallbackError) @@ -105,7 +105,7 @@ final class WebSocketSessionTests: XCTestCase { } webSocketTaskMock.receiveMessageResult = .failure(NSError.mock()) - sut.connect(on: URL.stub()) + sut.connect() XCTAssert(webSocketTaskMock.receiveCallsCount == 2) }