diff --git a/Example/DApp/ClientDelegate.swift b/Example/DApp/ClientDelegate.swift index 2fecc1d15..b958fd659 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, 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/Example/ExampleApp/Responder/ResponderViewController.swift b/Example/ExampleApp/Responder/ResponderViewController.swift index c5e662ca6..056bd298c 100644 --- a/Example/ExampleApp/Responder/ResponderViewController.swift +++ b/Example/ExampleApp/Responder/ResponderViewController.swift @@ -15,7 +15,6 @@ final class ResponderViewController: UIViewController { return WalletConnectClient( metadata: metadata, projectId: "52af113ee0c1e1a20f4995730196c13e", - isController: true, relayHost: "relay.dev.walletconnect.com" ) }() @@ -159,7 +158,7 @@ extension ResponderViewController: SessionViewControllerDelegate { print("did reject session") let proposal = currentProposal! currentProposal = nil - client.reject(proposal: proposal, reason: Reason(code: 0, message: "reject")) + client.reject(proposal: proposal, reason: .disapprovedChains) } } diff --git a/README.md b/README.md index 0da9866f2..b2ffdd919 100644 --- a/README.md +++ b/README.md @@ -24,10 +24,8 @@ You usually want to have a single instance of a client in you app. icons: [String]?) let client = WalletConnectClient(metadata: AppMetadata, projectId: String, - isController: Bool, relayHost: String) ``` -The `controller` client will always be the "wallet" which is exposing blockchain accounts to a "dapp" and therefore is also in charge of signing. After instantiation of a client set its delegate. #### Pair Clients 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..b333fb66c 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.toHexString())") + @unknown default: print("Transport: Unexpected type of message received") } } diff --git a/Sources/WalletConnect/Engine/PairingEngine.swift b/Sources/WalletConnect/Engine/PairingEngine.swift index 29736ef5a..41e364c9c 100644 --- a/Sources/WalletConnect/Engine/PairingEngine.swift +++ b/Sources/WalletConnect/Engine/PairingEngine.swift @@ -13,7 +13,6 @@ final class PairingEngine { private let wcSubscriber: WCSubscribing private let relayer: WalletConnectRelaying private let crypto: CryptoStorageProtocol - private let isController: Bool private let sequencesStore: PairingSequenceStorage private var appMetadata: AppMetadata private var publishers = [AnyCancellable]() @@ -25,7 +24,6 @@ final class PairingEngine { crypto: CryptoStorageProtocol, subscriber: WCSubscribing, sequencesStore: PairingSequenceStorage, - isController: Bool, metadata: AppMetadata, logger: ConsoleLogging, topicGenerator: @escaping () -> String? = String.generateTopic) { @@ -34,7 +32,6 @@ final class PairingEngine { self.wcSubscriber = subscriber self.appMetadata = metadata self.sequencesStore = sequencesStore - self.isController = isController self.logger = logger self.topicInitializer = topicGenerator setUpWCRequestHandling() @@ -71,7 +68,7 @@ final class PairingEngine { let publicKey = try! crypto.createX25519KeyPair() let relay = RelayProtocolOptions(protocol: "waku", params: nil) - let uri = WalletConnectURI(topic: topic, publicKey: publicKey.hexRepresentation, isController: isController, relay: relay) + let uri = WalletConnectURI(topic: topic, publicKey: publicKey.hexRepresentation, isController: false, relay: relay) let pendingPairing = PairingSequence.buildProposed(uri: uri) sequencesStore.setSequence(pendingPairing) @@ -82,7 +79,7 @@ final class PairingEngine { func approve(_ pairingURI: WalletConnectURI) throws { let proposal = PairingProposal.createFromURI(pairingURI) - guard proposal.proposer.controller != isController else { + guard !proposal.proposer.controller else { throw WalletConnectError.internal(.unauthorizedMatchingController) } guard !hasPairing(for: proposal.topic) else { diff --git a/Sources/WalletConnect/Engine/SessionEngine.swift b/Sources/WalletConnect/Engine/SessionEngine.swift index 529fc5810..3fa684863 100644 --- a/Sources/WalletConnect/Engine/SessionEngine.swift +++ b/Sources/WalletConnect/Engine/SessionEngine.swift @@ -17,7 +17,6 @@ final class SessionEngine { private let wcSubscriber: WCSubscribing private let relayer: WalletConnectRelaying private let crypto: CryptoStorageProtocol - private var isController: Bool private var metadata: AppMetadata private var publishers = [AnyCancellable]() private let logger: ConsoleLogging @@ -27,7 +26,6 @@ final class SessionEngine { crypto: CryptoStorageProtocol, subscriber: WCSubscribing, sequencesStore: SessionSequenceStorage, - isController: Bool, metadata: AppMetadata, logger: ConsoleLogging, topicGenerator: @escaping () -> String? = String.generateTopic) { @@ -36,7 +34,6 @@ final class SessionEngine { self.metadata = metadata self.wcSubscriber = subscriber self.sequencesStore = sequencesStore - self.isController = isController self.logger = logger self.topicInitializer = topicGenerator setUpWCRequestHandling() @@ -72,7 +69,7 @@ final class SessionEngine { let proposal = SessionProposal( topic: pendingSessionTopic, relay: relay, - proposer: SessionType.Proposer(publicKey: publicKey.hexRepresentation, controller: isController, metadata: metadata), + proposer: SessionType.Proposer(publicKey: publicKey.hexRepresentation, controller: false, metadata: metadata), signal: SessionType.Signal(method: "pairing", params: SessionType.Signal.Params(topic: settledPairing.topic)), permissions: permissions, ttl: SessionSequence.timeToLivePending) @@ -132,8 +129,8 @@ final class SessionEngine { } } - func reject(proposal: SessionProposal, reason: Reason) { - let rejectParams = SessionType.RejectParams(reason: reason.toInternal()) + func reject(proposal: SessionProposal, reason: SessionType.Reason ) { + let rejectParams = SessionType.RejectParams(reason: reason) relayer.request(.wcSessionReject(rejectParams), onTopic: proposal.topic) { [weak self] result in self?.logger.debug("Reject result: \(result)") } @@ -205,7 +202,7 @@ final class SessionEngine { throw WalletConnectError.internal(.notApproved) // TODO: Use a suitable error cases } } - if !isController || session.settled?.status != .acknowledged { + if !session.isController || session.settled?.status != .acknowledged { throw WalletConnectError.unauthrorized(.unauthorizedUpdateRequest) } session.update(accounts) @@ -221,7 +218,7 @@ final class SessionEngine { guard session.isSettled else { throw WalletConnectError.sessionNotSettled(topic) } - guard isController else { + guard session.isController else { throw WalletConnectError.unauthorizedNonControllerCall } guard validatePermissions(permissions) else { @@ -333,10 +330,6 @@ final class SessionEngine { relayer.respondError(for: payload, reason: .unauthorizedUpdateRequest(context: .session)) return } - guard !isController else { - relayer.respondError(for: payload, reason: .unauthorizedMatchingController(isController: isController)) - return - } session.settled?.state = updateParams.state sequencesStore.setSequence(session) relayer.respondSuccess(for: payload) @@ -356,10 +349,6 @@ final class SessionEngine { relayer.respondError(for: payload, reason: .unauthorizedUpgradeRequest(context: .session)) return } - guard !isController else { - relayer.respondError(for: payload, reason: .unauthorizedMatchingController(isController: isController)) - return - } session.upgrade(upgradeParams.permissions) sequencesStore.setSequence(session) let newPermissions = session.settled!.permissions // We know session is settled @@ -439,16 +428,17 @@ final class SessionEngine { private func handleSessionApprove(_ approveParams: SessionType.ApproveParams, topic: String, requestId: Int64) { logger.debug("Responder Client approved session on topic: \(topic)") - logger.debug("isController: \(isController)") - guard !isController else { - logger.warn("Warning: Session Engine - Unexpected handleSessionApprove method call by non Controller client") - return - } guard let session = sequencesStore.getSequence(forTopic: topic), let pendingSession = session.pending else { logger.error("Could not find pending session for topic: \(topic)") return } + logger.debug("isController: \(session.isController)") + + guard !session.isController else { + logger.warn("Warning: Session Engine - Unexpected handleSessionApprove method call by non Controller client") + return + } logger.debug("handleSessionApprove") let agreementKeys = try! crypto.performKeyAgreement(selfPublicKey: try! session.getPublicKey(), peerPublicKey: approveParams.responder.publicKey) diff --git a/Sources/WalletConnect/RejectionReason.swift b/Sources/WalletConnect/RejectionReason.swift new file mode 100644 index 000000000..5b43588f2 --- /dev/null +++ b/Sources/WalletConnect/RejectionReason.swift @@ -0,0 +1,22 @@ + +import Foundation + +/// https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-25.md +public enum RejectionReason { + case disapprovedChains + case disapprovedMethods + case disapprovedNotificationTypes +} + +internal extension RejectionReason { + func internalRepresentation() -> SessionType.Reason { + switch self { + case .disapprovedChains: + return SessionType.Reason(code: 5000, message: "User disapproved requested chains") + case .disapprovedMethods: + return SessionType.Reason(code: 5001, message: "User disapproved requested json-rpc methods") + case .disapprovedNotificationTypes: + return SessionType.Reason(code: 5002, message: "User disapproved requested notification types") + } + } +} 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..bc45a407e 100644 --- a/Sources/WalletConnect/WalletConnectClient.swift +++ b/Sources/WalletConnect/WalletConnectClient.swift @@ -12,7 +12,7 @@ import UIKit /// /// ```swift /// let metadata = AppMetadata(name: String?, description: String?, url: String?, icons: [String]?) -/// let client = WalletConnectClient(metadata: AppMetadata, projectId: String, isController: Bool, relayHost: String) +/// let client = WalletConnectClient(metadata: AppMetadata, projectId: String, relayHost: String) /// ``` /// /// - Parameters: @@ -22,18 +22,13 @@ public final class WalletConnectClient { public weak var delegate: WalletConnectClientDelegate? public let logger: ConsoleLogging private let metadata: AppMetadata - private let isController: Bool 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 @@ -42,56 +37,60 @@ public final class WalletConnectClient { /// - Parameters: /// - metadata: describes your application and will define pairing appearance in a web browser. /// - 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. - public convenience init(metadata: AppMetadata, projectId: String, isController: Bool, relayHost: String, keyValueStorage: KeyValueStorage = UserDefaults.standard, clientName: String? = nil) { - self.init(metadata: metadata, projectId: projectId, isController: isController, relayHost: relayHost, logger: ConsoleLogger(loggingLevel: .off), keychain: KeychainStorage(uniqueIdentifier: clientName), keyValueStorage: keyValueStorage, clientName: clientName) + public convenience init(metadata: AppMetadata, projectId: String, relayHost: String, keyValueStorage: KeyValueStorage = UserDefaults.standard, clientName: String? = nil) { + self.init(metadata: metadata, projectId: projectId, relayHost: relayHost, logger: ConsoleLogger(loggingLevel: .off), keychain: KeychainStorage(uniqueIdentifier: clientName), keyValueStorage: keyValueStorage, clientName: clientName) } - init(metadata: AppMetadata, projectId: String, isController: Bool, relayHost: String, logger: ConsoleLogging, keychain: KeychainStorage, keyValueStorage: KeyValueStorage, clientName: String? = nil) { + init(metadata: AppMetadata, projectId: String, relayHost: String, 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 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.pairingEngine = PairingEngine(relay: relay, crypto: crypto, subscriber: WCSubscriber(relay: relay, logger: logger), sequencesStore: pairingSequencesStore, 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) + self.sessionEngine = SessionEngine(relay: relay, crypto: crypto, subscriber: WCSubscriber(relay: relay, logger: logger), sequencesStore: sessionSequencesStore, 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. + /// - 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, relayer: Relayer, keyValueStorage: KeyValueStorage = UserDefaults.standard, clientName: String? = nil) { + self.init(metadata: metadata, 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, relayer: Relayer, logger: ConsoleLogging, keychain: KeychainStorage, keyValueStorage: KeyValueStorage, clientName: String? = nil) { + self.metadata = metadata + 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, metadata: metadata, logger: logger) + self.sessionEngine = SessionEngine(relay: relay, crypto: crypto, subscriber: WCSubscriber(relay: relay, logger: logger), sequencesStore: sessionSequencesStore, metadata: metadata, logger: logger) + setUpEnginesCallbacks() } // MARK: - Public interface @@ -147,9 +146,9 @@ public final class WalletConnectClient { /// For the responder to reject a session proposal. /// - Parameters: /// - proposal: Session Proposal received from peer client in a WalletConnect delegate. - /// - reason: Reason why the session proposal was rejected. - public func reject(proposal: Session.Proposal, reason: Reason) { - sessionEngine.reject(proposal: proposal.proposal, reason: reason) + /// - reason: Reason why the session proposal was rejected. Conforms to CAIP25. + public func reject(proposal: Session.Proposal, reason: RejectionReason) { + sessionEngine.reject(proposal: proposal.proposal, reason: reason.internalRepresentation()) } /// For the responder to update the accounts @@ -328,27 +327,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/Sources/WalletConnect/Extensions/Data+Hex.swift b/Sources/WalletConnectUtils/Data+Hex.swift similarity index 100% rename from Sources/WalletConnect/Extensions/Data+Hex.swift rename to Sources/WalletConnectUtils/Data+Hex.swift diff --git a/Tests/IntegrationTests/ClientTest.swift b/Tests/IntegrationTests/ClientTest.swift index ecf9a5a83..df2716839 100644 --- a/Tests/IntegrationTests/ClientTest.swift +++ b/Tests/IntegrationTests/ClientTest.swift @@ -30,7 +30,6 @@ final class ClientTests: XCTestCase { let client = WalletConnectClient( metadata: AppMetadata(name: nil, description: nil, url: nil, icons: nil), projectId: projectId, - isController: isController, relayHost: relayHost, logger: logger, keychain: KeychainStorage(keychainService: KeychainServiceFake()), @@ -113,10 +112,10 @@ final class ClientTests: XCTestCase { let uri = try! proposer.client.connect(sessionPermissions: permissions)! _ = try! responder.client.pair(uri: uri) responder.onSessionProposal = {[unowned self] proposal in - self.responder.client.reject(proposal: proposal, reason: Reason(code: WalletConnectError.internal(.notApproved).code, message: WalletConnectError.internal(.notApproved).description)) + self.responder.client.reject(proposal: proposal, reason: .disapprovedChains) } proposer.onSessionRejected = { _, reason in - XCTAssertEqual(reason.code, WalletConnectError.internal(.notApproved).code) + XCTAssertEqual(reason.code, 5000) sessionRejectExpectation.fulfill() } waitForExpectations(timeout: defaultTimeout, handler: nil) 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) } diff --git a/Tests/WalletConnectTests/PairingEngineTests.swift b/Tests/WalletConnectTests/PairingEngineTests.swift index 6f0369b2f..9223b182b 100644 --- a/Tests/WalletConnectTests/PairingEngineTests.swift +++ b/Tests/WalletConnectTests/PairingEngineTests.swift @@ -43,7 +43,6 @@ final class PairingEngineTests: XCTestCase { crypto: cryptoMock, subscriber: subscriberMock, sequencesStore: storageMock, - isController: isController, metadata: meta, logger: logger, topicGenerator: topicGenerator.getTopic) diff --git a/Tests/WalletConnectTests/SessionEngineTests.swift b/Tests/WalletConnectTests/SessionEngineTests.swift index 9c480ad1a..eb9389bfb 100644 --- a/Tests/WalletConnectTests/SessionEngineTests.swift +++ b/Tests/WalletConnectTests/SessionEngineTests.swift @@ -14,7 +14,6 @@ final class SessionEngineTests: XCTestCase { var topicGenerator: TopicGenerator! - var isController: Bool! var metadata: AppMetadata! override func setUp() { @@ -34,23 +33,21 @@ final class SessionEngineTests: XCTestCase { engine = nil } - func setupEngine(isController: Bool) { + func setupEngine() { metadata = AppMetadata(name: nil, description: nil, url: nil, icons: nil) - self.isController = isController let logger = ConsoleLoggerMock() engine = SessionEngine( relay: relayMock, crypto: cryptoMock, subscriber: subscriberMock, sequencesStore: storageMock, - isController: isController, metadata: metadata, logger: logger, topicGenerator: topicGenerator.getTopic) } func testPropose() { - setupEngine(isController: false) + setupEngine() let pairing = Pairing.stub() @@ -77,7 +74,7 @@ final class SessionEngineTests: XCTestCase { } func testProposeResponseFailure() { - setupEngine(isController: false) + setupEngine() let pairing = Pairing.stub() let topicB = pairing.topic @@ -108,13 +105,13 @@ final class SessionEngineTests: XCTestCase { } func testApprove() { - setupEngine(isController: true) + setupEngine() let proposerPubKey = AgreementPrivateKey().publicKey.hexRepresentation let topicB = String.generateTopic()! let topicC = String.generateTopic()! let topicD = deriveTopic(publicKey: proposerPubKey, privateKey: cryptoMock.privateKeyStub) - let proposer = SessionType.Proposer(publicKey: proposerPubKey, controller: isController, metadata: metadata) + let proposer = SessionType.Proposer(publicKey: proposerPubKey, controller: true, metadata: metadata) let proposal = SessionProposal( topic: topicC, relay: RelayProtocolOptions(protocol: "", params: nil), @@ -139,7 +136,7 @@ final class SessionEngineTests: XCTestCase { } func testApprovalAcknowledgementSuccess() { - setupEngine(isController: true) + setupEngine() let proposerPubKey = AgreementPrivateKey().publicKey.hexRepresentation let topicB = String.generateTopic()! @@ -149,7 +146,7 @@ final class SessionEngineTests: XCTestCase { let agreementKeys = AgreementSecret.stub() cryptoMock.setAgreementSecret(agreementKeys, topic: topicC) - let proposer = SessionType.Proposer(publicKey: proposerPubKey, controller: isController, metadata: metadata) + let proposer = SessionType.Proposer(publicKey: proposerPubKey, controller: true, metadata: metadata) let proposal = SessionProposal( topic: topicC, relay: RelayProtocolOptions(protocol: "", params: nil), @@ -178,7 +175,7 @@ final class SessionEngineTests: XCTestCase { } func testApprovalAcknowledgementFailure() { - setupEngine(isController: true) + setupEngine() let proposerPubKey = AgreementPrivateKey().publicKey.hexRepresentation let selfPubKey = cryptoMock.privateKeyStub.publicKey.hexRepresentation @@ -189,7 +186,7 @@ final class SessionEngineTests: XCTestCase { let agreementKeys = AgreementSecret.stub() cryptoMock.setAgreementSecret(agreementKeys, topic: topicC) - let proposer = SessionType.Proposer(publicKey: proposerPubKey, controller: isController, metadata: metadata) + let proposer = SessionType.Proposer(publicKey: proposerPubKey, controller: true, metadata: metadata) let proposal = SessionProposal( topic: topicC, relay: RelayProtocolOptions(protocol: "", params: nil), @@ -223,7 +220,7 @@ final class SessionEngineTests: XCTestCase { } func testReceiveApprovalResponse() { - setupEngine(isController: false) + setupEngine() var approvedSession: Session? @@ -265,34 +262,34 @@ final class SessionEngineTests: XCTestCase { // MARK: - Update call tests func testUpdateSuccess() throws { - setupEngine(isController: true) - let session = SessionSequence.stubSettled() + setupEngine() + let session = SessionSequence.stubSettled(isSelfController: true) storageMock.setSequence(session) try engine.update(topic: session.topic, accounts: ["std:0:0"]) XCTAssertTrue(relayMock.didCallRequest) } func testUpdateErrorInvalidAccount() { - setupEngine(isController: true) - let session = SessionSequence.stubSettled() + setupEngine() + let session = SessionSequence.stubSettled(isSelfController: true) storageMock.setSequence(session) XCTAssertThrowsError(try engine.update(topic: session.topic, accounts: ["err"])) } func testUpdateErrorIfNonController() { - setupEngine(isController: false) - let session = SessionSequence.stubSettled() + setupEngine() + let session = SessionSequence.stubSettled(isSelfController: false) storageMock.setSequence(session) XCTAssertThrowsError(try engine.update(topic: session.topic, accounts: ["std:0:0"]), "Update must fail if called by a non-controller.") } func testUpdateErrorSessionNotFound() { - setupEngine(isController: true) + setupEngine() XCTAssertThrowsError(try engine.update(topic: "", accounts: ["std:0:0"]), "Update must fail if there is no session matching the target topic.") } func testUpdateErrorSessionNotSettled() { - setupEngine(isController: true) + setupEngine() let session = SessionSequence.stubPreSettled() storageMock.setSequence(session) XCTAssertThrowsError(try engine.update(topic: session.topic, accounts: ["std:0:0"]), "Update must fail if session is not on settled state.") @@ -301,16 +298,16 @@ final class SessionEngineTests: XCTestCase { // MARK: - Update peer response tests func testUpdatePeerSuccess() { - setupEngine(isController: false) - let session = SessionSequence.stubSettled(isPeerController: true) + setupEngine() + let session = SessionSequence.stubSettled(isSelfController: false) storageMock.setSequence(session) subscriberMock.onReceivePayload?(WCRequestSubscriptionPayload.stubUpdate(topic: session.topic)) XCTAssertTrue(relayMock.didRespondSuccess) } func testUpdatePeerErrorAccountInvalid() { - setupEngine(isController: false) - let session = SessionSequence.stubSettled(isPeerController: true) + setupEngine() + let session = SessionSequence.stubSettled(isSelfController: false) storageMock.setSequence(session) subscriberMock.onReceivePayload?(WCRequestSubscriptionPayload.stubUpdate(topic: session.topic, accounts: ["0"])) XCTAssertFalse(relayMock.didRespondSuccess) @@ -318,14 +315,14 @@ final class SessionEngineTests: XCTestCase { } func testUpdatePeerErrorNoSession() { - setupEngine(isController: false) + setupEngine() subscriberMock.onReceivePayload?(WCRequestSubscriptionPayload.stubUpdate(topic: "")) XCTAssertFalse(relayMock.didRespondSuccess) XCTAssertEqual(relayMock.lastErrorCode, 1301) } func testUpdatePeerErrorSessionNotSettled() { - setupEngine(isController: false) + setupEngine() let session = SessionSequence.stubPreSettled(isPeerController: true) // Session is not fully settled storageMock.setSequence(session) subscriberMock.onReceivePayload?(WCRequestSubscriptionPayload.stubUpdate(topic: session.topic)) @@ -334,31 +331,21 @@ final class SessionEngineTests: XCTestCase { } func testUpdatePeerErrorUnauthorized() { - setupEngine(isController: false) - let session = SessionSequence.stubSettled() // Peer is not a controller + setupEngine() + let session = SessionSequence.stubSettled(isSelfController: true) // Peer is not a controller storageMock.setSequence(session) subscriberMock.onReceivePayload?(WCRequestSubscriptionPayload.stubUpdate(topic: session.topic)) XCTAssertFalse(relayMock.didRespondSuccess) XCTAssertEqual(relayMock.lastErrorCode, 3003) } - - func testUpdatePeerErrorMatchingController() { - setupEngine(isController: true) // Update request received by a controller - let session = SessionSequence.stubSettled(isPeerController: true) - storageMock.setSequence(session) - subscriberMock.onReceivePayload?(WCRequestSubscriptionPayload.stubUpdate(topic: session.topic)) - XCTAssertFalse(relayMock.didRespondSuccess) - XCTAssertEqual(relayMock.lastErrorCode, 3005) - } - // TODO: Update acknowledgement tests // MARK: - Upgrade call tests func testUpgradeSuccess() throws { - setupEngine(isController: true) + setupEngine() let permissions = Session.Permissions.stub() - let session = SessionSequence.stubSettled() + let session = SessionSequence.stubSettled(isSelfController: true) storageMock.setSequence(session) try engine.upgrade(topic: session.topic, permissions: permissions) XCTAssertTrue(relayMock.didCallRequest) @@ -366,14 +353,14 @@ final class SessionEngineTests: XCTestCase { } func testUpgradeErrorSessionNotFound() { - setupEngine(isController: true) + setupEngine() XCTAssertThrowsError(try engine.upgrade(topic: "", permissions: Session.Permissions.stub())) { error in XCTAssertTrue(error.isNoSessionMatchingTopicError) } } func testUpgradeErrorSessionNotSettled() { - setupEngine(isController: true) + setupEngine() let session = SessionSequence.stubPreSettled() storageMock.setSequence(session) XCTAssertThrowsError(try engine.upgrade(topic: session.topic, permissions: Session.Permissions.stub())) { error in @@ -382,8 +369,8 @@ final class SessionEngineTests: XCTestCase { } func testUpgradeErrorInvalidPermissions() { - setupEngine(isController: true) - let session = SessionSequence.stubSettled() + setupEngine() + let session = SessionSequence.stubSettled(isSelfController: true) storageMock.setSequence(session) XCTAssertThrowsError(try engine.upgrade(topic: session.topic, permissions: Session.Permissions.stub(chains: [""]))) { error in XCTAssertTrue(error.isInvalidPermissionsError) @@ -397,8 +384,8 @@ final class SessionEngineTests: XCTestCase { } func testUpgradeErrorCalledByNonController() { - setupEngine(isController: false) - let session = SessionSequence.stubSettled() + setupEngine() + let session = SessionSequence.stubSettled(isSelfController: false) storageMock.setSequence(session) XCTAssertThrowsError(try engine.upgrade(topic: session.topic, permissions: Session.Permissions.stub())) { error in XCTAssertTrue(error.isUnauthorizedNonControllerCallError) @@ -408,9 +395,9 @@ final class SessionEngineTests: XCTestCase { // MARK: - Upgrade peer response tests func testUpgradePeerSuccess() { - setupEngine(isController: false) + setupEngine() var didCallbackUpgrade = false - let session = SessionSequence.stubSettled(isPeerController: true) + let session = SessionSequence.stubSettled(isSelfController: false) storageMock.setSequence(session) engine.onSessionUpgrade = { topic, _ in didCallbackUpgrade = true @@ -422,9 +409,9 @@ final class SessionEngineTests: XCTestCase { } func testUpgradePeerErrorInvalidPermissions() { - setupEngine(isController: false) + setupEngine() let invalidPermissions = SessionPermissions.stub(chains: [""]) - let session = SessionSequence.stubSettled(isPeerController: true) + let session = SessionSequence.stubSettled(isSelfController: false) storageMock.setSequence(session) subscriberMock.onReceivePayload?(WCRequestSubscriptionPayload.stubUpgrade(topic: session.topic, permissions: invalidPermissions)) XCTAssertFalse(relayMock.didRespondSuccess) @@ -432,14 +419,14 @@ final class SessionEngineTests: XCTestCase { } func testUpgradePeerErrorSessionNotFound() { - setupEngine(isController: false) + setupEngine() subscriberMock.onReceivePayload?(WCRequestSubscriptionPayload.stubUpgrade(topic: "")) XCTAssertFalse(relayMock.didRespondSuccess) XCTAssertEqual(relayMock.lastErrorCode, 1301) } func testUpgradePeerErrorSessionNotSettled() { - setupEngine(isController: false) + setupEngine() let session = SessionSequence.stubPreSettled(isPeerController: true) // Session is not fully settled storageMock.setSequence(session) subscriberMock.onReceivePayload?(WCRequestSubscriptionPayload.stubUpgrade(topic: session.topic)) @@ -448,22 +435,13 @@ final class SessionEngineTests: XCTestCase { } func testUpgradePeerErrorUnauthorized() { - setupEngine(isController: false) - let session = SessionSequence.stubSettled() // Peer is not a controller + setupEngine() + let session = SessionSequence.stubSettled(isSelfController: true) // Peer is not a controller storageMock.setSequence(session) subscriberMock.onReceivePayload?(WCRequestSubscriptionPayload.stubUpgrade(topic: session.topic)) XCTAssertFalse(relayMock.didRespondSuccess) XCTAssertEqual(relayMock.lastErrorCode, 3004) } - func testUpgradePeerErrorMatchingController() { - setupEngine(isController: true) // Upgrade request received by a controller - let session = SessionSequence.stubSettled(isPeerController: true) - storageMock.setSequence(session) - subscriberMock.onReceivePayload?(WCRequestSubscriptionPayload.stubUpgrade(topic: session.topic)) - XCTAssertFalse(relayMock.didRespondSuccess) - XCTAssertEqual(relayMock.lastErrorCode, 3005) - } - // TODO: Upgrade acknowledgement tests } diff --git a/Tests/WalletConnectTests/Stub/Session+Stub.swift b/Tests/WalletConnectTests/Stub/Session+Stub.swift index cc67dacf8..a711a2acf 100644 --- a/Tests/WalletConnectTests/Stub/Session+Stub.swift +++ b/Tests/WalletConnectTests/Stub/Session+Stub.swift @@ -20,13 +20,14 @@ extension SessionSequence { ) } - static func stubSettled(isPeerController: Bool = false) -> SessionSequence { + static func stubSettled(isSelfController: Bool) -> SessionSequence { let peerKey = AgreementPrivateKey().publicKey.hexRepresentation - let permissions = isPeerController ? SessionPermissions.stub(controllerKey: peerKey) : SessionPermissions.stub() + let selfKey = AgreementPrivateKey().publicKey.hexRepresentation + let permissions = isSelfController ? SessionPermissions.stub(controllerKey: selfKey) : SessionPermissions.stub(controllerKey: peerKey) return SessionSequence( topic: String.generateTopic()!, relay: RelayProtocolOptions.stub(), - selfParticipant: Participant.stub(), + selfParticipant: Participant.stub(publicKey: selfKey), expiryDate: Date.distantFuture, settledState: Settled( peer: Participant.stub(publicKey: peerKey),