diff --git a/Example/DApp/Auth/AuthCoordinator.swift b/Example/DApp/Auth/AuthCoordinator.swift index 231f0be4b..f5594f8ab 100644 --- a/Example/DApp/Auth/AuthCoordinator.swift +++ b/Example/DApp/Auth/AuthCoordinator.swift @@ -1,5 +1,6 @@ import SwiftUI import Auth +import WalletConnectPairing final class AuthCoordinator { @@ -30,8 +31,7 @@ final class AuthCoordinator { url: "wallet.connect", icons: ["https://avatars.githubusercontent.com/u/37784886"]) - Auth.configure(metadata: metadata, account: nil) - + Pair.configure(metadata: metadata) navigationController.viewControllers = [authViewController] } } diff --git a/Example/DApp/Sign/Connect/ConnectViewController.swift b/Example/DApp/Sign/Connect/ConnectViewController.swift index adebac5c3..15e50a9fd 100644 --- a/Example/DApp/Sign/Connect/ConnectViewController.swift +++ b/Example/DApp/Sign/Connect/ConnectViewController.swift @@ -1,10 +1,11 @@ import Foundation import UIKit import WalletConnectSign +import WalletConnectPairing class ConnectViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { let uri: WalletConnectURI - let activePairings: [Pairing] = Sign.instance.getPairings() + let activePairings: [Pairing] = Pair.instance.getPairings() let segmentedControl = UISegmentedControl(items: ["Pairings", "New Pairing"]) init(uri: WalletConnectURI) { diff --git a/Example/DApp/Sign/SelectChain/SelectChainViewController.swift b/Example/DApp/Sign/SelectChain/SelectChainViewController.swift index 3a78d7efc..7abd2b7d4 100644 --- a/Example/DApp/Sign/SelectChain/SelectChainViewController.swift +++ b/Example/DApp/Sign/SelectChain/SelectChainViewController.swift @@ -1,5 +1,6 @@ import Foundation import WalletConnectSign +import WalletConnectPairing import UIKit import Combine @@ -34,8 +35,9 @@ class SelectChainViewController: UIViewController, UITableViewDataSource { let blockchains: Set = [Blockchain("eip155:1")!, Blockchain("eip155:137")!] let namespaces: [String: ProposalNamespace] = ["eip155": ProposalNamespace(chains: blockchains, methods: methods, events: [], extensions: nil)] Task { - let uri = try await Sign.instance.connect(requiredNamespaces: namespaces) - showConnectScreen(uri: uri!) + let uri = try await Pair.instance.create() + try await Sign.instance.connect(requiredNamespaces: namespaces, topic: uri.topic) + showConnectScreen(uri: uri) } } diff --git a/Example/ExampleApp/SceneDelegate.swift b/Example/ExampleApp/SceneDelegate.swift index 1537a309b..0037fb05b 100644 --- a/Example/ExampleApp/SceneDelegate.swift +++ b/Example/ExampleApp/SceneDelegate.swift @@ -4,6 +4,7 @@ import Combine import WalletConnectSign import WalletConnectNetworking import WalletConnectRelay +import WalletConnectPairing import Starscream extension WebSocket: WebSocketConnecting { } @@ -27,7 +28,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { icons: ["https://avatars.githubusercontent.com/u/37784886"]) Networking.configure(projectId: InputConfig.projectId, socketFactory: SocketFactory()) - Sign.configure(metadata: metadata) + Pair.configure(metadata: metadata) #if DEBUG if CommandLine.arguments.contains("-cleanInstall") { try? Sign.instance.cleanup() @@ -56,7 +57,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { let wcUri = url.absoluteString.deletingPrefix("https://walletconnect.com/wc?uri=") Task(priority: .high) { - try! await Sign.instance.pair(uri: WalletConnectURI(string: wcUri)!) + try! await Pair.instance.pair(uri: WalletConnectURI(string: wcUri)!) } } } diff --git a/Example/ExampleApp/Wallet/WalletViewController.swift b/Example/ExampleApp/Wallet/WalletViewController.swift index d53343480..a888df7bd 100644 --- a/Example/ExampleApp/Wallet/WalletViewController.swift +++ b/Example/ExampleApp/Wallet/WalletViewController.swift @@ -1,6 +1,7 @@ import UIKit import WalletConnectSign import WalletConnectUtils +import WalletConnectPairing import WalletConnectRouter import Web3 import CryptoSwift @@ -122,7 +123,7 @@ final class WalletViewController: UIViewController { print("[WALLET] Pairing to: \(uri)") Task { do { - try await Sign.instance.pair(uri: uri) + try await Pair.instance.pair(uri: uri) } catch { print("[DAPP] Pairing connect error: \(error)") } diff --git a/Example/IntegrationTests/Sign/SignClientTests.swift b/Example/IntegrationTests/Sign/SignClientTests.swift index e0b463489..a38e2e84e 100644 --- a/Example/IntegrationTests/Sign/SignClientTests.swift +++ b/Example/IntegrationTests/Sign/SignClientTests.swift @@ -42,7 +42,6 @@ final class SignClientTests: XCTestCase { logger: logger, keyValueStorage: keyValueStorage, keychainStorage: keychain, - relayClient: relayClient, pairingClient: pairingClient, networkingClient: networkingClient ) @@ -134,24 +133,6 @@ final class SignClientTests: XCTestCase { wait(for: [sessionDeleteExpectation], timeout: InputConfig.defaultTimeout) } - func testPairingPing() async throws { - let pongResponseExpectation = expectation(description: "Ping sender receives a pong response") - - let uri = try await dapp.client.connect(requiredNamespaces: ProposalNamespace.stubRequired())! - try await wallet.client.pair(uri: uri) - - let pairing = wallet.client.getPairings().first! - - wallet.onPing = { topic in - XCTAssertEqual(topic, pairing.topic) - pongResponseExpectation.fulfill() - } - - try await wallet.client.ping(topic: pairing.topic) - - wait(for: [pongResponseExpectation], timeout: InputConfig.defaultTimeout) - } - func testSessionPing() async throws { let expectation = expectation(description: "Proposer receives ping response") diff --git a/Sources/Auth/Auth.swift b/Sources/Auth/Auth.swift index 022b40744..f6cf8da49 100644 --- a/Sources/Auth/Auth.swift +++ b/Sources/Auth/Auth.swift @@ -18,38 +18,24 @@ public class Auth { /// Auth client instance public static var instance: AuthClient = { - guard let config = Auth.config else { - fatalError("Error - you must call Auth.configure(_:) before accessing the shared instance.") - } + return AuthClientFactory.create( - metadata: config.metadata, - account: config.account, - networkingClient: Networking.instance, - pairingRegisterer: Pair.instance + metadata: Pair.metadata, + account: config?.account, + networkingClient: Networking.interactor, + pairingRegisterer: Pair.registerer ) }() + private static var config: Config? private init() { } - /// Auth instance config method + /// Auth instance wallet config method /// - Parameters: - /// - metadata: App metadata - /// - account: account that wallet will be authenticating with. Should be nil for non wallet clients. - @available(*, deprecated, message: "Use Pair.configure(metadata:) with Auth.configure(account:) instead") - static public func configure(metadata: AppMetadata, account: Account?) { - Auth.config = Auth.Config( - metadata: metadata, - account: account) - } - - /// Auth instance config method - /// - Parameters: - /// - account: account that wallet will be authenticating with. Should be nil for non wallet clients. - static public func configure(account: Account?) { - Auth.config = Auth.Config( - metadata: Pair.metadata, - account: account) + /// - account: account that wallet will be authenticating with. + static public func configure(account: Account) { + Auth.config = Auth.Config(account: account) } } diff --git a/Sources/Auth/AuthClientFactory.swift b/Sources/Auth/AuthClientFactory.swift index 6b8e8224a..c4e3f7e9f 100644 --- a/Sources/Auth/AuthClientFactory.swift +++ b/Sources/Auth/AuthClientFactory.swift @@ -7,14 +7,14 @@ import WalletConnectNetworking public struct AuthClientFactory { - public static func create(metadata: AppMetadata, account: Account?, networkingClient: NetworkingClient, pairingRegisterer: PairingRegisterer) -> AuthClient { + public static func create(metadata: AppMetadata, account: Account?, networkingClient: NetworkingInteractor, pairingRegisterer: PairingRegisterer) -> AuthClient { let logger = ConsoleLogger(loggingLevel: .off) let keyValueStorage = UserDefaults.standard let keychainStorage = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk") return AuthClientFactory.create(metadata: metadata, account: account, logger: logger, keyValueStorage: keyValueStorage, keychainStorage: keychainStorage, networkingClient: networkingClient, pairingRegisterer: pairingRegisterer) } - static func create(metadata: AppMetadata, account: Account?, logger: ConsoleLogging, keyValueStorage: KeyValueStorage, keychainStorage: KeychainStorageProtocol, networkingClient: NetworkingClient, pairingRegisterer: PairingRegisterer) -> AuthClient { + static func create(metadata: AppMetadata, account: Account?, logger: ConsoleLogging, keyValueStorage: KeyValueStorage, keychainStorage: KeychainStorageProtocol, networkingClient: NetworkingInteractor, pairingRegisterer: PairingRegisterer) -> AuthClient { let kms = KeyManagementService(keychain: keychainStorage) let history = RPCHistoryFactory.createForNetwork(keyValueStorage: keyValueStorage) let messageFormatter = SIWEMessageFormatter() diff --git a/Sources/Auth/AuthConfig.swift b/Sources/Auth/AuthConfig.swift index b364ec507..00a98faed 100644 --- a/Sources/Auth/AuthConfig.swift +++ b/Sources/Auth/AuthConfig.swift @@ -2,7 +2,6 @@ import Foundation extension Auth { struct Config { - let metadata: AppMetadata let account: Account? } } diff --git a/Sources/Chat/ChatClientFactory.swift b/Sources/Chat/ChatClientFactory.swift index 10848a6d7..b5af0d7b2 100644 --- a/Sources/Chat/ChatClientFactory.swift +++ b/Sources/Chat/ChatClientFactory.swift @@ -16,7 +16,7 @@ public struct ChatClientFactory { let topicToRegistryRecordStore = CodableStore(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.topicToInvitationPubKey.rawValue) let serialiser = Serializer(kms: kms) let rpcHistory = RPCHistoryFactory.createForNetwork(keyValueStorage: keyValueStorage) - let networkingInteractor = NetworkingClient(relayClient: relayClient, serializer: serialiser, logger: logger, rpcHistory: rpcHistory) + let networkingInteractor = NetworkingInteractor(relayClient: relayClient, serializer: serialiser, logger: logger, rpcHistory: rpcHistory) let invitePayloadStore = CodableStore>(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.invite.rawValue) let registryService = RegistryService(registry: registry, networkingInteractor: networkingInteractor, kms: kms, logger: logger, topicToRegistryRecordStore: topicToRegistryRecordStore) let threadStore = Database(keyValueStorage: keyValueStorage, identifier: StorageDomainIdentifiers.threads.rawValue) diff --git a/Sources/WalletConnectNetworking/Networking.swift b/Sources/WalletConnectNetworking/Networking.swift index cc8c5f063..41def110b 100644 --- a/Sources/WalletConnectNetworking/Networking.swift +++ b/Sources/WalletConnectNetworking/Networking.swift @@ -6,13 +6,17 @@ import Foundation public class Networking { /// Networking client instance - public static var instance: NetworkingClient = { - guard let config = Networking.config else { + public static var instance: NetworkingClient { + return Networking.interactor + } + + public static var interactor: NetworkingInteractor { + guard let _ = Networking.config else { fatalError("Error - you must call Networking.configure(_:) before accessing the shared instance.") } return NetworkingClientFactory.create(relayClient: Relay.instance) - }() + } private static var config: Config? @@ -43,4 +47,3 @@ public class Networking { socketConnectionType: socketConnectionType) } } - diff --git a/Sources/WalletConnectNetworking/NetworkingClient.swift b/Sources/WalletConnectNetworking/NetworkingClient.swift index 76e859428..db04cd32e 100644 --- a/Sources/WalletConnectNetworking/NetworkingClient.swift +++ b/Sources/WalletConnectNetworking/NetworkingClient.swift @@ -1,172 +1,9 @@ import Foundation import Combine -import JSONRPC import WalletConnectRelay -import WalletConnectUtils -import WalletConnectKMS -public class NetworkingClient: NetworkInteracting { - private var publishers = Set() - private let relayClient: RelayClient - private let serializer: Serializing - private let rpcHistory: RPCHistory - private let logger: ConsoleLogging - - private let requestPublisherSubject = PassthroughSubject<(topic: String, request: RPCRequest), Never>() - private let responsePublisherSubject = PassthroughSubject<(topic: String, request: RPCRequest, response: RPCResponse), Never>() - - public var requestPublisher: AnyPublisher<(topic: String, request: RPCRequest), Never> { - requestPublisherSubject.eraseToAnyPublisher() - } - - private var responsePublisher: AnyPublisher<(topic: String, request: RPCRequest, response: RPCResponse), Never> { - responsePublisherSubject.eraseToAnyPublisher() - } - - public var socketConnectionStatusPublisher: AnyPublisher - - public init( - relayClient: RelayClient, - serializer: Serializing, - logger: ConsoleLogging, - rpcHistory: RPCHistory - ) { - self.relayClient = relayClient - self.serializer = serializer - self.rpcHistory = rpcHistory - self.logger = logger - self.socketConnectionStatusPublisher = relayClient.socketConnectionStatusPublisher - setupRelaySubscribtion() - } - - private func setupRelaySubscribtion() { - relayClient.messagePublisher - .sink { [unowned self] (topic, message) in - manageSubscription(topic, message) - }.store(in: &publishers) - } - - public func subscribe(topic: String) async throws { - try await relayClient.subscribe(topic: topic) - } - - public func unsubscribe(topic: String) { - relayClient.unsubscribe(topic: topic) { [unowned self] error in - if let error = error { - logger.error(error) - } else { - rpcHistory.deleteAll(forTopic: topic) - } - } - } - - public func requestSubscription(on request: ProtocolMethod) -> AnyPublisher, Never> { - return requestPublisher - .filter { rpcRequest in - return rpcRequest.request.method == request.method - } - .compactMap { topic, rpcRequest in - guard let id = rpcRequest.id, let request = try? rpcRequest.params?.get(RequestParams.self) else { return nil } - return RequestSubscriptionPayload(id: id, topic: topic, request: request) - } - .eraseToAnyPublisher() - } - - public func responseSubscription(on request: ProtocolMethod) -> AnyPublisher, Never> { - return responsePublisher - .filter { rpcRequest in - return rpcRequest.request.method == request.method - } - .compactMap { topic, rpcRequest, rpcResponse in - guard - let id = rpcRequest.id, - let request = try? rpcRequest.params?.get(Request.self), - let response = try? rpcResponse.result?.get(Response.self) else { return nil } - return ResponseSubscriptionPayload(id: id, topic: topic, request: request, response: response) - } - .eraseToAnyPublisher() - } - - public func responseErrorSubscription(on request: ProtocolMethod) -> AnyPublisher, Never> { - return responsePublisher - .filter { $0.request.method == request.method } - .compactMap { (topic, rpcRequest, rpcResponse) in - guard let id = rpcResponse.id, let request = try? rpcRequest.params?.get(Request.self), let error = rpcResponse.error else { return nil } - return ResponseSubscriptionErrorPayload(id: id, topic: topic, request: request, error: error) - } - .eraseToAnyPublisher() - } - - public func request(_ request: RPCRequest, topic: String, protocolMethod: ProtocolMethod, envelopeType: Envelope.EnvelopeType) async throws { - try rpcHistory.set(request, forTopic: topic, emmitedBy: .local) - let message = try! serializer.serialize(topic: topic, encodable: request, envelopeType: envelopeType) - try await relayClient.publish(topic: topic, payload: message, tag: protocolMethod.requestConfig.tag, prompt: protocolMethod.requestConfig.prompt, ttl: protocolMethod.requestConfig.ttl) - } - - /// Completes with an acknowledgement from the relay network. - /// completes with error if networking client was not able to send a message - /// TODO - relay client should provide async function - continualion should be removed from here - public func requestNetworkAck(_ request: RPCRequest, topic: String, protocolMethod: ProtocolMethod) async throws { - do { - try rpcHistory.set(request, forTopic: topic, emmitedBy: .local) - let message = try serializer.serialize(topic: topic, encodable: request) - return try await withCheckedThrowingContinuation { continuation in - relayClient.publish(topic: topic, payload: message, tag: protocolMethod.requestConfig.tag, prompt: protocolMethod.requestConfig.prompt, ttl: protocolMethod.requestConfig.ttl) { error in - if let error = error { - continuation.resume(throwing: error) - } else { - continuation.resume() - } - } - } - } catch { - logger.error(error) - } - } - - public func respond(topic: String, response: RPCResponse, protocolMethod: ProtocolMethod, envelopeType: Envelope.EnvelopeType) async throws { - try rpcHistory.resolve(response) - let message = try! serializer.serialize(topic: topic, encodable: response, envelopeType: envelopeType) - try await relayClient.publish(topic: topic, payload: message, tag: protocolMethod.responseConfig.tag, prompt: protocolMethod.responseConfig.prompt, ttl: protocolMethod.responseConfig.ttl) - } - - public func respondSuccess(topic: String, requestId: RPCID, protocolMethod: ProtocolMethod, envelopeType: Envelope.EnvelopeType) async throws { - let response = RPCResponse(id: requestId, result: true) - try await respond(topic: topic, response: response, protocolMethod: protocolMethod, envelopeType: envelopeType) - } - - public func respondError(topic: String, requestId: RPCID, protocolMethod: ProtocolMethod, reason: Reason, envelopeType: Envelope.EnvelopeType) async throws { - let error = JSONRPCError(code: reason.code, message: reason.message) - let response = RPCResponse(id: requestId, error: error) - try await respond(topic: topic, response: response, protocolMethod: protocolMethod, envelopeType: envelopeType) - } - - private func manageSubscription(_ topic: String, _ encodedEnvelope: String) { - if let deserializedJsonRpcRequest: RPCRequest = serializer.tryDeserialize(topic: topic, encodedEnvelope: encodedEnvelope) { - handleRequest(topic: topic, request: deserializedJsonRpcRequest) - } else if let response: RPCResponse = serializer.tryDeserialize(topic: topic, encodedEnvelope: encodedEnvelope) { - handleResponse(response: response) - } else { - logger.debug("Networking Interactor - Received unknown object type from networking relay") - } - } - - private func handleRequest(topic: String, request: RPCRequest) { - do { - try rpcHistory.set(request, forTopic: topic, emmitedBy: .remote) - requestPublisherSubject.send((topic, request)) - } catch { - logger.debug(error) - } - } - - private func handleResponse(response: RPCResponse) { - do { - try rpcHistory.resolve(response) - let record = rpcHistory.get(recordId: response.id!)! - responsePublisherSubject.send((record.topic, record.request, response)) - } catch { - logger.debug("Handle json rpc response error: \(error)") - } - } +public protocol NetworkingClient { + var socketConnectionStatusPublisher: AnyPublisher { get } + func connect() throws + func disconnect(closeCode: URLSessionWebSocketTask.CloseCode) throws } diff --git a/Sources/WalletConnectNetworking/NetworkingClientFactory.swift b/Sources/WalletConnectNetworking/NetworkingClientFactory.swift index 4466b2849..36df0f6fc 100644 --- a/Sources/WalletConnectNetworking/NetworkingClientFactory.swift +++ b/Sources/WalletConnectNetworking/NetworkingClientFactory.swift @@ -5,21 +5,21 @@ import WalletConnectUtils public struct NetworkingClientFactory { - public static func create(relayClient: RelayClient) -> NetworkingClient { + public static func create(relayClient: RelayClient) -> NetworkingInteractor { let logger = ConsoleLogger(loggingLevel: .off) let keyValueStorage = UserDefaults.standard let keychainStorage = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk") return NetworkingClientFactory.create(relayClient: relayClient, logger: logger, keychainStorage: keychainStorage, keyValueStorage: keyValueStorage) } - public static func create(relayClient: RelayClient, logger: ConsoleLogging, keychainStorage: KeychainStorageProtocol, keyValueStorage: KeyValueStorage) -> NetworkingClient{ + public static func create(relayClient: RelayClient, logger: ConsoleLogging, keychainStorage: KeychainStorageProtocol, keyValueStorage: KeyValueStorage) -> NetworkingInteractor{ let kms = KeyManagementService(keychain: keychainStorage) let serializer = Serializer(kms: kms) let rpcHistory = RPCHistoryFactory.createForNetwork(keyValueStorage: keyValueStorage) - return NetworkingClient( + return NetworkingInteractor( relayClient: relayClient, serializer: serializer, logger: logger, diff --git a/Sources/WalletConnectNetworking/NetworkingInteractor.swift b/Sources/WalletConnectNetworking/NetworkingInteractor.swift new file mode 100644 index 000000000..4e025d8de --- /dev/null +++ b/Sources/WalletConnectNetworking/NetworkingInteractor.swift @@ -0,0 +1,183 @@ +import Foundation +import Combine +import JSONRPC +import WalletConnectRelay +import WalletConnectUtils +import WalletConnectKMS + +public class NetworkingInteractor: NetworkInteracting { + private var publishers = Set() + private let relayClient: RelayClient + private let serializer: Serializing + private let rpcHistory: RPCHistory + private let logger: ConsoleLogging + + private let requestPublisherSubject = PassthroughSubject<(topic: String, request: RPCRequest), Never>() + private let responsePublisherSubject = PassthroughSubject<(topic: String, request: RPCRequest, response: RPCResponse), Never>() + + public var requestPublisher: AnyPublisher<(topic: String, request: RPCRequest), Never> { + requestPublisherSubject.eraseToAnyPublisher() + } + + private var responsePublisher: AnyPublisher<(topic: String, request: RPCRequest, response: RPCResponse), Never> { + responsePublisherSubject.eraseToAnyPublisher() + } + + public var socketConnectionStatusPublisher: AnyPublisher + + public init( + relayClient: RelayClient, + serializer: Serializing, + logger: ConsoleLogging, + rpcHistory: RPCHistory + ) { + self.relayClient = relayClient + self.serializer = serializer + self.rpcHistory = rpcHistory + self.logger = logger + self.socketConnectionStatusPublisher = relayClient.socketConnectionStatusPublisher + setupRelaySubscribtion() + } + + private func setupRelaySubscribtion() { + relayClient.messagePublisher + .sink { [unowned self] (topic, message) in + manageSubscription(topic, message) + }.store(in: &publishers) + } + + public func subscribe(topic: String) async throws { + try await relayClient.subscribe(topic: topic) + } + + public func unsubscribe(topic: String) { + relayClient.unsubscribe(topic: topic) { [unowned self] error in + if let error = error { + logger.error(error) + } else { + rpcHistory.deleteAll(forTopic: topic) + } + } + } + + public func requestSubscription(on request: ProtocolMethod) -> AnyPublisher, Never> { + return requestPublisher + .filter { rpcRequest in + return rpcRequest.request.method == request.method + } + .compactMap { topic, rpcRequest in + guard let id = rpcRequest.id, let request = try? rpcRequest.params?.get(RequestParams.self) else { return nil } + return RequestSubscriptionPayload(id: id, topic: topic, request: request) + } + .eraseToAnyPublisher() + } + + public func responseSubscription(on request: ProtocolMethod) -> AnyPublisher, Never> { + return responsePublisher + .filter { rpcRequest in + return rpcRequest.request.method == request.method + } + .compactMap { topic, rpcRequest, rpcResponse in + guard + let id = rpcRequest.id, + let request = try? rpcRequest.params?.get(Request.self), + let response = try? rpcResponse.result?.get(Response.self) else { return nil } + return ResponseSubscriptionPayload(id: id, topic: topic, request: request, response: response) + } + .eraseToAnyPublisher() + } + + public func responseErrorSubscription(on request: ProtocolMethod) -> AnyPublisher, Never> { + return responsePublisher + .filter { $0.request.method == request.method } + .compactMap { (topic, rpcRequest, rpcResponse) in + guard let id = rpcResponse.id, let request = try? rpcRequest.params?.get(Request.self), let error = rpcResponse.error else { return nil } + return ResponseSubscriptionErrorPayload(id: id, topic: topic, request: request, error: error) + } + .eraseToAnyPublisher() + } + + public func request(_ request: RPCRequest, topic: String, protocolMethod: ProtocolMethod, envelopeType: Envelope.EnvelopeType) async throws { + try rpcHistory.set(request, forTopic: topic, emmitedBy: .local) + let message = try! serializer.serialize(topic: topic, encodable: request, envelopeType: envelopeType) + try await relayClient.publish(topic: topic, payload: message, tag: protocolMethod.requestConfig.tag, prompt: protocolMethod.requestConfig.prompt, ttl: protocolMethod.requestConfig.ttl) + } + + /// Completes with an acknowledgement from the relay network. + /// completes with error if networking client was not able to send a message + /// TODO - relay client should provide async function - continualion should be removed from here + public func requestNetworkAck(_ request: RPCRequest, topic: String, protocolMethod: ProtocolMethod) async throws { + do { + try rpcHistory.set(request, forTopic: topic, emmitedBy: .local) + let message = try serializer.serialize(topic: topic, encodable: request) + return try await withCheckedThrowingContinuation { continuation in + relayClient.publish(topic: topic, payload: message, tag: protocolMethod.requestConfig.tag, prompt: protocolMethod.requestConfig.prompt, ttl: protocolMethod.requestConfig.ttl) { error in + if let error = error { + continuation.resume(throwing: error) + } else { + continuation.resume() + } + } + } + } catch { + logger.error(error) + } + } + + public func respond(topic: String, response: RPCResponse, protocolMethod: ProtocolMethod, envelopeType: Envelope.EnvelopeType) async throws { + try rpcHistory.resolve(response) + let message = try! serializer.serialize(topic: topic, encodable: response, envelopeType: envelopeType) + try await relayClient.publish(topic: topic, payload: message, tag: protocolMethod.responseConfig.tag, prompt: protocolMethod.responseConfig.prompt, ttl: protocolMethod.responseConfig.ttl) + } + + public func respondSuccess(topic: String, requestId: RPCID, protocolMethod: ProtocolMethod, envelopeType: Envelope.EnvelopeType) async throws { + let response = RPCResponse(id: requestId, result: true) + try await respond(topic: topic, response: response, protocolMethod: protocolMethod, envelopeType: envelopeType) + } + + public func respondError(topic: String, requestId: RPCID, protocolMethod: ProtocolMethod, reason: Reason, envelopeType: Envelope.EnvelopeType) async throws { + let error = JSONRPCError(code: reason.code, message: reason.message) + let response = RPCResponse(id: requestId, error: error) + try await respond(topic: topic, response: response, protocolMethod: protocolMethod, envelopeType: envelopeType) + } + + private func manageSubscription(_ topic: String, _ encodedEnvelope: String) { + if let deserializedJsonRpcRequest: RPCRequest = serializer.tryDeserialize(topic: topic, encodedEnvelope: encodedEnvelope) { + handleRequest(topic: topic, request: deserializedJsonRpcRequest) + } else if let response: RPCResponse = serializer.tryDeserialize(topic: topic, encodedEnvelope: encodedEnvelope) { + handleResponse(response: response) + } else { + logger.debug("Networking Interactor - Received unknown object type from networking relay") + } + } + + private func handleRequest(topic: String, request: RPCRequest) { + do { + try rpcHistory.set(request, forTopic: topic, emmitedBy: .remote) + requestPublisherSubject.send((topic, request)) + } catch { + logger.debug(error) + } + } + + private func handleResponse(response: RPCResponse) { + do { + try rpcHistory.resolve(response) + let record = rpcHistory.get(recordId: response.id!)! + responsePublisherSubject.send((record.topic, record.request, response)) + } catch { + logger.debug("Handle json rpc response error: \(error)") + } + } +} + + +extension NetworkingInteractor: NetworkingClient { + public func connect() throws { + try relayClient.connect() + } + + public func disconnect(closeCode: URLSessionWebSocketTask.CloseCode) throws { + try relayClient.disconnect(closeCode: closeCode) + } +} diff --git a/Sources/WalletConnectPairing/Pair.swift b/Sources/WalletConnectPairing/Pair.swift index ae1f223ad..86680b758 100644 --- a/Sources/WalletConnectPairing/Pair.swift +++ b/Sources/WalletConnectPairing/Pair.swift @@ -5,12 +5,14 @@ import Combine public class Pair { /// Pairing client instance - public static var instance: PairingClient = { - guard let config = Pair.config else { - fatalError("Error - you must call Pair.configure(_:) before accessing the shared instance.") - } - return PairingClientFactory.create(networkingClient: Networking.instance) - }() + public static var instance: PairingInteracting { + return Pair.client + } + + public static var registerer: PairingRegisterer { + return Pair.client + } + public static var metadata: AppMetadata { guard let metadata = config?.metadata else { @@ -30,3 +32,13 @@ public class Pair { Pair.config = Pair.Config(metadata: metadata) } } + +private extension Pair { + + static var client: PairingClient = { + guard let config = Pair.config else { + fatalError("Error - you must call Pair.configure(_:) before accessing the shared instance.") + } + return PairingClientFactory.create(networkingClient: Networking.interactor) + }() +} diff --git a/Sources/WalletConnectPairing/PairingClient.swift b/Sources/WalletConnectPairing/PairingClient.swift index a73bdb010..784d55fba 100644 --- a/Sources/WalletConnectPairing/PairingClient.swift +++ b/Sources/WalletConnectPairing/PairingClient.swift @@ -5,7 +5,7 @@ import WalletConnectNetworking import Combine import JSONRPC -public class PairingClient: PairingRegisterer { +public class PairingClient: PairingRegisterer, PairingInteracting { public var pingResponsePublisher: AnyPublisher<(String), Never> { pingResponsePublisherSubject.eraseToAnyPublisher() } diff --git a/Sources/WalletConnectPairing/PairingClientFactory.swift b/Sources/WalletConnectPairing/PairingClientFactory.swift index b95a435ea..eb019917a 100644 --- a/Sources/WalletConnectPairing/PairingClientFactory.swift +++ b/Sources/WalletConnectPairing/PairingClientFactory.swift @@ -6,14 +6,14 @@ import WalletConnectNetworking public struct PairingClientFactory { - public static func create(networkingClient: NetworkingClient) -> PairingClient { + public static func create(networkingClient: NetworkingInteractor) -> PairingClient { let logger = ConsoleLogger(loggingLevel: .off) let keyValueStorage = UserDefaults.standard let keychainStorage = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk") return PairingClientFactory.create(logger: logger, keyValueStorage: keyValueStorage, keychainStorage: keychainStorage, networkingClient: networkingClient) } - public static func create(logger: ConsoleLogging, keyValueStorage: KeyValueStorage, keychainStorage: KeychainStorageProtocol, networkingClient: NetworkingClient) -> PairingClient { + public static func create(logger: ConsoleLogging, keyValueStorage: KeyValueStorage, keychainStorage: KeychainStorageProtocol, networkingClient: NetworkingInteractor) -> PairingClient { let pairingStore = PairingStorage(storage: SequenceStore(store: .init(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.pairings.rawValue))) let kms = KeyManagementService(keychain: keychainStorage) let appPairService = AppPairService(networkingInteractor: networkingClient, kms: kms, pairingStorage: pairingStore) diff --git a/Sources/WalletConnectPairing/PairingInteracting.swift b/Sources/WalletConnectPairing/PairingInteracting.swift new file mode 100644 index 000000000..90b85bd51 --- /dev/null +++ b/Sources/WalletConnectPairing/PairingInteracting.swift @@ -0,0 +1,18 @@ +import Foundation +import WalletConnectUtils + +public protocol PairingInteracting { + func pair(uri: WalletConnectURI) async throws + + func create() async throws -> WalletConnectURI + + func getPairings() -> [Pairing] + + func getPairing(for topic: String) throws -> Pairing + + func ping(topic: String) async throws + + func disconnect(topic: String) async throws + + func cleanup() throws +} diff --git a/Sources/WalletConnectPairing/Services/Common/CleanupService.swift b/Sources/WalletConnectPairing/Services/Common/CleanupService.swift index 6a5a01334..2ee49d54d 100644 --- a/Sources/WalletConnectPairing/Services/Common/CleanupService.swift +++ b/Sources/WalletConnectPairing/Services/Common/CleanupService.swift @@ -1,7 +1,6 @@ import Foundation import WalletConnectKMS import WalletConnectUtils -import WalletConnectPairing final class CleanupService { diff --git a/Sources/WalletConnectPush/PushClientFactory.swift b/Sources/WalletConnectPush/PushClientFactory.swift index e4b3dde6a..6a1b79429 100644 --- a/Sources/WalletConnectPush/PushClientFactory.swift +++ b/Sources/WalletConnectPush/PushClientFactory.swift @@ -7,7 +7,7 @@ import WalletConnectPairing public struct PushClientFactory { - static public func create(logger: ConsoleLogging, keyValueStorage: KeyValueStorage, keychainStorage: KeychainStorageProtocol, networkingClient: NetworkingClient, pairingClient: PairingClient) -> PushClient { + static public func create(logger: ConsoleLogging, keyValueStorage: KeyValueStorage, keychainStorage: KeychainStorageProtocol, networkingClient: NetworkingInteractor, pairingClient: PairingClient) -> PushClient { let kms = KeyManagementService(keychain: keychainStorage) let pushProposer = PushProposer(networkingInteractor: networkingClient, kms: kms, logger: logger) let proposalResponseSubscriber = ProposalResponseSubscriber(networkingInteractor: networkingClient, kms: kms, logger: logger) diff --git a/Sources/WalletConnectSign/Services/DisconnectService.swift b/Sources/WalletConnectSign/Services/DisconnectService.swift index b4975c749..a0fab00e8 100644 --- a/Sources/WalletConnectSign/Services/DisconnectService.swift +++ b/Sources/WalletConnectSign/Services/DisconnectService.swift @@ -1,30 +1,24 @@ import Foundation -import WalletConnectPairing class DisconnectService { enum Errors: Error { - case objectForTopicNotFound + case sessionForTopicNotFound } private let deleteSessionService: DeleteSessionService private let sessionStorage: WCSessionStorage - private let pairingClient: PairingClient init(deleteSessionService: DeleteSessionService, - sessionStorage: WCSessionStorage, - pairingClient: PairingClient) { + sessionStorage: WCSessionStorage) { self.deleteSessionService = deleteSessionService self.sessionStorage = sessionStorage - self.pairingClient = pairingClient } func disconnect(topic: String) async throws { - if let _ = try? pairingClient.getPairing(for: topic) { - try await pairingClient.disconnect(topic: topic) - } else if sessionStorage.hasSession(forTopic: topic) { + if sessionStorage.hasSession(forTopic: topic) { try await deleteSessionService.delete(topic: topic) } else { - throw Errors.objectForTopicNotFound + throw Errors.sessionForTopicNotFound } } } diff --git a/Sources/WalletConnectSign/Sign/Sign.swift b/Sources/WalletConnectSign/Sign/Sign.swift index 5b0b024e5..d61e7b0ff 100644 --- a/Sources/WalletConnectSign/Sign/Sign.swift +++ b/Sources/WalletConnectSign/Sign/Sign.swift @@ -28,9 +28,8 @@ public class Sign { public static var instance: SignClient = { return SignClientFactory.create( metadata: Sign.metadata ?? Pair.metadata, - relayClient: Relay.instance, - pairingClient: Pair.instance, - networkingClient: Networking.instance + pairingClient: Pair.instance as! PairingClient, + networkingClient: Networking.instance as! NetworkingInteractor ) }() diff --git a/Sources/WalletConnectSign/Sign/SignClient.swift b/Sources/WalletConnectSign/Sign/SignClient.swift index 6ef0b254c..388f33692 100644 --- a/Sources/WalletConnectSign/Sign/SignClient.swift +++ b/Sources/WalletConnectSign/Sign/SignClient.swift @@ -1,7 +1,6 @@ import Foundation import Combine import JSONRPC -import WalletConnectRelay import WalletConnectUtils import WalletConnectKMS import WalletConnectNetworking @@ -13,6 +12,9 @@ import WalletConnectPairing /// /// Access via `Sign.instance` public final class SignClient { + enum Errors: Error { + case sessionForTopicNotFound + } // MARK: - Public Properties @@ -97,7 +99,7 @@ public final class SignClient { // MARK: - Private properties private let pairingClient: PairingClient - private let relayClient: RelayClient + private let networkingClient: NetworkingInteractor private let sessionEngine: SessionEngine private let approveEngine: ApproveEngine private let disconnectService: DisconnectService @@ -126,7 +128,7 @@ public final class SignClient { // MARK: - Initialization init(logger: ConsoleLogging, - relayClient: RelayClient, + networkingClient: NetworkingInteractor, sessionEngine: SessionEngine, approveEngine: ApproveEngine, pairingPingService: PairingPingService, @@ -140,7 +142,7 @@ public final class SignClient { pairingClient: PairingClient ) { self.logger = logger - self.relayClient = relayClient + self.networkingClient = networkingClient self.sessionEngine = sessionEngine self.approveEngine = approveEngine self.pairingPingService = pairingPingService @@ -165,6 +167,7 @@ public final class SignClient { /// - requiredNamespaces: required namespaces for a session /// - 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. + @available(*, deprecated, message: "use Pair.instance.create() and connect(requiredNamespaces: [String: ProposalNamespace]): instead") public func connect(requiredNamespaces: [String: ProposalNamespace], topic: String? = nil) async throws -> WalletConnectURI? { logger.debug("Connecting Application") if let topic = topic { @@ -186,6 +189,21 @@ public final class SignClient { } } + /// For a dApp to propose a session to a wallet. + /// Function will propose a session on existing pairing. + /// - Parameters: + /// - requiredNamespaces: required namespaces for a session + /// - topic: pairing topic + public func connect(requiredNamespaces: [String: ProposalNamespace], topic: String) async throws { + logger.debug("Connecting Application") + try pairingClient.validatePairingExistance(topic) + try await appProposeService.propose( + pairingTopic: topic, + namespaces: requiredNamespaces, + relay: RelayProtocolOptions(protocol: "irn", data: nil) + ) + } + /// For wallet to receive a session proposal from a dApp /// Responder should call this function in order to accept peer's pairing and be able to subscribe for future session proposals. /// - Parameter uri: Pairing URI that is commonly presented as a QR code by a dapp. @@ -193,6 +211,7 @@ public final class SignClient { /// Should Error: /// - When URI has invalid format or missing params /// - When topic is already in use + @available(*, deprecated, message: "use Pair.instance.pair(uri: WalletConnectURI): instead") public func pair(uri: WalletConnectURI) async throws { try await pairingClient.pair(uri: uri) } @@ -251,17 +270,12 @@ public final class SignClient { /// /// Should Error: /// - When the session topic is not found - /// - When the response is neither result or error /// /// - Parameters: - /// - topic: Topic of a session or a pairing - /// - completion: Result will be success on response or an error + /// - topic: Topic of a session public func ping(topic: String) async throws { - if let _ = try? pairingClient.validatePairingExistance(topic) { - try await pairingPingService.ping(topic: topic) - } else if sessionEngine.hasSession(for: topic) { - try await sessionPingService.ping(topic: topic) - } + guard sessionEngine.hasSession(for: topic) else { throw Errors.sessionForTopicNotFound } + try await sessionPingService.ping(topic: topic) } /// For the wallet to emit an event to a dApp @@ -297,6 +311,7 @@ public final class SignClient { /// Query pairings /// - Returns: All pairings + @available(*, deprecated, message: "use Pair.instance.getPairings(uri: WalletConnectURI): instead") public func getPairings() -> [Pairing] { pairingClient.getPairings() } @@ -382,7 +397,7 @@ public final class SignClient { } private func setUpConnectionObserving() { - relayClient.socketConnectionStatusPublisher.sink { [weak self] status in + networkingClient.socketConnectionStatusPublisher.sink { [weak self] status in self?.socketConnectionStatusPublisherSubject.send(status) }.store(in: &publishers) } diff --git a/Sources/WalletConnectSign/Sign/SignClientFactory.swift b/Sources/WalletConnectSign/Sign/SignClientFactory.swift index ba59acee8..a1c086783 100644 --- a/Sources/WalletConnectSign/Sign/SignClientFactory.swift +++ b/Sources/WalletConnectSign/Sign/SignClientFactory.swift @@ -16,14 +16,14 @@ public struct SignClientFactory { /// - keyValueStorage: by default WalletConnect SDK will store sequences in UserDefaults /// /// 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 static func create(metadata: AppMetadata, relayClient: RelayClient, pairingClient: PairingClient, networkingClient: NetworkingClient) -> SignClient { + public static func create(metadata: AppMetadata, pairingClient: PairingClient, networkingClient: NetworkingInteractor) -> SignClient { let logger = ConsoleLogger(loggingLevel: .off) let keyValueStorage = UserDefaults.standard let keychainStorage = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk") - return SignClientFactory.create(metadata: metadata, logger: logger, keyValueStorage: keyValueStorage, keychainStorage: keychainStorage, relayClient: relayClient, pairingClient: pairingClient, networkingClient: networkingClient) + return SignClientFactory.create(metadata: metadata, logger: logger, keyValueStorage: keyValueStorage, keychainStorage: keychainStorage, pairingClient: pairingClient, networkingClient: networkingClient) } - static func create(metadata: AppMetadata, logger: ConsoleLogging, keyValueStorage: KeyValueStorage, keychainStorage: KeychainStorageProtocol, relayClient: RelayClient, pairingClient: PairingClient, networkingClient: NetworkingClient) -> SignClient { + static func create(metadata: AppMetadata, logger: ConsoleLogging, keyValueStorage: KeyValueStorage, keychainStorage: KeychainStorageProtocol, pairingClient: PairingClient, networkingClient: NetworkingInteractor) -> SignClient { let kms = KeyManagementService(keychain: keychainStorage) let rpcHistory = RPCHistoryFactory.createForNetwork(keyValueStorage: keyValueStorage) let pairingStore = PairingStorage(storage: SequenceStore(store: .init(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.pairings.rawValue))) @@ -36,14 +36,14 @@ public struct SignClientFactory { let approveEngine = ApproveEngine(networkingInteractor: networkingClient, proposalPayloadsStore: proposalPayloadsStore, sessionToPairingTopic: sessionToPairingTopic, pairingRegisterer: pairingClient, metadata: metadata, kms: kms, logger: logger, pairingStore: pairingStore, sessionStore: sessionStore) let cleanupService = CleanupService(pairingStore: pairingStore, sessionStore: sessionStore, kms: kms, sessionToPairingTopic: sessionToPairingTopic) let deleteSessionService = DeleteSessionService(networkingInteractor: networkingClient, kms: kms, sessionStore: sessionStore, logger: logger) - let disconnectService = DisconnectService(deleteSessionService: deleteSessionService, sessionStorage: sessionStore, pairingClient: pairingClient) + let disconnectService = DisconnectService(deleteSessionService: deleteSessionService, sessionStorage: sessionStore) let sessionPingService = SessionPingService(sessionStorage: sessionStore, networkingInteractor: networkingClient, logger: logger) let pairingPingService = PairingPingService(pairingStorage: pairingStore, networkingInteractor: networkingClient, logger: logger) let appProposerService = AppProposeService(metadata: metadata, networkingInteractor: networkingClient, kms: kms, logger: logger) let client = SignClient( logger: logger, - relayClient: relayClient, + networkingClient: networkingClient, sessionEngine: sessionEngine, approveEngine: approveEngine, pairingPingService: pairingPingService,