From 4c87e5655b7089d439692ae111fbc7ac4f8b0b12 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Tue, 10 Sep 2024 11:10:50 +0200 Subject: [PATCH 01/14] savepoint --- .github/workflows/cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 6c110f787..0dfbfbbef 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -28,4 +28,4 @@ jobs: - name: Lint CocoaPods run: | - pod lib lint --verbose --no-clean --quick --allow-warnings --platforms=ios WalletConnectSwiftV2.podspec \ No newline at end of file + pod lib lint --verbose --no-clean --quick --allow-warnings --platforms=ios reown-swift.podspec From 7daf920af52f844c562c3e5be544cce0d42e95d5 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Tue, 10 Sep 2024 22:16:14 +0200 Subject: [PATCH 02/14] add better logging for socket connection --- Example/ExampleApp.xcodeproj/project.pbxproj | 14 ++--- Sources/WalletConnectRelay/Dispatching.swift | 5 ++ .../AutomaticSocketConnectionHandler.swift | 53 +++++++++++++++---- 3 files changed, 52 insertions(+), 20 deletions(-) diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index 4b19b0b33..cc49adc14 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -2419,11 +2419,9 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = PNDecryptionService/PNDecryptionService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 184; - DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = W5R8AG9K22; + DEVELOPMENT_TEAM = W5R8AG9K22; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = PNDecryptionService/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = PNDecryptionService; @@ -2438,7 +2436,6 @@ PRODUCT_BUNDLE_IDENTIFIER = com.walletconnect.walletapp.PNDecryptionService; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore com.walletconnect.walletapp.PNDecryptionService"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; @@ -2564,11 +2561,9 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = WalletApp/WalletApp.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 184; - DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = W5R8AG9K22; + DEVELOPMENT_TEAM = W5R8AG9K22; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = WalletApp/Other/Info.plist; @@ -2587,7 +2582,6 @@ PRODUCT_BUNDLE_IDENTIFIER = com.walletconnect.walletapp; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore com.walletconnect.walletapp"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/Sources/WalletConnectRelay/Dispatching.swift b/Sources/WalletConnectRelay/Dispatching.swift index 48010907f..1dee272d9 100644 --- a/Sources/WalletConnectRelay/Dispatching.swift +++ b/Sources/WalletConnectRelay/Dispatching.swift @@ -69,21 +69,26 @@ final class Dispatcher: NSObject, Dispatching { } func protectedSend(_ string: String, completion: @escaping (Error?) -> Void) { + logger.debug("will try to send a socket frame") // Check if the socket is already connected and ready to send if socket.isConnected && networkMonitor.isConnected { + logger.debug("sending a socket frame") send(string, completion: completion) return } + logger.debug("Socket is not connected, will try to connect to send a frame") // Start the connection process if not already connected Task { do { // Await the connection handler to establish the connection try await socketConnectionHandler.handleInternalConnect() + logger.debug("internal connect successful, will try to send a socket frame") // If successful, send the message send(string, completion: completion) } catch { + logger.debug("failed to handle internal connect") // If an error occurs during connection, complete with that error completion(error) } diff --git a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift index 6b17bd42d..b5133bdab 100644 --- a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift +++ b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift @@ -51,19 +51,23 @@ class AutomaticSocketConnectionHandler { func connect() { // Start the connection process + logger.debug("Starting connection process.") isConnecting = true socket.connect() } private func setUpSocketStatusObserving() { + logger.debug("Setting up socket status observing.") socketStatusProvider.socketConnectionStatusPublisher .sink { [unowned self] status in switch status { case .connected: + logger.debug("Socket connected.") isConnecting = false reconnectionAttempts = 0 // Reset reconnection attempts on successful connection stopPeriodicReconnectionTimer() // Stop any ongoing periodic reconnection attempts case .disconnected: + logger.debug("Socket disconnected.") if isConnecting { // Handle reconnection logic handleFailedConnectionAndReconnectIfNeeded() @@ -89,13 +93,18 @@ class AutomaticSocketConnectionHandler { } private func stopPeriodicReconnectionTimer() { + logger.debug("Stopping periodic reconnection timer.") reconnectionTimer?.cancel() reconnectionTimer = nil } private func startPeriodicReconnectionTimerIfNeeded() { - guard reconnectionTimer == nil else {return} + guard reconnectionTimer == nil else { + logger.debug("Reconnection timer is already running.") + return + } + logger.debug("Starting periodic reconnection timer.") reconnectionTimer = DispatchSource.makeTimerSource(queue: concurrentQueue) let initialDelay: DispatchTime = .now() + periodicReconnectionInterval @@ -112,48 +121,59 @@ class AutomaticSocketConnectionHandler { } private func setUpStateObserving() { + logger.debug("Setting up app state observing.") appStateObserver.onWillEnterBackground = { [unowned self] in + logger.debug("App will enter background. Registering background task.") registerBackgroundTask() } appStateObserver.onWillEnterForeground = { [unowned self] in + logger.debug("App will enter foreground. Reconnecting if needed.") reconnectIfNeeded() } } private func setUpNetworkMonitoring() { - networkMonitor.networkConnectionStatusPublisher.sink { [unowned self] networkConnectionStatus in - if networkConnectionStatus == .connected { - reconnectIfNeeded() + logger.debug("Setting up network monitoring.") + networkMonitor.networkConnectionStatusPublisher + .sink { [unowned self] networkConnectionStatus in + if networkConnectionStatus == .connected { + logger.debug("Network connected. Reconnecting if needed.") + reconnectIfNeeded() + } } - } - .store(in: &publishers) + .store(in: &publishers) } private func registerBackgroundTask() { + logger.debug("Registering background task.") backgroundTaskRegistrar.register(name: "Finish Network Tasks") { [unowned self] in endBackgroundTask() } } private func endBackgroundTask() { + logger.debug("Ending background task. Disconnecting socket.") socket.disconnect() } func reconnectIfNeeded() { // Check if client has active subscriptions and only then attempt to reconnect + logger.debug("Checking if reconnection is needed.") if !socket.isConnected && subscriptionsTracker.isSubscribed() { + logger.debug("Socket is not connected, but there are active subscriptions. Reconnecting...") connect() } } - var requestTimeout: TimeInterval = 15 + var requestTimeout: TimeInterval = 15 } // MARK: - SocketConnectionHandler extension AutomaticSocketConnectionHandler: SocketConnectionHandler { func handleInternalConnect() async throws { + logger.debug("Handling internal connection.") let maxAttempts = maxImmediateAttempts var attempts = 0 var isResumed = false // Track if continuation has been resumed @@ -161,6 +181,7 @@ extension AutomaticSocketConnectionHandler: SocketConnectionHandler { // Start the connection process immediately if not already connecting if !isConnecting { + logger.debug("Not already connecting. Starting connection.") connect() // This will set isConnecting = true and attempt to connect } @@ -172,7 +193,10 @@ extension AutomaticSocketConnectionHandler: SocketConnectionHandler { let connection = connectionStatusPublisher.connect() // Ensure connection is canceled when done - defer { connection.cancel() } + defer { + logger.debug("Cancelling connection status publisher.") + connection.cancel() + } // Use a Combine publisher to monitor disconnection and timeout try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in @@ -181,18 +205,20 @@ extension AutomaticSocketConnectionHandler: SocketConnectionHandler { cancellable = connectionStatusPublisher .setFailureType(to: NetworkError.self) // Set failure type to NetworkError .timeout(.seconds(requestTimeout), scheduler: concurrentQueue, customError: { NetworkError.connectionFailed }) - .sink(receiveCompletion: { completion in + .sink(receiveCompletion: { [unowned self] completion in guard !isResumed else { return } // Ensure continuation is only resumed once isResumed = true cancellable?.cancel() // Cancel the subscription to prevent further events // Handle only the failure case, as .finished is not expected to be meaningful here if case .failure(let error) = completion { + logger.debug("Connection failed with error: \(error).") continuation.resume(throwing: error) // Timeout or connection failure } }, receiveValue: { [unowned self] status in guard !isResumed else { return } // Ensure continuation is only resumed once if status == .connected { + logger.debug("Connection succeeded.") isResumed = true cancellable?.cancel() // Cancel the subscription to prevent further events continuation.resume() // Successfully connected @@ -201,6 +227,7 @@ extension AutomaticSocketConnectionHandler: SocketConnectionHandler { logger.debug("Disconnection observed, incrementing attempts to \(attempts)") if attempts >= maxAttempts { + logger.debug("Max attempts reached. Failing with connection error.") isResumed = true cancellable?.cancel() // Cancel the subscription to prevent further events continuation.resume(throwing: NetworkError.connectionFailed) @@ -214,15 +241,21 @@ extension AutomaticSocketConnectionHandler: SocketConnectionHandler { } func handleConnect() throws { + logger.debug("Manual connect requested but forbidden.") throw Errors.manualSocketConnectionForbidden } func handleDisconnect(closeCode: URLSessionWebSocketTask.CloseCode) throws { + logger.debug("Manual disconnect requested but forbidden.") throw Errors.manualSocketDisconnectionForbidden } func handleDisconnection() async { - guard await appStateObserver.currentState == .foreground else { return } + logger.debug("Handling disconnection.") + guard await appStateObserver.currentState == .foreground else { + logger.debug("App is not in foreground. No reconnection will be attempted.") + return + } reconnectIfNeeded() } } From 0712bb24fbf703675e177b8c5767eaa96e3f4820 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Wed, 11 Sep 2024 08:50:54 +0200 Subject: [PATCH 03/14] handle trying to pair stale pairing --- .../Services/Wallet/WalletPairService.swift | 8 ++++++-- .../WalletConnectSign/Engine/Common/ApproveEngine.swift | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Sources/WalletConnectPairing/Services/Wallet/WalletPairService.swift b/Sources/WalletConnectPairing/Services/Wallet/WalletPairService.swift index a025e49e1..2f2f2d0c9 100644 --- a/Sources/WalletConnectPairing/Services/Wallet/WalletPairService.swift +++ b/Sources/WalletConnectPairing/Services/Wallet/WalletPairService.swift @@ -2,6 +2,7 @@ import Foundation actor WalletPairService { enum Errors: Error { + case noPendingRequestsForPairing(topic: String) case networkNotConnected } @@ -70,13 +71,15 @@ extension WalletPairService { guard let pairing = pairingStorage.getPairing(forTopic: topic), pairing.requestReceived else { return false } - + let pendingRequests = history.getPending() .compactMap { record -> RPCRequest? in (record.topic == pairing.topic) ? record.request : nil } - guard !pendingRequests.isEmpty else { return false } + guard !pendingRequests.isEmpty else { + throw Errors.noPendingRequestsForPairing(topic: topic) + } pendingRequests.forEach { request in eventsClient.saveTraceEvent(PairingExecutionTraceEvents.emitSessionProposal) networkingInteractor.handleHistoryRequest(topic: topic, request: request) @@ -103,6 +106,7 @@ extension WalletPairService { extension WalletPairService.Errors: LocalizedError { var errorDescription: String? { switch self { + case .noPendingRequestsForPairing(let topic): return "No pending requests for pairing, topic: \(topic)" case .networkNotConnected: return "Pairing failed. You seem to be offline" } } diff --git a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift index ad2c52b20..a56f514c7 100644 --- a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift @@ -173,7 +173,7 @@ final class ApproveEngine { sessionStore.setSession(session) Task { - removePairing(pairingTopic: pairingTopic) + networkingInteractor.unsubscribe(topic: pairingTopic) } onSessionSettle?(session.publicRepresentation()) eventsClient.saveTraceEvent(SessionApproveExecutionTraceEvents.sessionSettleSuccess) @@ -202,7 +202,7 @@ final class ApproveEngine { if let pairingTopic = rpcHistory.get(recordId: payload.id)?.topic { Task { - removePairing(pairingTopic: pairingTopic) + networkingInteractor.unsubscribe(topic: pairingTopic) } } From fea452b6108ae0d65e4c2facab346a34929ca8bf Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Wed, 11 Sep 2024 09:26:33 +0200 Subject: [PATCH 04/14] update disconnection handling --- .../AutomaticSocketConnectionHandler.swift | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift index b5133bdab..a3992fa00 100644 --- a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift +++ b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift @@ -163,6 +163,8 @@ class AutomaticSocketConnectionHandler { if !socket.isConnected && subscriptionsTracker.isSubscribed() { logger.debug("Socket is not connected, but there are active subscriptions. Reconnecting...") connect() + } else { + logger.debug("Will not attempt to reconnect") } } @@ -252,10 +254,10 @@ extension AutomaticSocketConnectionHandler: SocketConnectionHandler { func handleDisconnection() async { logger.debug("Handling disconnection.") - guard await appStateObserver.currentState == .foreground else { - logger.debug("App is not in foreground. No reconnection will be attempted.") - return - } +// guard await appStateObserver.currentState == .foreground else { +// logger.debug("App is not in foreground. No reconnection will be attempted.") +// return +// } reconnectIfNeeded() } } From c65e4809131249408bfa151a8be7f243eb3215f7 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Thu, 12 Sep 2024 14:32:09 +0200 Subject: [PATCH 05/14] update appkit header --- .../xcshareddata/swiftpm/Package.resolved | 126 ------------------ Sources/ReownAppKit/Core/AppKit.swift | 2 +- 2 files changed, 1 insertion(+), 127 deletions(-) diff --git a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 628dc6270..0e559edd2 100644 --- a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,60 +1,6 @@ { "object": { "pins": [ - { - "package": "Atlantis", - "repositoryURL": "https://github.com/ProxymanApp/atlantis", - "state": { - "branch": null, - "revision": "5145a7041ec71421d09653db87dcc80c81792004", - "version": "1.24.0" - } - }, - { - "package": "BigInt", - "repositoryURL": "https://github.com/attaswift/BigInt.git", - "state": { - "branch": null, - "revision": "793a7fac0bfc318e85994bf6900652e827aef33e", - "version": "5.4.1" - } - }, - { - "package": "CryptoSwift", - "repositoryURL": "https://github.com/krzyzanowskim/CryptoSwift.git", - "state": { - "branch": null, - "revision": "678d442c6f7828def400a70ae15968aef67ef52d", - "version": "1.8.3" - } - }, - { - "package": "HDWalletKit", - "repositoryURL": "https://github.com/WalletConnect/HDWallet", - "state": { - "branch": "develop", - "revision": "748a85b1dfe9a2fa592bd9266c5a926e4e1d3f44", - "version": null - } - }, - { - "package": "Mixpanel", - "repositoryURL": "https://github.com/mixpanel/mixpanel-swift", - "state": { - "branch": "master", - "revision": "61ce9b40817466fb1334db1d7a582fbaf616ab4c", - "version": null - } - }, - { - "package": "PromiseKit", - "repositoryURL": "https://github.com/mxcl/PromiseKit.git", - "state": { - "branch": null, - "revision": "8a98e31a47854d3180882c8068cc4d9381bf382d", - "version": "6.22.1" - } - }, { "package": "QRCode", "repositoryURL": "https://github.com/WalletConnect/QRCode", @@ -64,33 +10,6 @@ "version": "14.3.1" } }, - { - "package": "secp256k1", - "repositoryURL": "https://github.com/Boilertalk/secp256k1.swift.git", - "state": { - "branch": null, - "revision": "cd187c632fb812fd93711a9f7e644adb7e5f97f0", - "version": "0.1.7" - } - }, - { - "package": "SolanaSwift", - "repositoryURL": "https://github.com/flypaper0/solana-swift", - "state": { - "branch": "feature/available-13", - "revision": "a98811518e0a90c2dfc60c30cfd3ec85c33b6790", - "version": null - } - }, - { - "package": "Starscream", - "repositoryURL": "https://github.com/daltoniam/Starscream", - "state": { - "branch": null, - "revision": "a063fda2b8145a231953c20e7a646be254365396", - "version": "3.1.2" - } - }, { "package": "SwiftDocCPlugin", "repositoryURL": "https://github.com/apple/swift-docc-plugin", @@ -136,42 +55,6 @@ "version": "1.1.6" } }, - { - "package": "SwiftMessages", - "repositoryURL": "https://github.com/SwiftKickMobile/SwiftMessages", - "state": { - "branch": null, - "revision": "62e12e138fc3eedf88c7553dd5d98712aa119f40", - "version": "9.0.9" - } - }, - { - "package": "swiftui-async-button", - "repositoryURL": "https://github.com/lorenzofiamingo/swiftui-async-button", - "state": { - "branch": null, - "revision": "9fe9ccddf59c7e4185aa978547fbb9d95236455e", - "version": "1.1.0" - } - }, - { - "package": "Task_retrying", - "repositoryURL": "https://github.com/bigearsenal/task-retrying-swift.git", - "state": { - "branch": null, - "revision": "1249b3524378423c848cef39fb220041e00a08ec", - "version": "1.0.4" - } - }, - { - "package": "TweetNacl", - "repositoryURL": "https://github.com/bitmark-inc/tweetnacl-swiftwrap.git", - "state": { - "branch": null, - "revision": "f8fd111642bf2336b11ef9ea828510693106e954", - "version": "1.1.0" - } - }, { "package": "CoinbaseWalletSDK", "repositoryURL": "https://github.com/WalletConnect/wallet-mobile-sdk", @@ -180,15 +63,6 @@ "revision": "b6dfb7d6b8447c7c5b238a10443a1ac28223f38f", "version": "1.0.0" } - }, - { - "package": "Web3", - "repositoryURL": "https://github.com/WalletConnect/Web3.swift", - "state": { - "branch": null, - "revision": "569255adcfff0b37e4cb8004aea29d0e2d6266df", - "version": "1.0.2" - } } ] }, diff --git a/Sources/ReownAppKit/Core/AppKit.swift b/Sources/ReownAppKit/Core/AppKit.swift index 2d2cece57..b3f16f0c4 100644 --- a/Sources/ReownAppKit/Core/AppKit.swift +++ b/Sources/ReownAppKit/Core/AppKit.swift @@ -64,7 +64,7 @@ public class AppKit { return "swift-\(version)" }() - static let sdkType = "w3m" + static let sdkType = "appkit" let projectId: String var metadata: AppMetadata From b645e6c88730e7c6a36c804986ff047b4c174975 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Sat, 14 Sep 2024 11:49:24 +0200 Subject: [PATCH 06/14] improve logging --- .../xcshareddata/swiftpm/Package.resolved | 126 ++++++++++++++++++ .../AutomaticSocketConnectionHandler.swift | 3 + .../SocketStatusProvider.swift | 15 ++- 3 files changed, 143 insertions(+), 1 deletion(-) diff --git a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 0e559edd2..628dc6270 100644 --- a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,6 +1,60 @@ { "object": { "pins": [ + { + "package": "Atlantis", + "repositoryURL": "https://github.com/ProxymanApp/atlantis", + "state": { + "branch": null, + "revision": "5145a7041ec71421d09653db87dcc80c81792004", + "version": "1.24.0" + } + }, + { + "package": "BigInt", + "repositoryURL": "https://github.com/attaswift/BigInt.git", + "state": { + "branch": null, + "revision": "793a7fac0bfc318e85994bf6900652e827aef33e", + "version": "5.4.1" + } + }, + { + "package": "CryptoSwift", + "repositoryURL": "https://github.com/krzyzanowskim/CryptoSwift.git", + "state": { + "branch": null, + "revision": "678d442c6f7828def400a70ae15968aef67ef52d", + "version": "1.8.3" + } + }, + { + "package": "HDWalletKit", + "repositoryURL": "https://github.com/WalletConnect/HDWallet", + "state": { + "branch": "develop", + "revision": "748a85b1dfe9a2fa592bd9266c5a926e4e1d3f44", + "version": null + } + }, + { + "package": "Mixpanel", + "repositoryURL": "https://github.com/mixpanel/mixpanel-swift", + "state": { + "branch": "master", + "revision": "61ce9b40817466fb1334db1d7a582fbaf616ab4c", + "version": null + } + }, + { + "package": "PromiseKit", + "repositoryURL": "https://github.com/mxcl/PromiseKit.git", + "state": { + "branch": null, + "revision": "8a98e31a47854d3180882c8068cc4d9381bf382d", + "version": "6.22.1" + } + }, { "package": "QRCode", "repositoryURL": "https://github.com/WalletConnect/QRCode", @@ -10,6 +64,33 @@ "version": "14.3.1" } }, + { + "package": "secp256k1", + "repositoryURL": "https://github.com/Boilertalk/secp256k1.swift.git", + "state": { + "branch": null, + "revision": "cd187c632fb812fd93711a9f7e644adb7e5f97f0", + "version": "0.1.7" + } + }, + { + "package": "SolanaSwift", + "repositoryURL": "https://github.com/flypaper0/solana-swift", + "state": { + "branch": "feature/available-13", + "revision": "a98811518e0a90c2dfc60c30cfd3ec85c33b6790", + "version": null + } + }, + { + "package": "Starscream", + "repositoryURL": "https://github.com/daltoniam/Starscream", + "state": { + "branch": null, + "revision": "a063fda2b8145a231953c20e7a646be254365396", + "version": "3.1.2" + } + }, { "package": "SwiftDocCPlugin", "repositoryURL": "https://github.com/apple/swift-docc-plugin", @@ -55,6 +136,42 @@ "version": "1.1.6" } }, + { + "package": "SwiftMessages", + "repositoryURL": "https://github.com/SwiftKickMobile/SwiftMessages", + "state": { + "branch": null, + "revision": "62e12e138fc3eedf88c7553dd5d98712aa119f40", + "version": "9.0.9" + } + }, + { + "package": "swiftui-async-button", + "repositoryURL": "https://github.com/lorenzofiamingo/swiftui-async-button", + "state": { + "branch": null, + "revision": "9fe9ccddf59c7e4185aa978547fbb9d95236455e", + "version": "1.1.0" + } + }, + { + "package": "Task_retrying", + "repositoryURL": "https://github.com/bigearsenal/task-retrying-swift.git", + "state": { + "branch": null, + "revision": "1249b3524378423c848cef39fb220041e00a08ec", + "version": "1.0.4" + } + }, + { + "package": "TweetNacl", + "repositoryURL": "https://github.com/bitmark-inc/tweetnacl-swiftwrap.git", + "state": { + "branch": null, + "revision": "f8fd111642bf2336b11ef9ea828510693106e954", + "version": "1.1.0" + } + }, { "package": "CoinbaseWalletSDK", "repositoryURL": "https://github.com/WalletConnect/wallet-mobile-sdk", @@ -63,6 +180,15 @@ "revision": "b6dfb7d6b8447c7c5b238a10443a1ac28223f38f", "version": "1.0.0" } + }, + { + "package": "Web3", + "repositoryURL": "https://github.com/WalletConnect/Web3.swift", + "state": { + "branch": null, + "revision": "569255adcfff0b37e4cb8004aea29d0e2d6266df", + "version": "1.0.2" + } } ] }, diff --git a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift index a3992fa00..205bf2ada 100644 --- a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift +++ b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift @@ -53,6 +53,7 @@ class AutomaticSocketConnectionHandler { // Start the connection process logger.debug("Starting connection process.") isConnecting = true + logger.debug("socket request: \(socket.request.debugDescription)") socket.connect() } @@ -85,6 +86,7 @@ class AutomaticSocketConnectionHandler { if reconnectionAttempts < maxImmediateAttempts { reconnectionAttempts += 1 logger.debug("Immediate reconnection attempt \(reconnectionAttempts) of \(maxImmediateAttempts)") + logger.debug("socket request: \(socket.request.debugDescription)") socket.connect() } else { logger.debug("Max immediate reconnection attempts reached. Switching to periodic reconnection every \(periodicReconnectionInterval) seconds.") @@ -112,6 +114,7 @@ class AutomaticSocketConnectionHandler { reconnectionTimer?.setEventHandler { [unowned self] in logger.debug("Periodic reconnection attempt...") + logger.debug("socket request: \(socket.request.debugDescription)") socket.connect() // Attempt to reconnect // The socketConnectionStatusPublisher handler will stop the timer and reset states if connection is successful diff --git a/Sources/WalletConnectRelay/SocketStatusProvider.swift b/Sources/WalletConnectRelay/SocketStatusProvider.swift index 1003fe01e..43fc15c72 100644 --- a/Sources/WalletConnectRelay/SocketStatusProvider.swift +++ b/Sources/WalletConnectRelay/SocketStatusProvider.swift @@ -27,7 +27,20 @@ class SocketStatusProvider: SocketStatusProviding { self.socketConnectionStatusPublisherSubject.send(.connected) } socket.onDisconnect = { [unowned self] error in - logger.debug("Socket disconnected with error: \(error?.localizedDescription ?? "Unknown error")") + if let error = error { + logger.debug("Socket disconnected with error: \(error.localizedDescription)") + + let errorMirror = Mirror(reflecting: error) + logger.debug("Error type: \(type(of: error))") + + for child in errorMirror.children { + if let label = child.label { + logger.debug("\(label): \(child.value)") + } + } + } else { + logger.debug("Socket disconnected with unknown error.") + } self.socketConnectionStatusPublisherSubject.send(.disconnected) } } From 03aa25aa1113ec0a96037ff0d07f615ea1f4065f Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Sat, 14 Sep 2024 11:50:29 +0200 Subject: [PATCH 07/14] dapp profiles --- Example/ExampleApp.xcodeproj/project.pbxproj | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index cc49adc14..988d32e57 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -2343,11 +2343,9 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = DApp/DApp.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 184; - DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = W5R8AG9K22; + DEVELOPMENT_TEAM = W5R8AG9K22; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = DApp/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = dApp; @@ -2367,7 +2365,6 @@ PRODUCT_BUNDLE_IDENTIFIER = com.walletconnect.dapp; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore com.walletconnect.dapp"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; From 917aeab5b969a6fa81eb1cee0e7d5b2beb136ce5 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Sat, 14 Sep 2024 17:14:13 +0200 Subject: [PATCH 08/14] update automatic socket connection handler --- .../RelayClientEndToEndTests.swift | 2 +- .../RelayClientFactory.swift | 2 +- .../AutomaticSocketConnectionHandler.swift | 177 ++++++++++++------ .../SocketStatusProvider.swift | 11 +- .../SubscriptionsTracker.swift | 20 +- ...utomaticSocketConnectionHandlerTests.swift | 51 ++++- 6 files changed, 193 insertions(+), 70 deletions(-) diff --git a/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift b/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift index 4eccebd9b..ee395b0ce 100644 --- a/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift +++ b/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift @@ -53,7 +53,7 @@ final class RelayClientEndToEndTests: XCTestCase { ) let socketStatusProvider = SocketStatusProvider(socket: socket, logger: logger) - let socketConnectionHandler = AutomaticSocketConnectionHandler(socket: socket, subscriptionsTracker: SubscriptionsTracker(), logger: logger, socketStatusProvider: socketStatusProvider) + let socketConnectionHandler = AutomaticSocketConnectionHandler(socket: socket, subscriptionsTracker: SubscriptionsTracker(logger: logger), logger: logger, socketStatusProvider: socketStatusProvider) let dispatcher = Dispatcher( socketFactory: webSocketFactory, relayUrlFactory: urlFactory, diff --git a/Sources/WalletConnectRelay/RelayClientFactory.swift b/Sources/WalletConnectRelay/RelayClientFactory.swift index 0091d496c..cd6906f46 100644 --- a/Sources/WalletConnectRelay/RelayClientFactory.swift +++ b/Sources/WalletConnectRelay/RelayClientFactory.swift @@ -61,7 +61,7 @@ public struct RelayClientFactory { if let bundleId = Bundle.main.bundleIdentifier { socket.request.addValue(bundleId, forHTTPHeaderField: "Origin") } - let subscriptionsTracker = SubscriptionsTracker() + let subscriptionsTracker = SubscriptionsTracker(logger: logger) let socketStatusProvider = SocketStatusProvider(socket: socket, logger: logger) var socketConnectionHandler: SocketConnectionHandler! diff --git a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift index 205bf2ada..2f02a9ff2 100644 --- a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift +++ b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift @@ -10,6 +10,8 @@ class AutomaticSocketConnectionHandler { case manualSocketConnectionForbidden, manualSocketDisconnectionForbidden } + // MARK: - Dependencies + private let socket: WebSocketConnecting private let appStateObserver: AppStateObserving private let networkMonitor: NetworkMonitoring @@ -18,15 +20,25 @@ class AutomaticSocketConnectionHandler { private let subscriptionsTracker: SubscriptionsTracking private let socketStatusProvider: SocketStatusProviding - private var publishers = Set() - private let concurrentQueue = DispatchQueue(label: "com.walletconnect.sdk.automatic_socket_connection", qos: .utility, attributes: .concurrent) + // MARK: - Configuration - var reconnectionAttempts = 0 + var requestTimeout: TimeInterval = 15 let maxImmediateAttempts = 3 var periodicReconnectionInterval: TimeInterval = 5.0 + + // MARK: - State Variables (Accessed on syncQueue) + + var reconnectionAttempts = 0 var reconnectionTimer: DispatchSourceTimer? var isConnecting = false + // MARK: - Queues + + private let syncQueue = DispatchQueue(label: "com.walletconnect.sdk.automatic_socket_connection.sync", qos: .utility) + private var publishers = Set() + + // MARK: - Initialization + init( socket: WebSocketConnecting, networkMonitor: NetworkMonitoring = NetworkMonitor(), @@ -49,33 +61,45 @@ class AutomaticSocketConnectionHandler { setUpSocketStatusObserving() } + // MARK: - Connection Handling + func connect() { - // Start the connection process - logger.debug("Starting connection process.") - isConnecting = true - logger.debug("socket request: \(socket.request.debugDescription)") - socket.connect() + syncQueue.async { [unowned self] in + if isConnecting { + logger.debug("Already connecting. Ignoring connect request.") + return + } + logger.debug("Starting connection process.") + isConnecting = true + logger.debug("Socket request: \(socket.request.debugDescription)") + socket.connect() + } } + // MARK: - Socket Status Observing + private func setUpSocketStatusObserving() { logger.debug("Setting up socket status observing.") socketStatusProvider.socketConnectionStatusPublisher .sink { [unowned self] status in - switch status { - case .connected: - logger.debug("Socket connected.") - isConnecting = false - reconnectionAttempts = 0 // Reset reconnection attempts on successful connection - stopPeriodicReconnectionTimer() // Stop any ongoing periodic reconnection attempts - case .disconnected: - logger.debug("Socket disconnected.") - if isConnecting { - // Handle reconnection logic - handleFailedConnectionAndReconnectIfNeeded() - } else { - Task(priority: .high) { - await handleDisconnection() + syncQueue.async { [unowned self] in + switch status { + case .connected: + logger.debug("Socket connected.") + isConnecting = false + reconnectionAttempts = 0 // Reset reconnection attempts on successful connection + stopPeriodicReconnectionTimer() + case .disconnected: + logger.debug("Socket disconnected.") + if isConnecting { + logger.debug("Was in connecting state when disconnected.") + handleFailedConnectionAndReconnectIfNeeded() + } else { + Task(priority: .high) { + await handleDisconnection() + } } + isConnecting = false // Ensure isConnecting is reset } } } @@ -83,46 +107,57 @@ class AutomaticSocketConnectionHandler { } private func handleFailedConnectionAndReconnectIfNeeded() { + // This method is called within syncQueue + isConnecting = false if reconnectionAttempts < maxImmediateAttempts { reconnectionAttempts += 1 logger.debug("Immediate reconnection attempt \(reconnectionAttempts) of \(maxImmediateAttempts)") - logger.debug("socket request: \(socket.request.debugDescription)") - socket.connect() + logger.debug("Socket request: \(socket.request.debugDescription)") + // Attempt to reconnect + connect() // connect() will check isConnecting } else { logger.debug("Max immediate reconnection attempts reached. Switching to periodic reconnection every \(periodicReconnectionInterval) seconds.") startPeriodicReconnectionTimerIfNeeded() } } - private func stopPeriodicReconnectionTimer() { - logger.debug("Stopping periodic reconnection timer.") - reconnectionTimer?.cancel() - reconnectionTimer = nil - } - private func startPeriodicReconnectionTimerIfNeeded() { + // This method is called within syncQueue guard reconnectionTimer == nil else { logger.debug("Reconnection timer is already running.") return } logger.debug("Starting periodic reconnection timer.") - reconnectionTimer = DispatchSource.makeTimerSource(queue: concurrentQueue) + reconnectionTimer = DispatchSource.makeTimerSource(queue: syncQueue) let initialDelay: DispatchTime = .now() + periodicReconnectionInterval reconnectionTimer?.schedule(deadline: initialDelay, repeating: periodicReconnectionInterval) reconnectionTimer?.setEventHandler { [unowned self] in logger.debug("Periodic reconnection attempt...") - logger.debug("socket request: \(socket.request.debugDescription)") + logger.debug("Socket request: \(socket.request.debugDescription)") + if isConnecting { + logger.debug("Already connecting. Skipping periodic reconnection attempt.") + return + } + isConnecting = true socket.connect() // Attempt to reconnect - // The socketConnectionStatusPublisher handler will stop the timer and reset states if connection is successful } reconnectionTimer?.resume() } + private func stopPeriodicReconnectionTimer() { + // This method is called within syncQueue + logger.debug("Stopping periodic reconnection timer.") + reconnectionTimer?.cancel() + reconnectionTimer = nil + } + + // MARK: - App State Observing + private func setUpStateObserving() { logger.debug("Setting up app state observing.") appStateObserver.onWillEnterBackground = { [unowned self] in @@ -132,10 +167,12 @@ class AutomaticSocketConnectionHandler { appStateObserver.onWillEnterForeground = { [unowned self] in logger.debug("App will enter foreground. Reconnecting if needed.") - reconnectIfNeeded() + reconnectIfNeeded(willEnterForeground: true) } } + // MARK: - Network Monitoring + private func setUpNetworkMonitoring() { logger.debug("Setting up network monitoring.") networkMonitor.networkConnectionStatusPublisher @@ -148,6 +185,8 @@ class AutomaticSocketConnectionHandler { .store(in: &publishers) } + // MARK: - Background Task Handling + private func registerBackgroundTask() { logger.debug("Registering background task.") backgroundTaskRegistrar.register(name: "Finish Network Tasks") { [unowned self] in @@ -160,18 +199,35 @@ class AutomaticSocketConnectionHandler { socket.disconnect() } - func reconnectIfNeeded() { - // Check if client has active subscriptions and only then attempt to reconnect - logger.debug("Checking if reconnection is needed.") - if !socket.isConnected && subscriptionsTracker.isSubscribed() { - logger.debug("Socket is not connected, but there are active subscriptions. Reconnecting...") - connect() - } else { - logger.debug("Will not attempt to reconnect") + // MARK: - Reconnection Logic + + func reconnectIfNeeded(willEnterForeground: Bool = false) { + Task { [unowned self] in + let appState = await appStateObserver.currentState + logger.debug("App state: \(appState)") + + if !willEnterForeground { + guard appState == .foreground else { + logger.debug("App is not in the foreground. Reconnection will not be attempted.") + return + } + } else { + logger.debug("Bypassing app state check due to willEnterForeground = true") + } + + syncQueue.async { [unowned self] in + logger.debug("Checking if reconnection is needed: connected: \(socket.isConnected), isSubscribed: \(subscriptionsTracker.isSubscribed())") + + if !socket.isConnected { + logger.debug("Socket is not connected, Reconnecting...") + connect() + } else { + logger.debug("Will not attempt to reconnect") + } + } } } - var requestTimeout: TimeInterval = 15 } // MARK: - SocketConnectionHandler @@ -185,11 +241,20 @@ extension AutomaticSocketConnectionHandler: SocketConnectionHandler { let requestTimeout = self.requestTimeout // Timeout set at the class level // Start the connection process immediately if not already connecting - if !isConnecting { - logger.debug("Not already connecting. Starting connection.") - connect() // This will set isConnecting = true and attempt to connect + syncQueue.async { [unowned self] in + if !isConnecting { + logger.debug("Not already connecting. Will start connection.") + isConnecting = true + + logger.debug("Starting connection process.") + logger.debug("Socket request: \(socket.request.debugDescription)") + + // Start the connection + socket.connect() + } else { + logger.debug("Already connecting. Will not start new connection.") + } } - // Use Combine publisher to monitor connection status let connectionStatusPublisher = socketStatusProvider.socketConnectionStatusPublisher .share() @@ -209,15 +274,18 @@ extension AutomaticSocketConnectionHandler: SocketConnectionHandler { cancellable = connectionStatusPublisher .setFailureType(to: NetworkError.self) // Set failure type to NetworkError - .timeout(.seconds(requestTimeout), scheduler: concurrentQueue, customError: { NetworkError.connectionFailed }) + .timeout(.seconds(requestTimeout), scheduler: DispatchQueue.global(), customError: { NetworkError.connectionFailed }) .sink(receiveCompletion: { [unowned self] completion in guard !isResumed else { return } // Ensure continuation is only resumed once isResumed = true cancellable?.cancel() // Cancel the subscription to prevent further events - // Handle only the failure case, as .finished is not expected to be meaningful here if case .failure(let error) = completion { logger.debug("Connection failed with error: \(error).") + syncQueue.async { [unowned self] in + isConnecting = false + handleFailedConnectionAndReconnectIfNeeded() // Trigger reconnection + } continuation.resume(throwing: error) // Timeout or connection failure } }, receiveValue: { [unowned self] status in @@ -226,6 +294,9 @@ extension AutomaticSocketConnectionHandler: SocketConnectionHandler { logger.debug("Connection succeeded.") isResumed = true cancellable?.cancel() // Cancel the subscription to prevent further events + syncQueue.async { [unowned self] in + isConnecting = false + } continuation.resume() // Successfully connected } else if status == .disconnected { attempts += 1 @@ -235,6 +306,10 @@ extension AutomaticSocketConnectionHandler: SocketConnectionHandler { logger.debug("Max attempts reached. Failing with connection error.") isResumed = true cancellable?.cancel() // Cancel the subscription to prevent further events + syncQueue.async { [unowned self] in + isConnecting = false + handleFailedConnectionAndReconnectIfNeeded() // Trigger reconnection + } continuation.resume(throwing: NetworkError.connectionFailed) } } @@ -257,10 +332,6 @@ extension AutomaticSocketConnectionHandler: SocketConnectionHandler { func handleDisconnection() async { logger.debug("Handling disconnection.") -// guard await appStateObserver.currentState == .foreground else { -// logger.debug("App is not in foreground. No reconnection will be attempted.") -// return -// } reconnectIfNeeded() } } diff --git a/Sources/WalletConnectRelay/SocketStatusProvider.swift b/Sources/WalletConnectRelay/SocketStatusProvider.swift index 43fc15c72..b72986103 100644 --- a/Sources/WalletConnectRelay/SocketStatusProvider.swift +++ b/Sources/WalletConnectRelay/SocketStatusProvider.swift @@ -33,11 +33,12 @@ class SocketStatusProvider: SocketStatusProviding { let errorMirror = Mirror(reflecting: error) logger.debug("Error type: \(type(of: error))") - for child in errorMirror.children { - if let label = child.label { - logger.debug("\(label): \(child.value)") - } - } + let errorDetails = errorMirror.children.compactMap { child -> String? in + guard let label = child.label else { return nil } + return "\(label): \(child.value)" + }.joined(separator: ", ") + + logger.debug("Error details: \(errorDetails)") } else { logger.debug("Socket disconnected with unknown error.") } diff --git a/Sources/WalletConnectRelay/SubscriptionsTracker.swift b/Sources/WalletConnectRelay/SubscriptionsTracker.swift index 2684de202..4cfd08518 100644 --- a/Sources/WalletConnectRelay/SubscriptionsTracker.swift +++ b/Sources/WalletConnectRelay/SubscriptionsTracker.swift @@ -8,47 +8,62 @@ protocol SubscriptionsTracking { func getTopics() -> [String] } + public final class SubscriptionsTracker: SubscriptionsTracking { private var subscriptions: [String: String] = [:] private let concurrentQueue = DispatchQueue(label: "com.walletconnect.sdk.subscriptions_tracker", attributes: .concurrent) + private let logger: ConsoleLogging + + init(logger: ConsoleLogging) { + self.logger = logger + } func setSubscription(for topic: String, id: String) { + logger.debug("Setting subscription for topic: \(topic) with id: \(id)") concurrentQueue.async(flags: .barrier) { [unowned self] in self.subscriptions[topic] = id + self.logger.debug("Subscription set: \(self.subscriptions)") } } func getSubscription(for topic: String) -> String? { + logger.debug("Getting subscription for topic: \(topic)") var result: String? concurrentQueue.sync { [unowned self] in result = subscriptions[topic] + self.logger.debug("Retrieved subscription: \(String(describing: result)) for topic: \(topic)") } return result } func removeSubscription(for topic: String) { + logger.debug("Removing subscription for topic: \(topic)") concurrentQueue.async(flags: .barrier) { [unowned self] in - subscriptions[topic] = nil + self.subscriptions[topic] = nil + self.logger.debug("Subscription removed for topic: \(topic). Current subscriptions: \(self.subscriptions)") } } func isSubscribed() -> Bool { + logger.debug("Checking if there are any active subscriptions") var result = false concurrentQueue.sync { [unowned self] in result = !subscriptions.isEmpty + self.logger.debug("Is subscribed: \(result)") } return result } func getTopics() -> [String] { + logger.debug("Getting all subscription topics") var topics: [String] = [] concurrentQueue.sync { [unowned self] in topics = Array(subscriptions.keys) + self.logger.debug("Retrieved topics: \(topics)") } return topics } } - #if DEBUG final class SubscriptionsTrackerMock: SubscriptionsTracking { var isSubscribedReturnValue: Bool = false @@ -80,3 +95,4 @@ final class SubscriptionsTrackerMock: SubscriptionsTracking { } } #endif + diff --git a/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift b/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift index 54e5fd0ae..403c801a0 100644 --- a/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift +++ b/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift @@ -56,7 +56,17 @@ final class AutomaticSocketConnectionHandlerTests: XCTestCase { func testReconnectsOnEnterForeground() { subscriptionsTracker.isSubscribedReturnValue = true // Simulate that there are active subscriptions webSocketSession.disconnect() + + let expectation = XCTestExpectation(description: "WebSocket should connect on entering foreground") + + // Modify the webSocketSession mock to call this closure when connect() is called + webSocketSession.onConnect = { + expectation.fulfill() + } + appStateObserver.onWillEnterForeground?() + + wait(for: [expectation], timeout: 1.0) XCTAssertTrue(webSocketSession.isConnected) } @@ -86,8 +96,18 @@ final class AutomaticSocketConnectionHandlerTests: XCTestCase { webSocketSession.connect() appStateObserver.currentState = .foreground XCTAssertTrue(webSocketSession.isConnected) + + let expectation = XCTestExpectation(description: "WebSocket should reconnect on disconnection in foreground") + + // Modify the webSocketSession mock to call this closure when connect() is called + webSocketSession.onConnect = { + expectation.fulfill() + } + webSocketSession.disconnect() await sut.handleDisconnection() + + wait(for: [expectation], timeout: 1.0) XCTAssertTrue(webSocketSession.isConnected) } @@ -129,10 +149,17 @@ final class AutomaticSocketConnectionHandlerTests: XCTestCase { webSocketSession.disconnect() XCTAssertFalse(webSocketSession.isConnected) + let expectation = XCTestExpectation(description: "WebSocket should connect when reconnectIfNeeded is called") + + // Modify the webSocketSession mock to call this closure when connect() is called + webSocketSession.onConnect = { + expectation.fulfill() + } + // Trigger reconnect logic sut.reconnectIfNeeded() - // Expect the socket to be connected since there are subscriptions + wait(for: [expectation], timeout: 1.0) XCTAssertTrue(webSocketSession.isConnected) } @@ -159,10 +186,17 @@ final class AutomaticSocketConnectionHandlerTests: XCTestCase { webSocketSession.disconnect() XCTAssertFalse(webSocketSession.isConnected) + let expectation = XCTestExpectation(description: "WebSocket should connect when network becomes connected") + + // Modify the webSocketSession mock to call this closure when connect() is called + webSocketSession.onConnect = { + expectation.fulfill() + } + // Simulate network connection becomes satisfied networkMonitor.networkConnectionStatusPublisherSubject.send(.connected) - // Expect the socket to reconnect since there are subscriptions + wait(for: [expectation], timeout: 1.0) XCTAssertTrue(webSocketSession.isConnected) } @@ -217,7 +251,7 @@ final class AutomaticSocketConnectionHandlerTests: XCTestCase { func testPeriodicReconnectionAttempts() { subscriptionsTracker.isSubscribedReturnValue = true // Simulate that there are active subscriptions webSocketSession.disconnect() - sut.periodicReconnectionInterval = 0.0001 + sut.periodicReconnectionInterval = 0.1 // Set a shorter interval for the test sut.connect() // Start connection process // Simulate immediate reconnection attempts to switch to periodic @@ -228,17 +262,18 @@ final class AutomaticSocketConnectionHandlerTests: XCTestCase { // Ensure we have switched to periodic reconnection XCTAssertNotNil(sut.reconnectionTimer) - // Simulate the periodic timer firing without waiting for real time let expectation = XCTestExpectation(description: "Periodic reconnection attempt made") - sut.reconnectionTimer?.setEventHandler { - self.socketStatusProviderMock.simulateConnectionStatus(.connected) + + // Modify the webSocketSession mock to call this closure when connect() is called + webSocketSession.onConnect = { expectation.fulfill() } - wait(for: [expectation], timeout: 1) + // Wait for the periodic reconnection attempt + wait(for: [expectation], timeout: 1.0) // Check that the periodic reconnection attempt was made - XCTAssertTrue(webSocketSession.isConnected) // Assume that connection would have been attempted + XCTAssertTrue(webSocketSession.isConnected) } func testHandleInternalConnectThrowsAfterThreeDisconnections() async { From c22c2a42634cb3d6f42f4a3a0716893b1c7171b5 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Sun, 15 Sep 2024 14:03:00 +0200 Subject: [PATCH 09/14] check on issubscribed --- .../AutomaticSocketConnectionHandler.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift index 2f02a9ff2..e867baa2b 100644 --- a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift +++ b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift @@ -218,7 +218,7 @@ class AutomaticSocketConnectionHandler { syncQueue.async { [unowned self] in logger.debug("Checking if reconnection is needed: connected: \(socket.isConnected), isSubscribed: \(subscriptionsTracker.isSubscribed())") - if !socket.isConnected { + if !socket.isConnected && subscriptionsTracker.isSubscribed() { logger.debug("Socket is not connected, Reconnecting...") connect() } else { From 17813fb9bafbb79243c33096787fa79770e2b267 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Mon, 16 Sep 2024 11:26:01 +0200 Subject: [PATCH 10/14] fix tests --- .../AutomaticSocketConnectionHandler.swift | 28 +-- ...utomaticSocketConnectionHandlerTests.swift | 183 +++++++++--------- 2 files changed, 108 insertions(+), 103 deletions(-) diff --git a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift index e867baa2b..998acf8cc 100644 --- a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift +++ b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift @@ -240,21 +240,29 @@ extension AutomaticSocketConnectionHandler: SocketConnectionHandler { var isResumed = false // Track if continuation has been resumed let requestTimeout = self.requestTimeout // Timeout set at the class level + var shouldStartConnect = false + // Start the connection process immediately if not already connecting - syncQueue.async { [unowned self] in + syncQueue.sync { [unowned self] in if !isConnecting { logger.debug("Not already connecting. Will start connection.") isConnecting = true - - logger.debug("Starting connection process.") - logger.debug("Socket request: \(socket.request.debugDescription)") - - // Start the connection - socket.connect() + shouldStartConnect = true } else { logger.debug("Already connecting. Will not start new connection.") } } + + if !shouldStartConnect { + // Exit the function early since a connection is already in progress + return + } + + // Proceed to start the connection + logger.debug("Starting connection process.") + logger.debug("Socket request: \(socket.request.debugDescription)") + socket.connect() + // Use Combine publisher to monitor connection status let connectionStatusPublisher = socketStatusProvider.socketConnectionStatusPublisher .share() @@ -273,7 +281,7 @@ extension AutomaticSocketConnectionHandler: SocketConnectionHandler { var cancellable: AnyCancellable? cancellable = connectionStatusPublisher - .setFailureType(to: NetworkError.self) // Set failure type to NetworkError + .setFailureType(to: NetworkError.self) .timeout(.seconds(requestTimeout), scheduler: DispatchQueue.global(), customError: { NetworkError.connectionFailed }) .sink(receiveCompletion: { [unowned self] completion in guard !isResumed else { return } // Ensure continuation is only resumed once @@ -282,10 +290,6 @@ extension AutomaticSocketConnectionHandler: SocketConnectionHandler { if case .failure(let error) = completion { logger.debug("Connection failed with error: \(error).") - syncQueue.async { [unowned self] in - isConnecting = false - handleFailedConnectionAndReconnectIfNeeded() // Trigger reconnection - } continuation.resume(throwing: error) // Timeout or connection failure } }, receiveValue: { [unowned self] status in diff --git a/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift b/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift index 403c801a0..62e04a90f 100644 --- a/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift +++ b/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift @@ -38,10 +38,23 @@ final class AutomaticSocketConnectionHandlerTests: XCTestCase { } func testConnectsOnConnectionSatisfied() { + // Ensure the socket is disconnected webSocketSession.disconnect() - subscriptionsTracker.isSubscribedReturnValue = true // Simulate that there are active subscriptions + subscriptionsTracker.isSubscribedReturnValue = true // Simulate active subscriptions XCTAssertFalse(webSocketSession.isConnected) + + let expectation = XCTestExpectation(description: "WebSocket should connect when network becomes connected") + + // Assign onConnect closure to fulfill the expectation and set isConnected + webSocketSession.onConnect = { + self.webSocketSession.isConnected = true + expectation.fulfill() + } + + // Simulate network connection becoming connected networkMonitor.networkConnectionStatusPublisherSubject.send(.connected) + + wait(for: [expectation], timeout: 0.1) XCTAssertTrue(webSocketSession.isConnected) } @@ -200,83 +213,75 @@ final class AutomaticSocketConnectionHandlerTests: XCTestCase { XCTAssertTrue(webSocketSession.isConnected) } - func testReconnectsOnEnterForegroundWhenSubscribed() { - // Simulate that there are active subscriptions - subscriptionsTracker.isSubscribedReturnValue = true - - // Ensure socket is disconnected initially + func testReconnectsOnEnterForegroundWhenSubscribed() async { + subscriptionsTracker.isSubscribedReturnValue = true // Simulate that there are active subscriptions webSocketSession.disconnect() XCTAssertFalse(webSocketSession.isConnected) + let expectation = XCTestExpectation(description: "WebSocket should reconnect when entering foreground and subscriptions exist") + + // Set up the mock to fulfill expectation when connect is called + webSocketSession.onConnect = { + expectation.fulfill() + } + // Simulate entering foreground appStateObserver.onWillEnterForeground?() - // Expect the socket to reconnect since there are subscriptions + await fulfillment(of: [expectation], timeout: 0.01) + XCTAssertTrue(webSocketSession.isConnected) } - func testSwitchesToPeriodicReconnectionAfterMaxImmediateAttempts() { + func testSwitchesToPeriodicReconnectionAfterMaxImmediateAttempts() async { + subscriptionsTracker.isSubscribedReturnValue = true // Ensure subscriptions exist to allow reconnection sut.connect() // Start connection process // Simulate immediate reconnection attempts - for _ in 0...sut.maxImmediateAttempts { + for _ in 0.. Date: Mon, 16 Sep 2024 11:58:51 +0200 Subject: [PATCH 11/14] savepoint --- .../Sign/SignClientTests.swift | 6 ++++ .../AutomaticSocketConnectionHandler.swift | 28 +++++++++++-------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/Example/IntegrationTests/Sign/SignClientTests.swift b/Example/IntegrationTests/Sign/SignClientTests.swift index 4575da2db..3274ea31c 100644 --- a/Example/IntegrationTests/Sign/SignClientTests.swift +++ b/Example/IntegrationTests/Sign/SignClientTests.swift @@ -84,8 +84,14 @@ final class SignClientTests: XCTestCase { } override func tearDown() { + // Synchronously wait for 0.1 seconds + Thread.sleep(forTimeInterval: 0.1) + + // Now set properties to nil dapp = nil wallet = nil + + super.tearDown() // Ensure superclass tearDown is called } func testSessionPropose() async throws { diff --git a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift index 998acf8cc..15a90d81b 100644 --- a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift +++ b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift @@ -81,22 +81,25 @@ class AutomaticSocketConnectionHandler { private func setUpSocketStatusObserving() { logger.debug("Setting up socket status observing.") socketStatusProvider.socketConnectionStatusPublisher - .sink { [unowned self] status in - syncQueue.async { [unowned self] in + .sink { [weak self] status in + // self needs to be weak to avoid crashes in tests + guard let self = self else {return} + self.syncQueue.async { [weak self] in + guard let self = self else {return} switch status { case .connected: - logger.debug("Socket connected.") - isConnecting = false - reconnectionAttempts = 0 // Reset reconnection attempts on successful connection - stopPeriodicReconnectionTimer() + self.logger.debug("Socket connected.") + self.isConnecting = false + self.reconnectionAttempts = 0 // Reset reconnection attempts on successful connection + self.stopPeriodicReconnectionTimer() case .disconnected: - logger.debug("Socket disconnected.") - if isConnecting { - logger.debug("Was in connecting state when disconnected.") - handleFailedConnectionAndReconnectIfNeeded() + self.logger.debug("Socket disconnected.") + if self.isConnecting { + self.logger.debug("Was in connecting state when disconnected.") + self.handleFailedConnectionAndReconnectIfNeeded() } else { Task(priority: .high) { - await handleDisconnection() + await self.handleDisconnection() } } isConnecting = false // Ensure isConnecting is reset @@ -215,7 +218,8 @@ class AutomaticSocketConnectionHandler { logger.debug("Bypassing app state check due to willEnterForeground = true") } - syncQueue.async { [unowned self] in + syncQueue.async { [weak self] in + guard let self = self else {return} logger.debug("Checking if reconnection is needed: connected: \(socket.isConnected), isSubscribed: \(subscriptionsTracker.isSubscribed())") if !socket.isConnected && subscriptionsTracker.isSubscribed() { From 571b2cdaa53f9f72c37dc4588c5e32568f9d858b Mon Sep 17 00:00:00 2001 From: llbartekll Date: Mon, 16 Sep 2024 14:56:41 +0200 Subject: [PATCH 12/14] update teardown --- Tests/RelayerTests/RelayClientTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/RelayerTests/RelayClientTests.swift b/Tests/RelayerTests/RelayClientTests.swift index 884c8047f..37f9317f3 100644 --- a/Tests/RelayerTests/RelayClientTests.swift +++ b/Tests/RelayerTests/RelayClientTests.swift @@ -22,6 +22,7 @@ final class RelayClientTests: XCTestCase { } override func tearDown() { + Thread.sleep(forTimeInterval: 0.2) sut = nil dispatcher = nil } From d9fd69cfa9649abd1d3c0ce105ab8de2a8362a7d Mon Sep 17 00:00:00 2001 From: llbartekll Date: Mon, 16 Sep 2024 15:11:50 +0200 Subject: [PATCH 13/14] package rename --- Package.swift | 2 +- Sources/WalletConnectRelay/BundleFinder.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index 47ca841d9..2be0ed0e8 100644 --- a/Package.swift +++ b/Package.swift @@ -3,7 +3,7 @@ import PackageDescription let package = Package( - name: "WalletConnect", + name: "reown", platforms: [ .iOS(.v13), .macOS(.v11), diff --git a/Sources/WalletConnectRelay/BundleFinder.swift b/Sources/WalletConnectRelay/BundleFinder.swift index b237dbb00..450b17e2b 100644 --- a/Sources/WalletConnectRelay/BundleFinder.swift +++ b/Sources/WalletConnectRelay/BundleFinder.swift @@ -5,7 +5,7 @@ private class BundleFinder {} extension Foundation.Bundle { /// Returns the resource bundle associated with the current Swift module. static var resourceBundle: Bundle = { - let bundleName = "WalletConnect_WalletConnectRelay" + let bundleName = "reown_WalletConnectRelay" let candidates = [ // Bundle should be present here when the package is linked into an App. From f649d63ba6504650071cfa9f3c00fbdcf93e7f3d Mon Sep 17 00:00:00 2001 From: llbartekll Date: Mon, 16 Sep 2024 15:31:44 +0200 Subject: [PATCH 14/14] update tests --- .../RelayClientEndToEndTests.swift | 5 ++++ ...utomaticSocketConnectionHandlerTests.swift | 26 +------------------ Tests/RelayerTests/RelayClientTests.swift | 1 + 3 files changed, 7 insertions(+), 25 deletions(-) diff --git a/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift b/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift index ee395b0ce..bbab896af 100644 --- a/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift +++ b/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift @@ -80,6 +80,11 @@ final class RelayClientEndToEndTests: XCTestCase { return relayClient } + override func tearDown() { + Thread.sleep(forTimeInterval: 0.3) + super.tearDown() + } + func testSubscribe() { let relayClient = makeRelayClient(prefix: "") diff --git a/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift b/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift index 62e04a90f..4f33e007a 100644 --- a/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift +++ b/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift @@ -108,7 +108,6 @@ final class AutomaticSocketConnectionHandlerTests: XCTestCase { subscriptionsTracker.isSubscribedReturnValue = true // Simulate that there are active subscriptions webSocketSession.connect() appStateObserver.currentState = .foreground - XCTAssertTrue(webSocketSession.isConnected) let expectation = XCTestExpectation(description: "WebSocket should reconnect on disconnection in foreground") @@ -159,7 +158,6 @@ final class AutomaticSocketConnectionHandlerTests: XCTestCase { subscriptionsTracker.isSubscribedReturnValue = true // Ensure socket is disconnected initially - webSocketSession.disconnect() XCTAssertFalse(webSocketSession.isConnected) let expectation = XCTestExpectation(description: "WebSocket should connect when reconnectIfNeeded is called") @@ -191,28 +189,6 @@ final class AutomaticSocketConnectionHandlerTests: XCTestCase { XCTAssertFalse(webSocketSession.isConnected) } - func testReconnectsOnConnectionSatisfiedWhenSubscribed() { - // Simulate that there are active subscriptions - subscriptionsTracker.isSubscribedReturnValue = true - - // Ensure socket is disconnected initially - webSocketSession.disconnect() - XCTAssertFalse(webSocketSession.isConnected) - - let expectation = XCTestExpectation(description: "WebSocket should connect when network becomes connected") - - // Modify the webSocketSession mock to call this closure when connect() is called - webSocketSession.onConnect = { - expectation.fulfill() - } - - // Simulate network connection becomes satisfied - networkMonitor.networkConnectionStatusPublisherSubject.send(.connected) - - wait(for: [expectation], timeout: 1.0) - XCTAssertTrue(webSocketSession.isConnected) - } - func testReconnectsOnEnterForegroundWhenSubscribed() async { subscriptionsTracker.isSubscribedReturnValue = true // Simulate that there are active subscriptions webSocketSession.disconnect() @@ -295,7 +271,7 @@ final class AutomaticSocketConnectionHandlerTests: XCTestCase { } // Allow handleInternalConnect() to start observing - try await Task.sleep(nanoseconds: 10_000_000) // Wait 0.1 seconds + try await Task.sleep(nanoseconds: 100_000_000) // Wait 0.1 seconds // Simulate three disconnections for _ in 0..