diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index 5bf6b7261..14e537032 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -280,6 +280,13 @@ C5B2F7052970573D000DBA0E /* SolanaSwift in Frameworks */ = {isa = PBXBuildFile; productRef = C5B2F7042970573D000DBA0E /* SolanaSwift */; }; C5B2F71029705827000DBA0E /* EthereumTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F568C32795832A00D0A289 /* EthereumTransaction.swift */; }; C5CFBECC2AA99D5D00378D41 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = C5CFBECB2AA99D5D00378D41 /* Starscream */; }; + C5CFBECD2AA9A26E00378D41 /* DefaultSocketFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5506B4B2AA8DBCB0077AD54 /* DefaultSocketFactory.swift */; }; + C5CFBECE2AA9A2B300378D41 /* DefaultSocketFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5506B4B2AA8DBCB0077AD54 /* DefaultSocketFactory.swift */; }; + C5CFBECF2AA9A2B500378D41 /* DefaultSocketFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5506B4B2AA8DBCB0077AD54 /* DefaultSocketFactory.swift */; }; + C5CFBED12AA9A31500378D41 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = C5CFBED02AA9A31500378D41 /* Starscream */; }; + C5CFBED32AA9A31B00378D41 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = C5CFBED22AA9A31B00378D41 /* Starscream */; }; + C5CFBED52AA9A32100378D41 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = C5CFBED42AA9A32100378D41 /* Starscream */; }; + C5CFBED72AA9A32700378D41 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = C5CFBED62AA9A32700378D41 /* Starscream */; }; C5DD5BE1294E09E3008FD3A4 /* Web3Wallet in Frameworks */ = {isa = PBXBuildFile; productRef = C5DD5BE0294E09E3008FD3A4 /* Web3Wallet */; }; C5F32A2C2954814200A6476E /* ConnectionDetailsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5F32A2B2954814200A6476E /* ConnectionDetailsModule.swift */; }; C5F32A2E2954814A00A6476E /* ConnectionDetailsRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5F32A2D2954814A00A6476E /* ConnectionDetailsRouter.swift */; }; @@ -626,6 +633,7 @@ buildActionMask = 2147483647; files = ( 844749FD29B9E6B2005F520B /* WalletConnectNetworking in Frameworks */, + C5CFBED52AA9A32100378D41 /* Starscream in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -639,7 +647,6 @@ A5B6C0F12A6EAB0800927332 /* WalletConnectNotify in Frameworks */, A54195A52934E83F0035AD19 /* Web3 in Frameworks */, 8487A9442A836C2A0003D5AF /* Sentry in Frameworks */, - A5D85228286333E300DAF5C3 /* Starscream in Frameworks */, 84943C7B2A9BA206007EBAC2 /* Mixpanel in Frameworks */, A573C53929EC365000E3CBFD /* HDWalletKit in Frameworks */, A5BB7FA328B6A50400707FC6 /* WalletConnectAuth in Frameworks */, @@ -665,6 +672,7 @@ A59FAEC928B7B93A002BB66F /* Web3 in Frameworks */, A59F877628B5462900A9CD80 /* WalletConnectAuth in Frameworks */, A58EC611299D57B800F3452A /* AsyncButton in Frameworks */, + C5CFBED32AA9A31B00378D41 /* Starscream in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -682,6 +690,7 @@ A5E03DFF2864662500888481 /* WalletConnect in Frameworks */, A561C80529DFCD4500DF540D /* WalletConnectSync in Frameworks */, A50DF19D2A25084A0036EA6C /* WalletConnectHistory in Frameworks */, + C5CFBED72AA9A32700378D41 /* Starscream in Frameworks */, A5C8BE85292FE20B006CC85C /* Web3 in Frameworks */, 84DDB4ED28ABB663003D66ED /* WalletConnectAuth in Frameworks */, C5DD5BE1294E09E3008FD3A4 /* Web3Wallet in Frameworks */, @@ -698,11 +707,11 @@ A573C53D29EC366500E3CBFD /* HDWalletKit in Frameworks */, C56EE27D293F56F8004840D1 /* WalletConnectChat in Frameworks */, C5133A78294125CC00A8314C /* Web3 in Frameworks */, + C5CFBED12AA9A31500378D41 /* Starscream in Frameworks */, 84536D7429EEBCF0008EA8DB /* Web3Inbox in Frameworks */, C5B2F7052970573D000DBA0E /* SolanaSwift in Frameworks */, 8487A9462A836C3F0003D5AF /* Sentry in Frameworks */, C55D349929630D440004314A /* Web3Wallet in Frameworks */, - C56EE255293F569A004840D1 /* Starscream in Frameworks */, A5B6C0F52A6EAB2800927332 /* WalletConnectNotify in Frameworks */, C56EE27B293F56F8004840D1 /* WalletConnectAuth in Frameworks */, 84943C7D2A9BA328007EBAC2 /* Mixpanel in Frameworks */, @@ -1792,6 +1801,7 @@ name = RelayIntegrationTests; packageProductDependencies = ( 844749FC29B9E6B2005F520B /* WalletConnectNetworking */, + C5CFBED42AA9A32100378D41 /* Starscream */, ); productName = RelayIntegrationTests; productReference = 844749F329B9E5B9005F520B /* RelayIntegrationTests.xctest */; @@ -1866,6 +1876,7 @@ A58EC617299D665A00F3452A /* Web3Inbox */, A561C7FF29DF32CE00DF540D /* HDWalletKit */, CF25F2882A432476009C7E49 /* WalletConnectModal */, + C5CFBED22AA9A31B00378D41 /* Starscream */, ); productName = Showcase; productReference = A58E7CE828729F550082D443 /* Showcase.app */; @@ -1912,6 +1923,7 @@ A573C53A29EC365800E3CBFD /* HDWalletKit */, A50DF19C2A25084A0036EA6C /* WalletConnectHistory */, A5B6C0F22A6EAB1700927332 /* WalletConnectNotify */, + C5CFBED62AA9A32700378D41 /* Starscream */, ); productName = IntegrationTests; productReference = A5E03DED286464DB00888481 /* IntegrationTests.xctest */; @@ -1943,6 +1955,7 @@ 8487A9452A836C3F0003D5AF /* Sentry */, A5B6C0F42A6EAB2800927332 /* WalletConnectNotify */, 84943C7C2A9BA328007EBAC2 /* Mixpanel */, + C5CFBED02AA9A31500378D41 /* Starscream */, ); productName = ChatWallet; productReference = C56EE21B293F55ED004840D1 /* WalletApp.app */; @@ -2110,6 +2123,7 @@ 844749FF29B9EB3B005F520B /* KeychainStorageMock.swift in Sources */, 844749FE29B9EB1B005F520B /* InputConfig.swift in Sources */, 844749F629B9E5B9005F520B /* RelayClientEndToEndTests.swift in Sources */, + C5CFBECF2AA9A2B500378D41 /* DefaultSocketFactory.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2129,6 +2143,7 @@ A5A8E47E293A1CFE00FEB97D /* DefaultSignerFactory.swift in Sources */, A5A0843D29D2F624000B9B17 /* DefaultCryptoProvider.swift in Sources */, 84CE6448279AE68600142511 /* AccountRequestViewController.swift in Sources */, + C5CFBECD2AA9A26E00378D41 /* DefaultSocketFactory.swift in Sources */, A5BB7F9F28B69B7100707FC6 /* SignCoordinator.swift in Sources */, 84CE642127981DED00142511 /* SceneDelegate.swift in Sources */, 84CE644E279ED2FF00142511 /* SelectChainView.swift in Sources */, @@ -2268,6 +2283,7 @@ A50C036528AAD32200FE72D3 /* ClientDelegate.swift in Sources */, A5321C2B2A250367006CADC3 /* HistoryTests.swift in Sources */, A58A1ECC29BF458600A82A20 /* ENSResolverTests.swift in Sources */, + C5CFBECE2AA9A2B300378D41 /* DefaultSocketFactory.swift in Sources */, A5E03DFA286465C700888481 /* SignClientTests.swift in Sources */, 84A6E3C32A386BBC008A0571 /* Publisher.swift in Sources */, A54195A02934BFEF0035AD19 /* EIP1271VerifierTests.swift in Sources */, @@ -3301,6 +3317,26 @@ package = C5CFBECA2AA99D5D00378D41 /* XCRemoteSwiftPackageReference "Starscream" */; productName = Starscream; }; + C5CFBED02AA9A31500378D41 /* Starscream */ = { + isa = XCSwiftPackageProductDependency; + package = C5CFBECA2AA99D5D00378D41 /* XCRemoteSwiftPackageReference "Starscream" */; + productName = Starscream; + }; + C5CFBED22AA9A31B00378D41 /* Starscream */ = { + isa = XCSwiftPackageProductDependency; + package = C5CFBECA2AA99D5D00378D41 /* XCRemoteSwiftPackageReference "Starscream" */; + productName = Starscream; + }; + C5CFBED42AA9A32100378D41 /* Starscream */ = { + isa = XCSwiftPackageProductDependency; + package = C5CFBECA2AA99D5D00378D41 /* XCRemoteSwiftPackageReference "Starscream" */; + productName = Starscream; + }; + C5CFBED62AA9A32700378D41 /* Starscream */ = { + isa = XCSwiftPackageProductDependency; + package = C5CFBECA2AA99D5D00378D41 /* XCRemoteSwiftPackageReference "Starscream" */; + productName = Starscream; + }; C5DD5BE0294E09E3008FD3A4 /* Web3Wallet */ = { isa = XCSwiftPackageProductDependency; productName = Web3Wallet; diff --git a/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift b/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift index 2f584b3b3..f6bf47b34 100644 --- a/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift +++ b/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift @@ -2,6 +2,7 @@ import Foundation import Combine import XCTest import WalletConnectUtils +import Starscream @testable import WalletConnectRelay private class RelayKeychainStorageMock: KeychainStorageProtocol { diff --git a/Example/Shared/DefaultSocketFactory.swift b/Example/Shared/DefaultSocketFactory.swift index 4a9641d90..ecb95f2a1 100644 --- a/Example/Shared/DefaultSocketFactory.swift +++ b/Example/Shared/DefaultSocketFactory.swift @@ -5,7 +5,7 @@ import WalletConnectRelay extension WebSocket: WebSocketConnecting { public func reconnect() { - + connect() } } diff --git a/Example/WalletApp/ApplicationLayer/ConfigurationService.swift b/Example/WalletApp/ApplicationLayer/ConfigurationService.swift index 919d3292b..9a50fe4c1 100644 --- a/Example/WalletApp/ApplicationLayer/ConfigurationService.swift +++ b/Example/WalletApp/ApplicationLayer/ConfigurationService.swift @@ -1,11 +1,12 @@ import Foundation import Web3Inbox +import Web3Wallet final class ConfigurationService { func configure(importAccount: ImportAccount) { - Networking.configure(projectId: InputConfig.projectId, socketFactory: DefaultSocketFactory()) + Networking.configure(projectId: InputConfig.projectId, socketFactory: WalletConnectSocketClientFactory()) Networking.instance.setLogging(level: .debug) let metadata = AppMetadata( diff --git a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift index b362fcfb0..bda8fa78f 100644 --- a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift +++ b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift @@ -48,6 +48,8 @@ class AutomaticSocketConnectionHandler { networkMonitor.networkConnectionStatusPublisher.sink { [weak self] networkConnectionStatus in if networkConnectionStatus == .connected { self?.reconnectIfNeeded() + } else { + self?.socket.disconnect() } } .store(in: &publishers) diff --git a/Sources/WalletConnectRelay/WCWebSocketClient/UnfairLock.swift b/Sources/WalletConnectRelay/WCWebSocketClient/UnfairLock.swift index 7a5f07e86..bab37dabc 100644 --- a/Sources/WalletConnectRelay/WCWebSocketClient/UnfairLock.swift +++ b/Sources/WalletConnectRelay/WCWebSocketClient/UnfairLock.swift @@ -1,75 +1,21 @@ import Foundation import Combine -public final class UnfairLock { - @usableFromInline let os_lock: UnsafeMutablePointer +final class UnfairLock { + private var _lock: UnsafeMutablePointer - public init() { - self.os_lock = .allocate(capacity: 1) - os_lock.initialize(to: os_unfair_lock()) + init() { + _lock = UnsafeMutablePointer.allocate(capacity: 1) + _lock.initialize(to: os_unfair_lock()) } deinit { - os_lock.deallocate() + _lock.deallocate() } - @inlinable - @inline(__always) - public func lock() { - os_unfair_lock_lock(os_lock) - } - - @inlinable - @inline(__always) - public func unlock() { - os_unfair_lock_unlock(os_lock) - } - - @discardableResult - @inlinable - @inline(__always) - public func withLock(body: () throws -> Result) rethrows -> Result { - os_unfair_lock_lock(os_lock) - defer { os_unfair_lock_unlock(os_lock) } - return try body() - } - - @inlinable - @inline(__always) - public func withLock(body: () -> Void) { - os_unfair_lock_lock(os_lock) - defer { os_unfair_lock_unlock(os_lock) } - body() - } - - @inlinable - @inline(__always) - public func assertOwner() { - os_unfair_lock_assert_owner(os_lock) - } - - @inlinable - @inline(__always) - public func assertNotOwner() { - os_unfair_lock_assert_not_owner(os_lock) - } -} - -extension UnfairLock { - private final class LockAssertion: Cancellable { - private var _owner: UnfairLock - - init(owner: UnfairLock) { - self._owner = owner - os_unfair_lock_lock(owner.os_lock) - } - - __consuming func cancel() { - os_unfair_lock_unlock(_owner.os_lock) - } - } - - func acquire() -> Cancellable { - LockAssertion(owner: self) + func locked(_ f: () throws -> ReturnValue) rethrows -> ReturnValue { + os_unfair_lock_lock(_lock) + defer { os_unfair_lock_unlock(_lock) } + return try f() } } diff --git a/Sources/WalletConnectRelay/WCWebSocketClient/WCWebSocketClient.swift b/Sources/WalletConnectRelay/WCWebSocketClient/WalletConnectSocketClient.swift similarity index 56% rename from Sources/WalletConnectRelay/WCWebSocketClient/WCWebSocketClient.swift rename to Sources/WalletConnectRelay/WCWebSocketClient/WalletConnectSocketClient.swift index e76f949dd..c219e4822 100644 --- a/Sources/WalletConnectRelay/WCWebSocketClient/WCWebSocketClient.swift +++ b/Sources/WalletConnectRelay/WCWebSocketClient/WalletConnectSocketClient.swift @@ -1,16 +1,18 @@ import Foundation -enum WebSocketClientError: Error { +enum WalletConnectSocketClientError: Error { case errorWithCode(URLSessionWebSocketTask.CloseCode) } -struct WebSocketClientFactory: WebSocketFactory { - func create(with url: URL) -> WebSocketConnecting { - return WCWebSocketClient(url: url, logger: ConsoleLogger(loggingLevel: .debug)) +public struct WalletConnectSocketClientFactory: WebSocketFactory { + public init() { } + + public func create(with url: URL) -> WebSocketConnecting { + return WalletConnectSocketClient(url: url, logger: ConsoleLogger(loggingLevel: .debug)) } } -final class WCWebSocketClient: NSObject, WebSocketConnecting { +public final class WalletConnectSocketClient: NSObject, WebSocketConnecting { private var socket: URLSessionWebSocketTask? = nil private var url: URL private let logger: ConsoleLogging @@ -18,6 +20,44 @@ final class WCWebSocketClient: NSObject, WebSocketConnecting { private var _request: URLRequest private var _isConnected = false + // MARK: - WebSocketConnecting + public var isConnected: Bool { + get { + lock.locked { + _isConnected + } + } + + set { + lock.locked { + _isConnected = newValue + } + } + } + + public var request: URLRequest { + get { + lock.locked { + _request + } + } + + set { + lock.locked { + _request = newValue + if let url = _request.url { + self.url = url + createSocketConnection() + _isConnected = false + } + } + } + } + + public var onConnect: (() -> Void)? + public var onDisconnect: ((Error?) -> Void)? + public var onText: ((String) -> Void)? + private let lock = UnfairLock() init(url: URL, logger: ConsoleLogging) { @@ -27,96 +67,64 @@ final class WCWebSocketClient: NSObject, WebSocketConnecting { super.init() } - public func reconnect() { - let configuration = URLSessionConfiguration.default - let urlSession = URLSession(configuration: configuration, delegate: self, delegateQueue: OperationQueue()) - let urlRequest = URLRequest(url: url) - socket = urlSession.webSocketTask(with: urlRequest) - - isConnected = false - connect() - } - // MARK: - WebSocketConnecting - var isConnected: Bool { - get { lock.withLock { _isConnected } } - set { lock.withLock { _isConnected = newValue } } + public func connect() { + lock.locked { + logger.debug("[WebSocketClient]: Connect called 🔗 \(url.host ?? "nil")") + socket?.resume() + } } - var request: URLRequest { - get { lock.withLock { _request } } - set { - lock.withLock { - _request = newValue - if let url = request.url { - let configuration = URLSessionConfiguration.default - - let urlSession = URLSession(configuration: configuration, delegate: self, delegateQueue: OperationQueue()) - let urlRequest = URLRequest(url: url) - socket = urlSession.webSocketTask(with: urlRequest) - - isConnected = false - self.url = url - } - } + public func disconnect() { + lock.locked { + logger.debug("[WebSocketClient]: Disconnect called") + socket?.cancel() + _isConnected = false } } - var onConnect: (() -> Void)? - var onDisconnect: ((Error?) -> Void)? - var onText: ((String) -> Void)? - - func connect() { - lock.lock() - defer { - lock.unlock() + public func write(string: String, completion: (() -> Void)?) { + lock.locked { + let message = URLSessionWebSocketTask.Message.string(string) + socket?.send(message) { _ in + completion?() + } } - logger.debug("[WebSocketClient]: Connect called 🔗 \(url.host ?? "nil")") - socket?.resume() } - func disconnect() { - lock.lock() - defer { - lock.unlock() - } + public func reconnect() { + createSocketConnection() - logger.debug("[WebSocketClient]: Disconnect called") - socket?.cancel() isConnected = false + connect() } - func write(string: String, completion: (() -> Void)?) { - lock.lock() - defer { - lock.unlock() - } - - let message = URLSessionWebSocketTask.Message.string(string) - socket?.send(message) { _ in - completion?() - } + private func createSocketConnection() { + let configuration = URLSessionConfiguration.default + let urlSession = URLSession(configuration: configuration, delegate: self, delegateQueue: OperationQueue()) + let urlRequest = URLRequest(url: url) + socket = urlSession.webSocketTask(with: urlRequest) } } // MARK: - URLSessionWebSocketDelegate -extension WCWebSocketClient: URLSessionWebSocketDelegate { - func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) { +extension WalletConnectSocketClient: URLSessionWebSocketDelegate { + public func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) { isConnected = true logger.debug("[WebSocketClient]: Connected") onConnect?() receiveMessage() } - func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) { + public func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) { if isConnected { isConnected = false logger.debug("[WebSocketClient]: Did close with code: \(closeCode)") - onDisconnect?(WebSocketClientError.errorWithCode(closeCode)) + onDisconnect?(WalletConnectSocketClientError.errorWithCode(closeCode)) } } - func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { logger.debug("[WebSocketClient]: Did complete with error: \(error?.localizedDescription ?? "unknown")") }