diff --git a/Example/DApp/Connect/ConnectViewController.swift b/Example/DApp/Connect/ConnectViewController.swift index 4529d1ae7..68b1cace5 100644 --- a/Example/DApp/Connect/ConnectViewController.swift +++ b/Example/DApp/Connect/ConnectViewController.swift @@ -98,10 +98,9 @@ class ConnectViewController: UIViewController, UITableViewDataSource, UITableVie let blockchains: Set = [Blockchain("eip155:1")!, Blockchain("eip155:137")!] let methods: Set = ["eth_sendTransaction", "personal_sign", "eth_signTypedData"] let namespaces: Set = [Namespace(chains: blockchains, methods: methods, events: [])] - DispatchQueue.global().async { - ClientDelegate.shared.client.connect(namespaces: namespaces, topic: pairingTopic) { [weak self] _ in - self?.connectWithExampleWallet() - } + Task { + _ = try await ClientDelegate.shared.client.connect(namespaces: namespaces, topic: pairingTopic) + connectWithExampleWallet() } } } diff --git a/Example/DApp/SelectChain/SelectChainViewController.swift b/Example/DApp/SelectChain/SelectChainViewController.swift index 701aea351..8bd25d577 100644 --- a/Example/DApp/SelectChain/SelectChainViewController.swift +++ b/Example/DApp/SelectChain/SelectChainViewController.swift @@ -38,15 +38,9 @@ class SelectChainViewController: UIViewController, UITableViewDataSource { let methods: Set = ["eth_sendTransaction", "personal_sign", "eth_signTypedData"] let blockchains: Set = [Blockchain("eip155:1")!, Blockchain("eip155:137")!] let namespaces: Set = [Namespace(chains: blockchains, methods: methods, events: [])] - DispatchQueue.global().async { [weak self] in - self?.client.connect(namespaces: namespaces) { result in - switch result { - case .success(let uri): - self?.showConnectScreen(uriString: uri!) - case .failure(let error): - print("[PROPOSER] Pairing connect error: \(error)") - } - } + Task { + let uri = try await client.connect(namespaces: namespaces) + showConnectScreen(uriString: uri!) } } diff --git a/Example/ExampleApp/SceneDelegate.swift b/Example/ExampleApp/SceneDelegate.swift index 3a0991f43..de23ce81c 100644 --- a/Example/ExampleApp/SceneDelegate.swift +++ b/Example/ExampleApp/SceneDelegate.swift @@ -17,8 +17,16 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { return } let wcUri = incomingURL.absoluteString.deletingPrefix("https://walletconnect.com/wc?uri=") - let client = ((window!.rootViewController as! UINavigationController).viewControllers[0] as! WalletViewController).client - try? client.pair(uri: wcUri) + let vc = ((window!.rootViewController as! UINavigationController).viewControllers[0] as! WalletViewController) + vc.onClientConnected = { + Task { + do { + try await vc.client.pair(uri: wcUri) + } catch { + print(error) + } + } + } } } diff --git a/Example/ExampleApp/Wallet/WalletViewController.swift b/Example/ExampleApp/Wallet/WalletViewController.swift index e4d483203..69f3c23ec 100644 --- a/Example/ExampleApp/Wallet/WalletViewController.swift +++ b/Example/ExampleApp/Wallet/WalletViewController.swift @@ -21,6 +21,7 @@ final class WalletViewController: UIViewController { lazy var account = Signer.privateKey.address.hex(eip55: true) var sessionItems: [ActiveSessionItem] = [] var currentProposal: Session.Proposal? + var onClientConnected: (()->())? private let walletView: WalletView = { WalletView() @@ -94,10 +95,12 @@ final class WalletViewController: UIViewController { private func pairClient(uri: String) { print("[RESPONDER] Pairing to: \(uri)") - do { - try client.pair(uri: uri) - } catch { - print("[PROPOSER] Pairing connect error: \(error)") + Task { + do { + try await client.pair(uri: uri) + } catch { + print("[PROPOSER] Pairing connect error: \(error)") + } } } } @@ -152,7 +155,7 @@ extension WalletViewController: ScannerViewControllerDelegate { } extension WalletViewController: ProposalViewControllerDelegate { - + func didApproveSession() { print("[RESPONDER] Approving session...") let proposal = currentProposal! @@ -171,6 +174,7 @@ extension WalletViewController: ProposalViewControllerDelegate { extension WalletViewController: WalletConnectClientDelegate { func didConnect() { + onClientConnected?() print("Client connected") } diff --git a/Sources/Relayer/Relayer.swift b/Sources/Relayer/Relayer.swift index 3c05cc130..ec985256b 100644 --- a/Sources/Relayer/Relayer.swift +++ b/Sources/Relayer/Relayer.swift @@ -132,7 +132,8 @@ public final class Relayer { return request.id } - @discardableResult public func subscribe(topic: String, completion: @escaping (Error?) -> ()) -> Int64 { + @available(*, renamed: "subscribe(topic:)") + public func subscribe(topic: String, completion: @escaping (Error?) -> ()) { logger.debug("waku: Subscribing on Topic: \(topic)") let params = RelayJSONRPC.SubscribeParams(topic: topic) let request = JSONRPCRequest(method: RelayJSONRPC.Method.subscribe.rawValue, params: params) @@ -154,11 +155,22 @@ public final class Relayer { self?.concurrentQueue.async(flags: .barrier) { self?.subscriptions[topic] = subscriptionResponse.result } - completion(nil) } - return request.id } + public func subscribe(topic: String) async throws { + return try await withCheckedThrowingContinuation { continuation in + subscribe(topic: topic) { error in + if let error = error { + continuation.resume(throwing: error) + return + } + continuation.resume(returning: ()) + } + } + } + + @discardableResult public func unsubscribe(topic: String, completion: @escaping ((Error?) -> ())) -> Int64? { guard let subscriptionId = subscriptions[topic] else { completion(RelyerError.subscriptionIdNotFound) diff --git a/Sources/WalletConnect/Engine/PairingEngine.swift b/Sources/WalletConnect/Engine/Common/PairingEngine.swift similarity index 89% rename from Sources/WalletConnect/Engine/PairingEngine.swift rename to Sources/WalletConnect/Engine/Common/PairingEngine.swift index 421100d91..e8373f9a5 100644 --- a/Sources/WalletConnect/Engine/PairingEngine.swift +++ b/Sources/WalletConnect/Engine/Common/PairingEngine.swift @@ -59,23 +59,19 @@ final class PairingEngine { .map {Pairing(topic: $0.topic, peer: $0.peerMetadata, expiryDate: $0.expiryDate)} } - func create() -> WalletConnectURI? { + func create() async throws -> WalletConnectURI { let topic = topicInitializer() + try await networkingInteractor.subscribe(topic: topic) let symKey = try! kms.createSymmetricKey(topic) let pairing = WCPairing(topic: topic) let uri = WalletConnectURI(topic: topic, symKey: symKey.hexRepresentation, relay: pairing.relay) pairingStore.setPairing(pairing) - networkingInteractor.subscribe(topic: topic) return uri } - func propose(pairingTopic: String, namespaces: Set, relay: RelayProtocolOptions, completion: @escaping ((Error?) -> ())) { + + func propose(pairingTopic: String, namespaces: Set, relay: RelayProtocolOptions) async throws { logger.debug("Propose Session on topic: \(pairingTopic)") - do { - try Namespace.validate(namespaces) - } catch { - completion(error) - return - } + try Namespace.validate(namespaces) let publicKey = try! kms.createX25519KeyPair() let proposer = Participant( publicKey: publicKey.hexRepresentation, @@ -84,24 +80,17 @@ final class PairingEngine { relays: [relay], proposer: proposer, namespaces: namespaces) - networkingInteractor.requestNetworkAck(.wcSessionPropose(proposal), onTopic: pairingTopic) { [unowned self] error in - logger.debug("Received propose acknowledgement from network") - completion(error) - } - } - - func pair(_ uri: WalletConnectURI) throws { - guard !hasPairing(for: uri.topic) else { - throw WalletConnectError.pairingAlreadyExist + return try await withCheckedThrowingContinuation { continuation in + networkingInteractor.requestNetworkAck(.wcSessionPropose(proposal), onTopic: pairingTopic) { error in + if let error = error { + continuation.resume(throwing: error) + } else { + continuation.resume() + } + } } - var pairing = WCPairing(uri: uri) - let symKey = try! SymmetricKey(hex: uri.symKey) // FIXME: Malformed QR code from external source can crash the SDK - try! kms.setSymmetricKey(symKey, for: pairing.topic) - pairing.activate() - networkingInteractor.subscribe(topic: pairing.topic) - pairingStore.setPairing(pairing) } - + func ping(topic: String, completion: @escaping ((Result) -> ())) { guard pairingStore.hasPairing(forTopic: topic) else { logger.debug("Could not find pairing to ping for topic \(topic)") @@ -186,7 +175,7 @@ final class PairingEngine { .sink { [unowned self] (_) in let topics = pairingStore.getAll() .map{$0.topic} - topics.forEach{networkingInteractor.subscribe(topic: $0)} + topics.forEach{ topic in Task{try? await networkingInteractor.subscribe(topic: topic)}} }.store(in: &publishers) } diff --git a/Sources/WalletConnect/Engine/SessionEngine.swift b/Sources/WalletConnect/Engine/Common/SessionEngine.swift similarity index 95% rename from Sources/WalletConnect/Engine/SessionEngine.swift rename to Sources/WalletConnect/Engine/Common/SessionEngine.swift index 087b0ed47..d1713af5f 100644 --- a/Sources/WalletConnect/Engine/SessionEngine.swift +++ b/Sources/WalletConnect/Engine/Common/SessionEngine.swift @@ -48,7 +48,7 @@ final class SessionEngine { } func setSubscription(topic: String) { - networkingInteractor.subscribe(topic: topic) + Task { try? await networkingInteractor.subscribe(topic: topic) } } func hasSession(for topic: String) -> Bool { @@ -107,21 +107,16 @@ final class SessionEngine { } } - func emit(topic: String, event: SessionType.EventParams.Event, chainId: Blockchain, completion: ((Error?)->())?) { + func emit(topic: String, event: SessionType.EventParams.Event, chainId: Blockchain) async throws { guard let session = sessionStore.getSession(forTopic: topic), session.acknowledged else { logger.debug("Could not find session for topic \(topic)") return } let params = SessionType.EventParams(event: event, chainId: chainId) - do { - guard session.hasNamespace(for: chainId, event: event.name) else { - throw WalletConnectError.invalidEvent - } - networkingInteractor.request(.wcSessionEvent(params), onTopic: topic) - } catch let error as WalletConnectError { - logger.error(error) - completion?(error) - } catch {} + guard session.hasNamespace(for: chainId, event: event.name) else { + throw WalletConnectError.invalidEvent + } + try await networkingInteractor.request(.wcSessionEvent(params), onTopic: topic) } //MARK: - Private @@ -165,9 +160,8 @@ final class SessionEngine { settleParams: settleParams, acknowledged: false) logger.debug("Sending session settle request") - networkingInteractor.subscribe(topic: topic) + Task { try? await networkingInteractor.subscribe(topic: topic) } sessionStore.setSession(session) - networkingInteractor.request(.wcSessionSettle(settleParams), onTopic: topic) } @@ -262,7 +256,7 @@ final class SessionEngine { networkingInteractor.transportConnectionPublisher .sink { [unowned self] (_) in let topics = sessionStore.getAll().map{$0.topic} - topics.forEach{networkingInteractor.subscribe(topic: $0)} + topics.forEach{ topic in Task { try? await networkingInteractor.subscribe(topic: topic) } } }.store(in: &publishers) } diff --git a/Sources/WalletConnect/Engine/SessionStateMachines/SessionStateMachineValidating.swift b/Sources/WalletConnect/Engine/Common/SessionStateMachineValidating.swift similarity index 100% rename from Sources/WalletConnect/Engine/SessionStateMachines/SessionStateMachineValidating.swift rename to Sources/WalletConnect/Engine/Common/SessionStateMachineValidating.swift diff --git a/Sources/WalletConnect/Engine/SessionStateMachines/ControllerSessionStateMachine.swift b/Sources/WalletConnect/Engine/Controller/ControllerSessionStateMachine.swift similarity index 100% rename from Sources/WalletConnect/Engine/SessionStateMachines/ControllerSessionStateMachine.swift rename to Sources/WalletConnect/Engine/Controller/ControllerSessionStateMachine.swift diff --git a/Sources/WalletConnect/Engine/Controller/PairEngine.swift b/Sources/WalletConnect/Engine/Controller/PairEngine.swift new file mode 100644 index 000000000..73d45b4de --- /dev/null +++ b/Sources/WalletConnect/Engine/Controller/PairEngine.swift @@ -0,0 +1,33 @@ + +import Foundation +import WalletConnectKMS + +actor PairEngine { + private let networkingInteractor: NetworkInteracting + private let kms: KeyManagementServiceProtocol + private let pairingStore: WCPairingStorage + + init(networkingInteractor: NetworkInteracting, + kms: KeyManagementServiceProtocol, + pairingStore: WCPairingStorage) { + self.networkingInteractor = networkingInteractor + self.kms = kms + self.pairingStore = pairingStore + } + + func pair(_ uri: WalletConnectURI) async throws { + guard !hasPairing(for: uri.topic) else { + throw WalletConnectError.pairingAlreadyExist + } + var pairing = WCPairing(uri: uri) + try await networkingInteractor.subscribe(topic: pairing.topic) + let symKey = try! SymmetricKey(hex: uri.symKey) // FIXME: Malformed QR code from external source can crash the SDK + try! kms.setSymmetricKey(symKey, for: pairing.topic) + pairing.activate() + pairingStore.setPairing(pairing) + } + + func hasPairing(for topic: String) -> Bool { + return pairingStore.hasPairing(forTopic: topic) + } +} diff --git a/Sources/WalletConnect/Engine/SessionStateMachines/NonControllerSessionStateMachine.swift b/Sources/WalletConnect/Engine/NonController/NonControllerSessionStateMachine.swift similarity index 100% rename from Sources/WalletConnect/Engine/SessionStateMachines/NonControllerSessionStateMachine.swift rename to Sources/WalletConnect/Engine/NonController/NonControllerSessionStateMachine.swift diff --git a/Sources/WalletConnect/NetworkInteractor/NetworkInteractor.swift b/Sources/WalletConnect/NetworkInteractor/NetworkInteractor.swift index bd53130da..8c47ad635 100644 --- a/Sources/WalletConnect/NetworkInteractor/NetworkInteractor.swift +++ b/Sources/WalletConnect/NetworkInteractor/NetworkInteractor.swift @@ -30,7 +30,7 @@ protocol NetworkInteracting: AnyObject { func respond(topic: String, response: JsonRpcResult, completion: @escaping ((Error?)->())) func respondSuccess(for payload: WCRequestSubscriptionPayload) func respondError(for payload: WCRequestSubscriptionPayload, reason: ReasonCode) - func subscribe(topic: String) + func subscribe(topic: String) async throws func unsubscribe(topic: String) } @@ -169,12 +169,8 @@ class NetworkInteractor: NetworkInteracting { respond(topic: payload.topic, response: JsonRpcResult.error(response)) { _ in } // TODO: Move error handling to relayer package } - func subscribe(topic: String) { - networkRelayer.subscribe(topic: topic) { [weak self] error in - if let error = error { - self?.logger.error(error) - } - } + func subscribe(topic: String) async throws { + try await networkRelayer.subscribe(topic: topic) } func unsubscribe(topic: String) { diff --git a/Sources/WalletConnect/NetworkInteractor/NetworkRelaying.swift b/Sources/WalletConnect/NetworkInteractor/NetworkRelaying.swift index c3dc4713d..6ffe675b7 100644 --- a/Sources/WalletConnect/NetworkInteractor/NetworkRelaying.swift +++ b/Sources/WalletConnect/NetworkInteractor/NetworkRelaying.swift @@ -13,8 +13,8 @@ protocol NetworkRelaying { func publish(topic: String, payload: String, prompt: Bool) async throws /// - returns: request id @discardableResult func publish(topic: String, payload: String, prompt: Bool, onNetworkAcknowledge: @escaping ((Error?)->())) -> Int64 - /// - returns: request id - @discardableResult func subscribe(topic: String, completion: @escaping (Error?)->()) -> Int64 + func subscribe(topic: String, completion: @escaping (Error?)->()) + func subscribe(topic: String) async throws /// - returns: request id @discardableResult func unsubscribe(topic: String, completion: @escaping ((Error?)->())) -> Int64? } diff --git a/Sources/WalletConnect/WalletConnectClient.swift b/Sources/WalletConnect/WalletConnectClient.swift index a18b9306b..8872543de 100644 --- a/Sources/WalletConnect/WalletConnectClient.swift +++ b/Sources/WalletConnect/WalletConnectClient.swift @@ -26,12 +26,12 @@ public final class WalletConnectClient { private var publishers = [AnyCancellable]() private let metadata: AppMetadata private let pairingEngine: PairingEngine + private let pairEngine: PairEngine private let sessionEngine: SessionEngine private let nonControllerSessionStateMachine: NonControllerSessionStateMachine private let controllerSessionStateMachine: ControllerSessionStateMachine private let networkingInteractor: NetworkInteracting private let kms: KeyManagementService - private let pairingQueue = DispatchQueue(label: "com.walletconnect.sdk.client.pairing", qos: .userInitiated) private let history: JsonRpcHistory // MARK: - Initializers @@ -65,6 +65,7 @@ public final class WalletConnectClient { self.sessionEngine = SessionEngine(networkingInteractor: networkingInteractor, kms: kms, pairingStore: pairingStore, sessionStore: sessionStore, sessionToPairingTopic: sessionToPairingTopic, metadata: metadata, logger: logger) self.nonControllerSessionStateMachine = NonControllerSessionStateMachine(networkingInteractor: networkingInteractor, kms: kms, sessionStore: sessionStore, logger: logger) self.controllerSessionStateMachine = ControllerSessionStateMachine(networkingInteractor: networkingInteractor, kms: kms, sessionStore: sessionStore, logger: logger) + self.pairEngine = PairEngine(networkingInteractor: networkingInteractor, kms: kms, pairingStore: pairingStore) setUpConnectionObserving(relayClient: relayer) setUpEnginesCallbacks() } @@ -95,11 +96,12 @@ public final class WalletConnectClient { self.sessionEngine = SessionEngine(networkingInteractor: networkingInteractor, kms: kms, pairingStore: pairingStore, sessionStore: sessionStore, sessionToPairingTopic: sessionToPairingTopic, metadata: metadata, logger: logger) self.nonControllerSessionStateMachine = NonControllerSessionStateMachine(networkingInteractor: networkingInteractor, kms: kms, sessionStore: sessionStore, logger: logger) self.controllerSessionStateMachine = ControllerSessionStateMachine(networkingInteractor: networkingInteractor, kms: kms, sessionStore: sessionStore, logger: logger) + self.pairEngine = PairEngine(networkingInteractor: networkingInteractor, kms: kms, pairingStore: pairingStore) setUpConnectionObserving(relayClient: relayer) setUpEnginesCallbacks() } - func setUpConnectionObserving(relayClient: Relayer) { + private func setUpConnectionObserving(relayClient: Relayer) { relayClient.socketConnectionStatusPublisher.sink { [weak self] status in switch status { case .connected: @@ -117,45 +119,22 @@ public final class WalletConnectClient { /// - Parameter sessionPermissions: The session permissions the responder will be requested for. /// - Parameter topic: Optional parameter - use it if you already have an established pairing with peer client. /// - Returns: Pairing URI that should be shared with responder out of bound. Common way is to present it as a QR code. Pairing URI will be nil if you are going to establish a session on existing Pairing and `topic` function parameter was provided. - public func connect(namespaces: Set, topic: String? = nil, completion: @escaping ((Result)->())) { + public func connect(namespaces: Set, topic: String? = nil) async throws -> String? { logger.debug("Connecting Application") if let topic = topic { guard let pairing = pairingEngine.getSettledPairing(for: topic) else { - completion(.failure(WalletConnectError.noPairingMatchingTopic(topic))) - return + throw WalletConnectError.noPairingMatchingTopic(topic) } logger.debug("Proposing session on existing pairing") - pairingEngine.propose(pairingTopic: topic, namespaces: namespaces, relay: pairing.relay) { error in - if let error = error { - completion(.failure(error)) - } else { - completion(.success(nil)) - } - } + try await pairingEngine.propose(pairingTopic: topic, namespaces: namespaces, relay: pairing.relay) + return nil } else { - guard let pairingURI = pairingEngine.create() else { - completion(.failure(WalletConnectError.pairingProposalFailed)) - return - } - pairingEngine.propose(pairingTopic: pairingURI.topic, namespaces: namespaces ,relay: pairingURI.relay) { error in - if let error = error { - completion(.failure(error)) - } else { - completion(.success(pairingURI.absoluteString)) - } - } + let pairingURI = try await pairingEngine.create() + try await pairingEngine.propose(pairingTopic: pairingURI.topic, namespaces: namespaces ,relay: pairingURI.relay) + return pairingURI.absoluteString } } - public func connect(namespaces: Set, topic: String? = nil) async throws -> String? { - return try await withCheckedThrowingContinuation { continuation in - connect(namespaces: namespaces, topic: topic) { result in - continuation.resume(with: result) - } - } - } - - /// For responder to receive a session proposal from a proposer /// Responder should call this function in order to accept peer's pairing proposal and be able to subscribe for future session proposals. /// - Parameter uri: Pairing URI that is commonly presented as a QR code by a dapp. @@ -163,13 +142,11 @@ public final class WalletConnectClient { /// Should Error: /// - When URI is invalid format or missing params /// - When topic is already in use - public func pair(uri: String) throws { + public func pair(uri: String) async throws { guard let pairingURI = WalletConnectURI(string: uri) else { throw WalletConnectError.malformedPairingURI } - try pairingQueue.sync { - try pairingEngine.pair(pairingURI) - } + try await pairEngine.pair(pairingURI) } /// For the responder to approve a session proposal. @@ -271,8 +248,8 @@ public final class WalletConnectClient { /// - topic: Session topic /// - params: Event Parameters /// - completion: calls a handler upon completion - public func emit(topic: String, event: Session.Event, chainId: Blockchain, completion: ((Error?)->())?) { - sessionEngine.emit(topic: topic, event: event.internalRepresentation(), chainId: chainId, completion: completion) + public func emit(topic: String, event: Session.Event, chainId: Blockchain) async throws { + try await sessionEngine.emit(topic: topic, event: event.internalRepresentation(), chainId: chainId) } /// For the proposer and responder to terminate a session diff --git a/Tests/IntegrationTests/ClientTest.swift b/Tests/IntegrationTests/ClientTest.swift index ae60fe273..d33b270a5 100644 --- a/Tests/IntegrationTests/ClientTest.swift +++ b/Tests/IntegrationTests/ClientTest.swift @@ -8,14 +8,14 @@ import TestingUtils final class ClientTests: XCTestCase { - + let defaultTimeout: TimeInterval = 5.0 - + let relayHost = "relay.walletconnect.com" let projectId = "8ba9ee138960775e5231b70cc5ef1c3a" var proposer: ClientDelegate! var responder: ClientDelegate! - + override func setUp() { proposer = Self.makeClientDelegate(isController: false, relayHost: relayHost, prefix: "🍏P", projectId: projectId) responder = Self.makeClientDelegate(isController: true, relayHost: relayHost, prefix: "🍎R", projectId: projectId) @@ -33,7 +33,7 @@ final class ClientTests: XCTestCase { keyValueStorage: RuntimeKeyValueStorage()) return ClientDelegate(client: client) } - + private func waitClientsConnected() async { let group = DispatchGroup() group.enter() @@ -47,14 +47,14 @@ final class ClientTests: XCTestCase { group.wait() return } - + func testNewPairingPing() async { let responderReceivesPingResponseExpectation = expectation(description: "Responder receives ping response") await waitClientsConnected() let uri = try! await proposer.client.connect(namespaces: [Namespace.stub()])! - - try! responder.client.pair(uri: uri) + + try! await responder.client.pair(uri: uri) let pairing = responder.client.getSettledPairings().first! responder.client.ping(topic: pairing.topic) { response in XCTAssertTrue(response.isSuccess) @@ -68,9 +68,9 @@ final class ClientTests: XCTestCase { let proposerSettlesSessionExpectation = expectation(description: "Proposer settles session") let responderSettlesSessionExpectation = expectation(description: "Responder settles session") let account = Account("eip155:1:0xab16a96d359ec26a11e2c2b3d8f8b8942d5bfcdb")! - + let uri = try! await proposer.client.connect(namespaces: [Namespace.stub()])! - try! responder.client.pair(uri: uri) + try! await responder.client.pair(uri: uri) responder.onSessionProposal = { [unowned self] proposal in try? self.responder.client.approve(proposalId: proposal.id, accounts: [account], namespaces: []) } @@ -86,7 +86,7 @@ final class ClientTests: XCTestCase { } wait(for: [proposerSettlesSessionExpectation, responderSettlesSessionExpectation], timeout: defaultTimeout) } - + func testNewSessionOnExistingPairing() async { await waitClientsConnected() let proposerSettlesSessionExpectation = expectation(description: "Proposer settles session") @@ -96,7 +96,7 @@ final class ClientTests: XCTestCase { var initiatedSecondSession = false let uri = try! await proposer.client.connect(namespaces: [Namespace.stub()])! - try! responder.client.pair(uri: uri) + try! await responder.client.pair(uri: uri) responder.onSessionProposal = { [unowned self] proposal in try? responder.client.approve(proposalId: proposal.id, accounts: [], namespaces: []) @@ -121,7 +121,7 @@ final class ClientTests: XCTestCase { await waitClientsConnected() let sessionRejectExpectation = expectation(description: "Proposer is notified on session rejection") let uri = try! await proposer.client.connect(namespaces: [Namespace.stub()])! - _ = try! responder.client.pair(uri: uri) + _ = try! await responder.client.pair(uri: uri) responder.onSessionProposal = {[unowned self] proposal in self.responder.client.reject(proposal: proposal, reason: .disapprovedChains) @@ -132,12 +132,12 @@ final class ClientTests: XCTestCase { } wait(for: [sessionRejectExpectation], timeout: defaultTimeout) } - + func testDeleteSession() async { await waitClientsConnected() let sessionDeleteExpectation = expectation(description: "Responder is notified on session deletion") let uri = try! await proposer.client.connect(namespaces: [Namespace.stub()])! - _ = try! responder.client.pair(uri: uri) + _ = try! await responder.client.pair(uri: uri) responder.onSessionProposal = {[unowned self] proposal in try? self.responder.client.approve(proposalId: proposal.id, accounts: [], namespaces: []) } @@ -151,7 +151,7 @@ final class ClientTests: XCTestCase { } wait(for: [sessionDeleteExpectation], timeout: defaultTimeout) } - + func testProposerRequestSessionRequest() async { await waitClientsConnected() let requestExpectation = expectation(description: "Responder receives request") @@ -161,7 +161,7 @@ final class ClientTests: XCTestCase { let responseParams = "0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b915621c" let uri = try! await proposer.client.connect(namespaces: [Namespace.stub(methods: [method])])! - _ = try! responder.client.pair(uri: uri) + _ = try! await responder.client.pair(uri: uri) responder.onSessionProposal = {[unowned self] proposal in try? self.responder.client.approve(proposalId: proposal.id, accounts: [], namespaces: proposal.namespaces) } @@ -191,8 +191,8 @@ final class ClientTests: XCTestCase { } wait(for: [requestExpectation, responseExpectation], timeout: defaultTimeout) } - - + + func testSessionRequestFailureResponse() async { await waitClientsConnected() let failureResponseExpectation = expectation(description: "Proposer receives failure response") @@ -200,7 +200,7 @@ final class ClientTests: XCTestCase { let params = [try! JSONDecoder().decode(EthSendTransaction.self, from: ethSendTransaction.data(using: .utf8)!)] let error = JSONRPCErrorResponse.Error(code: 0, message: "error_message") let uri = try! await proposer.client.connect(namespaces: [Namespace.stub(methods: [method])])! - _ = try! responder.client.pair(uri: uri) + _ = try! await responder.client.pair(uri: uri) responder.onSessionProposal = {[unowned self] proposal in try? self.responder.client.approve(proposalId: proposal.id, accounts: [], namespaces: proposal.namespaces) } @@ -226,13 +226,13 @@ final class ClientTests: XCTestCase { } wait(for: [failureResponseExpectation], timeout: defaultTimeout) } - + func testSessionPing() async { await waitClientsConnected() let proposerReceivesPingResponseExpectation = expectation(description: "Proposer receives ping response") let uri = try! await proposer.client.connect(namespaces: [Namespace.stub()])! - try! responder.client.pair(uri: uri) + try! await responder.client.pair(uri: uri) responder.onSessionProposal = { [unowned self] proposal in try? self.responder.client.approve(proposalId: proposal.id, accounts: [], namespaces: []) } @@ -244,7 +244,7 @@ final class ClientTests: XCTestCase { } wait(for: [proposerReceivesPingResponseExpectation], timeout: defaultTimeout) } - + func testSuccessfulSessionUpdateAccounts() async { await waitClientsConnected() let proposerSessionUpdateExpectation = expectation(description: "Proposer updates session on responder request") @@ -252,7 +252,7 @@ final class ClientTests: XCTestCase { let account = Account("eip155:1:0xab16a96d359ec26a11e2c2b3d8f8b8942d5bfcdb")! let updateAccounts: Set = [Account("eip155:1:0xab16a96d359ec26a11e2c2b3d8f8b8942d5bfcdf")!] let uri = try! await proposer.client.connect(namespaces: [Namespace.stub()])! - try! responder.client.pair(uri: uri) + try! await responder.client.pair(uri: uri) responder.onSessionProposal = { [unowned self] proposal in try? self.responder.client.approve(proposalId: proposal.id, accounts: [account], namespaces: []) } @@ -269,14 +269,14 @@ final class ClientTests: XCTestCase { } wait(for: [proposerSessionUpdateExpectation, responderSessionUpdateExpectation], timeout: defaultTimeout) } - + func testSuccessfulSessionUpdateNamespaces() async { await waitClientsConnected() let proposerSessionUpdateExpectation = expectation(description: "Proposer updates session methods on responder request") let responderSessionUpdateExpectation = expectation(description: "Responder updates session methods on proposer response") let uri = try! await proposer.client.connect(namespaces: [Namespace.stub()])! let namespacesToUpdateWith: Set = [Namespace(chains: [Blockchain("eip155:1")!, Blockchain("eip155:137")!], methods: ["xyz"], events: ["abc"])] - try! responder.client.pair(uri: uri) + try! await responder.client.pair(uri: uri) responder.onSessionProposal = { [unowned self] proposal in try? self.responder.client.approve(proposalId: proposal.id, accounts: [], namespaces: []) } @@ -293,13 +293,13 @@ final class ClientTests: XCTestCase { } wait(for: [proposerSessionUpdateExpectation, responderSessionUpdateExpectation], timeout: defaultTimeout) } - + func testSuccessfulSessionUpdateExpiry() async { await waitClientsConnected() let proposerSessionUpdateExpectation = expectation(description: "Proposer updates session expiry on responder request") let responderSessionUpdateExpectation = expectation(description: "Responder updates session expiry on proposer response") let uri = try! await proposer.client.connect(namespaces: [Namespace.stub()])! - try! responder.client.pair(uri: uri) + try! await responder.client.pair(uri: uri) responder.onSessionProposal = { [unowned self] proposal in try? self.responder.client.approve(proposalId: proposal.id, accounts: [], namespaces: []) } @@ -315,20 +315,20 @@ final class ClientTests: XCTestCase { } wait(for: [proposerSessionUpdateExpectation, responderSessionUpdateExpectation], timeout: defaultTimeout) } - + func testSessionEventSucceeds() async { await waitClientsConnected() let proposerReceivesEventExpectation = expectation(description: "Proposer receives event") let namespace = Namespace(chains: [Blockchain("eip155:1")!], methods: [], events: ["type1"]) // TODO: Fix namespace with empty chain array / protocol change let uri = try! await proposer.client.connect(namespaces: [namespace])! - try! responder.client.pair(uri: uri) + try! await responder.client.pair(uri: uri) let event = Session.Event(name: "type1", data: AnyCodable("event_data")) responder.onSessionProposal = { [unowned self] proposal in try? self.responder.client.approve(proposalId: proposal.id, accounts: [], namespaces: [namespace]) } responder.onSessionSettled = { [unowned self] session in - responder.client.emit(topic: session.topic, event: event, chainId: Blockchain("eip155:1")!, completion: nil) + Task{try? await responder.client.emit(topic: session.topic, event: event, chainId: Blockchain("eip155:1")!)} } proposer.onEventReceived = { event, _ in XCTAssertEqual(event, event) @@ -336,22 +336,20 @@ final class ClientTests: XCTestCase { } wait(for: [proposerReceivesEventExpectation], timeout: defaultTimeout) } - + func testSessionEventFails() async { await waitClientsConnected() let proposerReceivesEventExpectation = expectation(description: "Proposer receives event") proposerReceivesEventExpectation.isInverted = true let uri = try! await proposer.client.connect(namespaces: [Namespace.stub()])! - try! responder.client.pair(uri: uri) + try! await responder.client.pair(uri: uri) let event = Session.Event(name: "type2", data: AnyCodable("event_data")) responder.onSessionProposal = { [unowned self] proposal in try? self.responder.client.approve(proposalId: proposal.id, accounts: [], namespaces: []) } proposer.onSessionSettled = { [unowned self] session in - proposer.client.emit(topic: session.topic, event: event, chainId: Blockchain("eip155:1")!) { error in - XCTAssertNotNil(error) - } + Task {await XCTAssertThrowsErrorAsync(try await proposer.client.emit(topic: session.topic, event: event, chainId: Blockchain("eip155:1")!))} } responder.onEventReceived = { _, _ in XCTFail() diff --git a/Tests/RelayerTests/WakuRelayTests.swift b/Tests/RelayerTests/WakuRelayTests.swift index b3461afbc..bef90265c 100644 --- a/Tests/RelayerTests/WakuRelayTests.swift +++ b/Tests/RelayerTests/WakuRelayTests.swift @@ -36,19 +36,6 @@ class WakuRelayTests: XCTestCase { waitForExpectations(timeout: 0.001, handler: nil) } - func testCompletionOnSubscribe() { - let subscribeExpectation = expectation(description: "subscribe completes with no error") - let topic = "0987" - let requestId = wakuRelay.subscribe(topic: topic) { error in - XCTAssertNil(error) - subscribeExpectation.fulfill() - } - let subscriptionId = "sub-id" - let subscribeResponse = JSONRPCResponse(id: requestId, result: subscriptionId) - dispatcher.onMessage?(try! subscribeResponse.json()) - waitForExpectations(timeout: 0.001, handler: nil) - } - func testPublishRequestAcknowledge() { let acknowledgeExpectation = expectation(description: "completion with no error on waku request acknowledge after publish") let requestId = wakuRelay.publish(topic: "", payload: "{}", onNetworkAcknowledge: { error in diff --git a/Tests/TestingUtils/XCTest.swift b/Tests/TestingUtils/XCTest.swift new file mode 100644 index 000000000..890b2110c --- /dev/null +++ b/Tests/TestingUtils/XCTest.swift @@ -0,0 +1,20 @@ + +import Foundation +import XCTest + +extension XCTest { + public func XCTAssertThrowsErrorAsync( + _ expression: @autoclosure () async throws -> T, + _ message: @autoclosure () -> String = "", + file: StaticString = #filePath, + line: UInt = #line, + _ errorHandler: (_ error: Error) -> Void = { _ in } + ) async { + do { + _ = try await expression() + XCTFail(message(), file: file, line: line) + } catch { + errorHandler(error) + } + } +} diff --git a/Tests/WalletConnectTests/Mocks/MockedNetworkRelayer.swift b/Tests/WalletConnectTests/Mocks/MockedNetworkRelayer.swift index 8d8fc5972..6487f212e 100644 --- a/Tests/WalletConnectTests/Mocks/MockedNetworkRelayer.swift +++ b/Tests/WalletConnectTests/Mocks/MockedNetworkRelayer.swift @@ -4,6 +4,8 @@ import Foundation @testable import WalletConnect class MockedNetworkRelayer: NetworkRelaying { + func subscribe(topic: String) async throws {} + var socketConnectionStatusPublisherSubject = PassthroughSubject() var socketConnectionStatusPublisher: AnyPublisher { socketConnectionStatusPublisherSubject.eraseToAnyPublisher() @@ -11,7 +13,6 @@ class MockedNetworkRelayer: NetworkRelaying { func publish(topic: String, payload: String, prompt: Bool) async throws { self.prompt = prompt - } var onMessage: ((String, String) -> ())? @@ -23,8 +24,7 @@ class MockedNetworkRelayer: NetworkRelaying { return 0 } - func subscribe(topic: String, completion: @escaping (Error?) -> ()) -> Int64 { - return 0 + func subscribe(topic: String, completion: @escaping (Error?) -> ()) { } func unsubscribe(topic: String, completion: @escaping ((Error?) -> ())) -> Int64? { diff --git a/Tests/WalletConnectTests/Mocks/MockedRelay.swift b/Tests/WalletConnectTests/Mocks/MockedRelay.swift index d6ea06429..f96054aab 100644 --- a/Tests/WalletConnectTests/Mocks/MockedRelay.swift +++ b/Tests/WalletConnectTests/Mocks/MockedRelay.swift @@ -51,6 +51,7 @@ class MockedWCRelay: NetworkInteracting { func requestNetworkAck(_ wcMethod: WCMethod, onTopic topic: String, completion: @escaping ((Error?) -> ())) { requestCallCount += 1 requests.append((topic, wcMethod.asRequest())) + completion(nil) } func requestPeerResponse(_ wcMethod: WCMethod, onTopic topic: String, completion: ((Result, JSONRPCErrorResponse>) -> ())?) { diff --git a/Tests/WalletConnectTests/PairEngineTests.swift b/Tests/WalletConnectTests/PairEngineTests.swift new file mode 100644 index 000000000..3435183cc --- /dev/null +++ b/Tests/WalletConnectTests/PairEngineTests.swift @@ -0,0 +1,63 @@ +import XCTest +@testable import WalletConnect +@testable import TestingUtils +@testable import WalletConnectKMS +import WalletConnectUtils + + +final class PairEngineTests: XCTestCase { + + var engine: PairEngine! + + var networkingInteractor: MockedWCRelay! + var storageMock: WCPairingStorageMock! + var cryptoMock: KeyManagementServiceMock! + var proposalPayloadsStore: KeyValueStore! + + var topicGenerator: TopicGenerator! + + override func setUp() { + networkingInteractor = MockedWCRelay() + storageMock = WCPairingStorageMock() + cryptoMock = KeyManagementServiceMock() + topicGenerator = TopicGenerator() + proposalPayloadsStore = KeyValueStore(defaults: RuntimeKeyValueStorage(), identifier: "") + setupEngine() + } + + override func tearDown() { + networkingInteractor = nil + storageMock = nil + cryptoMock = nil + engine = nil + } + + func setupEngine() { + engine = PairEngine( + networkingInteractor: networkingInteractor, + kms: cryptoMock, + pairingStore: storageMock) + } + + func testPairMultipleTimesOnSameURIThrows() async { + let uri = WalletConnectURI.stub() + for i in 1...10 { + usleep(100) + if i == 1 { + XCTAssertNoThrow(Task{try await engine.pair(uri)}) + } else { + await XCTAssertThrowsErrorAsync(try await engine.pair(uri)) + } + } + } + + + func testPair() async { + let uri = WalletConnectURI.stub() + let topic = uri.topic + try! await engine.pair(uri) + XCTAssert(networkingInteractor.didSubscribe(to: topic), "Responder must subscribe to pairing topic.") + XCTAssert(cryptoMock.hasSymmetricKey(for: topic), "Responder must store the symmetric key matching the pairing topic") + XCTAssert(storageMock.hasPairing(forTopic: topic), "The engine must store a pairing") + } +} diff --git a/Tests/WalletConnectTests/PairingEngineTests.swift b/Tests/WalletConnectTests/PairingEngineTests.swift index 4036321c2..537dbfd6b 100644 --- a/Tests/WalletConnectTests/PairingEngineTests.swift +++ b/Tests/WalletConnectTests/PairingEngineTests.swift @@ -8,6 +8,7 @@ func deriveTopic(publicKey: String, privateKey: AgreementPrivateKey) -> String { try! KeyManagementService.generateAgreementKey(from: privateKey, peerPublicKey: publicKey).derivedTopic() } + final class PairingEngineTests: XCTestCase { var engine: PairingEngine! @@ -50,40 +51,20 @@ final class PairingEngineTests: XCTestCase { proposalPayloadsStore: proposalPayloadsStore) } - func testPairMultipleTimesOnSameURIThrows() { - let uri = WalletConnectURI.stub() - for i in 1...10 { - if i == 1 { - XCTAssertNoThrow(try engine.pair(uri)) - } else { - XCTAssertThrowsError(try engine.pair(uri)) - } - } - } - - func testCreate() { - let uri = engine.create()! + func testCreate() async { + let uri = try! await engine.create() XCTAssert(cryptoMock.hasSymmetricKey(for: uri.topic), "Proposer must store the symmetric key matching the URI.") XCTAssert(storageMock.hasPairing(forTopic: uri.topic), "The engine must store a pairing after creating one") XCTAssert(networkingInteractor.didSubscribe(to: uri.topic), "Proposer must subscribe to pairing topic.") XCTAssert(storageMock.getPairing(forTopic: uri.topic)?.active == false, "Recently created pairing must be inactive.") } - func testPair() { - let uri = WalletConnectURI.stub() - let topic = uri.topic - try! engine.pair(uri) - XCTAssert(networkingInteractor.didSubscribe(to: topic), "Responder must subscribe to pairing topic.") - XCTAssert(cryptoMock.hasSymmetricKey(for: topic), "Responder must store the symmetric key matching the pairing topic") - XCTAssert(storageMock.hasPairing(forTopic: topic), "The engine must store a pairing") - } - - func testPropose() { + func testPropose() async { let pairing = Pairing.stub() let topicA = pairing.topic let relayOptions = RelayProtocolOptions(protocol: "", data: nil) - engine.propose(pairingTopic: pairing.topic, namespaces: [Namespace.stub()], relay: relayOptions) {_ in} + try! await engine.propose(pairingTopic: pairing.topic, namespaces: [Namespace.stub()], relay: relayOptions) guard let publishTopic = networkingInteractor.requests.first?.topic, let proposal = networkingInteractor.requests.first?.request.sessionProposal else { @@ -124,14 +105,14 @@ final class PairingEngineTests: XCTestCase { XCTAssertEqual(networkingInteractor.didRespondOnTopic!, topicA, "Responder must respond on topic A") } - func testHandleSessionProposeResponse() { - let uri = engine.create()! + func testHandleSessionProposeResponse() async { + let uri = try! await engine.create() let pairing = storageMock.getPairing(forTopic: uri.topic)! let topicA = pairing.topic let relayOptions = RelayProtocolOptions(protocol: "", data: nil) // Client proposes session - engine.propose(pairingTopic: pairing.topic, namespaces: [Namespace.stub()], relay: relayOptions){_ in} + try! await engine.propose(pairingTopic: pairing.topic, namespaces: [Namespace.stub()], relay: relayOptions) guard let request = networkingInteractor.requests.first?.request, let proposal = networkingInteractor.requests.first?.request.sessionProposal else { @@ -164,14 +145,14 @@ final class PairingEngineTests: XCTestCase { XCTAssertEqual(topicB, sessionTopic, "Responder engine calls back with session topic") } - func testSessionProposeError() { - let uri = engine.create()! + func testSessionProposeError() async { + let uri = try! await engine.create() let pairing = storageMock.getPairing(forTopic: uri.topic)! let topicA = pairing.topic let relayOptions = RelayProtocolOptions(protocol: "", data: nil) // Client propose session - engine.propose(pairingTopic: pairing.topic, namespaces: [Namespace.stub()], relay: relayOptions){_ in} + try! await engine.propose(pairingTopic: pairing.topic, namespaces: [Namespace.stub()], relay: relayOptions) guard let request = networkingInteractor.requests.first?.request, let proposal = networkingInteractor.requests.first?.request.sessionProposal else { @@ -187,14 +168,14 @@ final class PairingEngineTests: XCTestCase { XCTAssertFalse(cryptoMock.hasPrivateKey(for: proposal.proposer.publicKey), "Proposer must remove private key for rejected session") } - func testSessionProposeErrorOnActivePairing() { - let uri = engine.create()! + func testSessionProposeErrorOnActivePairing() async { + let uri = try! await engine.create() let pairing = storageMock.getPairing(forTopic: uri.topic)! let topicA = pairing.topic let relayOptions = RelayProtocolOptions(protocol: "", data: nil) // Client propose session - engine.propose(pairingTopic: pairing.topic, namespaces: [Namespace.stub()], relay: relayOptions){_ in} + try? await engine.propose(pairingTopic: pairing.topic, namespaces: [Namespace.stub()], relay: relayOptions) guard let request = networkingInteractor.requests.first?.request, let proposal = networkingInteractor.requests.first?.request.sessionProposal else { @@ -214,8 +195,8 @@ final class PairingEngineTests: XCTestCase { XCTAssertFalse(cryptoMock.hasPrivateKey(for: proposal.proposer.publicKey), "Proposer must remove private key for rejected session") } - func testPairingExpiration() { - let uri = engine.create()! + func testPairingExpiration() async { + let uri = try! await engine.create() let pairing = storageMock.getPairing(forTopic: uri.topic)! storageMock.onPairingExpiration?(pairing) XCTAssertFalse(cryptoMock.hasSymmetricKey(for: uri.topic)) diff --git a/Tests/WalletConnectTests/SessionEngineTests.swift b/Tests/WalletConnectTests/SessionEngineTests.swift index 639fb33ce..69b0e1523 100644 --- a/Tests/WalletConnectTests/SessionEngineTests.swift +++ b/Tests/WalletConnectTests/SessionEngineTests.swift @@ -56,11 +56,9 @@ final class SessionEngineTests: XCTestCase { let agreementKeys = AgreementKeys.stub() let topicB = String.generateTopic() cryptoMock.setAgreementSecret(agreementKeys, topic: topicB) - let proposal = SessionProposal.stub(proposerPubKey: AgreementPrivateKey().publicKey.hexRepresentation) - try? engine.settle(topic: topicB, proposal: proposal, accounts: [], namespaces: [Namespace.stub()]) - + usleep(100) XCTAssertTrue(storageMock.hasSession(forTopic: topicB), "Responder must persist session on topic B") XCTAssert(networkingInteractor.didSubscribe(to: topicB), "Responder must subscribe for topic B") XCTAssertTrue(networkingInteractor.didCallRequest, "Responder must send session settle payload on topic B")