diff --git a/Example/IntegrationTests/Push/NotifyTests.swift b/Example/IntegrationTests/Push/NotifyTests.swift index 97edfdd0a..9c9fdc96d 100644 --- a/Example/IntegrationTests/Push/NotifyTests.swift +++ b/Example/IntegrationTests/Push/NotifyTests.swift @@ -136,13 +136,13 @@ final class NotifyTests: XCTestCase { let expectation = expectation(description: "expects client B to receive subscription after both clients are registered and client A creates one") expectation.assertForOverFulfill = false + var subscription: NotifySubscription! + let clientB = makeWalletClient(prefix: "👐🏼 Wallet B: ") clientB.subscriptionsPublisher.sink { subscriptions in - guard let subscription = subscriptions.first else { return } - Task(priority: .high) { - try await clientB.deleteSubscription(topic: subscription.topic) - expectation.fulfill() - } + guard let newSubscription = subscriptions.first else { return } + subscription = newSubscription + expectation.fulfill() }.store(in: &publishers) try! await walletNotifyClientA.register(account: account, domain: gmDappDomain, onSign: sign) @@ -150,6 +150,8 @@ final class NotifyTests: XCTestCase { try! await walletNotifyClientA.subscribe(appDomain: gmDappDomain, account: account) wait(for: [expectation], timeout: InputConfig.defaultTimeout) + + try await clientB.deleteSubscription(topic: subscription.topic) } func testWalletCreatesAndUpdatesSubscription() async { diff --git a/Example/PNDecryptionService/Info.plist b/Example/PNDecryptionService/Info.plist index 57421ebf9..cb9e830fd 100644 --- a/Example/PNDecryptionService/Info.plist +++ b/Example/PNDecryptionService/Info.plist @@ -1,13 +1,20 @@ - - NSExtension - - NSExtensionPointIdentifier - com.apple.usernotifications.service - NSExtensionPrincipalClass - $(PRODUCT_MODULE_NAME).NotificationService - - + + NSExtension + + NSExtensionAttributes + + IntentsSupported + + INSendMessageIntent + + + NSExtensionPointIdentifier + com.apple.usernotifications.service + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).NotificationService + + diff --git a/Example/PNDecryptionService/NotificationService.swift b/Example/PNDecryptionService/NotificationService.swift index 50023dd13..70232ec62 100644 --- a/Example/PNDecryptionService/NotificationService.swift +++ b/Example/PNDecryptionService/NotificationService.swift @@ -1,42 +1,115 @@ import UserNotifications import WalletConnectNotify -import os +import Intents class NotificationService: UNNotificationServiceExtension { var contentHandler: ((UNNotificationContent) -> Void)? - var bestAttemptContent: UNMutableNotificationContent? + var bestAttemptContent: UNNotificationContent? override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { self.contentHandler = contentHandler - bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) - if let bestAttemptContent = bestAttemptContent { - let topic = bestAttemptContent.userInfo["topic"] as! String - let ciphertext = bestAttemptContent.userInfo["blob"] as! String - NSLog("Push decryption, topic=%@", topic) + self.bestAttemptContent = request.content + + if let content = bestAttemptContent, + let topic = content.userInfo["topic"] as? String, + let ciphertext = content.userInfo["blob"] as? String { + do { let service = NotifyDecryptionService(groupIdentifier: "group.com.walletconnect.sdk") let pushMessage = try service.decryptMessage(topic: topic, ciphertext: ciphertext) - bestAttemptContent.title = pushMessage.title - bestAttemptContent.body = pushMessage.body - contentHandler(bestAttemptContent) - return + let updatedContent = try handle(content: content, pushMessage: pushMessage, topic: topic) + + let mutableContent = updatedContent.mutableCopy() as! UNMutableNotificationContent + mutableContent.title = pushMessage.title + mutableContent.body = pushMessage.body + + contentHandler(mutableContent) } catch { - NSLog("Push decryption, error=%@", error.localizedDescription) - bestAttemptContent.title = "" - bestAttemptContent.body = error.localizedDescription + let mutableContent = content.mutableCopy() as! UNMutableNotificationContent + mutableContent.title = "Error" + mutableContent.body = error.localizedDescription + + contentHandler(mutableContent) } - contentHandler(bestAttemptContent) } } override func serviceExtensionTimeWillExpire() { // Called just before the extension will be terminated by the system. // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. - if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent { + if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent { contentHandler(bestAttemptContent) } } +} + +private extension NotificationService { + + func handle(content: UNNotificationContent, pushMessage: NotifyMessage, topic: String) throws -> UNNotificationContent { + let iconUrl = try pushMessage.icon.asURL() + + let senderThumbnailImageData = try Data(contentsOf: iconUrl) + let senderThumbnailImageFileUrl = try downloadAttachment(data: senderThumbnailImageData, fileName: iconUrl.lastPathComponent) + let senderThumbnailImageFileData = try Data(contentsOf: senderThumbnailImageFileUrl) + let senderAvatar = INImage(imageData: senderThumbnailImageFileData) + + var personNameComponents = PersonNameComponents() + personNameComponents.nickname = pushMessage.title + + let senderPerson = INPerson( + personHandle: INPersonHandle(value: topic, type: .unknown), + nameComponents: personNameComponents, + displayName: pushMessage.title, + image: senderAvatar, + contactIdentifier: nil, + customIdentifier: topic, + isMe: false, + suggestionType: .none + ) + + let selfPerson = INPerson( + personHandle: INPersonHandle(value: "0", type: .unknown), + nameComponents: nil, + displayName: nil, + image: nil, + contactIdentifier: nil, + customIdentifier: nil, + isMe: true, + suggestionType: .none + ) + + let incomingMessagingIntent = INSendMessageIntent( + recipients: [selfPerson], + outgoingMessageType: .outgoingMessageText, + content: pushMessage.body, + speakableGroupName: nil, + conversationIdentifier: pushMessage.type, + serviceName: nil, + sender: senderPerson, + attachments: [] + ) + + incomingMessagingIntent.setImage(senderAvatar, forParameterNamed: \.sender) + + let interaction = INInteraction(intent: incomingMessagingIntent, response: nil) + interaction.direction = .incoming + interaction.donate(completion: nil) + + return try content.updating(from: incomingMessagingIntent) + } + + func downloadAttachment(data: Data, fileName: String) throws -> URL { + let fileManager = FileManager.default + let tmpSubFolderName = ProcessInfo.processInfo.globallyUniqueString + let tmpSubFolderURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(tmpSubFolderName, isDirectory: true) + + try fileManager.createDirectory(at: tmpSubFolderURL, withIntermediateDirectories: true, attributes: nil) + let fileURL = tmpSubFolderURL.appendingPathComponent(fileName) + try data.write(to: fileURL) + + return fileURL + } } diff --git a/Example/WalletApp/Other/Info.plist b/Example/WalletApp/Other/Info.plist index 2f0f15d26..25606209f 100644 --- a/Example/WalletApp/Other/Info.plist +++ b/Example/WalletApp/Other/Info.plist @@ -2,6 +2,10 @@ + NSUserActivityTypes + + INSendMessageIntent + CFBundleIconName AppIcon CFBundleURLTypes diff --git a/Example/WalletApp/PresentationLayer/Wallet/ConnectionDetails/ConnectionDetailsView.swift b/Example/WalletApp/PresentationLayer/Wallet/ConnectionDetails/ConnectionDetailsView.swift index 65a838b11..5174bc80e 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/ConnectionDetails/ConnectionDetailsView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/ConnectionDetails/ConnectionDetailsView.swift @@ -2,7 +2,13 @@ import SwiftUI struct ConnectionDetailsView: View { @EnvironmentObject var presenter: ConnectionDetailsPresenter - + + private var dateFormatter: DateFormatter { + let formatter = DateFormatter() + formatter.dateFormat = "HH:mm:ss E, d MMM y" + return formatter + } + var body: some View { ZStack { Color.grey100 @@ -145,6 +151,39 @@ struct ConnectionDetailsView: View { .padding(.top, 30) } + VStack(alignment: .leading) { + Text("Expiry") + .font(.system(size: 15, weight: .semibold, design: .rounded)) + .foregroundColor(.whiteBackground) + .padding(.horizontal, 8) + .padding(.vertical, 5) + .background(Color.grey70) + .cornerRadius(28, corners: .allCorners) + .padding(.leading, 15) + .padding(.top, 9) + + VStack(spacing: 0) { + TagsView(items: [dateFormatter.string(from: presenter.session.expiryDate)]) { + Text($0) + .foregroundColor(.cyanBackround) + .font(.system(size: 13, weight: .semibold, design: .rounded)) + .padding(.horizontal, 8) + .padding(.vertical, 3) + .background(Color.cyanBackround.opacity(0.2)) + .cornerRadius(10, corners: .allCorners) + } + .padding(10) + } + .background(Color.whiteBackground) + .cornerRadius(20, corners: .allCorners) + .padding(.horizontal, 5) + .padding(.bottom, 5) + } + .background(Color("grey95")) + .cornerRadius(25, corners: .allCorners) + .padding(.horizontal, 20) + .padding(.top, 30) + Button { presenter.onDelete() } label: { diff --git a/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsInteractor.swift b/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsInteractor.swift index 3eeeb8ea3..1a1bd5b8f 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsInteractor.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsInteractor.swift @@ -36,20 +36,12 @@ final class NotificationsInteractor { return try await withCheckedThrowingContinuation { continuation in var cancellable: AnyCancellable? cancellable = subscriptionsPublisher - .setFailureType(to: Error.self) - .timeout(10, scheduler: RunLoop.main, customError: { Errors.subscribeTimeout }) - .sink(receiveCompletion: { completion in - defer { cancellable?.cancel() } - switch completion { - case .failure(let error): continuation.resume(with: .failure(error)) - case .finished: break - } - }, receiveValue: { subscriptions in + .sink { subscriptions in guard subscriptions.contains(where: { $0.metadata.url == domain }) else { return } cancellable?.cancel() continuation.resume(with: .success(())) - }) - + } + Task { [cancellable] in do { try await Notify.instance.subscribe(appDomain: domain, account: importAccount.account) diff --git a/Example/WalletApp/WalletApp.entitlements b/Example/WalletApp/WalletApp.entitlements index 2dccdcca9..46de0afbc 100644 --- a/Example/WalletApp/WalletApp.entitlements +++ b/Example/WalletApp/WalletApp.entitlements @@ -4,6 +4,8 @@ aps-environment development + com.apple.developer.usernotifications.communication + com.apple.security.application-groups group.com.walletconnect.sdk diff --git a/Example/WalletApp/WalletAppRelease.entitlements b/Example/WalletApp/WalletAppRelease.entitlements index 315a4dbfc..7028e40d9 100644 --- a/Example/WalletApp/WalletAppRelease.entitlements +++ b/Example/WalletApp/WalletAppRelease.entitlements @@ -4,6 +4,8 @@ aps-environment production + com.apple.developer.usernotifications.communication + com.apple.security.application-groups group.com.walletconnect.sdk diff --git a/Sources/WalletConnectNotify/Client/Wallet/NotifyClient.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifyClient.swift index 29a58e77f..b4e04c5be 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/NotifyClient.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifyClient.swift @@ -78,7 +78,7 @@ public class NotifyClient { public func register(account: Account, domain: String, isLimited: Bool = false, onSign: @escaping SigningCallback) async throws { try await identityService.register(account: account, domain: domain, isLimited: isLimited, onSign: onSign) notifyAccountProvider.setAccount(account) - subscriptionWatcher.start() + try await subscriptionWatcher.start() } public func unregister(account: Account) async throws { @@ -132,6 +132,11 @@ public class NotifyClient { #if targetEnvironment(simulator) extension NotifyClient { + + public var subscriptionChangedPublisher: AnyPublisher<[NotifySubscription], Never> { + return notifySubscriptionsChangedRequestSubscriber.subscriptionChangedPublisher + } + public func register(deviceToken: String) async throws { try await pushClient.register(deviceToken: deviceToken) } diff --git a/Sources/WalletConnectNotify/Client/Wallet/NotifyClientFactory.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifyClientFactory.swift index f04b62a14..9d0e41f4a 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/NotifyClientFactory.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifyClientFactory.swift @@ -47,7 +47,7 @@ public struct NotifyClientFactory { let identityClient = IdentityClientFactory.create(keyserver: keyserverURL, keychain: keychainStorage, logger: logger) let notifyMessageSubscriber = NotifyMessageSubscriber(keyserver: keyserverURL, networkingInteractor: networkInteractor, identityClient: identityClient, notifyStorage: notifyStorage, crypto: crypto, logger: logger) let webDidResolver = NotifyWebDidResolver() - let deleteNotifySubscriptionRequester = DeleteNotifySubscriptionRequester(keyserver: keyserverURL, networkingInteractor: networkInteractor, identityClient: identityClient, webDidResolver: webDidResolver, kms: kms, logger: logger, notifyStorage: notifyStorage) + let deleteNotifySubscriptionRequester = DeleteNotifySubscriptionRequester(keyserver: keyserverURL, networkingInteractor: networkInteractor, identityClient: identityClient, kms: kms, logger: logger, notifyStorage: notifyStorage) let resubscribeService = NotifyResubscribeService(networkInteractor: networkInteractor, notifyStorage: notifyStorage, logger: logger) let notifyConfigProvider = NotifyConfigProvider(projectId: projectId, explorerHost: explorerHost) @@ -56,7 +56,7 @@ public struct NotifyClientFactory { let notifySubscribeResponseSubscriber = NotifySubscribeResponseSubscriber(networkingInteractor: networkInteractor, kms: kms, logger: logger, groupKeychainStorage: groupKeychainStorage, notifyStorage: notifyStorage, notifyConfigProvider: notifyConfigProvider) - let notifyUpdateRequester = NotifyUpdateRequester(keyserverURL: keyserverURL, webDidResolver: webDidResolver, identityClient: identityClient, networkingInteractor: networkInteractor, notifyConfigProvider: notifyConfigProvider, logger: logger, notifyStorage: notifyStorage) + let notifyUpdateRequester = NotifyUpdateRequester(keyserverURL: keyserverURL, identityClient: identityClient, networkingInteractor: networkInteractor, notifyConfigProvider: notifyConfigProvider, logger: logger, notifyStorage: notifyStorage) let notifyUpdateResponseSubscriber = NotifyUpdateResponseSubscriber(networkingInteractor: networkInteractor, logger: logger, notifyConfigProvider: notifyConfigProvider, notifyStorage: notifyStorage) diff --git a/Sources/WalletConnectNotify/Client/Wallet/NotifyStorage.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifyStorage.swift index a7e2c92b8..ab873dcf1 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/NotifyStorage.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifyStorage.swift @@ -89,7 +89,7 @@ final class NotifyStorage: NotifyStoring { func updateSubscription(_ subscription: NotifySubscription, scope: [String: ScopeValue], expiry: UInt64) { let expiry = Date(timeIntervalSince1970: TimeInterval(expiry)) - let updated = NotifySubscription(topic: subscription.topic, account: subscription.account, relay: subscription.relay, metadata: subscription.metadata, scope: scope, expiry: expiry, symKey: subscription.symKey) + let updated = NotifySubscription(topic: subscription.topic, account: subscription.account, relay: subscription.relay, metadata: subscription.metadata, scope: scope, expiry: expiry, symKey: subscription.symKey, appAuthenticationKey: subscription.appAuthenticationKey) subscriptionStore.set(element: updated, for: updated.account.absoluteString) updateSubscriptionSubject.send(updated) } diff --git a/Sources/WalletConnectNotify/Client/Wallet/NotifySubscriptionsBuilder.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifySubscriptionsBuilder.swift index d2c9cd582..0426ce460 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/NotifySubscriptionsBuilder.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifySubscriptionsBuilder.swift @@ -24,7 +24,8 @@ class NotifySubscriptionsBuilder { metadata: config.metadata, scope: scope, expiry: subscription.expiry, - symKey: subscription.symKey + symKey: subscription.symKey, + appAuthenticationKey: subscription.appAuthenticationKey )) } catch { continue diff --git a/Sources/WalletConnectNotify/Client/Wallet/NotifyWebDidResolver.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifyWebDidResolver.swift index 5312e28bd..33a8449e6 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/NotifyWebDidResolver.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifyWebDidResolver.swift @@ -5,23 +5,21 @@ final class NotifyWebDidResolver { private static var subscribeKey = "wc-notify-subscribe-key" private static var authenticationKey = "wc-notify-authentication-key" - func resolveAgreementKey(domain: String) async throws -> AgreementPublicKey { - let didDoc = try await resolveDidDoc(domainUrl: domain) - let subscribeKeyPath = "\(didDoc.id)#\(Self.subscribeKey)" - guard let verificationMethod = didDoc.verificationMethod.first(where: { verificationMethod in verificationMethod.id == subscribeKeyPath }) else { throw Errors.noVerificationMethodForKey } - guard verificationMethod.publicKeyJwk.crv == .X25519 else { throw Errors.unsupportedCurve} - let pubKeyBase64Url = verificationMethod.publicKeyJwk.x - return try AgreementPublicKey(base64url: pubKeyBase64Url) + func resolveDidDoc(appDomain: String) async throws -> WebDidDoc { + guard let didDocUrl = URL(string: "https://\(appDomain)/.well-known/did.json") else { throw Errors.invalidUrl } + let (data, _) = try await URLSession.shared.data(from: didDocUrl) + return try JSONDecoder().decode(WebDidDoc.self, from: data) } - // TODO - Add cache for diddocs + func resolveAgreementKey(didDoc: WebDidDoc) throws -> AgreementPublicKey { + let keypath = "\(didDoc.id)#\(Self.subscribeKey)" + let pubKeyBase64Url = try resolveKey(didDoc: didDoc, curve: .X25519, keyPath: keypath) + return try AgreementPublicKey(base64url: pubKeyBase64Url) + } - func resolveAuthenticationKey(domain: String) async throws -> Data { - let didDoc = try await resolveDidDoc(domainUrl: domain) - let authenticationKeyPath = "\(didDoc.id)#\(Self.authenticationKey)" - guard let verificationMethod = didDoc.verificationMethod.first(where: { verificationMethod in verificationMethod.id == authenticationKeyPath }) else { throw Errors.noVerificationMethodForKey } - guard verificationMethod.publicKeyJwk.crv == .Ed25519 else { throw Errors.unsupportedCurve} - let pubKeyBase64Url = verificationMethod.publicKeyJwk.x + func resolveAuthenticationKey(didDoc: WebDidDoc) throws -> Data { + let keyPath = "\(didDoc.id)#\(Self.authenticationKey)" + let pubKeyBase64Url = try resolveKey(didDoc: didDoc, curve: .Ed25519, keyPath: keyPath) guard let raw = Data(base64url: pubKeyBase64Url) else { throw Errors.invalidBase64urlString } return raw } @@ -36,9 +34,9 @@ private extension NotifyWebDidResolver { case unsupportedCurve } - func resolveDidDoc(domainUrl: String) async throws -> WebDidDoc { - guard let didDocUrl = URL(string: "https://\(domainUrl)/.well-known/did.json") else { throw Errors.invalidUrl } - let (data, _) = try await URLSession.shared.data(from: didDocUrl) - return try JSONDecoder().decode(WebDidDoc.self, from: data) + func resolveKey(didDoc: WebDidDoc, curve: WebDidDoc.PublicKeyJwk.Curve, keyPath: String) throws -> String { + guard let verificationMethod = didDoc.verificationMethod.first(where: { verificationMethod in verificationMethod.id == keyPath }) else { throw Errors.noVerificationMethodForKey } + guard verificationMethod.publicKeyJwk.crv == curve else { throw Errors.unsupportedCurve } + return verificationMethod.publicKeyJwk.x } } diff --git a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyDelete/DeleteNotifySubscriptionRequester.swift b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyDelete/DeleteNotifySubscriptionRequester.swift index ea3281849..a94e86420 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyDelete/DeleteNotifySubscriptionRequester.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyDelete/DeleteNotifySubscriptionRequester.swift @@ -7,7 +7,6 @@ class DeleteNotifySubscriptionRequester { private let keyserver: URL private let networkingInteractor: NetworkInteracting private let identityClient: IdentityClient - private let webDidResolver: NotifyWebDidResolver private let kms: KeyManagementServiceProtocol private let logger: ConsoleLogging private let notifyStorage: NotifyStorage @@ -16,7 +15,6 @@ class DeleteNotifySubscriptionRequester { keyserver: URL, networkingInteractor: NetworkInteracting, identityClient: IdentityClient, - webDidResolver: NotifyWebDidResolver, kms: KeyManagementServiceProtocol, logger: ConsoleLogging, notifyStorage: NotifyStorage @@ -24,7 +22,6 @@ class DeleteNotifySubscriptionRequester { self.keyserver = keyserver self.networkingInteractor = networkingInteractor self.identityClient = identityClient - self.webDidResolver = webDidResolver self.kms = kms self.logger = logger self.notifyStorage = notifyStorage @@ -37,10 +34,10 @@ class DeleteNotifySubscriptionRequester { else { throw Errors.notifySubscriptionNotFound} let protocolMethod = NotifyDeleteProtocolMethod() - let dappAuthenticationKey = try await webDidResolver.resolveAuthenticationKey(domain: subscription.metadata.url) + let dappAuthenticationKey = try DIDKey(did: subscription.appAuthenticationKey) let wrapper = try createJWTWrapper( - dappPubKey: DIDKey(rawData: dappAuthenticationKey), + dappPubKey: dappAuthenticationKey, reason: NotifyDeleteParams.userDisconnected.message, app: DIDWeb(host: subscription.metadata.url), account: subscription.account @@ -57,11 +54,7 @@ class DeleteNotifySubscriptionRequester { logger.debug("Subscription removed, topic: \(topic)") kms.deleteSymmetricKey(for: topic) - } - - - - + } } private extension DeleteNotifySubscriptionRequester { diff --git a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifySubscriptionsChanged/NotifySubscriptionsChangedRequestSubscriber.swift b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifySubscriptionsChanged/NotifySubscriptionsChangedRequestSubscriber.swift index 95d92a0a1..18c8bbc31 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifySubscriptionsChanged/NotifySubscriptionsChangedRequestSubscriber.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifySubscriptionsChanged/NotifySubscriptionsChangedRequestSubscriber.swift @@ -11,6 +11,12 @@ class NotifySubscriptionsChangedRequestSubscriber { private let notifyStorage: NotifyStorage private let notifySubscriptionsBuilder: NotifySubscriptionsBuilder + private let subscriptionChangedSubject = PassthroughSubject<[NotifySubscription], Never>() + + var subscriptionChangedPublisher: AnyPublisher<[NotifySubscription], Never> { + return subscriptionChangedSubject.eraseToAnyPublisher() + } + init( keyserver: URL, networkingInteractor: NetworkInteracting, @@ -47,37 +53,39 @@ class NotifySubscriptionsChangedRequestSubscriber { let oldSubscriptions = notifyStorage.getSubscriptions(account: account) let newSubscriptions = try await notifySubscriptionsBuilder.buildSubscriptions(jwtPayload.subscriptions) - + + subscriptionChangedSubject.send(newSubscriptions) + try Task.checkCancellation() let subscriptions = oldSubscriptions.difference(from: newSubscriptions) logger.debug("Received: \(newSubscriptions.count), changed: \(subscriptions.count)") - guard subscriptions.count > 0 else { return } + if subscriptions.count > 0 { + notifyStorage.replaceAllSubscriptions(newSubscriptions, account: account) - notifyStorage.replaceAllSubscriptions(newSubscriptions, account: account) + for subscription in newSubscriptions { + let symKey = try SymmetricKey(hex: subscription.symKey) + try groupKeychainStorage.add(symKey, forKey: subscription.topic) + try kms.setSymmetricKey(symKey, for: subscription.topic) + } - for subscription in newSubscriptions { - let symKey = try SymmetricKey(hex: subscription.symKey) - try groupKeychainStorage.add(symKey, forKey: subscription.topic) - try kms.setSymmetricKey(symKey, for: subscription.topic) - } + let topics = newSubscriptions.map { $0.topic } - let topics = newSubscriptions.map { $0.topic } + try await networkingInteractor.batchSubscribe(topics: topics) - try await networkingInteractor.batchSubscribe(topics: topics) + try Task.checkCancellation() - try Task.checkCancellation() + var logProperties = ["rpcId": payload.id.string] + for (index, subscription) in newSubscriptions.enumerated() { + let key = "subscription_\(index + 1)" + logProperties[key] = subscription.topic + } - var logProperties = ["rpcId": payload.id.string] - for (index, subscription) in newSubscriptions.enumerated() { - let key = "subscription_\(index + 1)" - logProperties[key] = subscription.topic + logger.debug("Updated Subscriptions by Subscriptions Changed Request", properties: logProperties) } - logger.debug("Updated Subscriptions by Subscriptions Changed Request", properties: logProperties) - try await respond(topic: payload.topic, account: jwtPayload.account, rpcId: payload.id, notifyServerAuthenticationKey: jwtPayload.notifyServerAuthenticationKey) } } diff --git a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyUpdate/NotifyUpdateRequester.swift b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyUpdate/NotifyUpdateRequester.swift index 7ddb67379..104c5d31a 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyUpdate/NotifyUpdateRequester.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyUpdate/NotifyUpdateRequester.swift @@ -10,7 +10,6 @@ class NotifyUpdateRequester: NotifyUpdateRequesting { } private let keyserverURL: URL - private let webDidResolver: NotifyWebDidResolver private let identityClient: IdentityClient private let networkingInteractor: NetworkInteracting private let notifyConfigProvider: NotifyConfigProvider @@ -19,7 +18,6 @@ class NotifyUpdateRequester: NotifyUpdateRequesting { init( keyserverURL: URL, - webDidResolver: NotifyWebDidResolver, identityClient: IdentityClient, networkingInteractor: NetworkInteracting, notifyConfigProvider: NotifyConfigProvider, @@ -27,7 +25,6 @@ class NotifyUpdateRequester: NotifyUpdateRequesting { notifyStorage: NotifyStorage ) { self.keyserverURL = keyserverURL - self.webDidResolver = webDidResolver self.identityClient = identityClient self.networkingInteractor = networkingInteractor self.notifyConfigProvider = notifyConfigProvider @@ -40,10 +37,10 @@ class NotifyUpdateRequester: NotifyUpdateRequesting { guard let subscription = notifyStorage.getSubscription(topic: topic) else { throw Errors.noSubscriptionForGivenTopic } - let dappAuthenticationKey = try await webDidResolver.resolveAuthenticationKey(domain: subscription.metadata.url) + let dappAuthenticationKey = try DIDKey(did: subscription.appAuthenticationKey) let request = try createJWTRequest( - dappPubKey: DIDKey(rawData: dappAuthenticationKey), + dappPubKey: dappAuthenticationKey, subscriptionAccount: subscription.account, appDomain: subscription.metadata.url, scope: scope ) diff --git a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyWatchSubscriptions/NotifyWatchSubscriptionsRequester.swift b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyWatchSubscriptions/NotifyWatchSubscriptionsRequester.swift index 72702c588..3b38ad939 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyWatchSubscriptions/NotifyWatchSubscriptionsRequester.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyWatchSubscriptions/NotifyWatchSubscriptionsRequester.swift @@ -41,8 +41,10 @@ class NotifyWatchSubscriptionsRequester: NotifyWatchSubscriptionsRequesting { logger.debug("Watching subscriptions") - let notifyServerPublicKey = try await webDidResolver.resolveAgreementKey(domain: notifyHost) - let notifyServerAuthenticationKey = try await webDidResolver.resolveAuthenticationKey(domain: notifyHost) + let didDoc = try await webDidResolver.resolveDidDoc(appDomain: notifyHost) + let notifyServerPublicKey = try webDidResolver.resolveAgreementKey(didDoc: didDoc) + let notifyServerAuthenticationKey = try webDidResolver.resolveAuthenticationKey(didDoc: didDoc) + let notifyServerAuthenticationDidKey = DIDKey(rawData: notifyServerAuthenticationKey) let watchSubscriptionsTopic = notifyServerPublicKey.rawRepresentation.sha256().toHexString() diff --git a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyWatchSubscriptions/NotifyWatchSubscriptionsResponseSubscriber.swift b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyWatchSubscriptions/NotifyWatchSubscriptionsResponseSubscriber.swift index 298932dec..55323843e 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyWatchSubscriptions/NotifyWatchSubscriptionsResponseSubscriber.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyWatchSubscriptions/NotifyWatchSubscriptionsResponseSubscriber.swift @@ -49,27 +49,28 @@ class NotifyWatchSubscriptionsResponseSubscriber { logger.debug("Received: \(newSubscriptions.count), changed: \(subscriptions.count)") - guard subscriptions.count > 0 else { return } - // TODO: unsubscribe for oldSubscriptions topics that are not included in new subscriptions - notifyStorage.replaceAllSubscriptions(newSubscriptions, account: account) - - for subscription in newSubscriptions { - let symKey = try SymmetricKey(hex: subscription.symKey) - try groupKeychainStorage.add(symKey, forKey: subscription.topic) - try kms.setSymmetricKey(symKey, for: subscription.topic) - } + if subscriptions.count > 0 { + // TODO: unsubscribe for oldSubscriptions topics that are not included in new subscriptions + notifyStorage.replaceAllSubscriptions(newSubscriptions, account: account) - try await networkingInteractor.batchSubscribe(topics: newSubscriptions.map { $0.topic }) + for subscription in newSubscriptions { + let symKey = try SymmetricKey(hex: subscription.symKey) + try groupKeychainStorage.add(symKey, forKey: subscription.topic) + try kms.setSymmetricKey(symKey, for: subscription.topic) + } - try Task.checkCancellation() + try await networkingInteractor.batchSubscribe(topics: newSubscriptions.map { $0.topic }) - var logProperties = [String: String]() - for (index, subscription) in newSubscriptions.enumerated() { - let key = "subscription_\(index + 1)" - logProperties[key] = subscription.topic - } + try Task.checkCancellation() - logger.debug("Updated Subscriptions with Watch Subscriptions Update, number of subscriptions: \(newSubscriptions.count)", properties: logProperties) + var logProperties = [String: String]() + for (index, subscription) in newSubscriptions.enumerated() { + let key = "subscription_\(index + 1)" + logProperties[key] = subscription.topic + } + + logger.debug("Updated Subscriptions with Watch Subscriptions Update, number of subscriptions: \(newSubscriptions.count)", properties: logProperties) + } } } diff --git a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_pushSubscribe/NotifySubscribeRequester.swift b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_pushSubscribe/NotifySubscribeRequester.swift index ad5c0cee0..32a875b43 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_pushSubscribe/NotifySubscribeRequester.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_pushSubscribe/NotifySubscribeRequester.swift @@ -36,9 +36,10 @@ class NotifySubscribeRequester { logger.debug("Subscribing for Notify, dappUrl: \(appDomain)") - let config = try await notifyConfigProvider.resolveNotifyConfig(appDomain: appDomain) + let config = await notifyConfigProvider.resolveNotifyConfig(appDomain: appDomain) - let peerPublicKey = try await webDidResolver.resolveAgreementKey(domain: appDomain) + let didDoc = try await webDidResolver.resolveDidDoc(appDomain: appDomain) + let peerPublicKey = try webDidResolver.resolveAgreementKey(didDoc: didDoc) let subscribeTopic = peerPublicKey.rawRepresentation.sha256().toHexString() let keysY = try generateAgreementKeys(peerPublicKey: peerPublicKey) diff --git a/Sources/WalletConnectNotify/Client/Wallet/SubscriptionWatcher.swift b/Sources/WalletConnectNotify/Client/Wallet/SubscriptionWatcher.swift index 00ae7bebc..a08c95d6b 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/SubscriptionWatcher.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/SubscriptionWatcher.swift @@ -6,16 +6,13 @@ import UIKit class SubscriptionWatcher { - private var timerCancellable: AnyCancellable? + private var timer: Timer? private var appLifecycleCancellable: AnyCancellable? private var notifyWatchSubscriptionsRequester: NotifyWatchSubscriptionsRequesting private let logger: ConsoleLogging - private let backgroundQueue = DispatchQueue(label: "com.walletconnect.subscriptionWatcher", qos: .background) private let notificationCenter: NotificationPublishing - private var watchSubscriptionsWorkItem: DispatchWorkItem? var timerInterval: TimeInterval = 5 * 60 - var debounceInterval: TimeInterval = 0.5 var onSetupTimer: (() -> Void)? init(notifyWatchSubscriptionsRequester: NotifyWatchSubscriptionsRequesting, @@ -28,57 +25,46 @@ class SubscriptionWatcher { deinit { stop() } - func start() { + func start() async throws { + setupAppLifecyclePublisher() setupTimer() - watchAppLifecycle() - watchSubscriptions() + + try await notifyWatchSubscriptionsRequester.watchSubscriptions() } func stop() { - timerCancellable?.cancel() + timer?.invalidate() appLifecycleCancellable?.cancel() - watchSubscriptionsWorkItem?.cancel() } } -internal extension SubscriptionWatcher { - - func watchSubscriptions() { - watchSubscriptionsWorkItem?.cancel() - - let workItem = DispatchWorkItem { [weak self] in - self?.logger.debug("Will watch subscriptions") - Task(priority: .background) { [weak self] in try await self?.notifyWatchSubscriptionsRequester.watchSubscriptions() } - } - - watchSubscriptionsWorkItem = workItem - DispatchQueue.main.asyncAfter(deadline: .now() + debounceInterval, execute: workItem) - } +private extension SubscriptionWatcher { - func watchAppLifecycle() { + func setupAppLifecyclePublisher() { #if os(iOS) appLifecycleCancellable = notificationCenter.publisher(for: UIApplication.willEnterForegroundNotification) .receive(on: RunLoop.main) .sink { [weak self] _ in - self?.logger.debug("Will setup Subscription Watcher after app entered foreground") - self?.setupTimer() - self?.backgroundQueue.async { - self?.watchSubscriptions() - } + guard let self else { return } + self.logger.debug("SubscriptionWatcher entered foreground event") + self.watchSubscriptions() } #endif } func setupTimer() { - onSetupTimer?() - logger.debug("Setting up Subscription Watcher timer") - timerCancellable?.cancel() - timerCancellable = Timer.publish(every: timerInterval, on: .main, in: .common) - .autoconnect() - .sink { [weak self] _ in - self?.backgroundQueue.async { - self?.watchSubscriptions() - } - } + timer?.invalidate() + timer = Timer.scheduledTimer(withTimeInterval: timerInterval, repeats: true) { [weak self] _ in + guard let self else { return } + self.logger.debug("SubscriptionWatcher scheduled event") + self.watchSubscriptions() + } + RunLoop.main.add(timer!, forMode: .common) + } + + func watchSubscriptions() { + Task(priority: .high) { + try await self.notifyWatchSubscriptionsRequester.watchSubscriptions() + } } } diff --git a/Sources/WalletConnectNotify/Types/DataStructures/NotifyServerSubscription.swift b/Sources/WalletConnectNotify/Types/DataStructures/NotifyServerSubscription.swift index 56ebbd352..bdce74bac 100644 --- a/Sources/WalletConnectNotify/Types/DataStructures/NotifyServerSubscription.swift +++ b/Sources/WalletConnectNotify/Types/DataStructures/NotifyServerSubscription.swift @@ -6,4 +6,5 @@ struct NotifyServerSubscription: Codable, Equatable { let scope: [String] let symKey: String let expiry: Date + let appAuthenticationKey: String } diff --git a/Sources/WalletConnectNotify/Types/DataStructures/NotifySubscription.swift b/Sources/WalletConnectNotify/Types/DataStructures/NotifySubscription.swift index cc47fce34..cb4982555 100644 --- a/Sources/WalletConnectNotify/Types/DataStructures/NotifySubscription.swift +++ b/Sources/WalletConnectNotify/Types/DataStructures/NotifySubscription.swift @@ -8,6 +8,7 @@ public struct NotifySubscription: DatabaseObject { public let scope: [String: ScopeValue] public let expiry: Date public let symKey: String + public let appAuthenticationKey: String public var databaseId: String { return topic diff --git a/Sources/WalletConnectRelay/PackageConfig.json b/Sources/WalletConnectRelay/PackageConfig.json index 7db8bf6cc..78e802ff1 100644 --- a/Sources/WalletConnectRelay/PackageConfig.json +++ b/Sources/WalletConnectRelay/PackageConfig.json @@ -1 +1 @@ -{"version": "1.9.1"} +{"version": "1.9.2"} diff --git a/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift index 3e6cec6bf..185d0d444 100644 --- a/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift @@ -50,9 +50,9 @@ final class SessionEngine { func hasSession(for topic: String) -> Bool { return sessionStore.hasSession(forTopic: topic) } - + func getSessions() -> [Session] { - sessionStore.getAll().map {$0.publicRepresentation()} + sessionStore.getAll().map { $0.publicRepresentation() } } func request(_ request: Request) async throws { diff --git a/Sources/WalletConnectSign/Engine/Common/SessionExtendRequestSubscriber.swift b/Sources/WalletConnectSign/Engine/Common/SessionExtendRequestSubscriber.swift new file mode 100644 index 000000000..af291b26e --- /dev/null +++ b/Sources/WalletConnectSign/Engine/Common/SessionExtendRequestSubscriber.swift @@ -0,0 +1,66 @@ +import Foundation +import Combine + +final class SessionExtendRequestSubscriber { + var onExtend: ((String, Date) -> Void)? + + private let sessionStore: WCSessionStorage + private let networkingInteractor: NetworkInteracting + private var publishers = [AnyCancellable]() + private let logger: ConsoleLogging + + init( + networkingInteractor: NetworkInteracting, + sessionStore: WCSessionStorage, + logger: ConsoleLogging + ) { + self.networkingInteractor = networkingInteractor + self.sessionStore = sessionStore + self.logger = logger + + setupSubscriptions() + } +} + +// MARK: - Private functions +extension SessionExtendRequestSubscriber { + private func setupSubscriptions() { + networkingInteractor.requestSubscription(on: SessionExtendProtocolMethod()) + .sink { [unowned self] (payload: RequestSubscriptionPayload) in + onSessionUpdateExpiry(payload: payload, updateExpiryParams: payload.request) + }.store(in: &publishers) + } + + private func onSessionUpdateExpiry(payload: SubscriptionPayload, updateExpiryParams: SessionType.UpdateExpiryParams) { + let protocolMethod = SessionExtendProtocolMethod() + let topic = payload.topic + guard var session = sessionStore.getSession(forTopic: topic) else { + return respondError(payload: payload, reason: .noSessionForTopic, protocolMethod: protocolMethod) + } + guard session.peerIsController else { + return respondError(payload: payload, reason: .unauthorizedExtendRequest, protocolMethod: protocolMethod) + } + do { + try session.updateExpiry(to: updateExpiryParams.expiry) + } catch { + return respondError(payload: payload, reason: .invalidExtendRequest, protocolMethod: protocolMethod) + } + sessionStore.setSession(session) + + Task(priority: .high) { + try await networkingInteractor.respondSuccess(topic: payload.topic, requestId: payload.id, protocolMethod: protocolMethod) + } + + onExtend?(session.topic, session.expiryDate) + } + + private func respondError(payload: SubscriptionPayload, reason: SignReasonCode, protocolMethod: ProtocolMethod) { + Task(priority: .high) { + do { + try await networkingInteractor.respondError(topic: payload.topic, requestId: payload.id, protocolMethod: protocolMethod, reason: reason) + } catch { + logger.error("Respond Error failed with: \(error.localizedDescription)") + } + } + } +} diff --git a/Sources/WalletConnectSign/Engine/Common/SessionExtendRequester.swift b/Sources/WalletConnectSign/Engine/Common/SessionExtendRequester.swift new file mode 100644 index 000000000..5b815cb14 --- /dev/null +++ b/Sources/WalletConnectSign/Engine/Common/SessionExtendRequester.swift @@ -0,0 +1,27 @@ +import Foundation + +final class SessionExtendRequester { + private let sessionStore: WCSessionStorage + private let networkingInteractor: NetworkInteracting + + init( + sessionStore: WCSessionStorage, + networkingInteractor: NetworkInteracting + ) { + self.sessionStore = sessionStore + self.networkingInteractor = networkingInteractor + } + + func extend(topic: String, by ttl: Int64) async throws { + guard var session = sessionStore.getSession(forTopic: topic) else { + throw WalletConnectError.noSessionMatchingTopic(topic) + } + + let protocolMethod = SessionExtendProtocolMethod() + try session.updateExpiry(by: ttl) + let newExpiry = Int64(session.expiryDate.timeIntervalSince1970) + sessionStore.setSession(session) + let request = RPCRequest(method: protocolMethod.method, params: SessionType.UpdateExpiryParams(expiry: newExpiry)) + try await networkingInteractor.request(request, topic: topic, protocolMethod: protocolMethod) + } +} diff --git a/Sources/WalletConnectSign/Engine/Common/SessionExtendResponseSubscriber.swift b/Sources/WalletConnectSign/Engine/Common/SessionExtendResponseSubscriber.swift new file mode 100644 index 000000000..a802da680 --- /dev/null +++ b/Sources/WalletConnectSign/Engine/Common/SessionExtendResponseSubscriber.swift @@ -0,0 +1,48 @@ +import Foundation +import Combine + +final class SessionExtendResponseSubscriber { + var onExtend: ((String, Date) -> Void)? + + private let sessionStore: WCSessionStorage + private let networkingInteractor: NetworkInteracting + private var publishers = [AnyCancellable]() + private let logger: ConsoleLogging + + init( + networkingInteractor: NetworkInteracting, + sessionStore: WCSessionStorage, + logger: ConsoleLogging + ) { + self.networkingInteractor = networkingInteractor + self.sessionStore = sessionStore + self.logger = logger + + setupSubscriptions() + } + + // MARK: - Handle Response + private func setupSubscriptions() { + networkingInteractor.responseSubscription(on: SessionExtendProtocolMethod()) + .sink { [unowned self] (payload: ResponseSubscriptionPayload) in + handleUpdateExpiryResponse(payload: payload) + } + .store(in: &publishers) + } + + private func handleUpdateExpiryResponse(payload: ResponseSubscriptionPayload) { + guard var session = sessionStore.getSession(forTopic: payload.topic) else { return } + switch payload.response { + case .response: + do { + try session.updateExpiry(to: payload.request.expiry) + sessionStore.setSession(session) + onExtend?(session.topic, session.expiryDate) + } catch { + logger.error("Update expiry error: \(error.localizedDescription)") + } + case .error: + logger.error("Peer failed to extend session") + } + } +} diff --git a/Sources/WalletConnectSign/Engine/Controller/ControllerSessionStateMachine.swift b/Sources/WalletConnectSign/Engine/Controller/ControllerSessionStateMachine.swift index eb6becf93..d95a2b8b1 100644 --- a/Sources/WalletConnectSign/Engine/Controller/ControllerSessionStateMachine.swift +++ b/Sources/WalletConnectSign/Engine/Controller/ControllerSessionStateMachine.swift @@ -2,9 +2,7 @@ import Foundation import Combine final class ControllerSessionStateMachine { - var onNamespacesUpdate: ((String, [String: SessionNamespace]) -> Void)? - var onExtend: ((String, Date) -> Void)? private let sessionStore: WCSessionStorage private let networkingInteractor: NetworkInteracting @@ -35,31 +33,13 @@ final class ControllerSessionStateMachine { try await networkingInteractor.request(request, topic: topic, protocolMethod: protocolMethod) } - func extend(topic: String, by ttl: Int64) async throws { - var session = try getSession(for: topic) - let protocolMethod = SessionExtendProtocolMethod() - try validateController(session) - try session.updateExpiry(by: ttl) - let newExpiry = Int64(session.expiryDate.timeIntervalSince1970 ) - sessionStore.setSession(session) - let request = RPCRequest(method: protocolMethod.method, params: SessionType.UpdateExpiryParams(expiry: newExpiry)) - try await networkingInteractor.request(request, topic: topic, protocolMethod: protocolMethod) - } - // MARK: - Handle Response - private func setupSubscriptions() { networkingInteractor.responseSubscription(on: SessionUpdateProtocolMethod()) .sink { [unowned self] (payload: ResponseSubscriptionPayload) in handleUpdateResponse(payload: payload) } .store(in: &publishers) - - networkingInteractor.responseSubscription(on: SessionExtendProtocolMethod()) - .sink { [unowned self] (payload: ResponseSubscriptionPayload) in - handleUpdateExpiryResponse(payload: payload) - } - .store(in: &publishers) } private func handleUpdateResponse(payload: ResponseSubscriptionPayload) { @@ -80,22 +60,6 @@ final class ControllerSessionStateMachine { } } - private func handleUpdateExpiryResponse(payload: ResponseSubscriptionPayload) { - guard var session = sessionStore.getSession(forTopic: payload.topic) else { return } - switch payload.response { - case .response: - do { - try session.updateExpiry(to: payload.request.expiry) - sessionStore.setSession(session) - onExtend?(session.topic, session.expiryDate) - } catch { - logger.error("Update expiry error: \(error.localizedDescription)") - } - case .error: - logger.error("Peer failed to extend session") - } - } - // MARK: - Private private func getSession(for topic: String) throws -> WCSession { if let session = sessionStore.getSession(forTopic: topic) { diff --git a/Sources/WalletConnectSign/Engine/NonController/NonControllerSessionStateMachine.swift b/Sources/WalletConnectSign/Engine/NonController/NonControllerSessionStateMachine.swift index c7dba0e07..b371a579b 100644 --- a/Sources/WalletConnectSign/Engine/NonController/NonControllerSessionStateMachine.swift +++ b/Sources/WalletConnectSign/Engine/NonController/NonControllerSessionStateMachine.swift @@ -2,9 +2,7 @@ import Foundation import Combine final class NonControllerSessionStateMachine { - var onNamespacesUpdate: ((String, [String: SessionNamespace]) -> Void)? - var onExtend: ((String, Date) -> Void)? private let sessionStore: WCSessionStorage private let networkingInteractor: NetworkInteracting @@ -12,10 +10,12 @@ final class NonControllerSessionStateMachine { private var publishers = [AnyCancellable]() private let logger: ConsoleLogging - init(networkingInteractor: NetworkInteracting, - kms: KeyManagementServiceProtocol, - sessionStore: WCSessionStorage, - logger: ConsoleLogging) { + init( + networkingInteractor: NetworkInteracting, + kms: KeyManagementServiceProtocol, + sessionStore: WCSessionStorage, + logger: ConsoleLogging + ) { self.networkingInteractor = networkingInteractor self.kms = kms self.sessionStore = sessionStore @@ -28,11 +28,6 @@ final class NonControllerSessionStateMachine { .sink { [unowned self] (payload: RequestSubscriptionPayload) in onSessionUpdateNamespacesRequest(payload: payload, updateParams: payload.request) }.store(in: &publishers) - - networkingInteractor.requestSubscription(on: SessionExtendProtocolMethod()) - .sink { [unowned self] (payload: RequestSubscriptionPayload) in - onSessionUpdateExpiry(payload: payload, updateExpiryParams: payload.request) - }.store(in: &publishers) } private func respondError(payload: SubscriptionPayload, reason: SignReasonCode, protocolMethod: ProtocolMethod) { @@ -72,27 +67,4 @@ final class NonControllerSessionStateMachine { onNamespacesUpdate?(session.topic, updateParams.namespaces) } - - private func onSessionUpdateExpiry(payload: SubscriptionPayload, updateExpiryParams: SessionType.UpdateExpiryParams) { - let protocolMethod = SessionExtendProtocolMethod() - let topic = payload.topic - guard var session = sessionStore.getSession(forTopic: topic) else { - return respondError(payload: payload, reason: .noSessionForTopic, protocolMethod: protocolMethod) - } - guard session.peerIsController else { - return respondError(payload: payload, reason: .unauthorizedExtendRequest, protocolMethod: protocolMethod) - } - do { - try session.updateExpiry(to: updateExpiryParams.expiry) - } catch { - return respondError(payload: payload, reason: .invalidExtendRequest, protocolMethod: protocolMethod) - } - sessionStore.setSession(session) - - Task(priority: .high) { - try await networkingInteractor.respondSuccess(topic: payload.topic, requestId: payload.id, protocolMethod: protocolMethod) - } - - onExtend?(session.topic, session.expiryDate) - } } diff --git a/Sources/WalletConnectSign/Sign/SignClient.swift b/Sources/WalletConnectSign/Sign/SignClient.swift index 57dd897a2..1ea172041 100644 --- a/Sources/WalletConnectSign/Sign/SignClient.swift +++ b/Sources/WalletConnectSign/Sign/SignClient.swift @@ -109,6 +109,9 @@ public final class SignClient: SignClientProtocol { private let sessionPingService: SessionPingService private let nonControllerSessionStateMachine: NonControllerSessionStateMachine private let controllerSessionStateMachine: ControllerSessionStateMachine + private let sessionExtendRequester: SessionExtendRequester + private let sessionExtendRequestSubscriber: SessionExtendRequestSubscriber + private let sessionExtendResponseSubscriber: SessionExtendResponseSubscriber private let appProposeService: AppProposeService private let historyService: HistoryService private let cleanupService: SignCleanupService @@ -138,6 +141,9 @@ public final class SignClient: SignClientProtocol { sessionPingService: SessionPingService, nonControllerSessionStateMachine: NonControllerSessionStateMachine, controllerSessionStateMachine: ControllerSessionStateMachine, + sessionExtendRequester: SessionExtendRequester, + sessionExtendRequestSubscriber: SessionExtendRequestSubscriber, + sessionExtendResponseSubscriber: SessionExtendResponseSubscriber, appProposeService: AppProposeService, disconnectService: DisconnectService, historyService: HistoryService, @@ -152,6 +158,9 @@ public final class SignClient: SignClientProtocol { self.sessionPingService = sessionPingService self.nonControllerSessionStateMachine = nonControllerSessionStateMachine self.controllerSessionStateMachine = controllerSessionStateMachine + self.sessionExtendRequester = sessionExtendRequester + self.sessionExtendRequestSubscriber = sessionExtendRequestSubscriber + self.sessionExtendResponseSubscriber = sessionExtendResponseSubscriber self.appProposeService = appProposeService self.historyService = historyService self.cleanupService = cleanupService @@ -259,13 +268,13 @@ public final class SignClient: SignClientProtocol { try await controllerSessionStateMachine.update(topic: topic, namespaces: namespaces) } - /// For wallet to extend a session to 7 days + /// For dapp and wallet to extend a session to 7 days /// - Parameters: /// - topic: Topic of the session that is intended to be extended. public func extend(topic: String) async throws { let ttl: Int64 = Session.defaultTimeToLive if sessionEngine.hasSession(for: topic) { - try await controllerSessionStateMachine.extend(topic: topic, by: ttl) + try await sessionExtendRequester.extend(topic: topic, by: ttl) } } @@ -399,13 +408,13 @@ public final class SignClient: SignClientProtocol { controllerSessionStateMachine.onNamespacesUpdate = { [unowned self] topic, namespaces in sessionUpdatePublisherSubject.send((topic, namespaces)) } - controllerSessionStateMachine.onExtend = { [unowned self] topic, date in - sessionExtendPublisherSubject.send((topic, date)) - } nonControllerSessionStateMachine.onNamespacesUpdate = { [unowned self] topic, namespaces in sessionUpdatePublisherSubject.send((topic, namespaces)) } - nonControllerSessionStateMachine.onExtend = { [unowned self] topic, date in + sessionExtendRequestSubscriber.onExtend = { [unowned self] topic, date in + sessionExtendPublisherSubject.send((topic, date)) + } + sessionExtendResponseSubscriber.onExtend = { [unowned self] topic, date in sessionExtendPublisherSubject.send((topic, date)) } sessionEngine.onEventReceived = { [unowned self] topic, event, chainId in diff --git a/Sources/WalletConnectSign/Sign/SignClientFactory.swift b/Sources/WalletConnectSign/Sign/SignClientFactory.swift index a7903a01b..cfaaf355f 100644 --- a/Sources/WalletConnectSign/Sign/SignClientFactory.swift +++ b/Sources/WalletConnectSign/Sign/SignClientFactory.swift @@ -30,6 +30,9 @@ public struct SignClientFactory { let sessionEngine = SessionEngine(networkingInteractor: networkingClient, historyService: historyService, verifyContextStore: verifyContextStore, verifyClient: verifyClient, kms: kms, sessionStore: sessionStore, logger: logger) let nonControllerSessionStateMachine = NonControllerSessionStateMachine(networkingInteractor: networkingClient, kms: kms, sessionStore: sessionStore, logger: logger) let controllerSessionStateMachine = ControllerSessionStateMachine(networkingInteractor: networkingClient, kms: kms, sessionStore: sessionStore, logger: logger) + let sessionExtendRequester = SessionExtendRequester(sessionStore: sessionStore, networkingInteractor: networkingClient) + let sessionExtendRequestSubscriber = SessionExtendRequestSubscriber(networkingInteractor: networkingClient, sessionStore: sessionStore, logger: logger) + let sessionExtendResponseSubscriber = SessionExtendResponseSubscriber(networkingInteractor: networkingClient, sessionStore: sessionStore, logger: logger) let sessionTopicToProposal = CodableStore(defaults: RuntimeKeyValueStorage(), identifier: SignStorageIdentifiers.sessionTopicToProposal.rawValue) let approveEngine = ApproveEngine( networkingInteractor: networkingClient, @@ -60,6 +63,9 @@ public struct SignClientFactory { sessionPingService: sessionPingService, nonControllerSessionStateMachine: nonControllerSessionStateMachine, controllerSessionStateMachine: controllerSessionStateMachine, + sessionExtendRequester: sessionExtendRequester, + sessionExtendRequestSubscriber: sessionExtendRequestSubscriber, + sessionExtendResponseSubscriber: sessionExtendResponseSubscriber, appProposeService: appProposerService, disconnectService: disconnectService, historyService: historyService, diff --git a/Sources/WalletConnectSign/Types/Session/WCSession.swift b/Sources/WalletConnectSign/Types/Session/WCSession.swift index f9acf29e6..30d237a85 100644 --- a/Sources/WalletConnectSign/Types/Session/WCSession.swift +++ b/Sources/WalletConnectSign/Types/Session/WCSession.swift @@ -158,7 +158,7 @@ struct WCSession: SequenceObject, Equatable { mutating func updateExpiry(to expiry: Int64) throws { let newExpiryDate = Date(timeIntervalSince1970: TimeInterval(expiry)) let maxExpiryDate = Date(timeIntervalSinceNow: TimeInterval(WCSession.defaultTimeToLive)) - guard newExpiryDate >= expiryDate && newExpiryDate <= maxExpiryDate else { + guard newExpiryDate.millisecondsSince1970 >= (expiryDate.millisecondsSince1970 / 1000) && newExpiryDate <= maxExpiryDate else { throw WalletConnectError.invalidUpdateExpiryValue } self.expiryDate = newExpiryDate diff --git a/Tests/NotifyTests/Stubs/NotifySubscription.swift b/Tests/NotifyTests/Stubs/NotifySubscription.swift index 3e9a1892e..7fd10597e 100644 --- a/Tests/NotifyTests/Stubs/NotifySubscription.swift +++ b/Tests/NotifyTests/Stubs/NotifySubscription.swift @@ -15,7 +15,8 @@ extension NotifySubscription { metadata: metadata, scope: ["test": ScopeValue(id: "id", name: "name", description: "desc", enabled: true)], expiry: expiry, - symKey: symKey + symKey: symKey, + appAuthenticationKey: "did:key:z6MkpTEGT75mnz8TiguXYYVnS1GbsNCdLo72R7kUCLShTuFV" ) } } diff --git a/Tests/NotifyTests/SubscriptionWatcherTests.swift b/Tests/NotifyTests/SubscriptionWatcherTests.swift index e2d389759..a00bd7362 100644 --- a/Tests/NotifyTests/SubscriptionWatcherTests.swift +++ b/Tests/NotifyTests/SubscriptionWatcherTests.swift @@ -16,8 +16,6 @@ class SubscriptionWatcherTests: XCTestCase { mockLogger = ConsoleLoggerMock() mockNotificationCenter = MockNotificationCenter() sut = SubscriptionWatcher(notifyWatchSubscriptionsRequester: mockRequester, logger: mockLogger, notificationCenter: mockNotificationCenter) - sut.debounceInterval = 0.0001 - sut.start() } override func tearDown() { @@ -28,39 +26,36 @@ class SubscriptionWatcherTests: XCTestCase { super.tearDown() } - func testWatchSubscriptions() { + func testWatchSubscriptions() async throws { let expectation = XCTestExpectation(description: "Expect watchSubscriptions to be called") mockRequester.onWatchSubscriptions = { expectation.fulfill() } - sut.watchSubscriptions() + try await sut.start() wait(for: [expectation], timeout: 0.5) } - func testWatchAppLifecycleReactsToEnterForegroundNotification() { - let setupExpectation = XCTestExpectation(description: "Expect setupTimer to be called on app enter foreground") + func testWatchAppLifecycleReactsToEnterForegroundNotification() async throws { let watchSubscriptionsExpectation = XCTestExpectation(description: "Expect watchSubscriptions to be called on app enter foreground") - - sut.onSetupTimer = { - setupExpectation.fulfill() - } + watchSubscriptionsExpectation.expectedFulfillmentCount = 2 mockRequester.onWatchSubscriptions = { watchSubscriptionsExpectation.fulfill() } - mockNotificationCenter.post(name: UIApplication.willEnterForegroundNotification) + try await sut.start() - wait(for: [setupExpectation, watchSubscriptionsExpectation], timeout: 0.5) + await mockNotificationCenter.post(name: UIApplication.willEnterForegroundNotification) + + wait(for: [watchSubscriptionsExpectation], timeout: 0.5) } - func testTimerTriggeringWatchSubscriptionsMultipleTimes() { + func testTimerTriggeringWatchSubscriptionsMultipleTimes() async throws { sut.timerInterval = 0.0001 - sut.setupTimer() let expectation = XCTestExpectation(description: "Expect watchSubscriptions to be called multiple times") expectation.expectedFulfillmentCount = 3 @@ -69,6 +64,8 @@ class SubscriptionWatcherTests: XCTestCase { expectation.fulfill() } + try await sut.start() + wait(for: [expectation], timeout: 0.5) } } diff --git a/Tests/WalletConnectSignTests/ControllerSessionStateMachineTests.swift b/Tests/WalletConnectSignTests/ControllerSessionStateMachineTests.swift index 539b335ec..f2a2a1772 100644 --- a/Tests/WalletConnectSignTests/ControllerSessionStateMachineTests.swift +++ b/Tests/WalletConnectSignTests/ControllerSessionStateMachineTests.swift @@ -6,6 +6,7 @@ import WalletConnectKMS class ControllerSessionStateMachineTests: XCTestCase { var sut: ControllerSessionStateMachine! + var sessionExtendRequester: SessionExtendRequester! var networkingInteractor: NetworkingInteractorMock! var storageMock: WCSessionStorageMock! var cryptoMock: KeyManagementServiceMock! @@ -15,6 +16,7 @@ class ControllerSessionStateMachineTests: XCTestCase { storageMock = WCSessionStorageMock() cryptoMock = KeyManagementServiceMock() sut = ControllerSessionStateMachine(networkingInteractor: networkingInteractor, kms: cryptoMock, sessionStore: storageMock, logger: ConsoleLoggerMock()) + sessionExtendRequester = SessionExtendRequester(sessionStore: storageMock, networkingInteractor: networkingInteractor) } override func tearDown() { @@ -66,33 +68,17 @@ class ControllerSessionStateMachineTests: XCTestCase { let session = WCSession.stub(isSelfController: true, expiryDate: tomorrow) storageMock.setSession(session) let twoDays = 2*Time.day - await XCTAssertNoThrowAsync(try await sut.extend(topic: session.topic, by: Int64(twoDays))) + await XCTAssertNoThrowAsync(try await sessionExtendRequester.extend(topic: session.topic, by: Int64(twoDays))) let extendedSession = storageMock.getAll().first {$0.topic == session.topic}! XCTAssertEqual(extendedSession.expiryDate.timeIntervalSinceReferenceDate, TimeTraveler.dateByAdding(days: 2).timeIntervalSinceReferenceDate, accuracy: 1) } - func testUpdateExpirySessionNotSettled() async { - let tomorrow = TimeTraveler.dateByAdding(days: 1) - let session = WCSession.stub(isSelfController: false, expiryDate: tomorrow, acknowledged: false) - storageMock.setSession(session) - let twoDays = 2*Time.day - await XCTAssertThrowsErrorAsync(try await sut.extend(topic: session.topic, by: Int64(twoDays))) - } - - func testUpdateExpiryOnNonControllerClient() async { - let tomorrow = TimeTraveler.dateByAdding(days: 1) - let session = WCSession.stub(isSelfController: false, expiryDate: tomorrow) - storageMock.setSession(session) - let twoDays = 2*Time.day - await XCTAssertThrowsErrorAsync( try await sut.extend(topic: session.topic, by: Int64(twoDays))) - } - func testUpdateExpiryTtlTooHigh() async { let tomorrow = TimeTraveler.dateByAdding(days: 1) let session = WCSession.stub(isSelfController: true, expiryDate: tomorrow) storageMock.setSession(session) let tenDays = 10*Time.day - await XCTAssertThrowsErrorAsync( try await sut.extend(topic: session.topic, by: Int64(tenDays))) + await XCTAssertThrowsErrorAsync( try await sessionExtendRequester.extend(topic: session.topic, by: Int64(tenDays))) } func testUpdateExpiryTtlTooLow() async { @@ -100,6 +86,6 @@ class ControllerSessionStateMachineTests: XCTestCase { let session = WCSession.stub(isSelfController: true, expiryDate: dayAfterTommorow) storageMock.setSession(session) let oneDay = Int64(1*Time.day) - await XCTAssertThrowsErrorAsync( try await sut.extend(topic: session.topic, by: oneDay)) + await XCTAssertThrowsErrorAsync( try await sessionExtendRequester.extend(topic: session.topic, by: oneDay)) } } diff --git a/Tests/WalletConnectSignTests/NonControllerSessionStateMachineTests.swift b/Tests/WalletConnectSignTests/NonControllerSessionStateMachineTests.swift index aa4c917ca..7304ea2c8 100644 --- a/Tests/WalletConnectSignTests/NonControllerSessionStateMachineTests.swift +++ b/Tests/WalletConnectSignTests/NonControllerSessionStateMachineTests.swift @@ -67,20 +67,6 @@ class NonControllerSessionStateMachineTests: XCTestCase { } // MARK: - Update Expiry - - func testPeerUpdateExpirySuccess() { - let tomorrow = TimeTraveler.dateByAdding(days: 1) - let session = WCSession.stub(isSelfController: false, expiryDate: tomorrow) - storageMock.setSession(session) - let twoDaysFromNowTimestamp = Int64(TimeTraveler.dateByAdding(days: 2).timeIntervalSince1970) - - networkingInteractor.requestPublisherSubject.send((session.topic, RPCRequest.stubUpdateExpiry(expiry: twoDaysFromNowTimestamp), Data(), Date(), nil)) - let extendedSession = storageMock.getAll().first {$0.topic == session.topic}! - print(extendedSession.expiryDate) - - XCTAssertEqual(extendedSession.expiryDate.timeIntervalSince1970, TimeTraveler.dateByAdding(days: 2).timeIntervalSince1970, accuracy: 1) - } - func testPeerUpdateExpiryUnauthorized() { let tomorrow = TimeTraveler.dateByAdding(days: 1) let session = WCSession.stub(isSelfController: true, expiryDate: tomorrow)