diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 500646255..1f26d47f9 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -13,7 +13,8 @@ env: PACKAGE_VERSION: ${{ github.event.pull_request.title }} jobs: set-user-agent: - runs-on: macos-latest + runs-on: + group: apple-silicon steps: - uses: actions/checkout@v2 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d11425c1c..df62cc8a9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,8 @@ on: jobs: build: - runs-on: macos-12 + runs-on: + group: apple-silicon steps: - uses: actions/checkout@v3 diff --git a/Example/ExampleApp.xcodeproj/IntegrationTests.xctestplan b/Example/ExampleApp.xcodeproj/IntegrationTests.xctestplan index 896a5c3c7..e3aa93b52 100644 --- a/Example/ExampleApp.xcodeproj/IntegrationTests.xctestplan +++ b/Example/ExampleApp.xcodeproj/IntegrationTests.xctestplan @@ -53,6 +53,7 @@ "AuthTests\/testEIP1271RespondSuccess()", "ChatTests", "ENSResolverTests", + "HistoryTests", "SyncDerivationServiceTests", "SyncTests" ], diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index 5816bd42b..0b8a898c9 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -117,6 +117,7 @@ A5629AE828772A0100094373 /* InviteViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5629AE728772A0100094373 /* InviteViewModel.swift */; }; A5629AEA2877F2D600094373 /* WalletConnectChat in Frameworks */ = {isa = PBXBuildFile; productRef = A5629AE92877F2D600094373 /* WalletConnectChat */; }; A5629AF22877F75100094373 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = A5629AF12877F75100094373 /* Starscream */; }; + A56AC8F22AD88A5A001C8FAA /* Sequence.swift in Sources */ = {isa = PBXBuildFile; fileRef = A56AC8F12AD88A5A001C8FAA /* Sequence.swift */; }; A573C53729EC34A600E3CBFD /* SyncDerivationServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A573C53629EC34A600E3CBFD /* SyncDerivationServiceTests.swift */; }; A573C53929EC365000E3CBFD /* HDWalletKit in Frameworks */ = {isa = PBXBuildFile; productRef = A573C53829EC365000E3CBFD /* HDWalletKit */; }; A573C53B29EC365800E3CBFD /* HDWalletKit in Frameworks */ = {isa = PBXBuildFile; productRef = A573C53A29EC365800E3CBFD /* HDWalletKit */; }; @@ -478,6 +479,7 @@ A5629AE32876E6D200094373 /* ThreadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadViewModel.swift; sourceTree = ""; }; A5629AE728772A0100094373 /* InviteViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteViewModel.swift; sourceTree = ""; }; A5629AEF2877F73000094373 /* DefaultSocketFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultSocketFactory.swift; sourceTree = ""; }; + A56AC8F12AD88A5A001C8FAA /* Sequence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sequence.swift; sourceTree = ""; }; A573C53629EC34A600E3CBFD /* SyncDerivationServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncDerivationServiceTests.swift; sourceTree = ""; }; A57879702A4EDC8100F8D10B /* TextFieldView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldView.swift; sourceTree = ""; }; A578FA312873036400AA7720 /* InputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputView.swift; sourceTree = ""; }; @@ -1126,6 +1128,14 @@ path = Chat; sourceTree = ""; }; + A56AC8F02AD88A4B001C8FAA /* Foundation */ = { + isa = PBXGroup; + children = ( + A56AC8F12AD88A5A001C8FAA /* Sequence.swift */, + ); + path = Foundation; + sourceTree = ""; + }; A574B3592964570000C2BB91 /* Web3Inbox */ = { isa = PBXGroup; children = ( @@ -1664,6 +1674,7 @@ C56EE262293F56D6004840D1 /* Extensions */ = { isa = PBXGroup; children = ( + A56AC8F02AD88A4B001C8FAA /* Foundation */, 84F568C32795832A00D0A289 /* EthereumTransaction.swift */, C56EE26D293F56D6004840D1 /* SwiftUI */, C56EE269293F56D6004840D1 /* UIKit */, @@ -2438,6 +2449,7 @@ C5B2F6FB297055B0000DBA0E /* ETHSigner.swift in Sources */, C56EE274293F56D7004840D1 /* SceneViewController.swift in Sources */, A5D610D42AB35BED00C20083 /* FailableDecodable.swift in Sources */, + A56AC8F22AD88A5A001C8FAA /* Sequence.swift in Sources */, 847BD1E5298A806800076C90 /* NotificationsPresenter.swift in Sources */, A50D53C12ABA055700A4FD8B /* NotifyPreferencesModule.swift in Sources */, A5B4F7C52ABB20AE0099AF7C /* SubscriptionRouter.swift in Sources */, @@ -3223,7 +3235,7 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/WalletConnect/web3modal-swift"; requirement = { - branch = "feat/ui-package"; + branch = develop; kind = branch; }; }; diff --git a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 7a1658ba6..600596394 100644 --- a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -167,8 +167,8 @@ "package": "swift-web3modal", "repositoryURL": "https://github.com/WalletConnect/web3modal-swift", "state": { - "branch": "feat/ui-package", - "revision": "c87ab80f45452e1980b018bfabd8ec1f672c5ed3", + "branch": "develop", + "revision": "24602e2acee171fac26aa978b30666deec68a08f", "version": null } } diff --git a/Example/IntegrationTests/Chat/ChatTests.swift b/Example/IntegrationTests/Chat/ChatTests.swift index 0d44be8b6..04cdc7e68 100644 --- a/Example/IntegrationTests/Chat/ChatTests.swift +++ b/Example/IntegrationTests/Chat/ChatTests.swift @@ -74,7 +74,9 @@ final class ChatTests: XCTestCase { let historyClient = HistoryClientFactory.create( historyUrl: "https://history.walletconnect.com", relayUrl: "wss://relay.walletconnect.com", - keychain: keychain + keyValueStorage: keyValueStorage, + keychain: keychain, + logger: logger ) let clientId = try! networkingInteractor.getClientId() diff --git a/Example/IntegrationTests/History/HistoryTests.swift b/Example/IntegrationTests/History/HistoryTests.swift index 40d52f5c0..d96e0f811 100644 --- a/Example/IntegrationTests/History/HistoryTests.swift +++ b/Example/IntegrationTests/History/HistoryTests.swift @@ -18,9 +18,11 @@ final class HistoryTests: XCTestCase { override func setUp() { let keychain1 = KeychainStorageMock() let keychain2 = KeychainStorageMock() + let logger1 = ConsoleLoggerMock() + let defaults1 = RuntimeKeyValueStorage() relayClient1 = makeRelayClient(prefix: "🐄", keychain: keychain1) relayClient2 = makeRelayClient(prefix: "🐫", keychain: keychain2) - historyClient = makeHistoryClient(keychain: keychain1) + historyClient = makeHistoryClient(defaults: defaults1, keychain: keychain1, logger: logger1) } private func makeRelayClient(prefix: String, keychain: KeychainStorageProtocol) -> RelayClient { @@ -33,8 +35,8 @@ final class HistoryTests: XCTestCase { logger: ConsoleLogger(prefix: prefix + " [Relay]", loggingLevel: .debug)) } - private func makeHistoryClient(keychain: KeychainStorageProtocol) -> HistoryNetworkService { - let clientIdStorage = ClientIdStorage(keychain: keychain) + private func makeHistoryClient(defaults: KeyValueStorage, keychain: KeychainStorageProtocol, logger: ConsoleLogging) -> HistoryNetworkService { + let clientIdStorage = ClientIdStorage(defaults: defaults, keychain: keychain, logger: logger) return HistoryNetworkService(clientIdStorage: clientIdStorage) } diff --git a/Example/IntegrationTests/Pairing/PairingTests.swift b/Example/IntegrationTests/Pairing/PairingTests.swift index a2b49e07b..f7ade4b87 100644 --- a/Example/IntegrationTests/Pairing/PairingTests.swift +++ b/Example/IntegrationTests/Pairing/PairingTests.swift @@ -78,11 +78,14 @@ final class PairingTests: XCTestCase { let prefix = "🐶 Wallet: " let (pairingClient, networkingInteractor, keychain, keyValueStorage) = makeClientDependencies(prefix: prefix) let notifyLogger = ConsoleLogger(prefix: prefix + " [Notify]", loggingLevel: .debug) + let defaults = RuntimeKeyValueStorage() walletPairingClient = pairingClient let historyClient = HistoryClientFactory.create( historyUrl: "https://history.walletconnect.com", relayUrl: "wss://relay.walletconnect.com", - keychain: keychain + keyValueStorage: defaults, + keychain: keychain, + logger: notifyLogger ) appAuthClient = AuthClientFactory.create( metadata: AppMetadata(name: name, description: "", url: "", icons: [""]), diff --git a/Example/IntegrationTests/Push/NotifyTests.swift b/Example/IntegrationTests/Push/NotifyTests.swift index 00783bd2a..a0bfed40e 100644 --- a/Example/IntegrationTests/Push/NotifyTests.swift +++ b/Example/IntegrationTests/Push/NotifyTests.swift @@ -70,10 +70,12 @@ final class NotifyTests: XCTestCase { let notifyLogger = ConsoleLogger(prefix: prefix + " [Notify]", loggingLevel: .debug) let pushClient = PushClientFactory.create(projectId: "", pushHost: "echo.walletconnect.com", + keyValueStorage: keyValueStorage, keychainStorage: keychain, environment: .sandbox) let keyserverURL = URL(string: "https://keys.walletconnect.com")! - let client = NotifyClientFactory.create(keyserverURL: keyserverURL, + let client = NotifyClientFactory.create(projectId: InputConfig.projectId, + keyserverURL: keyserverURL, logger: notifyLogger, keyValueStorage: keyValueStorage, keychainStorage: keychain, @@ -95,7 +97,7 @@ final class NotifyTests: XCTestCase { walletNotifyClientA.subscriptionsPublisher .sink { [unowned self] subscriptions in - guard let subscription = subscriptions.first else {return} + guard let subscription = subscriptions.first else { return } Task(priority: .high) { try await walletNotifyClientA.deleteSubscription(topic: subscription.topic) expectation.fulfill() @@ -114,17 +116,15 @@ final class NotifyTests: XCTestCase { let clientB = makeWalletClient(prefix: "👐🏼 Wallet B: ") clientB.subscriptionsPublisher.sink { subscriptions in + guard let subscription = subscriptions.first else { return } Task(priority: .high) { - if !subscriptions.isEmpty { - expectation.fulfill() - } + try await clientB.deleteSubscription(topic: subscription.topic) + expectation.fulfill() } }.store(in: &publishers) try! await walletNotifyClientA.register(account: account, domain: gmDappDomain, onSign: sign) try! await walletNotifyClientA.subscribe(appDomain: gmDappDomain, account: account) - - sleep(1) try! await clientB.register(account: account, domain: gmDappDomain, onSign: sign) wait(for: [expectation], timeout: InputConfig.defaultTimeout) @@ -136,18 +136,15 @@ final class NotifyTests: XCTestCase { let clientB = makeWalletClient(prefix: "👐🏼 Wallet B: ") clientB.subscriptionsPublisher.sink { subscriptions in + guard let subscription = subscriptions.first else { return } Task(priority: .high) { - if !subscriptions.isEmpty { - expectation.fulfill() - } + try await clientB.deleteSubscription(topic: subscription.topic) + expectation.fulfill() } }.store(in: &publishers) try! await walletNotifyClientA.register(account: account, domain: gmDappDomain, onSign: sign) try! await clientB.register(account: account, domain: gmDappDomain, onSign: sign) - - sleep(1) - try! await walletNotifyClientA.subscribe(appDomain: gmDappDomain, account: account) wait(for: [expectation], timeout: InputConfig.defaultTimeout) @@ -155,14 +152,14 @@ final class NotifyTests: XCTestCase { func testWalletCreatesAndUpdatesSubscription() async { let expectation = expectation(description: "expects to create and update notify subscription") - let updateScope: Set = ["alerts"] + let updateScope: Set = ["8529aae8-cb26-4d49-922e-eb099044bebe"] expectation.assertForOverFulfill = false var didUpdate = false walletNotifyClientA.subscriptionsPublisher .sink { [unowned self] subscriptions in - guard let subscription = subscriptions.first else {return} - let updatedScope = Set(subscription.scope.filter{ $0.value.enabled == true }.keys) + guard let subscription = subscriptions.first else { return } + let updatedScope = Set(subscription.scope.filter { $0.value.enabled == true }.keys) if !didUpdate { didUpdate = true @@ -187,12 +184,12 @@ final class NotifyTests: XCTestCase { func testNotifyServerSubscribeAndNotifies() async throws { let subscribeExpectation = expectation(description: "creates notify subscription") let messageExpectation = expectation(description: "receives a notify message") - let notifyMessage = NotifyMessage.stub() + let notifyMessage = NotifyMessage.stub(type: "8529aae8-cb26-4d49-922e-eb099044bebe") var didNotify = false walletNotifyClientA.subscriptionsPublisher .sink { subscriptions in - guard let subscription = subscriptions.first else {return} + guard let subscription = subscriptions.first else { return } let notifier = Publisher() if !didNotify { didNotify = true diff --git a/Example/IntegrationTests/Stubs/PushMessage.swift b/Example/IntegrationTests/Stubs/PushMessage.swift index 1ad6880ee..18387013b 100644 --- a/Example/IntegrationTests/Stubs/PushMessage.swift +++ b/Example/IntegrationTests/Stubs/PushMessage.swift @@ -2,12 +2,12 @@ import Foundation import WalletConnectNotify extension NotifyMessage { - static func stub() -> NotifyMessage { + static func stub(type: String) -> NotifyMessage { return NotifyMessage( title: "swift_test", - body: "gm_hourly", + body: "body", icon: "https://images.unsplash.com/photo-1581224463294-908316338239?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=250&q=80", url: "https://web3inbox.com", - type: "private") + type: type) } } diff --git a/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift b/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift index 9dc595c95..e6e5263ae 100644 --- a/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift +++ b/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift @@ -31,7 +31,9 @@ final class RelayClientEndToEndTests: XCTestCase { private var publishers = Set() func makeRelayClient(prefix: String) -> RelayClient { - let clientIdStorage = ClientIdStorage(keychain: KeychainStorageMock()) + let keyValueStorage = RuntimeKeyValueStorage() + let logger = ConsoleLogger(prefix: prefix, loggingLevel: .debug) + let clientIdStorage = ClientIdStorage(defaults: keyValueStorage, keychain: KeychainStorageMock(), logger: logger) let socketAuthenticator = ClientIdAuthenticator( clientIdStorage: clientIdStorage, url: InputConfig.relayUrl @@ -43,7 +45,6 @@ final class RelayClientEndToEndTests: XCTestCase { ) let socket = WebSocket(url: urlFactory.create(fallback: false)) let webSocketFactory = WebSocketFactoryMock(webSocket: socket) - let logger = ConsoleLogger(prefix: prefix, loggingLevel: .debug) let dispatcher = Dispatcher( socketFactory: webSocketFactory, relayUrlFactory: urlFactory, @@ -51,7 +52,6 @@ final class RelayClientEndToEndTests: XCTestCase { logger: logger ) let keychain = KeychainStorageMock() - let keyValueStorage = RuntimeKeyValueStorage() let relayClient = RelayClientFactory.create( relayHost: InputConfig.relayHost, projectId: InputConfig.projectId, diff --git a/Example/WalletApp/Common/Extensions/Foundation/Sequence.swift b/Example/WalletApp/Common/Extensions/Foundation/Sequence.swift new file mode 100644 index 000000000..b1b86da7d --- /dev/null +++ b/Example/WalletApp/Common/Extensions/Foundation/Sequence.swift @@ -0,0 +1,21 @@ +import Foundation + +extension Sequence { + func sorted( + by firstPredicate: (Element, Element) -> Bool, + _ secondPredicate: (Element, Element) -> Bool, + _ otherPredicates: ((Element, Element) -> Bool)... + ) -> [Element] { + return sorted(by:) { lhs, rhs in + if firstPredicate(lhs, rhs) { return true } + if firstPredicate(rhs, lhs) { return false } + if secondPredicate(lhs, rhs) { return true } + if secondPredicate(rhs, lhs) { return false } + for predicate in otherPredicates { + if predicate(lhs, rhs) { return true } + if predicate(rhs, lhs) { return false } + } + return false + } + } +} diff --git a/Example/WalletApp/PresentationLayer/Wallet/Notifications/Models/SubscriptionsViewModel.swift b/Example/WalletApp/PresentationLayer/Wallet/Notifications/Models/SubscriptionsViewModel.swift index 366ce4409..6e0f12f42 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Notifications/Models/SubscriptionsViewModel.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Notifications/Models/SubscriptionsViewModel.swift @@ -5,6 +5,12 @@ typealias SubscriptionScope = [String: ScopeValue] struct SubscriptionsViewModel: Identifiable { let subscription: NotifySubscription + let messages: [NotifyMessageRecord]? + + init(subscription: NotifySubscription, messages: [NotifyMessageRecord]? = nil) { + self.subscription = subscription + self.messages = messages + } var id: String { return subscription.topic @@ -33,4 +39,12 @@ struct SubscriptionsViewModel: Identifiable { var scope: SubscriptionScope { return subscription.scope } + + var messagesCount: Int { + return messages?.count ?? 0 + } + + var hasMessage: Bool { + return messagesCount != 0 + } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsInteractor.swift b/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsInteractor.swift index eacede968..3eeeb8ea3 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsInteractor.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsInteractor.swift @@ -1,3 +1,4 @@ +import Foundation import WalletConnectNotify import Combine @@ -14,7 +15,7 @@ final class NotificationsInteractor { } func getSubscriptions() -> [NotifySubscription] { - let subs = Notify.instance.getActiveSubscriptions() + let subs = Notify.instance.getActiveSubscriptions(account: importAccount.account) return subs } @@ -32,14 +33,53 @@ final class NotificationsInteractor { } func subscribe(domain: String) async throws { - try await Notify.instance.subscribe(appDomain: domain, account: importAccount.account) + 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 + 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) + } catch { + cancellable?.cancel() + continuation.resume(throwing: error) + } + } + } } func unsubscribe(topic: String) async throws { try await Notify.instance.deleteSubscription(topic: topic) } - func messagesCount(subscription: NotifySubscription) -> Int { - return Notify.instance.getMessageHistory(topic: subscription.topic).count + func messages(for subscription: NotifySubscription) -> [NotifyMessageRecord] { + return Notify.instance.getMessageHistory(topic: subscription.topic) + } +} + +private extension NotificationsInteractor { + + enum Errors: Error, LocalizedError { + case subscribeTimeout + + var errorDescription: String? { + switch self { + case .subscribeTimeout: + return "Subscribe method timeout" + } + } } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsPresenter.swift index ecb906cc4..91a573857 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsPresenter.swift @@ -13,18 +13,20 @@ final class NotificationsPresenter: ObservableObject { var subscriptionViewModels: [SubscriptionsViewModel] { return subscriptions - .map { SubscriptionsViewModel(subscription: $0) } - .sorted { lhs, rhs in - return interactor.messagesCount(subscription: lhs.subscription) > interactor.messagesCount(subscription: rhs.subscription) - } + .map { SubscriptionsViewModel(subscription: $0, messages: interactor.messages(for: $0)) } + .sorted(by: + { $0.messagesCount > $1.messagesCount }, + { $0.name < $1.name } + ) } var listingViewModels: [ListingViewModel] { return listings .map { ListingViewModel(listing: $0) } - .sorted { lhs, rhs in - return subscription(forListing: lhs) != nil && subscription(forListing: rhs) == nil - } + .sorted(by: + { subscription(forListing: $0) != nil && subscription(forListing: $1) == nil }, + { $0.title < $1.title } + ) } init(interactor: NotificationsInteractor, router: NotificationsRouter) { diff --git a/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsView.swift b/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsView.swift index a4e250e60..73151545c 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsView.swift @@ -124,6 +124,20 @@ struct NotificationsView: View { .foregroundColor(.Foreground150) .font(.system(size: 14, weight: .regular, design: .rounded)) } + + Spacer() + + if subscription.hasMessage { + VStack{ + Text(String(subscription.messagesCount)) + .foregroundColor(.Inverse100) + .font(.system(size: 13, weight: .medium).monospacedDigit()) + .padding(.horizontal, 8) + .padding(.vertical, 4) + }.background { + Capsule().foregroundColor(.blue100) + } + } } } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/NotifySettings/NotifyPreferencesPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/NotifySettings/NotifyPreferencesPresenter.swift index c811c4314..d2689effe 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/NotifySettings/NotifyPreferencesPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/NotifySettings/NotifyPreferencesPresenter.swift @@ -10,7 +10,7 @@ final class NotifyPreferencesPresenter: ObservableObject { private var disposeBag = Set() var subscriptionViewModel: SubscriptionsViewModel { - return SubscriptionsViewModel(subscription: subscription) + return SubscriptionsViewModel(subscription: subscription, messages: []) } var preferences: [String] { diff --git a/Example/WalletApp/PresentationLayer/Wallet/NotifySettings/NotifyPreferencesView.swift b/Example/WalletApp/PresentationLayer/Wallet/NotifySettings/NotifyPreferencesView.swift index 764907d7a..327c2b4d6 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/NotifySettings/NotifyPreferencesView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/NotifySettings/NotifyPreferencesView.swift @@ -53,10 +53,10 @@ struct NotifyPreferencesView: View { Toggle(isOn: .init(get: { viewModel.update[title]?.enabled ?? value.enabled }, set: { newValue in - viewModel.update[title] = ScopeValue(description: value.description, enabled: newValue) + viewModel.update[title] = ScopeValue(id: value.id, name: value.name, description: value.description, enabled: newValue) })) { VStack(alignment: .leading, spacing: 4) { - Text(title) + Text(value.name) .foregroundColor(.primary) .font(.system(size: 14, weight: .semibold)) diff --git a/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift index 2cd2fc2d8..103002dd6 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift @@ -15,7 +15,7 @@ final class SessionRequestPresenter: ObservableObject { var message: String { let message = try? sessionRequest.params.get([String].self) let decryptedMessage = message.map { String(data: Data(hex: $0.first ?? ""), encoding: .utf8) } - return (decryptedMessage ?? "Failed to decrypt") ?? "Failed to decrypt" + return (decryptedMessage ?? String(describing: sessionRequest.params.value)) ?? String(describing: sessionRequest.params.value) } @Published var showError = false diff --git a/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsInteractor.swift b/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsInteractor.swift index 9b9e2779d..93592457e 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsInteractor.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsInteractor.swift @@ -1,3 +1,9 @@ +import Foundation +import WalletConnectNotify + final class SettingsInteractor { + func notifyUnregister(account: Account) async throws { + try await Notify.instance.unregister(account: account) + } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsPresenter.swift index 7a705a6bb..fb827251e 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsPresenter.swift @@ -38,10 +38,12 @@ final class SettingsPresenter: ObservableObject { return deviceToken } - func logoutPressed() { + func logoutPressed() async throws { + guard let account = accountStorage.importAccount?.account else { return } + try await interactor.notifyUnregister(account: account) accountStorage.importAccount = nil UserDefaults.standard.set(nil, forKey: "deviceToken") - router.presentWelcome() + await router.presentWelcome() } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsRouter.swift b/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsRouter.swift index 4069cd33d..7ef69186e 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsRouter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsRouter.swift @@ -10,7 +10,7 @@ final class SettingsRouter { self.app = app } - func presentWelcome() { + @MainActor func presentWelcome() async { WelcomeModule.create(app: app).present() } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsView.swift b/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsView.swift index 49578cf6e..137296793 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsView.swift @@ -1,4 +1,5 @@ import SwiftUI +import AsyncButton struct SettingsView: View { @@ -19,8 +20,8 @@ struct SettingsView: View { } Section { - Button { - viewModel.logoutPressed() + AsyncButton { + try await viewModel.logoutPressed() } label: { Text("Log out") .foregroundColor(.red) diff --git a/Sources/Chat/ChatClient.swift b/Sources/Chat/ChatClient.swift index 445c96ab8..13b66378b 100644 --- a/Sources/Chat/ChatClient.swift +++ b/Sources/Chat/ChatClient.swift @@ -115,9 +115,8 @@ public class ChatClient { /// Unregisters a blockchain account with previously registered identity key /// Must not unregister invite key but must stop listening for invites - /// - Parameter onSign: Callback for signing CAIP-122 message to verify blockchain account ownership - public func unregister(account: Account, onSign: @escaping SigningCallback) async throws { - try await identityClient.unregister(account: account, onSign: onSign) + public func unregister(account: Account) async throws { + try await identityClient.unregister(account: account) } /// Queries the keyserver with a blockchain account diff --git a/Sources/WalletConnectHistory/HistoryClientFactory.swift b/Sources/WalletConnectHistory/HistoryClientFactory.swift index 5168430a3..6ca9b78ab 100644 --- a/Sources/WalletConnectHistory/HistoryClientFactory.swift +++ b/Sources/WalletConnectHistory/HistoryClientFactory.swift @@ -4,15 +4,19 @@ class HistoryClientFactory { static func create() -> HistoryClient { let keychain = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk") + let keyValueStorage = UserDefaults.standard + let logger = ConsoleLogger() return HistoryClientFactory.create( historyUrl: "https://history.walletconnect.com", relayUrl: "wss://relay.walletconnect.com", - keychain: keychain + keyValueStorage: keyValueStorage, + keychain: keychain, + logger: logger ) } - static func create(historyUrl: String, relayUrl: String, keychain: KeychainStorageProtocol) -> HistoryClient { - let clientIdStorage = ClientIdStorage(keychain: keychain) + static func create(historyUrl: String, relayUrl: String, keyValueStorage: KeyValueStorage, keychain: KeychainStorageProtocol, logger: ConsoleLogging) -> HistoryClient { + let clientIdStorage = ClientIdStorage(defaults: keyValueStorage, keychain: keychain, logger: logger) let kms = KeyManagementService(keychain: keychain) let serializer = Serializer(kms: kms, logger: ConsoleLogger(prefix: "🔐", loggingLevel: .off)) let historyNetworkService = HistoryNetworkService(clientIdStorage: clientIdStorage) diff --git a/Sources/WalletConnectIdentity/IdentityClient.swift b/Sources/WalletConnectIdentity/IdentityClient.swift index 3fb32c92a..a776adb09 100644 --- a/Sources/WalletConnectIdentity/IdentityClient.swift +++ b/Sources/WalletConnectIdentity/IdentityClient.swift @@ -34,8 +34,8 @@ public final class IdentityClient { return inviteKey } - public func unregister(account: Account, onSign: SigningCallback) async throws { - try await identityService.unregister(account: account, onSign: onSign) + public func unregister(account: Account) async throws { + try await identityService.unregister(account: account) logger.debug("Did unregister an account: \(account)") } diff --git a/Sources/WalletConnectIdentity/IdentityService.swift b/Sources/WalletConnectIdentity/IdentityService.swift index a267a6a1b..0a974c474 100644 --- a/Sources/WalletConnectIdentity/IdentityService.swift +++ b/Sources/WalletConnectIdentity/IdentityService.swift @@ -58,7 +58,7 @@ actor IdentityService { return try storage.saveInviteKey(inviteKey, for: account) } - func unregister(account: Account, onSign: SigningCallback) async throws { + func unregister(account: Account) async throws { let identityKey = try storage.getIdentityKey(for: account) let identityPublicKey = DIDKey(rawData: identityKey.publicKey.rawRepresentation) let idAuth = try makeIDAuth(account: account, issuer: identityPublicKey, claims: UnregisterIdentityClaims.self) diff --git a/Sources/WalletConnectJWT/JSONEncoder+JWT.swift b/Sources/WalletConnectJWT/JSONEncoder+JWT.swift new file mode 100644 index 000000000..7f73ad98c --- /dev/null +++ b/Sources/WalletConnectJWT/JSONEncoder+JWT.swift @@ -0,0 +1,11 @@ +import Foundation + +extension JSONEncoder { + + public static var jwt: JSONEncoder { + let jsonEncoder = JSONEncoder() + jsonEncoder.outputFormatting = .withoutEscapingSlashes + jsonEncoder.dateEncodingStrategy = .secondsSince1970 + return jsonEncoder + } +} diff --git a/Sources/WalletConnectJWT/JWT.swift b/Sources/WalletConnectJWT/JWT.swift index 5e3259440..d37728840 100644 --- a/Sources/WalletConnectJWT/JWT.swift +++ b/Sources/WalletConnectJWT/JWT.swift @@ -7,12 +7,12 @@ struct JWT: Codable, Equatable { let signature: String let string: String - init(claims: JWTClaims, signer: JWTSigning) throws { + init(claims: JWTClaims, signer: JWTSigning, jsonEncoder: JSONEncoder = .jwt) throws { self.header = JWTHeader(alg: signer.alg) self.claims = claims - let headerString = try header.encode() - let claimsString = try claims.encode() + let headerString = try header.encode(jsonEncoder: jsonEncoder) + let claimsString = try claims.encode(jsonEncoder: jsonEncoder) let signature = try signer.sign(header: headerString, claims: claimsString) self.signature = signature diff --git a/Sources/WalletConnectJWT/JWTEncodable.swift b/Sources/WalletConnectJWT/JWTEncodable.swift index 04505fdb5..e612e47f8 100644 --- a/Sources/WalletConnectJWT/JWTEncodable.swift +++ b/Sources/WalletConnectJWT/JWTEncodable.swift @@ -1,17 +1,14 @@ import Foundation public protocol JWTEncodable: Codable, Equatable { - func encode() throws -> String + func encode(jsonEncoder: JSONEncoder) throws -> String static func decode(from string: String) throws -> Self } extension JWTEncodable { - public func encode() throws -> String { - let jsonEncoder = JSONEncoder() - jsonEncoder.outputFormatting = .withoutEscapingSlashes - jsonEncoder.dateEncodingStrategy = .secondsSince1970 + public func encode(jsonEncoder: JSONEncoder) throws -> String { let data = try jsonEncoder.encode(self) return JWTEncoder.base64urlEncodedString(data: data) } diff --git a/Sources/WalletConnectKMS/Crypto/KeyManagementService.swift b/Sources/WalletConnectKMS/Crypto/KeyManagementService.swift index 90a6455fa..53a271b14 100644 --- a/Sources/WalletConnectKMS/Crypto/KeyManagementService.swift +++ b/Sources/WalletConnectKMS/Crypto/KeyManagementService.swift @@ -7,16 +7,19 @@ public protocol KeyManagementServiceProtocol { func setPublicKey(publicKey: AgreementPublicKey, for topic: String) throws func setAgreementSecret(_ agreementSecret: AgreementKeys, topic: String) throws func setSymmetricKey(_ symmetricKey: SymmetricKey, for topic: String) throws + func setTopic(_ topic: String, for key: String) throws func getPrivateKey(for publicKey: AgreementPublicKey) throws -> AgreementPrivateKey? func getAgreementSecret(for topic: String) -> AgreementKeys? func getSymmetricKey(for topic: String) -> SymmetricKey? func getSymmetricKeyRepresentable(for topic: String) -> Data? func getPublicKey(for topic: String) -> AgreementPublicKey? + func getTopic(for key: String) -> String? func deletePrivateKey(for publicKey: String) func deleteAgreementSecret(for topic: String) func deleteSymmetricKey(for topic: String) func deletePublicKey(for topic: String) func deleteAll() throws + func deleteTopic(for key: String) func performKeyAgreement(selfPublicKey: AgreementPublicKey, peerPublicKey hexRepresentation: String) throws -> AgreementKeys } @@ -63,6 +66,14 @@ public class KeyManagementService: KeyManagementServiceProtocol { try keychain.add(topic, forKey: key) } + public func deleteTopic(for key: String) { + do { + try keychain.delete(key: key) + } catch { + print("Error deleting topic: \(error)") + } + } + public func getSymmetricKey(for topic: String) -> SymmetricKey? { do { return try keychain.read(key: topic) as SymmetricKey diff --git a/Sources/WalletConnectNotify/Client/Wallet/Extensions/Array.swift b/Sources/WalletConnectNotify/Client/Wallet/Extensions/Array.swift new file mode 100644 index 000000000..bd295ee30 --- /dev/null +++ b/Sources/WalletConnectNotify/Client/Wallet/Extensions/Array.swift @@ -0,0 +1,10 @@ +import Foundation + +extension Array where Element: Hashable { + + func difference(from other: [Element]) -> [Element] { + let thisSet = Set(self) + let otherSet = Set(other) + return Array(thisSet.symmetricDifference(otherSet)) + } +} diff --git a/Sources/WalletConnectNotify/Client/Wallet/NotifyAccountProvider.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifyAccountProvider.swift new file mode 100644 index 000000000..8a702fabd --- /dev/null +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifyAccountProvider.swift @@ -0,0 +1,22 @@ +import Foundation + +final class NotifyAccountProvider { + enum Errors: Error { + case currentAccountNotFound + } + + private var currentAccount: Account? + + func setAccount(_ account: Account) { + self.currentAccount = account + } + + func logout() { + self.currentAccount = nil + } + + func getCurrentAccount() throws -> Account { + guard let currentAccount else { throw Errors.currentAccountNotFound } + return currentAccount + } +} diff --git a/Sources/WalletConnectNotify/Client/Wallet/NotifyClient.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifyClient.swift index 47ffbceec..29a58e77f 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/NotifyClient.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifyClient.swift @@ -25,6 +25,7 @@ public class NotifyClient { private let pushClient: PushClient private let identityService: NotifyIdentityService private let notifyStorage: NotifyStorage + private let notifyAccountProvider: NotifyAccountProvider private let notifyMessageSubscriber: NotifyMessageSubscriber private let resubscribeService: NotifyResubscribeService private let notifySubscribeResponseSubscriber: NotifySubscribeResponseSubscriber @@ -32,6 +33,7 @@ public class NotifyClient { private let notifyUpdateResponseSubscriber: NotifyUpdateResponseSubscriber private let subscriptionsAutoUpdater: SubscriptionsAutoUpdater private let notifyWatchSubscriptionsResponseSubscriber: NotifyWatchSubscriptionsResponseSubscriber + private let notifyWatcherAgreementKeysProvider: NotifyWatcherAgreementKeysProvider private let notifySubscriptionsChangedRequestSubscriber: NotifySubscriptionsChangedRequestSubscriber private let subscriptionWatcher: SubscriptionWatcher @@ -47,8 +49,10 @@ public class NotifyClient { notifySubscribeResponseSubscriber: NotifySubscribeResponseSubscriber, notifyUpdateRequester: NotifyUpdateRequester, notifyUpdateResponseSubscriber: NotifyUpdateResponseSubscriber, + notifyAccountProvider: NotifyAccountProvider, subscriptionsAutoUpdater: SubscriptionsAutoUpdater, notifyWatchSubscriptionsResponseSubscriber: NotifyWatchSubscriptionsResponseSubscriber, + notifyWatcherAgreementKeysProvider: NotifyWatcherAgreementKeysProvider, notifySubscriptionsChangedRequestSubscriber: NotifySubscriptionsChangedRequestSubscriber, subscriptionWatcher: SubscriptionWatcher ) { @@ -63,15 +67,26 @@ public class NotifyClient { self.notifySubscribeResponseSubscriber = notifySubscribeResponseSubscriber self.notifyUpdateRequester = notifyUpdateRequester self.notifyUpdateResponseSubscriber = notifyUpdateResponseSubscriber + self.notifyAccountProvider = notifyAccountProvider self.subscriptionsAutoUpdater = subscriptionsAutoUpdater self.notifyWatchSubscriptionsResponseSubscriber = notifyWatchSubscriptionsResponseSubscriber + self.notifyWatcherAgreementKeysProvider = notifyWatcherAgreementKeysProvider self.notifySubscriptionsChangedRequestSubscriber = notifySubscriptionsChangedRequestSubscriber self.subscriptionWatcher = subscriptionWatcher } 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) - subscriptionWatcher.setAccount(account) + notifyAccountProvider.setAccount(account) + subscriptionWatcher.start() + } + + public func unregister(account: Account) async throws { + try await identityService.unregister(account: account) + notifyWatcherAgreementKeysProvider.removeAgreement(account: account) + notifyStorage.clearDatabase(account: account) + notifyAccountProvider.logout() + subscriptionWatcher.stop() } public func setLogging(level: LoggingLevel) { @@ -79,41 +94,15 @@ public class NotifyClient { } public func subscribe(appDomain: String, account: Account) async throws { - 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 - guard subscriptions.contains(where: { $0.metadata.url == appDomain }) else { return } - cancellable?.cancel() - continuation.resume(with: .success(())) - }) - - Task { [cancellable] in - do { - try await notifySubscribeRequester.subscribe(appDomain: appDomain, account: account) - } catch { - cancellable?.cancel() - continuation.resume(throwing: error) - } - } - } + try await notifySubscribeRequester.subscribe(appDomain: appDomain, account: account) } public func update(topic: String, scope: Set) async throws { try await notifyUpdateRequester.update(topic: topic, scope: scope) } - public func getActiveSubscriptions() -> [NotifySubscription] { - return notifyStorage.getSubscriptions() + public func getActiveSubscriptions(account: Account) -> [NotifySubscription] { + return notifyStorage.getSubscriptions(account: account) } public func getMessageHistory(topic: String) -> [NotifyMessageRecord] { @@ -141,20 +130,6 @@ public class NotifyClient { } } -private extension NotifyClient { - - enum Errors: Error, LocalizedError { - case subscribeTimeout - - var errorDescription: String? { - switch self { - case .subscribeTimeout: - return "Subscribe method timeout" - } - } - } -} - #if targetEnvironment(simulator) extension NotifyClient { public func register(deviceToken: String) async throws { diff --git a/Sources/WalletConnectNotify/Client/Wallet/NotifyClientFactory.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifyClientFactory.swift index 5f7d3ec22..8fac80ce5 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/NotifyClientFactory.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifyClientFactory.swift @@ -2,7 +2,7 @@ import Foundation public struct NotifyClientFactory { - public static func create(groupIdentifier: String, networkInteractor: NetworkInteracting, pairingRegisterer: PairingRegisterer, pushClient: PushClient, crypto: CryptoProvider, notifyHost: String) -> NotifyClient { + public static func create(projectId: String, groupIdentifier: String, networkInteractor: NetworkInteracting, pairingRegisterer: PairingRegisterer, pushClient: PushClient, crypto: CryptoProvider, notifyHost: String) -> NotifyClient { let logger = ConsoleLogger(prefix: "🔔",loggingLevel: .debug) let keyValueStorage = UserDefaults.standard let keyserverURL = URL(string: "https://keys.walletconnect.com")! @@ -10,6 +10,7 @@ public struct NotifyClientFactory { let groupKeychainService = GroupKeychainStorage(serviceIdentifier: groupIdentifier) return NotifyClientFactory.create( + projectId: projectId, keyserverURL: keyserverURL, logger: logger, keyValueStorage: keyValueStorage, @@ -24,6 +25,7 @@ public struct NotifyClientFactory { } static func create( + projectId: String, keyserverURL: URL, logger: ConsoleLogging, keyValueStorage: KeyValueStorage, @@ -38,19 +40,19 @@ public struct NotifyClientFactory { let kms = KeyManagementService(keychain: keychainStorage) let subscriptionStore = KeyedDatabase(storage: keyValueStorage, identifier: NotifyStorageIdntifiers.notifySubscription) let messagesStore = KeyedDatabase(storage: keyValueStorage, identifier: NotifyStorageIdntifiers.notifyMessagesRecords) - let notifyStorage = NotifyStorage(subscriptionStore: subscriptionStore, messagesStore: messagesStore) + let notifyAccountProvider = NotifyAccountProvider() + let notifyStorage = NotifyStorage(subscriptionStore: subscriptionStore, messagesStore: messagesStore, accountProvider: notifyAccountProvider) 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 resubscribeService = NotifyResubscribeService(networkInteractor: networkInteractor, notifyStorage: notifyStorage, logger: logger) - let dappsMetadataStore = CodableStore(defaults: keyValueStorage, identifier: NotifyStorageIdntifiers.dappsMetadataStore) - let notifyConfigProvider = NotifyConfigProvider() + let notifyConfigProvider = NotifyConfigProvider(projectId: projectId) - let notifySubscribeRequester = NotifySubscribeRequester(keyserverURL: keyserverURL, networkingInteractor: networkInteractor, identityClient: identityClient, logger: logger, kms: kms, webDidResolver: webDidResolver, notifyConfigProvider: notifyConfigProvider, dappsMetadataStore: dappsMetadataStore) + let notifySubscribeRequester = NotifySubscribeRequester(keyserverURL: keyserverURL, networkingInteractor: networkInteractor, identityClient: identityClient, logger: logger, kms: kms, webDidResolver: webDidResolver, notifyConfigProvider: notifyConfigProvider) - let notifySubscribeResponseSubscriber = NotifySubscribeResponseSubscriber(networkingInteractor: networkInteractor, kms: kms, logger: logger, groupKeychainStorage: groupKeychainStorage, notifyStorage: notifyStorage, dappsMetadataStore: dappsMetadataStore, notifyConfigProvider: notifyConfigProvider) + 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) @@ -58,7 +60,8 @@ public struct NotifyClientFactory { let subscriptionsAutoUpdater = SubscriptionsAutoUpdater(notifyUpdateRequester: notifyUpdateRequester, logger: logger, notifyStorage: notifyStorage) - let notifyWatchSubscriptionsRequester = NotifyWatchSubscriptionsRequester(keyserverURL: keyserverURL, networkingInteractor: networkInteractor, identityClient: identityClient, logger: logger, kms: kms, webDidResolver: webDidResolver, notifyHost: notifyHost) + let notifyWatcherAgreementKeysProvider = NotifyWatcherAgreementKeysProvider(kms: kms) + let notifyWatchSubscriptionsRequester = NotifyWatchSubscriptionsRequester(keyserverURL: keyserverURL, networkingInteractor: networkInteractor, identityClient: identityClient, logger: logger, webDidResolver: webDidResolver, notifyAccountProvider: notifyAccountProvider, notifyWatcherAgreementKeysProvider: notifyWatcherAgreementKeysProvider, notifyHost: notifyHost) let notifySubscriptionsBuilder = NotifySubscriptionsBuilder(notifyConfigProvider: notifyConfigProvider) let notifyWatchSubscriptionsResponseSubscriber = NotifyWatchSubscriptionsResponseSubscriber(networkingInteractor: networkInteractor, kms: kms, logger: logger, notifyStorage: notifyStorage, groupKeychainStorage: groupKeychainStorage, notifySubscriptionsBuilder: notifySubscriptionsBuilder) let notifySubscriptionsChangedRequestSubscriber = NotifySubscriptionsChangedRequestSubscriber(keyserver: keyserverURL, networkingInteractor: networkInteractor, kms: kms, identityClient: identityClient, logger: logger, groupKeychainStorage: groupKeychainStorage, notifyStorage: notifyStorage, notifySubscriptionsBuilder: notifySubscriptionsBuilder) @@ -79,8 +82,10 @@ public struct NotifyClientFactory { notifySubscribeResponseSubscriber: notifySubscribeResponseSubscriber, notifyUpdateRequester: notifyUpdateRequester, notifyUpdateResponseSubscriber: notifyUpdateResponseSubscriber, + notifyAccountProvider: notifyAccountProvider, subscriptionsAutoUpdater: subscriptionsAutoUpdater, - notifyWatchSubscriptionsResponseSubscriber: notifyWatchSubscriptionsResponseSubscriber, + notifyWatchSubscriptionsResponseSubscriber: notifyWatchSubscriptionsResponseSubscriber, + notifyWatcherAgreementKeysProvider: notifyWatcherAgreementKeysProvider, notifySubscriptionsChangedRequestSubscriber: notifySubscriptionsChangedRequestSubscriber, subscriptionWatcher: subscriptionWatcher ) diff --git a/Sources/WalletConnectNotify/Client/Wallet/NotifyConfig.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifyConfig.swift new file mode 100644 index 000000000..b0e88b181 --- /dev/null +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifyConfig.swift @@ -0,0 +1,35 @@ +import Foundation + +struct NotifyConfig: Codable { + struct NotificationType: Codable { + let id: String + let name: String + let description: String + } + struct ImageUrl: Codable { + let sm: String? + let md: String? + let lg: String? + } + let id: String + let name: String + let homepage: String + let description: String + let dapp_url: String + let image_url: ImageUrl? + let notificationTypes: [NotificationType] + + var appDomain: String { + return URL(string: dapp_url)?.host ?? dapp_url + } + + var metadata: AppMetadata { + return AppMetadata( + name: name, + description: + description, + url: appDomain, + icons: [image_url?.sm, image_url?.md, image_url?.lg].compactMap { $0 } + ) + } +} diff --git a/Sources/WalletConnectNotify/Client/Wallet/NotifyConfigAPI.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifyConfigAPI.swift new file mode 100644 index 000000000..362fce2fb --- /dev/null +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifyConfigAPI.swift @@ -0,0 +1,33 @@ +import Foundation + +enum NotifyConfigAPI: HTTPService { + + var path: String { + return "/w3i/v1/notify-config" + } + + var method: HTTPMethod { + return .get + } + + var body: Data? { + return nil + } + + var queryParameters: [String : String]? { + switch self { + case .notifyDApps(let projectId, let appDomain): + return ["projectId": projectId, "appDomain": appDomain] + } + } + + var additionalHeaderFields: [String : String]? { + return nil + } + + var scheme: String { + return "https" + } + + case notifyDApps(projectId: String, appDomain: String) +} diff --git a/Sources/WalletConnectNotify/Client/Wallet/NotifyConfigProvider.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifyConfigProvider.swift index 99a292a6d..8b196f592 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/NotifyConfigProvider.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifyConfigProvider.swift @@ -1,29 +1,48 @@ - import Foundation actor NotifyConfigProvider { - enum Errors: Error { - case invalidUrl + + private let projectId: String + + private var cache: [String: NotifyConfig] = [:] + + init(projectId: String) { + self.projectId = projectId } - private var cache = [String: Set]() + func resolveNotifyConfig(appDomain: String) async -> NotifyConfig { + if let config = cache[appDomain] { + return config + } - func getSubscriptionScope(appDomain: String) async throws -> Set { - if let availableScope = cache[appDomain] { - return availableScope + do { + let httpClient = HTTPNetworkClient(host: "explorer-api.walletconnect.com") + let request = NotifyConfigAPI.notifyDApps(projectId: projectId, appDomain: appDomain) + let response = try await httpClient.request(NotifyConfigResponse.self, at: request) + let config = response.data + cache[appDomain] = config + return config + } catch { + return emptyConfig(appDomain: appDomain) } - guard let notifyConfigUrl = URL(string: "https://\(appDomain)/.well-known/wc-notify-config.json") else { throw Errors.invalidUrl } - let (data, _) = try await URLSession.shared.data(from: notifyConfigUrl) - let config = try JSONDecoder().decode(NotificationConfig.self, from: data) - let availableScope = Set(config.types) - cache[appDomain] = availableScope - return availableScope + } +} + +private extension NotifyConfigProvider { + + struct NotifyConfigResponse: Codable { + let data: NotifyConfig } - func getMetadata(appDomain: String) async throws -> AppMetadata { - guard let notifyConfigUrl = URL(string: "https://\(appDomain)/.well-known/wc-notify-config.json") else { throw Errors.invalidUrl } - let (data, _) = try await URLSession.shared.data(from: notifyConfigUrl) - let config = try JSONDecoder().decode(NotificationConfig.self, from: data) - return AppMetadata(name: config.name, description: config.description, url: appDomain, icons: config.icons) + func emptyConfig(appDomain: String) -> NotifyConfig { + return NotifyConfig( + id: UUID().uuidString, + name: appDomain, + homepage: "https://\(appDomain)", + description: "", + dapp_url: "https://\(appDomain)", + image_url: nil, + notificationTypes: [] + ) } } diff --git a/Sources/WalletConnectNotify/Client/Wallet/NotifyIdentityService.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifyIdentityService.swift index fc8091317..cf7b39101 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/NotifyIdentityService.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifyIdentityService.swift @@ -13,13 +13,16 @@ final class NotifyIdentityService { } public func register(account: Account, domain: String, isLimited: Bool, onSign: @escaping SigningCallback) async throws { - let statement = makeStatement(isLimited: isLimited, domain: domain, keyserverHost: keyserverURL.host!) - let pubKey = try await identityClient.register(account: account, + let statement = makeStatement(isLimited: isLimited) + _ = try await identityClient.register(account: account, domain: domain, statement: statement, resources: [keyserverURL.absoluteString], onSign: onSign) - logger.debug("Did register an account: \(account)") + } + + public func unregister(account: Account) async throws { + try await identityClient.unregister(account: account) } func isIdentityRegistered(account: Account) -> Bool { @@ -29,12 +32,12 @@ final class NotifyIdentityService { private extension NotifyIdentityService { - func makeStatement(isLimited: Bool, domain: String, keyserverHost: String) -> String { + func makeStatement(isLimited: Bool) -> String { switch isLimited { case true: - return "I further authorize this DAPP to send and receive messages on my behalf for this domain and manage my identity at \(keyserverHost)." + return "I further authorize this app to send and receive messages on my behalf for THIS domain using my WalletConnect identity. Read more at https://walletconnect.com/identity" case false: - return "I further authorize this WALLET to send and receive messages on my behalf for ALL domains and manage my identity at \(keyserverHost)." + return "I further authorize this app to send and receive messages on my behalf for ALL domains using my WalletConnect identity. Read more at https://walletconnect.com/identity" } } } diff --git a/Sources/WalletConnectNotify/Client/Wallet/NotifyResubscribeService.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifyResubscribeService.swift index 80fbd12ec..18ee0048d 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/NotifyResubscribeService.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifyResubscribeService.swift @@ -16,11 +16,11 @@ final class NotifyResubscribeService { setUpResubscription() } - func setUpResubscription() { + private func setUpResubscription() { networkInteractor.socketConnectionStatusPublisher .sink { [unowned self] status in guard status == .connected else { return } - let topics = notifyStorage.getSubscriptions().map{$0.topic} + let topics = notifyStorage.getAllSubscriptions().map { $0.topic } logger.debug("Resubscribing to notify subscription topics: \(topics)", properties: ["topics": topics.joined(separator: ", ")]) Task(priority: .high) { try await networkInteractor.batchSubscribe(topics: topics) diff --git a/Sources/WalletConnectNotify/Client/Wallet/NotifyStorage.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifyStorage.swift index f08498f21..a7e2c92b8 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/NotifyStorage.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifyStorage.swift @@ -2,10 +2,12 @@ import Foundation import Combine protocol NotifyStoring { - func getSubscriptions() -> [NotifySubscription] + func getAllSubscriptions() -> [NotifySubscription] + func getSubscriptions(account: Account) -> [NotifySubscription] func getSubscription(topic: String) -> NotifySubscription? func setSubscription(_ subscription: NotifySubscription) async throws func deleteSubscription(topic: String) async throws + func clearDatabase(account: Account) } final class NotifyStorage: NotifyStoring { @@ -21,6 +23,8 @@ final class NotifyStorage: NotifyStoring { private let subscriptionsSubject = PassthroughSubject<[NotifySubscription], Never>() private let messagesSubject = PassthroughSubject<[NotifyMessageRecord], Never>() + private let accountProvider: NotifyAccountProvider + var newSubscriptionPublisher: AnyPublisher { return newSubscriptionSubject.eraseToAnyPublisher() } @@ -37,23 +41,24 @@ final class NotifyStorage: NotifyStoring { return subscriptionsSubject.eraseToAnyPublisher() } - var messagesPublisher: AnyPublisher<[NotifyMessageRecord], Never> { - return messagesSubject.eraseToAnyPublisher() - } - - init(subscriptionStore: KeyedDatabase, messagesStore: KeyedDatabase) { + init(subscriptionStore: KeyedDatabase, messagesStore: KeyedDatabase, accountProvider: NotifyAccountProvider) { self.subscriptionStore = subscriptionStore self.messagesStore = messagesStore + self.accountProvider = accountProvider setupSubscriptions() } // MARK: Subscriptions - func getSubscriptions() -> [NotifySubscription] { + func getAllSubscriptions() -> [NotifySubscription] { return subscriptionStore.getAll() } + func getSubscriptions(account: Account) -> [NotifySubscription] { + return subscriptionStore.getAll(for: account.absoluteString) + } + func getSubscription(topic: String) -> NotifySubscription? { return subscriptionStore.getAll().first(where: { $0.topic == topic }) } @@ -64,11 +69,7 @@ final class NotifyStorage: NotifyStoring { } func replaceAllSubscriptions(_ subscriptions: [NotifySubscription], account: Account) { - subscriptionStore.deleteAll(for: account.absoluteString) - // todo - compare old with new = delete messages for removed subscriptions - //messages for new subscriptions are not required - subscriptionStore.set(elements: subscriptions, for: account.absoluteString) - subscriptionsSubject.send(subscriptions) + subscriptionStore.replace(elements: subscriptions, for: account.absoluteString) } func deleteSubscription(topic: String) throws { @@ -79,6 +80,13 @@ final class NotifyStorage: NotifyStoring { deleteSubscriptionSubject.send(topic) } + func clearDatabase(account: Account) { + for subscription in getSubscriptions(account: account) { + deleteMessages(topic: subscription.topic) + } + subscriptionStore.deleteAll(for: account.absoluteString) + } + 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) @@ -89,7 +97,7 @@ final class NotifyStorage: NotifyStoring { // MARK: Messages func messagesPublisher(topic: String) -> AnyPublisher<[NotifyMessageRecord], Never> { - return messagesPublisher + return messagesSubject .map { $0.filter { $0.topic == topic } } .eraseToAnyPublisher() } @@ -125,7 +133,8 @@ private extension NotifyStorage { } subscriptionStore.onUpdate = { [unowned self] in - subscriptionsSubject.send(subscriptionStore.getAll()) + guard let account = try? accountProvider.getCurrentAccount() else { return } + subscriptionsSubject.send(getSubscriptions(account: account)) } } } diff --git a/Sources/WalletConnectNotify/Client/Wallet/NotifySubscriptionsBuilder.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifySubscriptionsBuilder.swift index fb1d0a325..d2c9cd582 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/NotifySubscriptionsBuilder.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifySubscriptionsBuilder.swift @@ -11,27 +11,37 @@ class NotifySubscriptionsBuilder { var result = [NotifySubscription]() for subscription in notifyServerSubscriptions { - let scope = try await buildScope(selectedScope: subscription.scope, appDomain: subscription.appDomain) - guard let metadata = try? await notifyConfigProvider.getMetadata(appDomain: subscription.appDomain), - let topic = try? SymmetricKey(hex: subscription.symKey).derivedTopic() else { continue } - - let notifySubscription = NotifySubscription(topic: topic, - account: subscription.account, - relay: RelayProtocolOptions(protocol: "irn", data: nil), - metadata: metadata, - scope: scope, - expiry: subscription.expiry, - symKey: subscription.symKey) - result.append(notifySubscription) + let config = await notifyConfigProvider.resolveNotifyConfig(appDomain: subscription.appDomain) + + do { + let topic = try SymmetricKey(hex: subscription.symKey).derivedTopic() + let scope = try await buildScope(selectedScope: subscription.scope, availableScope: config.notificationTypes) + + result.append(NotifySubscription( + topic: topic, + account: subscription.account, + relay: RelayProtocolOptions(protocol: "irn", data: nil), + metadata: config.metadata, + scope: scope, + expiry: subscription.expiry, + symKey: subscription.symKey + )) + } catch { + continue + } } return result } - private func buildScope(selectedScope: [String], appDomain: String) async throws -> [String: ScopeValue] { - let availableScope = try await notifyConfigProvider.getSubscriptionScope(appDomain: appDomain) + private func buildScope(selectedScope: [String], availableScope: [NotifyConfig.NotificationType]) async throws -> [String: ScopeValue] { return availableScope.reduce(into: [:]) { - $0[$1.name] = ScopeValue(description: $1.description, enabled: selectedScope.contains($1.name)) + $0[$1.id] = ScopeValue( + id: $1.id, + name: $1.name, + description: $1.description, + enabled: selectedScope.contains($1.id) + ) } } } diff --git a/Sources/WalletConnectNotify/Client/Wallet/NotifyWatcherAgreementKeysProvider.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifyWatcherAgreementKeysProvider.swift new file mode 100644 index 000000000..5789f26c3 --- /dev/null +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifyWatcherAgreementKeysProvider.swift @@ -0,0 +1,54 @@ +import Foundation + +final class NotifyWatcherAgreementKeysProvider { + + private let kms: KeyManagementServiceProtocol + + init(kms: KeyManagementServiceProtocol) { + self.kms = kms + } + + func generateAgreementKeysIfNeeded(notifyServerPublicKey: AgreementPublicKey, account: Account) throws -> (responseTopic: String, selfPubKeyY: Data) { + + let keyYStorageKey = storageKey(account: account) + + if + let responseTopic = kms.getTopic(for: keyYStorageKey), + let agreement = kms.getAgreementSecret(for: responseTopic), + let recoveredAgreement = try? kms.performKeyAgreement( + selfPublicKey: agreement.publicKey, + peerPublicKey: notifyServerPublicKey.hexRepresentation + ), agreement == recoveredAgreement + { + return (responseTopic: responseTopic, selfPubKeyY: agreement.publicKey.rawRepresentation) + } + else { + let selfPubKeyY = try kms.createX25519KeyPair() + let watchSubscriptionsTopic = notifyServerPublicKey.rawRepresentation.sha256().toHexString() + + let agreementKeys = try kms.performKeyAgreement(selfPublicKey: selfPubKeyY, peerPublicKey: notifyServerPublicKey.hexRepresentation) + + try kms.setSymmetricKey(agreementKeys.sharedKey, for: watchSubscriptionsTopic) + let responseTopic = agreementKeys.derivedTopic() + + try kms.setAgreementSecret(agreementKeys, topic: responseTopic) + + // save for later under dapp's account + pub key + try kms.setTopic(responseTopic, for: keyYStorageKey) + + return (responseTopic: responseTopic, selfPubKeyY: selfPubKeyY.rawRepresentation) + } + } + + func removeAgreement(account: Account) { + let keyYStorageKey = storageKey(account: account) + kms.deleteTopic(for: keyYStorageKey) + } +} + +private extension NotifyWatcherAgreementKeysProvider { + + func storageKey(account: Account) -> String { + return "watchSubscriptionResponseTopic_\(account.absoluteString)" + } +} diff --git a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifySubscriptionsChanged/NotifySubscriptionsChangedRequestSubscriber.swift b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifySubscriptionsChanged/NotifySubscriptionsChangedRequestSubscriber.swift index 3c3f9e48a..95d92a0a1 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifySubscriptionsChanged/NotifySubscriptionsChangedRequestSubscriber.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifySubscriptionsChanged/NotifySubscriptionsChangedRequestSubscriber.swift @@ -43,13 +43,18 @@ class NotifySubscriptionsChangedRequestSubscriber { let (jwtPayload, _) = try NotifySubscriptionsChangedRequestPayload.decodeAndVerify(from: payload.request) let account = jwtPayload.account - // todo varify signature with notify server diddoc authentication key + // TODO: varify signature with notify server diddoc authentication key - let oldSubscriptions = notifyStorage.getSubscriptions() + let oldSubscriptions = notifyStorage.getSubscriptions(account: account) let newSubscriptions = try await notifySubscriptionsBuilder.buildSubscriptions(jwtPayload.subscriptions) - logger.debug("number of subscriptions: \(newSubscriptions.count)") + + try Task.checkCancellation() + + let subscriptions = oldSubscriptions.difference(from: newSubscriptions) + + logger.debug("Received: \(newSubscriptions.count), changed: \(subscriptions.count)") - guard newSubscriptions != oldSubscriptions else {return} + guard subscriptions.count > 0 else { return } notifyStorage.replaceAllSubscriptions(newSubscriptions, account: account) diff --git a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyWatchSubscriptions/NotifyWatchSubscriptionsRequester.swift b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyWatchSubscriptions/NotifyWatchSubscriptionsRequester.swift index 3d90c7d56..72702c588 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyWatchSubscriptions/NotifyWatchSubscriptionsRequester.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyWatchSubscriptions/NotifyWatchSubscriptionsRequester.swift @@ -2,7 +2,6 @@ import Foundation import Combine protocol NotifyWatchSubscriptionsRequesting { - func setAccount(_ account: Account) func watchSubscriptions() async throws } @@ -11,37 +10,34 @@ class NotifyWatchSubscriptionsRequester: NotifyWatchSubscriptionsRequesting { private let keyserverURL: URL private let identityClient: IdentityClient private let networkingInteractor: NetworkInteracting - private let kms: KeyManagementService private let logger: ConsoleLogging private let webDidResolver: NotifyWebDidResolver + private let notifyAccountProvider: NotifyAccountProvider + private let notifyWatcherAgreementKeysProvider: NotifyWatcherAgreementKeysProvider private let notifyHost: String - private var account: Account? private var publishers = Set() init(keyserverURL: URL, networkingInteractor: NetworkInteracting, identityClient: IdentityClient, logger: ConsoleLogging, - kms: KeyManagementService, webDidResolver: NotifyWebDidResolver, + notifyAccountProvider: NotifyAccountProvider, + notifyWatcherAgreementKeysProvider: NotifyWatcherAgreementKeysProvider, notifyHost: String ) { self.keyserverURL = keyserverURL self.identityClient = identityClient self.networkingInteractor = networkingInteractor self.logger = logger - self.kms = kms self.webDidResolver = webDidResolver + self.notifyAccountProvider = notifyAccountProvider + self.notifyWatcherAgreementKeysProvider = notifyWatcherAgreementKeysProvider self.notifyHost = notifyHost } - func setAccount(_ account: Account) { - self.account = account - } - func watchSubscriptions() async throws { - - guard let account = account else { return } + let account = try notifyAccountProvider.getCurrentAccount() logger.debug("Watching subscriptions") @@ -50,20 +46,16 @@ class NotifyWatchSubscriptionsRequester: NotifyWatchSubscriptionsRequesting { let notifyServerAuthenticationDidKey = DIDKey(rawData: notifyServerAuthenticationKey) let watchSubscriptionsTopic = notifyServerPublicKey.rawRepresentation.sha256().toHexString() - let (responseTopic, selfPubKeyY) = try generateAgreementKeysIfNeeded(notifyServerPublicKey: notifyServerPublicKey, account: account) - - + let (responseTopic, selfPubKeyY) = try notifyWatcherAgreementKeysProvider.generateAgreementKeysIfNeeded(notifyServerPublicKey: notifyServerPublicKey, account: account) logger.debug("setting symm key for response topic \(responseTopic)") let protocolMethod = NotifyWatchSubscriptionsProtocolMethod() - let watchSubscriptionsAuthWrapper = try await createJWTWrapper( notifyServerAuthenticationDidKey: notifyServerAuthenticationDidKey, subscriptionAccount: account) - let request = RPCRequest(method: protocolMethod.method, params: watchSubscriptionsAuthWrapper) logger.debug("Subscribing to response topic: \(responseTopic)") @@ -73,32 +65,6 @@ class NotifyWatchSubscriptionsRequester: NotifyWatchSubscriptionsRequesting { try await networkingInteractor.request(request, topic: watchSubscriptionsTopic, protocolMethod: protocolMethod, envelopeType: .type1(pubKey: selfPubKeyY)) } - - private func generateAgreementKeysIfNeeded(notifyServerPublicKey: AgreementPublicKey, account: Account) throws -> (responseTopic: String, selfPubKeyY: Data) { - - let keyYStorageKey = "\(account)_\(notifyServerPublicKey.hexRepresentation)" - - if let responseTopic = kms.getTopic(for: keyYStorageKey), - let selfPubKeyY = kms.getAgreementSecret(for: responseTopic)?.publicKey { - return (responseTopic: responseTopic, selfPubKeyY: selfPubKeyY.rawRepresentation) - } else { - let selfPubKeyY = try kms.createX25519KeyPair() - let watchSubscriptionsTopic = notifyServerPublicKey.rawRepresentation.sha256().toHexString() - - let agreementKeys = try kms.performKeyAgreement(selfPublicKey: selfPubKeyY, peerPublicKey: notifyServerPublicKey.hexRepresentation) - - try kms.setSymmetricKey(agreementKeys.sharedKey, for: watchSubscriptionsTopic) - let responseTopic = agreementKeys.derivedTopic() - - try kms.setAgreementSecret(agreementKeys, topic: responseTopic) - - // save for later under dapp's account + pub key - try kms.setTopic(responseTopic, for: keyYStorageKey) - - return (responseTopic: responseTopic, selfPubKeyY: selfPubKeyY.rawRepresentation) - } - } - private func createJWTWrapper(notifyServerAuthenticationDidKey: DIDKey, subscriptionAccount: Account) async throws -> NotifyWatchSubscriptionsPayload.Wrapper { let jwtPayload = NotifyWatchSubscriptionsPayload(notifyServerAuthenticationKey: notifyServerAuthenticationDidKey, keyserver: keyserverURL, subscriptionAccount: subscriptionAccount) return try identityClient.signAndCreateWrapper( @@ -110,8 +76,6 @@ class NotifyWatchSubscriptionsRequester: NotifyWatchSubscriptionsRequesting { #if DEBUG class MockNotifyWatchSubscriptionsRequester: NotifyWatchSubscriptionsRequesting { - func setAccount(_ account: WalletConnectUtils.Account) {} - var onWatchSubscriptions: (() -> Void)? func watchSubscriptions() async throws { diff --git a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyWatchSubscriptions/NotifyWatchSubscriptionsResponseSubscriber.swift b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyWatchSubscriptions/NotifyWatchSubscriptionsResponseSubscriber.swift index e96c8c614..298932dec 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyWatchSubscriptions/NotifyWatchSubscriptionsResponseSubscriber.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyWatchSubscriptions/NotifyWatchSubscriptionsResponseSubscriber.swift @@ -38,17 +38,19 @@ class NotifyWatchSubscriptionsResponseSubscriber { let (watchSubscriptionPayloadRequest, _) = try NotifyWatchSubscriptionsPayload.decodeAndVerify(from: payload.request) let account = watchSubscriptionPayloadRequest.subscriptionAccount - // todo varify signature with notify server diddoc authentication key + // TODO: varify signature with notify server diddoc authentication key - let oldSubscriptions = notifyStorage.getSubscriptions() + let oldSubscriptions = notifyStorage.getSubscriptions(account: account) let newSubscriptions = try await notifySubscriptionsBuilder.buildSubscriptions(responsePayload.subscriptions) try Task.checkCancellation() - logger.debug("number of subscriptions: \(newSubscriptions.count)") + let subscriptions = oldSubscriptions.difference(from: newSubscriptions) - guard newSubscriptions != oldSubscriptions else {return} - // todo: unsubscribe for oldSubscriptions topics that are not included in new subscriptions + 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 { diff --git a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_pushSubscribe/NotifySubscribeRequester.swift b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_pushSubscribe/NotifySubscribeRequester.swift index 6601d77b7..ad5c0cee0 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_pushSubscribe/NotifySubscribeRequester.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_pushSubscribe/NotifySubscribeRequester.swift @@ -13,7 +13,6 @@ class NotifySubscribeRequester { private let kms: KeyManagementService private let logger: ConsoleLogging private let webDidResolver: NotifyWebDidResolver - private let dappsMetadataStore: CodableStore private let notifyConfigProvider: NotifyConfigProvider init(keyserverURL: URL, @@ -22,8 +21,7 @@ class NotifySubscribeRequester { logger: ConsoleLogging, kms: KeyManagementService, webDidResolver: NotifyWebDidResolver, - notifyConfigProvider: NotifyConfigProvider, - dappsMetadataStore: CodableStore + notifyConfigProvider: NotifyConfigProvider ) { self.keyserverURL = keyserverURL self.identityClient = identityClient @@ -31,7 +29,6 @@ class NotifySubscribeRequester { self.logger = logger self.kms = kms self.webDidResolver = webDidResolver - self.dappsMetadataStore = dappsMetadataStore self.notifyConfigProvider = notifyConfigProvider } @@ -39,16 +36,14 @@ class NotifySubscribeRequester { logger.debug("Subscribing for Notify, dappUrl: \(appDomain)") - let metadata = try await notifyConfigProvider.getMetadata(appDomain: appDomain) + let config = try await notifyConfigProvider.resolveNotifyConfig(appDomain: appDomain) - let peerPublicKey = try await webDidResolver.resolveAgreementKey(domain: metadata.url) + let peerPublicKey = try await webDidResolver.resolveAgreementKey(domain: appDomain) let subscribeTopic = peerPublicKey.rawRepresentation.sha256().toHexString() let keysY = try generateAgreementKeys(peerPublicKey: peerPublicKey) let responseTopic = keysY.derivedTopic() - - dappsMetadataStore.set(metadata, forKey: responseTopic) try kms.setSymmetricKey(keysY.sharedKey, for: subscribeTopic) try kms.setAgreementSecret(keysY, topic: responseTopic) @@ -80,10 +75,15 @@ class NotifySubscribeRequester { } private func createJWTWrapper(dappPubKey: DIDKey, subscriptionAccount: Account, appDomain: String) async throws -> NotifySubscriptionPayload.Wrapper { - let types = try await notifyConfigProvider.getSubscriptionScope(appDomain: appDomain) - let scope = types.map{$0.name}.joined(separator: " ") + let config = await notifyConfigProvider.resolveNotifyConfig(appDomain: appDomain) let app = DIDWeb(host: appDomain) - let jwtPayload = NotifySubscriptionPayload(dappPubKey: dappPubKey, keyserver: keyserverURL, subscriptionAccount: subscriptionAccount, app: app, scope: scope) + let jwtPayload = NotifySubscriptionPayload( + dappPubKey: dappPubKey, + keyserver: keyserverURL, + subscriptionAccount: subscriptionAccount, + app: app, + scope: config.notificationTypes.map { $0.id }.joined(separator: " ") + ) return try identityClient.signAndCreateWrapper( payload: jwtPayload, account: subscriptionAccount diff --git a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_pushSubscribe/NotifySubscribeResponseSubscriber.swift b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_pushSubscribe/NotifySubscribeResponseSubscriber.swift index 5dc224b6c..d8aa56a39 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_pushSubscribe/NotifySubscribeResponseSubscriber.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_pushSubscribe/NotifySubscribeResponseSubscriber.swift @@ -12,7 +12,6 @@ class NotifySubscribeResponseSubscriber { private let logger: ConsoleLogging private let notifyStorage: NotifyStorage private let groupKeychainStorage: KeychainStorageProtocol - private let dappsMetadataStore: CodableStore private let notifyConfigProvider: NotifyConfigProvider init(networkingInteractor: NetworkInteracting, @@ -20,7 +19,6 @@ class NotifySubscribeResponseSubscriber { logger: ConsoleLogging, groupKeychainStorage: KeychainStorageProtocol, notifyStorage: NotifyStorage, - dappsMetadataStore: CodableStore, notifyConfigProvider: NotifyConfigProvider ) { self.networkingInteractor = networkingInteractor @@ -28,7 +26,6 @@ class NotifySubscribeResponseSubscriber { self.logger = logger self.groupKeychainStorage = groupKeychainStorage self.notifyStorage = notifyStorage - self.dappsMetadataStore = dappsMetadataStore self.notifyConfigProvider = notifyConfigProvider subscribeForSubscriptionResponse() } diff --git a/Sources/WalletConnectNotify/Client/Wallet/SubscriptionWatcher.swift b/Sources/WalletConnectNotify/Client/Wallet/SubscriptionWatcher.swift index a98d70a05..00ae7bebc 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/SubscriptionWatcher.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/SubscriptionWatcher.swift @@ -26,26 +26,23 @@ class SubscriptionWatcher { self.notificationCenter = notificationCenter } - 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() - } - } - } + deinit { stop() } - func setAccount(_ account: Account) { - notifyWatchSubscriptionsRequester.setAccount(account) + func start() { setupTimer() watchAppLifecycle() watchSubscriptions() } + func stop() { + timerCancellable?.cancel() + appLifecycleCancellable?.cancel() + watchSubscriptionsWorkItem?.cancel() + } +} + +internal extension SubscriptionWatcher { + func watchSubscriptions() { watchSubscriptionsWorkItem?.cancel() @@ -71,4 +68,17 @@ class SubscriptionWatcher { } #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() + } + } + } } diff --git a/Sources/WalletConnectNotify/Client/Wallet/SubscriptionsAutoUpdater.swift b/Sources/WalletConnectNotify/Client/Wallet/SubscriptionsAutoUpdater.swift index 4eff66e89..5aca863e3 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/SubscriptionsAutoUpdater.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/SubscriptionsAutoUpdater.swift @@ -16,7 +16,7 @@ class SubscriptionsAutoUpdater { } private func updateSubscriptionsIfNeeded() { - for subscription in notifyStorage.getSubscriptions() { + for subscription in notifyStorage.getAllSubscriptions() { if shouldUpdate(subscription: subscription) { let scope = Set(subscription.scope.filter{ $0.value.enabled == true }.keys) let topic = subscription.topic diff --git a/Sources/WalletConnectNotify/NotifyConfig.swift b/Sources/WalletConnectNotify/Notify+Config.swift similarity index 100% rename from Sources/WalletConnectNotify/NotifyConfig.swift rename to Sources/WalletConnectNotify/Notify+Config.swift diff --git a/Sources/WalletConnectNotify/Notify.swift b/Sources/WalletConnectNotify/Notify.swift index 44c178032..039c139d6 100644 --- a/Sources/WalletConnectNotify/Notify.swift +++ b/Sources/WalletConnectNotify/Notify.swift @@ -7,6 +7,7 @@ public class Notify { } Push.configure(pushHost: config.pushHost, environment: config.environment) return NotifyClientFactory.create( + projectId: Networking.projectId, groupIdentifier: config.groupIdentifier, networkInteractor: Networking.interactor, pairingRegisterer: Pair.registerer, diff --git a/Sources/WalletConnectNotify/NotifyStorageIdntifiers.swift b/Sources/WalletConnectNotify/NotifyStorageIdntifiers.swift index fb1b21c53..b68272b25 100644 --- a/Sources/WalletConnectNotify/NotifyStorageIdntifiers.swift +++ b/Sources/WalletConnectNotify/NotifyStorageIdntifiers.swift @@ -4,6 +4,5 @@ enum NotifyStorageIdntifiers { static let notifySubscription = "com.walletconnect.notify.notifySubscription" static let notifyMessagesRecords = "com.walletconnect.sdk.notifyMessagesRecords" - static let dappsMetadataStore = "com.walletconnect.sdk.dappsMetadataStore" static let coldStartStore = "com.walletconnect.sdk.coldStartStore" } diff --git a/Sources/WalletConnectNotify/Types/DataStructures/NotificationConfig.swift b/Sources/WalletConnectNotify/Types/DataStructures/NotificationConfig.swift deleted file mode 100644 index f5d138793..000000000 --- a/Sources/WalletConnectNotify/Types/DataStructures/NotificationConfig.swift +++ /dev/null @@ -1,10 +0,0 @@ - -import Foundation - -struct NotificationConfig: Codable { - let schemaVersion: Int - let name: String - let description: String - let icons: [String] - let types: [NotificationType] -} diff --git a/Sources/WalletConnectNotify/Types/DataStructures/NotificationType.swift b/Sources/WalletConnectNotify/Types/DataStructures/NotificationType.swift deleted file mode 100644 index b741c4a2f..000000000 --- a/Sources/WalletConnectNotify/Types/DataStructures/NotificationType.swift +++ /dev/null @@ -1,7 +0,0 @@ - -import Foundation - -public struct NotificationType: Codable, Hashable { - let name: String - let description: String -} diff --git a/Sources/WalletConnectNotify/Types/DataStructures/NotifySubscription.swift b/Sources/WalletConnectNotify/Types/DataStructures/NotifySubscription.swift index 38676d60a..cc47fce34 100644 --- a/Sources/WalletConnectNotify/Types/DataStructures/NotifySubscription.swift +++ b/Sources/WalletConnectNotify/Types/DataStructures/NotifySubscription.swift @@ -15,10 +15,14 @@ public struct NotifySubscription: DatabaseObject { } public struct ScopeValue: Codable, Equatable { + public let id: String + public let name: String public let description: String public let enabled: Bool - public init(description: String, enabled: Bool) { + public init(id: String, name: String, description: String, enabled: Bool) { + self.id = id + self.name = name self.description = description self.enabled = enabled } diff --git a/Sources/WalletConnectNotify/Types/JWTPayloads/notify_watch_subscriptions/NotifyWatchSubscriptionsPayload.swift b/Sources/WalletConnectNotify/Types/JWTPayloads/notify_watch_subscriptions/NotifyWatchSubscriptionsPayload.swift index fd92b9c79..0fad6272b 100644 --- a/Sources/WalletConnectNotify/Types/JWTPayloads/notify_watch_subscriptions/NotifyWatchSubscriptionsPayload.swift +++ b/Sources/WalletConnectNotify/Types/JWTPayloads/notify_watch_subscriptions/NotifyWatchSubscriptionsPayload.swift @@ -17,10 +17,30 @@ struct NotifyWatchSubscriptionsPayload: JWTClaimsCodable { let aud: String /// Blockchain account that notify subscription has been proposed for -`did:pkh` let sub: String + /// Dapp domain url + let app: String? static var action: String? { return "notify_watch_subscriptions" } + + // Note: - Overriding `encode(to encoder: Encoder)` implementation to force null app encoding + + enum CodingKeys: CodingKey { + case iat, exp, ksu, act, iss, aud, sub, app + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: NotifyWatchSubscriptionsPayload.Claims.CodingKeys.self) + try container.encode(self.iat, forKey: NotifyWatchSubscriptionsPayload.Claims.CodingKeys.iat) + try container.encode(self.exp, forKey: NotifyWatchSubscriptionsPayload.Claims.CodingKeys.exp) + try container.encode(self.ksu, forKey: NotifyWatchSubscriptionsPayload.Claims.CodingKeys.ksu) + try container.encodeIfPresent(self.act, forKey: NotifyWatchSubscriptionsPayload.Claims.CodingKeys.act) + try container.encode(self.iss, forKey: NotifyWatchSubscriptionsPayload.Claims.CodingKeys.iss) + try container.encode(self.aud, forKey: NotifyWatchSubscriptionsPayload.Claims.CodingKeys.aud) + try container.encode(self.sub, forKey: NotifyWatchSubscriptionsPayload.Claims.CodingKeys.sub) + try container.encode(self.app, forKey: NotifyWatchSubscriptionsPayload.Claims.CodingKeys.app) + } } struct Wrapper: JWTWrapper { @@ -59,7 +79,8 @@ struct NotifyWatchSubscriptionsPayload: JWTClaimsCodable { act: Claims.action, iss: iss, aud: notifyServerIdentityKey.did(variant: .ED25519), - sub: subscriptionAccount.did + sub: subscriptionAccount.did, + app: nil ) } diff --git a/Sources/WalletConnectPush/PushClientFactory.swift b/Sources/WalletConnectPush/PushClientFactory.swift index 65c733886..fb2fb0d10 100644 --- a/Sources/WalletConnectPush/PushClientFactory.swift +++ b/Sources/WalletConnectPush/PushClientFactory.swift @@ -6,10 +6,12 @@ public struct PushClientFactory { environment: APNSEnvironment) -> PushClient { let keychainStorage = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk") + let keyValueStorage = UserDefaults.standard return PushClientFactory.create( projectId: projectId, pushHost: pushHost, + keyValueStorage: keyValueStorage, keychainStorage: keychainStorage, environment: environment) } @@ -17,6 +19,7 @@ public struct PushClientFactory { public static func create( projectId: String, pushHost: String, + keyValueStorage: KeyValueStorage, keychainStorage: KeychainStorageProtocol, environment: APNSEnvironment ) -> PushClient { @@ -28,7 +31,7 @@ public struct PushClientFactory { let logger = ConsoleLogger(prefix: "👂🏻", loggingLevel: .off) let httpClient = HTTPNetworkClient(host: pushHost, session: session) - let clientIdStorage = ClientIdStorage(keychain: keychainStorage) + let clientIdStorage = ClientIdStorage(defaults: keyValueStorage, keychain: keychainStorage, logger: logger) let pushAuthenticator = PushAuthenticator(clientIdStorage: clientIdStorage, pushHost: pushHost) diff --git a/Sources/WalletConnectRelay/ClientAuth/ClientIdStorage.swift b/Sources/WalletConnectRelay/ClientAuth/ClientIdStorage.swift index 2e3c9cd7f..272451b96 100644 --- a/Sources/WalletConnectRelay/ClientAuth/ClientIdStorage.swift +++ b/Sources/WalletConnectRelay/ClientAuth/ClientIdStorage.swift @@ -6,26 +6,89 @@ public protocol ClientIdStoring { } public struct ClientIdStorage: ClientIdStoring { - private let key = "com.walletconnect.iridium.client_id" + private let oldStorageKey = "com.walletconnect.iridium.client_id" + private let publicStorageKey = "com.walletconnect.iridium.client_id.public" + + private let defaults: KeyValueStorage private let keychain: KeychainStorageProtocol + private let logger: ConsoleLogging - public init(keychain: KeychainStorageProtocol) { + public init(defaults: KeyValueStorage, keychain: KeychainStorageProtocol, logger: ConsoleLogging) { + self.defaults = defaults self.keychain = keychain + self.logger = logger + + migrateIfNeeded() } public func getOrCreateKeyPair() throws -> SigningPrivateKey { do { - return try keychain.read(key: key) + let publicPart = try getPublicPart() + return try getPrivatePart(for: publicPart) } catch { let privateKey = SigningPrivateKey() - try keychain.add(privateKey, forKey: key) + try setPrivatePart(privateKey) + setPublicPart(privateKey.publicKey) return privateKey } } public func getClientId() throws -> String { - let privateKey: SigningPrivateKey = try keychain.read(key: key) - let pubKey = privateKey.publicKey.rawRepresentation - return DIDKey(rawData: pubKey).did(variant: .ED25519) + let pubKey = try getPublicPart() + let _ = try getPrivatePart(for: pubKey) + return DIDKey(rawData: pubKey.rawRepresentation).did(variant: .ED25519) + } +} + +private extension ClientIdStorage { + + enum Errors: Error { + case publicPartNotFound + case privatePartNotFound + } + + func migrateIfNeeded() { + guard let privateKey: SigningPrivateKey = try? keychain.read(key: oldStorageKey) else { + return + } + + do { + try setPrivatePart(privateKey) + setPublicPart(privateKey.publicKey) + try keychain.delete(key: oldStorageKey) + logger.debug("ClientID migrated") + } catch { + logger.debug("ClientID migration failed with: \(error.localizedDescription)") + } + } + + func getPublicPart() throws -> SigningPublicKey { + guard let data = defaults.data(forKey: publicStorageKey) else { + throw Errors.publicPartNotFound + } + return try SigningPublicKey(rawRepresentation: data) + } + + func setPublicPart(_ newValue: SigningPublicKey) { + defaults.set(newValue.rawRepresentation, forKey: publicStorageKey) + } + + func getPrivatePart(for publicPart: SigningPublicKey) throws -> SigningPrivateKey { + do { + return try keychain.read(key: publicPart.storageId) + } catch { + throw Errors.privatePartNotFound + } + } + + func setPrivatePart(_ newValue: SigningPrivateKey) throws { + try keychain.add(newValue, forKey: newValue.publicKey.storageId) + } +} + +private extension SigningPublicKey { + + var storageId: String { + return rawRepresentation.sha256().toHexString() } } diff --git a/Sources/WalletConnectRelay/PackageConfig.json b/Sources/WalletConnectRelay/PackageConfig.json index 40d0cfc51..52e73e014 100644 --- a/Sources/WalletConnectRelay/PackageConfig.json +++ b/Sources/WalletConnectRelay/PackageConfig.json @@ -1 +1 @@ -{"version": "1.8.7"} +{"version": "1.9.0"} diff --git a/Sources/WalletConnectRelay/RelayClientFactory.swift b/Sources/WalletConnectRelay/RelayClientFactory.swift index 6748811fb..48e7f766a 100644 --- a/Sources/WalletConnectRelay/RelayClientFactory.swift +++ b/Sources/WalletConnectRelay/RelayClientFactory.swift @@ -39,7 +39,7 @@ public struct RelayClientFactory { logger: ConsoleLogging ) -> RelayClient { - let clientIdStorage = ClientIdStorage(keychain: keychainStorage) + let clientIdStorage = ClientIdStorage(defaults: keyValueStorage, keychain: keychainStorage, logger: logger) let socketAuthenticator = ClientIdAuthenticator( clientIdStorage: clientIdStorage, diff --git a/Sources/WalletConnectSign/Services/App/AppProposeService.swift b/Sources/WalletConnectSign/Services/App/AppProposeService.swift index 541999a1a..a92b80bea 100644 --- a/Sources/WalletConnectSign/Services/App/AppProposeService.swift +++ b/Sources/WalletConnectSign/Services/App/AppProposeService.swift @@ -43,7 +43,7 @@ final class AppProposeService { relays: [relay], proposer: proposer, requiredNamespaces: namespaces, - optionalNamespaces: optionalNamespaces, + optionalNamespaces: optionalNamespaces ?? [:], sessionProperties: sessionProperties ) diff --git a/Sources/WalletConnectSign/Services/HistoryService.swift b/Sources/WalletConnectSign/Services/HistoryService.swift index 5a47a81d1..2a5974471 100644 --- a/Sources/WalletConnectSign/Services/HistoryService.swift +++ b/Sources/WalletConnectSign/Services/HistoryService.swift @@ -92,7 +92,7 @@ private extension HistoryService { pairingTopic: record.topic, proposer: proposal.proposer.metadata, requiredNamespaces: proposal.requiredNamespaces, - optionalNamespaces: proposal.optionalNamespaces, + optionalNamespaces: proposal.optionalNamespaces ?? [:], sessionProperties: proposal.sessionProperties, proposal: proposal ) diff --git a/Sources/WalletConnectSign/Types/Session/SessionProposal.swift b/Sources/WalletConnectSign/Types/Session/SessionProposal.swift index 9a10e3380..fa1ee979a 100644 --- a/Sources/WalletConnectSign/Types/Session/SessionProposal.swift +++ b/Sources/WalletConnectSign/Types/Session/SessionProposal.swift @@ -13,7 +13,7 @@ struct SessionProposal: Codable, Equatable { pairingTopic: pairingTopic, proposer: proposer.metadata, requiredNamespaces: requiredNamespaces, - optionalNamespaces: optionalNamespaces, + optionalNamespaces: optionalNamespaces ?? [:], sessionProperties: sessionProperties, proposal: self ) diff --git a/Sources/WalletConnectUtils/KeyedDatabase.swift b/Sources/WalletConnectUtils/KeyedDatabase.swift index c619d62d5..5677946ae 100644 --- a/Sources/WalletConnectUtils/KeyedDatabase.swift +++ b/Sources/WalletConnectUtils/KeyedDatabase.swift @@ -61,9 +61,20 @@ public class KeyedDatabase where Element: DatabaseObject { var map = index[key] ?? [:] for element in elements { - guard - map[element.databaseId] == nil else { continue } - map[element.databaseId] = element + map[element.databaseId] = element + } + + index[key] = map + + return true + } + + @discardableResult + public func replace(elements: [Element], for key: String) -> Bool { + var map: [String: Element] = [:] + + for element in elements { + map[element.databaseId] = element } index[key] = map diff --git a/Sources/WalletConnectVerify/OriginVerifier.swift b/Sources/WalletConnectVerify/OriginVerifier.swift index 10b403f81..0689088d2 100644 --- a/Sources/WalletConnectVerify/OriginVerifier.swift +++ b/Sources/WalletConnectVerify/OriginVerifier.swift @@ -5,15 +5,11 @@ public final class OriginVerifier { case registrationFailed } - private var verifyHost: String + private var verifyHost = "verify.walletconnect.com" /// The property is used to determine whether verify.walletconnect.org will be used /// in case verify.walletconnect.com doesn't respond for some reason (most likely due to being blocked in the user's location). private var fallback = false - - init(verifyHost: String) { - self.verifyHost = verifyHost - } - + func verifyOrigin(assertionId: String) async throws -> VerifyResponse { let sessionConfiguration = URLSessionConfiguration.default sessionConfiguration.timeoutIntervalForRequest = 5.0 @@ -27,7 +23,7 @@ public final class OriginVerifier { VerifyResponse.self, at: VerifyAPI.resolve(assertionId: assertionId) ) - guard let origin = response.origin else { + guard let _ = response.origin else { throw Errors.registrationFailed } return response diff --git a/Sources/WalletConnectVerify/VerifyClient.swift b/Sources/WalletConnectVerify/VerifyClient.swift index 37b74dfea..e528f2b60 100644 --- a/Sources/WalletConnectVerify/VerifyClient.swift +++ b/Sources/WalletConnectVerify/VerifyClient.swift @@ -14,16 +14,12 @@ public actor VerifyClient: VerifyClientProtocol { let originVerifier: OriginVerifier let assertionRegistrer: AssertionRegistrer let appAttestationRegistrer: AppAttestationRegistrer - - private let verifyHost: String init( - verifyHost: String, originVerifier: OriginVerifier, assertionRegistrer: AssertionRegistrer, appAttestationRegistrer: AppAttestationRegistrer ) { - self.verifyHost = verifyHost self.originVerifier = originVerifier self.assertionRegistrer = assertionRegistrer self.appAttestationRegistrer = appAttestationRegistrer @@ -41,21 +37,18 @@ public actor VerifyClient: VerifyClientProtocol { guard isScam == nil else { return VerifyContext( origin: origin, - validation: .scam, - verifyUrl: verifyHost + validation: .scam ) } if let origin, let originUrl = URL(string: origin), let domainUrl = URL(string: domain) { return VerifyContext( origin: origin, - validation: (originUrl.host == domainUrl.host) ? .valid : .invalid, - verifyUrl: verifyHost + validation: (originUrl.host == domainUrl.host) ? .valid : .invalid ) } else { return VerifyContext( origin: origin, - validation: .unknown, - verifyUrl: verifyHost + validation: .unknown ) } } @@ -75,7 +68,7 @@ public struct VerifyClientMock: VerifyClientProtocol { } public func createVerifyContext(origin: String?, domain: String, isScam: Bool?) -> VerifyContext { - return VerifyContext(origin: "domain.com", validation: .valid, verifyUrl: "verify.walletconnect.com") + return VerifyContext(origin: "domain.com", validation: .valid) } } diff --git a/Sources/WalletConnectVerify/VerifyClientFactory.swift b/Sources/WalletConnectVerify/VerifyClientFactory.swift index 1d00a6af1..8b230fc5f 100644 --- a/Sources/WalletConnectVerify/VerifyClientFactory.swift +++ b/Sources/WalletConnectVerify/VerifyClientFactory.swift @@ -1,8 +1,8 @@ import Foundation public class VerifyClientFactory { - public static func create(verifyHost: String = "verify.walletconnect.com") -> VerifyClient { - let originVerifier = OriginVerifier(verifyHost: verifyHost) + public static func create() -> VerifyClient { + let originVerifier = OriginVerifier() let assertionRegistrer = AssertionRegistrer() let logger = ConsoleLogger(loggingLevel: .off) let keyValueStorage = UserDefaults.standard @@ -18,7 +18,6 @@ public class VerifyClientFactory { keyAttestationService: keyAttestationService ) return VerifyClient( - verifyHost: verifyHost, originVerifier: originVerifier, assertionRegistrer: assertionRegistrer, appAttestationRegistrer: appAttestationRegistrer diff --git a/Sources/WalletConnectVerify/VerifyContext.swift b/Sources/WalletConnectVerify/VerifyContext.swift index 85382e090..e85613493 100644 --- a/Sources/WalletConnectVerify/VerifyContext.swift +++ b/Sources/WalletConnectVerify/VerifyContext.swift @@ -8,11 +8,9 @@ public struct VerifyContext: Equatable, Hashable, Codable { public let origin: String? public let validation: ValidationStatus - public let verifyUrl: String - public init(origin: String?, validation: ValidationStatus, verifyUrl: String) { + public init(origin: String?, validation: ValidationStatus) { self.origin = origin self.validation = validation - self.verifyUrl = verifyUrl } } diff --git a/Tests/CommonsTests/AnyCodableTests.swift b/Tests/CommonsTests/AnyCodableTests.swift index bce2de631..9fe96b88d 100644 --- a/Tests/CommonsTests/AnyCodableTests.swift +++ b/Tests/CommonsTests/AnyCodableTests.swift @@ -28,7 +28,7 @@ private struct SampleStruct: Codable, Equatable { SampleStruct( bool: true, int: 1337, - double: 13.37, + double: 13, string: "verystringwow", object: SubObject( string: "0xdeadbeef" @@ -40,7 +40,7 @@ private struct SampleStruct: Codable, Equatable { { "bool": true, "int": 1337, - "double": 13.37, + "double": 13, "string": "verystringwow", "object": { "string": "0xdeadbeef" @@ -52,7 +52,7 @@ private struct SampleStruct: Codable, Equatable { { "bool": ****, "int": 1337, - "double": 13.37, + "double": 13, "string": "verystringwow", } """.data(using: .utf8)! diff --git a/Tests/NotifyTests/Mocks/MockNotifyStoring.swift b/Tests/NotifyTests/Mocks/MockNotifyStoring.swift index 4f1be0991..bd773936c 100644 --- a/Tests/NotifyTests/Mocks/MockNotifyStoring.swift +++ b/Tests/NotifyTests/Mocks/MockNotifyStoring.swift @@ -2,20 +2,25 @@ import Foundation @testable import WalletConnectNotify class MockNotifyStoring: NotifyStoring { + var subscriptions: [NotifySubscription] init(subscriptions: [NotifySubscription]) { self.subscriptions = subscriptions } - func getSubscriptions() -> [NotifySubscription] { - return subscriptions + func getSubscriptions(account: Account) -> [NotifySubscription] { + return subscriptions.filter { $0.account == account } } func getSubscription(topic: String) -> NotifySubscription? { return subscriptions.first { $0.topic == topic } } + func getAllSubscriptions() -> [WalletConnectNotify.NotifySubscription] { + return subscriptions + } + func setSubscription(_ subscription: NotifySubscription) async throws { if let index = subscriptions.firstIndex(where: { $0.topic == subscription.topic }) { subscriptions[index] = subscription @@ -24,6 +29,10 @@ class MockNotifyStoring: NotifyStoring { } } + func clearDatabase(account: WalletConnectUtils.Account) { + subscriptions = subscriptions.filter { $0.account != account } + } + func deleteSubscription(topic: String) async throws { subscriptions.removeAll(where: { $0.topic == topic }) } diff --git a/Tests/NotifyTests/Stubs/NotifySubscription.swift b/Tests/NotifyTests/Stubs/NotifySubscription.swift index 7251c8477..3e9a1892e 100644 --- a/Tests/NotifyTests/Stubs/NotifySubscription.swift +++ b/Tests/NotifyTests/Stubs/NotifySubscription.swift @@ -13,7 +13,7 @@ extension NotifySubscription { account: account, relay: relay, metadata: metadata, - scope: ["test": ScopeValue(description: "desc", enabled: true)], + scope: ["test": ScopeValue(id: "id", name: "name", description: "desc", enabled: true)], expiry: expiry, symKey: symKey ) diff --git a/Tests/NotifyTests/SubscriptionWatcherTests.swift b/Tests/NotifyTests/SubscriptionWatcherTests.swift index 68549edae..e2d389759 100644 --- a/Tests/NotifyTests/SubscriptionWatcherTests.swift +++ b/Tests/NotifyTests/SubscriptionWatcherTests.swift @@ -16,9 +16,8 @@ class SubscriptionWatcherTests: XCTestCase { mockLogger = ConsoleLoggerMock() mockNotificationCenter = MockNotificationCenter() sut = SubscriptionWatcher(notifyWatchSubscriptionsRequester: mockRequester, logger: mockLogger, notificationCenter: mockNotificationCenter) - let account = Account("eip155:1:0x1AAe9864337E821f2F86b5D27468C59AA333C877")! sut.debounceInterval = 0.0001 - sut.setAccount(account) + sut.start() } override func tearDown() { diff --git a/Tests/RelayerTests/AuthTests/ClientIdStorageTests.swift b/Tests/RelayerTests/AuthTests/ClientIdStorageTests.swift index 05532fa8d..45b147637 100644 --- a/Tests/RelayerTests/AuthTests/ClientIdStorageTests.swift +++ b/Tests/RelayerTests/AuthTests/ClientIdStorageTests.swift @@ -8,17 +8,20 @@ final class ClientIdStorageTests: XCTestCase { var sut: ClientIdStorage! var keychain: KeychainStorageMock! + var defaults: RuntimeKeyValueStorage! override func setUp() { keychain = KeychainStorageMock() - sut = ClientIdStorage(keychain: keychain) + defaults = RuntimeKeyValueStorage() + sut = ClientIdStorage(defaults: defaults, keychain: keychain, logger: ConsoleLoggerMock()) } func testGetOrCreate() throws { XCTAssertThrowsError(try keychain.read(key: "com.walletconnect.iridium.client_id") as SigningPrivateKey) let saved = try sut.getOrCreateKeyPair() - XCTAssertEqual(saved, try keychain.read(key: "com.walletconnect.iridium.client_id")) + let storageId = saved.publicKey.rawRepresentation.sha256().toHexString() + XCTAssertEqual(saved, try keychain.read(key: storageId)) let restored = try sut.getOrCreateKeyPair() XCTAssertEqual(saved, restored) @@ -27,11 +30,51 @@ final class ClientIdStorageTests: XCTestCase { func testGetClientId() throws { let didKey = try DIDKey(did: "did:key:z6MkodHZwneVRShtaLf8JKYkxpDGp1vGZnpGmdBpX8M2exxH") + /// Initial state + XCTAssertThrowsError(try sut.getClientId()) + let privateKey = try SigningPrivateKey(rawRepresentation: didKey.rawData) - try keychain.add(privateKey, forKey: "com.walletconnect.iridium.client_id") + + defaults.set(privateKey.publicKey.rawRepresentation, forKey: "com.walletconnect.iridium.client_id.public") + + /// Private part not found + XCTAssertThrowsError(try sut.getClientId()) + + let storageId = privateKey.publicKey.rawRepresentation.sha256().toHexString() + try keychain.add(privateKey, forKey: storageId) let clientId = try sut.getClientId() let didPublicKey = DIDKey(rawData: privateKey.publicKey.rawRepresentation) + XCTAssertEqual(clientId, didPublicKey.did(variant: .ED25519)) } + + func testMigration() throws { + let defaults = RuntimeKeyValueStorage() + let keychain = KeychainStorageMock() + let clientId = SigningPrivateKey() + + try keychain.add(clientId, forKey: "com.walletconnect.iridium.client_id") + + // Migration on init + let clientIdStorage = ClientIdStorage(defaults: defaults, keychain: keychain, logger: ConsoleLoggerMock()) + + let publicPartData = defaults.data(forKey: "com.walletconnect.iridium.client_id.public")! + let publicPart = try SigningPublicKey(rawRepresentation: publicPartData) + + let privatePartStorageId = publicPart.rawRepresentation.sha256().toHexString() + let privatePart: SigningPrivateKey = try keychain.read(key: privatePartStorageId) + + XCTAssertEqual(publicPart, clientId.publicKey) + XCTAssertEqual(privatePart, clientId) + + let oldClientId: SigningPrivateKey? = try? keychain.read(key: "com.walletconnect.iridium.client_id") + XCTAssertNil(oldClientId) + + let restoredPrivatePart = try clientIdStorage.getOrCreateKeyPair() + XCTAssertEqual(restoredPrivatePart, clientId) + + let restoredPublicPart = try clientIdStorage.getClientId() + XCTAssertEqual(restoredPublicPart, DIDKey(rawData: clientId.publicKey.rawRepresentation).did(variant: .ED25519)) + } } diff --git a/Tests/RelayerTests/AuthTests/EdDSASignerTests.swift b/Tests/RelayerTests/AuthTests/EdDSASignerTests.swift index 87d5ae4c3..cbd8d2878 100644 --- a/Tests/RelayerTests/AuthTests/EdDSASignerTests.swift +++ b/Tests/RelayerTests/AuthTests/EdDSASignerTests.swift @@ -11,8 +11,8 @@ final class EdDSASignerTests: XCTestCase { let keyRaw = Data(hex: "58e0254c211b858ef7896b00e3f36beeb13d568d47c6031c4218b87718061295") let signingKey = try! SigningPrivateKey(rawRepresentation: keyRaw) sut = EdDSASigner(signingKey) - let header = try! JWTHeader(alg: "EdDSA").encode() - let claims = try! RelayAuthPayload.Claims.stub().encode() + let header = try! JWTHeader(alg: "EdDSA").encode(jsonEncoder: .jwt) + let claims = try! RelayAuthPayload.Claims.stub().encode(jsonEncoder: .jwt) let signature = try! sut.sign(header: header, claims: claims) XCTAssertNotNil(signature) } diff --git a/Tests/RelayerTests/AuthTests/JWTTests.swift b/Tests/RelayerTests/AuthTests/JWTTests.swift index 1d07ff0c7..9e662ff72 100644 --- a/Tests/RelayerTests/AuthTests/JWTTests.swift +++ b/Tests/RelayerTests/AuthTests/JWTTests.swift @@ -4,12 +4,15 @@ import XCTest @testable import WalletConnectJWT final class JWTTests: XCTestCase { - let expectedJWT = "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2NTY5MTAwOTcsImV4cCI6MTY1Njk5NjQ5NywiaXNzIjoiZGlkOmtleTp6Nk1rb2RIWnduZVZSU2h0YUxmOEpLWWt4cERHcDF2R1pucEdtZEJwWDhNMmV4eEgiLCJzdWIiOiJjNDc5ZmU1ZGM0NjRlNzcxZTc4YjE5M2QyMzlhNjViNThkMjc4Y2FkMWMzNGJmYjBiNTcxNmU1YmI1MTQ5MjhlIiwiYXVkIjoid3NzOi8vcmVsYXkud2FsbGV0Y29ubmVjdC5jb20ifQ.0JkxOM-FV21U7Hk-xycargj_qNRaYV2H5HYtE4GzAeVQYiKWj7YySY5AdSqtCgGzX4Gt98XWXn2kSr9rE1qvCA" + let expectedJWT = "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJ3c3M6Ly9yZWxheS53YWxsZXRjb25uZWN0LmNvbSIsImV4cCI6MTY1Njk5NjQ5NywiaWF0IjoxNjU2OTEwMDk3LCJpc3MiOiJkaWQ6a2V5Ono2TWtvZEhad25lVlJTaHRhTGY4SktZa3hwREdwMXZHWm5wR21kQnBYOE0yZXh4SCIsInN1YiI6ImM0NzlmZTVkYzQ2NGU3NzFlNzhiMTkzZDIzOWE2NWI1OGQyNzhjYWQxYzM0YmZiMGI1NzE2ZTViYjUxNDkyOGUifQ.0JkxOM-FV21U7Hk-xycargj_qNRaYV2H5HYtE4GzAeVQYiKWj7YySY5AdSqtCgGzX4Gt98XWXn2kSr9rE1qvCA" func testJWTEncoding() throws { let signer = EdDSASignerMock() signer.signature = "0JkxOM-FV21U7Hk-xycargj_qNRaYV2H5HYtE4GzAeVQYiKWj7YySY5AdSqtCgGzX4Gt98XWXn2kSr9rE1qvCA" - let jwt = try JWT(claims: RelayAuthPayload.Claims.stub(), signer: signer) + let jsonEncoder = JSONEncoder() + jsonEncoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes] + jsonEncoder.dateEncodingStrategy = .secondsSince1970 + let jwt = try JWT(claims: RelayAuthPayload.Claims.stub(), signer: signer, jsonEncoder: jsonEncoder) XCTAssertEqual(expectedJWT, jwt.string) } diff --git a/Tests/RelayerTests/DispatcherTests.swift b/Tests/RelayerTests/DispatcherTests.swift index 35660c6f1..4ab1c0a48 100644 --- a/Tests/RelayerTests/DispatcherTests.swift +++ b/Tests/RelayerTests/DispatcherTests.swift @@ -60,8 +60,10 @@ final class DispatcherTests: XCTestCase { webSocket = WebSocketMock() let webSocketFactory = WebSocketFactoryMock(webSocket: webSocket) networkMonitor = NetworkMonitoringMock() + let defaults = RuntimeKeyValueStorage() + let logger = ConsoleLoggerMock() let keychainStorageMock = DispatcherKeychainStorageMock() - let clientIdStorage = ClientIdStorage(keychain: keychainStorageMock) + let clientIdStorage = ClientIdStorage(defaults: defaults, keychain: keychainStorageMock, logger: logger) let socketAuthenticator = ClientIdAuthenticator( clientIdStorage: clientIdStorage, url: "wss://relay.walletconnect.com" diff --git a/Tests/TestingUtils/Mocks/KeyManagementServiceMock.swift b/Tests/TestingUtils/Mocks/KeyManagementServiceMock.swift index 935043f72..29fe44452 100644 --- a/Tests/TestingUtils/Mocks/KeyManagementServiceMock.swift +++ b/Tests/TestingUtils/Mocks/KeyManagementServiceMock.swift @@ -2,10 +2,12 @@ import Foundation @testable import WalletConnectKMS final class KeyManagementServiceMock: KeyManagementServiceProtocol { + private(set) var privateKeys: [String: AgreementPrivateKey] = [:] private(set) var symmetricKeys: [String: SymmetricKey] = [:] private(set) var agreementKeys: [String: AgreementKeys] = [:] private(set) var publicKeys: [String: AgreementPublicKey] = [:] + private(set) var topics: [String: String] = [:] func getSymmetricKeyRepresentable(for topic: String) -> Data? { if let key = getAgreementSecret(for: topic)?.sharedKey { @@ -95,6 +97,18 @@ final class KeyManagementServiceMock: KeyManagementServiceProtocol { symmetricKeys = [:] agreementKeys = [:] } + + func setTopic(_ topic: String, for key: String) throws { + topics[key] = topic + } + + func getTopic(for key: String) -> String? { + return topics[key] + } + + func deleteTopic(for key: String) { + topics[key] = nil + } } extension KeyManagementServiceMock {