From 78493a4a708d634baf2dc88637b9f7d3f7c5059c Mon Sep 17 00:00:00 2001 From: Radek Novak Date: Tue, 16 May 2023 18:58:48 +0200 Subject: [PATCH 01/85] UI improvements + Sign integration --- .../Chat/Import/ImportView.swift | 6 +- Sources/Web3Modal/Extensions/Collection.swift | 8 +++ Sources/Web3Modal/Extensions/Color.swift | 58 +++++++++++++++++++ 3 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 Sources/Web3Modal/Extensions/Collection.swift create mode 100644 Sources/Web3Modal/Extensions/Color.swift diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportView.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportView.swift index 9e0584723..a7ab737cf 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportView.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportView.swift @@ -17,9 +17,9 @@ struct ImportView: View { VStack { -// BrandButton(title: "Web3Modal") { -// try await presenter.didPressWeb3Modal() -// } + BrandButton(title: "Web3Modal") { + try await presenter.didPressWeb3Modal() + } BrandButton(title: "Ok, done" ) { try await presenter.didPressImport() diff --git a/Sources/Web3Modal/Extensions/Collection.swift b/Sources/Web3Modal/Extensions/Collection.swift new file mode 100644 index 000000000..0287d3bb4 --- /dev/null +++ b/Sources/Web3Modal/Extensions/Collection.swift @@ -0,0 +1,8 @@ +import Foundation + +extension Collection { + /// Returns the element at the specified index if it is within bounds, otherwise nil. + subscript(safe index: Index) -> Element? { + return indices.contains(index) ? self[index] : nil + } +} diff --git a/Sources/Web3Modal/Extensions/Color.swift b/Sources/Web3Modal/Extensions/Color.swift new file mode 100644 index 000000000..84e687266 --- /dev/null +++ b/Sources/Web3Modal/Extensions/Color.swift @@ -0,0 +1,58 @@ +import SwiftUI + +extension Color { + static let foreground1 = Color("foreground1", bundle: .module) + static let foreground2 = Color("foreground2", bundle: .module) + static let foreground3 = Color("foreground3", bundle: .module) + static let foregroundInverse = Color("foregroundInverse", bundle: .module) + static let background1 = Color("background1", bundle: .module) + static let background2 = Color("background2", bundle: .module) + static let background3 = Color("background3", bundle: .module) + static let negative = Color("negative", bundle: .module) + static let thickOverlay = Color("thickOverlay", bundle: .module) + static let thinOverlay = Color("thinOverlay", bundle: .module) + static let accent = Color("accent", bundle: .module) +} + +@available(iOS 15.0, *) +struct Color_Previews: PreviewProvider { + static var allColors: [(String, Color)] { + [ + ("foreground1", Color("foreground1", bundle: .module)), + ("foreground2", Color("foreground2", bundle: .module)), + ("foreground3", Color("foreground3", bundle: .module)), + ("foregroundInverse", Color("foregroundInverse", bundle: .module)), + ("background1", Color("background1", bundle: .module)), + ("background2", Color("background2", bundle: .module)), + ("background3", Color("background3", bundle: .module)), + ("negative", Color("negative", bundle: .module)), + ("thickOverlay", Color("thickOverlay", bundle: .module)), + ("thinOverlay", Color("thinOverlay", bundle: .module)), + ("accent", Color("accent", bundle: .module)), + ] + } + + static var previews: some View { + VStack { + let columns = [ + GridItem(.adaptive(minimum: 150)), + ] + + LazyVGrid(columns: columns, alignment: .leading) { + ForEach(allColors, id: \.1) { name, color in + + VStack(alignment: .leading) { + RoundedRectangle(cornerRadius: 12) + .fill(color) + .frame(width: 62, height: 62) + + Text(name).bold() + } + .font(.footnote) + } + } + } + .padding() + .previewLayout(.sizeThatFits) + } +} From 8417519cab37d8f918e7daffdb5fd4aa63a0bd48 Mon Sep 17 00:00:00 2001 From: Radek Novak Date: Tue, 16 May 2023 19:05:39 +0200 Subject: [PATCH 02/85] Fetching and displaying Wallets --- .../xcshareddata/xcschemes/Web3Modal.xcscheme | 66 ++++++++++ Example/ExampleApp.xcodeproj/project.pbxproj | 8 +- .../Configurator/AppearanceConfigurator.swift | 2 +- Sources/Web3Modal/Environment+ProjectId.swift | 12 ++ Sources/Web3Modal/Modal/ModalInteractor.swift | 8 +- Sources/Web3Modal/Modal/ModalSheet.swift | 106 ++++++++-------- Sources/Web3Modal/Modal/ModalViewModel.swift | 24 ++-- .../Networking/Common/Endpoint.swift | 113 ++++++++++++++++++ .../Networking/Common/HttpService.swift | 58 +++++++++ .../Networking/Explorer/ExplorerAPI.swift | 38 ++++++ .../Explorer/ListingsResponse.swift | 34 ++++++ 11 files changed, 392 insertions(+), 77 deletions(-) create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/Web3Modal.xcscheme create mode 100644 Sources/Web3Modal/Environment+ProjectId.swift create mode 100644 Sources/Web3Modal/Networking/Common/Endpoint.swift create mode 100644 Sources/Web3Modal/Networking/Common/HttpService.swift create mode 100644 Sources/Web3Modal/Networking/Explorer/ExplorerAPI.swift create mode 100644 Sources/Web3Modal/Networking/Explorer/ListingsResponse.swift diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Web3Modal.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Web3Modal.xcscheme new file mode 100644 index 000000000..8096d2466 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/Web3Modal.xcscheme @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index 98b7c6e9e..2b1dec9c9 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -2641,11 +2641,10 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 7; - DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=iphoneos*]" = W5R8AG9K22; + DEVELOPMENT_TEAM = W5R8AG9K22; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Showcase/Other/Info.plist; INFOPLIST_KEY_NSCameraUsageDescription = "Allow the app to scan for QR codes"; @@ -2663,7 +2662,6 @@ PRODUCT_BUNDLE_IDENTIFIER = com.walletconnect.chat; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match Development com.walletconnect.chat"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/Example/Showcase/Classes/ApplicationLayer/Configurator/AppearanceConfigurator.swift b/Example/Showcase/Classes/ApplicationLayer/Configurator/AppearanceConfigurator.swift index 5ad69dfb5..d6bb58b30 100644 --- a/Example/Showcase/Classes/ApplicationLayer/Configurator/AppearanceConfigurator.swift +++ b/Example/Showcase/Classes/ApplicationLayer/Configurator/AppearanceConfigurator.swift @@ -14,6 +14,6 @@ struct AppearanceConfigurator: Configurator { UINavigationBar.appearance().scrollEdgeAppearance = appearance UINavigationBar.appearance().compactAppearance = appearance - UIApplication.currentWindow.overrideUserInterfaceStyle = .dark +// UIApplication.currentWindow.overrideUserInterfaceStyle = .dark } } diff --git a/Sources/Web3Modal/Environment+ProjectId.swift b/Sources/Web3Modal/Environment+ProjectId.swift new file mode 100644 index 000000000..30e272aab --- /dev/null +++ b/Sources/Web3Modal/Environment+ProjectId.swift @@ -0,0 +1,12 @@ +import SwiftUI + +private struct ProjectIdKey: EnvironmentKey { + static let defaultValue: String = "" +} + +extension EnvironmentValues { + var projectId: String { + get { self[ProjectIdKey.self] } + set { self[ProjectIdKey.self] = newValue } + } +} diff --git a/Sources/Web3Modal/Modal/ModalInteractor.swift b/Sources/Web3Modal/Modal/ModalInteractor.swift index a89a1b0db..3435860b8 100644 --- a/Sources/Web3Modal/Modal/ModalInteractor.swift +++ b/Sources/Web3Modal/Modal/ModalInteractor.swift @@ -20,10 +20,10 @@ extension ModalSheet { Networking.configure(projectId: projectId, socketFactory: socketFactory) } -// func getListings() async throws -> [Listing] { -// let listingResponse = try await ExplorerApi.live().getMobileListings(projectId) -// return listingResponse.listings.values.compactMap { $0 } -// } + func getListings() async throws -> [Listing] { + let listingResponse = try await ExplorerApi.live().getMobileListings(projectId) + return listingResponse.listings.values.compactMap { $0 } + } func connect() async throws -> WalletConnectURI { diff --git a/Sources/Web3Modal/Modal/ModalSheet.swift b/Sources/Web3Modal/Modal/ModalSheet.swift index 3df7d7cf3..6f5ff3092 100644 --- a/Sources/Web3Modal/Modal/ModalSheet.swift +++ b/Sources/Web3Modal/Modal/ModalSheet.swift @@ -84,26 +84,22 @@ public struct ModalSheet: View { private func content() -> some View { switch viewModel.destination { case .wallets: - - Text("TBD in subsequent PR") - -// ZStack { -// VStack { -// HStack { -// ForEach(0..<4) { wallet in -// gridItem(for: wallet) -// } -// } -// HStack { -// ForEach(4..<7) { wallet in -// gridItem(for: wallet) -// } -// } -// } -// -// Spacer().frame(height: 200) -// } - + ZStack { + VStack { + HStack { + ForEach(0..<4) { wallet in + gridItem(for: wallet) + } + } + HStack { + ForEach(4..<7) { wallet in + gridItem(for: wallet) + } + } + } + + Spacer().frame(height: 200) + } case .help: WhatIsWalletView() case .qr: @@ -117,41 +113,41 @@ public struct ModalSheet: View { } } -// @ViewBuilder -// private func gridItem(for index: Int) -> some View { -// let wallet: Listing = viewModel.wallets.indices.contains(index) ? viewModel.wallets[index] : nil -// -// if #available(iOS 14.0, *) { -// VStack { -// AsyncImage(url: wallet != nil ? viewModel.imageUrl(for: wallet!) : nil) { image in -// image -// .resizable() -// .scaledToFit() -// } placeholder: { -// Color.foreground3 -// } -// .cornerRadius(8) -// .overlay( -// RoundedRectangle(cornerRadius: 8) -// .stroke(.gray.opacity(0.4), lineWidth: 1) -// ) -// -// Text(wallet?.name ?? "WalletName") -// .font(.system(size: 12)) -// .foregroundColor(.foreground1) -// .padding(.horizontal, 12) -// .fixedSize(horizontal: true, vertical: true) -// -// Text("RECENT") -// .opacity(0) -// .font(.system(size: 10)) -// .foregroundColor(.foreground3) -// .padding(.horizontal, 12) -// } -// .redacted(reason: wallet == nil ? .placeholder : []) -// .frame(maxWidth: 80, maxHeight: 96) -// } -// } + @ViewBuilder + private func gridItem(for index: Int) -> some View { + let wallet: Listing? = viewModel.wallets.indices.contains(index) ? viewModel.wallets[index] : nil + + if #available(iOS 14.0, *) { + VStack { + AsyncImage(url: wallet != nil ? viewModel.imageUrl(for: wallet!) : nil) { image in + image + .resizable() + .scaledToFit() + } placeholder: { + Color.foreground3 + } + .cornerRadius(8) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(.gray.opacity(0.4), lineWidth: 1) + ) + + Text(wallet?.name ?? "WalletName") + .font(.system(size: 12)) + .foregroundColor(.foreground1) + .padding(.horizontal, 12) + .fixedSize(horizontal: true, vertical: true) + + Text("RECENT") + .opacity(0) + .font(.system(size: 10)) + .foregroundColor(.foreground3) + .padding(.horizontal, 12) + } + .redacted(reason: wallet == nil ? .placeholder : []) + .frame(maxWidth: 80, maxHeight: 96) + } + } private func helpButton() -> some View { Button(action: { diff --git a/Sources/Web3Modal/Modal/ModalViewModel.swift b/Sources/Web3Modal/Modal/ModalViewModel.swift index 05633ca9c..65b1c2752 100644 --- a/Sources/Web3Modal/Modal/ModalViewModel.swift +++ b/Sources/Web3Modal/Modal/ModalViewModel.swift @@ -31,7 +31,7 @@ extension ModalSheet { @Published var uri: String? @Published var destination: Destination = .wallets @Published var errorMessage: String? -// @Published var wallets: [Listing] = [] + @Published var wallets: [Listing] = [] init(isShown: Binding, projectId: String, interactor: Interactor) { self.isShown = isShown @@ -47,17 +47,17 @@ extension ModalSheet { .store(in: &disposeBag) } -// @MainActor -// func fetchWallets() async { -// do { -// -// try await Task.sleep(nanoseconds: 1_000_000_000) -// wallets = try await interactor.getListings() -// } catch { -// print(error) -// errorMessage = error.localizedDescription -// } -// } + @MainActor + func fetchWallets() async { + do { + + try await Task.sleep(nanoseconds: 1_000_000_000) + wallets = try await interactor.getListings() + } catch { + print(error) + errorMessage = error.localizedDescription + } + } @MainActor func createURI() async { diff --git a/Sources/Web3Modal/Networking/Common/Endpoint.swift b/Sources/Web3Modal/Networking/Common/Endpoint.swift new file mode 100644 index 000000000..1962d080b --- /dev/null +++ b/Sources/Web3Modal/Networking/Common/Endpoint.swift @@ -0,0 +1,113 @@ +import Foundation + +struct Endpoint { + let path: String + let queryItems: [URLQueryItem] + let headers: [Headers] + let method: Method + let host: String + let body: Data? + let validResponseCodes: Set + + public enum Method: String { + case GET + case POST + case PUT + case PATCH + case DELETE + } + + enum Headers { + /// Standard headers used for every network call + case standard + + public var makeHeader: [String: String] { + switch self { + case .standard: + return [ + "Content-Type": "application/json", + ] + } + } + } + + var urlRequest: URLRequest { + var urlRequest = URLRequest(url: urlForRequest) + urlRequest.httpMethod = method.rawValue + urlRequest.httpBody = body + urlRequest.allHTTPHeaderFields = makeHTTPHeaders(headers) + return urlRequest + } + + private var urlForRequest: URL { + var components = URLComponents() + components.scheme = "https" + components.host = host + components.path = path + + if !queryItems.isEmpty { + components.queryItems = queryItems + } + + guard + let url = components.url + else { + preconditionFailure( + """ + Failed to construct valid url, if setting up new endpoint + make sure you have prefix / in path such as /v1/users + """ + ) + } + + return url + } + + private func makeHTTPHeaders(_ headers: [Headers]) -> [String: String] { + headers.reduce(into: [String: String]()) { result, nextElement in + result = result.merging(nextElement.makeHeader) { _, new in new } + } + } +} + +extension Endpoint.Headers: Equatable { + static func == (lhs: Endpoint.Headers, rhs: Endpoint.Headers) -> Bool { + lhs.makeHeader == rhs.makeHeader + } +} + +extension Endpoint { + + /// Un-authenticated endpoint. + /// - Parameters: + /// - path: Path for your endpoint` + /// - headers: Specific headers + /// - method: .GET, .POST etc + /// - host: Host url + /// - shouldEncodePath: This setting affects how your url is constructed. + /// - body: If you need to pass parameters, provide them here. + /// - validResponseCodes: This is set to default value `Set(200 ..< 300)` + /// and can be overridden if needed + /// - Returns: Endpoint with URLRequest that gets passed directly to HttpService. + static func bare( + path: String, + queryItems: [URLQueryItem] = [], + headers: [Endpoint.Headers] = [], + method: Endpoint.Method, + host: String, + body: Data? = nil, + validResponseCodes: Set = Set(200 ..< 300) + ) -> Self { + var headers = headers + headers.append(.standard) + return Self( + path: path, + queryItems: queryItems, + headers: headers, + method: method, + host: host, + body: body, + validResponseCodes: validResponseCodes + ) + } +} diff --git a/Sources/Web3Modal/Networking/Common/HttpService.swift b/Sources/Web3Modal/Networking/Common/HttpService.swift new file mode 100644 index 000000000..3b4fe6971 --- /dev/null +++ b/Sources/Web3Modal/Networking/Common/HttpService.swift @@ -0,0 +1,58 @@ +import Foundation + +struct HttpService { + var performRequest: (_ endpoint: Endpoint) async throws -> Result +} + +extension HttpService { + + static var live: Self = .init(performRequest: { endpoint in + + let (data, response) = try await URLSession.shared.data(for: endpoint.urlRequest) + + let error = errorForResponse(response, data, validResponseCodes: endpoint.validResponseCodes) + if let error = error { + return .failure(error) + } else { + return .success(data) + } + }) + + private static func errorForResponse( + _ response: URLResponse?, + _ data: Data?, validResponseCodes: Set + ) -> Error? { + guard let httpResponse = response as? HTTPURLResponse else { + return nil + } + + if !validResponseCodes.contains(httpResponse.statusCode) { + return Errors.badResponseCode( + code: httpResponse.statusCode, + payload: data + ) + } + + return nil + } + + enum Errors: Error, Equatable { + case emptyResponse + case badResponseCode(code: Int, payload: Data?) + + public var properties: [String: String] { + switch self { + case let .badResponseCode(code, _): + return [ + "category": "http_error", + "http_code": "\(String(code))" + ] + case .emptyResponse: + return [ + "category": "payload", + "message": "Failed for empty response" + ] + } + } + } +} diff --git a/Sources/Web3Modal/Networking/Explorer/ExplorerAPI.swift b/Sources/Web3Modal/Networking/Explorer/ExplorerAPI.swift new file mode 100644 index 000000000..c9a0c8d70 --- /dev/null +++ b/Sources/Web3Modal/Networking/Explorer/ExplorerAPI.swift @@ -0,0 +1,38 @@ +import Foundation + +struct ExplorerApi { + let getMobileListings: @Sendable (_ projectID: String) async throws -> ListingsResponse +} + +extension ExplorerApi { + static func live(httpService: HttpService = .live) -> Self { + .init( + getMobileListings: { projectId in + + let endpoint = Endpoint.bare( + path: "/w3m/v1/getMobileListings", + queryItems: [ + .init(name: "projectId", value: projectId), + .init(name: "page", value: "1"), + .init(name: "entries", value: "9"), + .init(name: "platforms", value: "ios,mac"), + ], + method: .GET, + host: "explorer-api.walletconnect.com" + ) + + let response = try await httpService.performRequest(endpoint) + + switch response { + case let .success(data): + + let listings = try JSONDecoder().decode(ListingsResponse.self, from: data) + + return listings + case let .failure(error): + throw error + } + } + ) + } +} diff --git a/Sources/Web3Modal/Networking/Explorer/ListingsResponse.swift b/Sources/Web3Modal/Networking/Explorer/ListingsResponse.swift new file mode 100644 index 000000000..91160ee85 --- /dev/null +++ b/Sources/Web3Modal/Networking/Explorer/ListingsResponse.swift @@ -0,0 +1,34 @@ +import Foundation + +struct ListingsResponse: Codable { + let listings: [String: Listing] +} + +struct Listing: Codable, Hashable, Identifiable { + let id: String + let name: String + let homepage: String + let imageId: String + let app: App + let mobile: Mobile + + private enum CodingKeys: String, CodingKey { + case id + case name + case homepage + case imageId = "image_id" + case app + case mobile + } + + struct App: Codable, Hashable { + let ios: String? + let mac: String? + let safari: String? + } + + struct Mobile: Codable, Hashable { + let native: String? + let universal: String? + } +} From 6160fd1940b734a2eb0a2e24d646a04a9cdbb349 Mon Sep 17 00:00:00 2001 From: Radek Novak Date: Thu, 18 May 2023 09:10:56 +0200 Subject: [PATCH 03/85] Use new endpoint + order field --- .../Chat/Import/ImportView.swift | 2 +- Sources/Web3Modal/Extensions/Collection.swift | 8 --- Sources/Web3Modal/Extensions/Color.swift | 58 ------------------- Sources/Web3Modal/Modal/ModalInteractor.swift | 2 +- Sources/Web3Modal/Modal/ModalSheet.swift | 4 +- Sources/Web3Modal/Modal/ModalViewModel.swift | 17 +++--- .../Networking/Explorer/ExplorerAPI.swift | 7 +-- .../Explorer/ListingsResponse.swift | 2 + Sources/Web3Modal/Resources/Asset.swift | 8 +-- Sources/Web3Modal/UI/AsyncImage.swift | 1 + 10 files changed, 24 insertions(+), 85 deletions(-) delete mode 100644 Sources/Web3Modal/Extensions/Collection.swift delete mode 100644 Sources/Web3Modal/Extensions/Color.swift diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportView.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportView.swift index a7ab737cf..9de69d9f3 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportView.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportView.swift @@ -17,7 +17,7 @@ struct ImportView: View { VStack { - BrandButton(title: "Web3Modal") { + BrandButton(title: "Web3Modal WIP") { try await presenter.didPressWeb3Modal() } diff --git a/Sources/Web3Modal/Extensions/Collection.swift b/Sources/Web3Modal/Extensions/Collection.swift deleted file mode 100644 index 0287d3bb4..000000000 --- a/Sources/Web3Modal/Extensions/Collection.swift +++ /dev/null @@ -1,8 +0,0 @@ -import Foundation - -extension Collection { - /// Returns the element at the specified index if it is within bounds, otherwise nil. - subscript(safe index: Index) -> Element? { - return indices.contains(index) ? self[index] : nil - } -} diff --git a/Sources/Web3Modal/Extensions/Color.swift b/Sources/Web3Modal/Extensions/Color.swift deleted file mode 100644 index 84e687266..000000000 --- a/Sources/Web3Modal/Extensions/Color.swift +++ /dev/null @@ -1,58 +0,0 @@ -import SwiftUI - -extension Color { - static let foreground1 = Color("foreground1", bundle: .module) - static let foreground2 = Color("foreground2", bundle: .module) - static let foreground3 = Color("foreground3", bundle: .module) - static let foregroundInverse = Color("foregroundInverse", bundle: .module) - static let background1 = Color("background1", bundle: .module) - static let background2 = Color("background2", bundle: .module) - static let background3 = Color("background3", bundle: .module) - static let negative = Color("negative", bundle: .module) - static let thickOverlay = Color("thickOverlay", bundle: .module) - static let thinOverlay = Color("thinOverlay", bundle: .module) - static let accent = Color("accent", bundle: .module) -} - -@available(iOS 15.0, *) -struct Color_Previews: PreviewProvider { - static var allColors: [(String, Color)] { - [ - ("foreground1", Color("foreground1", bundle: .module)), - ("foreground2", Color("foreground2", bundle: .module)), - ("foreground3", Color("foreground3", bundle: .module)), - ("foregroundInverse", Color("foregroundInverse", bundle: .module)), - ("background1", Color("background1", bundle: .module)), - ("background2", Color("background2", bundle: .module)), - ("background3", Color("background3", bundle: .module)), - ("negative", Color("negative", bundle: .module)), - ("thickOverlay", Color("thickOverlay", bundle: .module)), - ("thinOverlay", Color("thinOverlay", bundle: .module)), - ("accent", Color("accent", bundle: .module)), - ] - } - - static var previews: some View { - VStack { - let columns = [ - GridItem(.adaptive(minimum: 150)), - ] - - LazyVGrid(columns: columns, alignment: .leading) { - ForEach(allColors, id: \.1) { name, color in - - VStack(alignment: .leading) { - RoundedRectangle(cornerRadius: 12) - .fill(color) - .frame(width: 62, height: 62) - - Text(name).bold() - } - .font(.footnote) - } - } - } - .padding() - .previewLayout(.sizeThatFits) - } -} diff --git a/Sources/Web3Modal/Modal/ModalInteractor.swift b/Sources/Web3Modal/Modal/ModalInteractor.swift index 3435860b8..e305b6968 100644 --- a/Sources/Web3Modal/Modal/ModalInteractor.swift +++ b/Sources/Web3Modal/Modal/ModalInteractor.swift @@ -21,7 +21,7 @@ extension ModalSheet { } func getListings() async throws -> [Listing] { - let listingResponse = try await ExplorerApi.live().getMobileListings(projectId) + let listingResponse = try await ExplorerApi.live().getListings(projectId) return listingResponse.listings.values.compactMap { $0 } } diff --git a/Sources/Web3Modal/Modal/ModalSheet.swift b/Sources/Web3Modal/Modal/ModalSheet.swift index 6f5ff3092..6a517ce55 100644 --- a/Sources/Web3Modal/Modal/ModalSheet.swift +++ b/Sources/Web3Modal/Modal/ModalSheet.swift @@ -19,7 +19,7 @@ public struct ModalSheet: View { .onAppear { Task { await viewModel.createURI() -// await viewModel.fetchWallets() + await viewModel.fetchWallets() } } .background( @@ -125,7 +125,9 @@ public struct ModalSheet: View { .scaledToFit() } placeholder: { Color.foreground3 + .frame(width: 60, height: 60) } + .animation(.default) .cornerRadius(8) .overlay( RoundedRectangle(cornerRadius: 8) diff --git a/Sources/Web3Modal/Modal/ModalViewModel.swift b/Sources/Web3Modal/Modal/ModalViewModel.swift index 65b1c2752..5cfd93f76 100644 --- a/Sources/Web3Modal/Modal/ModalViewModel.swift +++ b/Sources/Web3Modal/Modal/ModalViewModel.swift @@ -50,9 +50,10 @@ extension ModalSheet { @MainActor func fetchWallets() async { do { - - try await Task.sleep(nanoseconds: 1_000_000_000) - wallets = try await interactor.getListings() + let wallets = try await interactor.getListings() + // Small deliberate delay to ensure animations execute properly + try await Task.sleep(nanoseconds: 500_000_000) + self.wallets = wallets.sorted { $0.order < $1.order } } catch { print(error) errorMessage = error.localizedDescription @@ -81,10 +82,10 @@ extension ModalSheet { UIPasteboard.general.string = uri } -// func imageUrl(for listing: Listing) -> URL? { -// let urlString = "https://explorer-api.walletconnect.com/v3/logo/md/\(listing.imageId)?projectId=\(projectId)" -// -// return URL(string: urlString) -// } + func imageUrl(for listing: Listing) -> URL? { + let urlString = "https://explorer-api.walletconnect.com/v3/logo/md/\(listing.imageId)?projectId=\(projectId)" + + return URL(string: urlString) + } } } diff --git a/Sources/Web3Modal/Networking/Explorer/ExplorerAPI.swift b/Sources/Web3Modal/Networking/Explorer/ExplorerAPI.swift index c9a0c8d70..49d977972 100644 --- a/Sources/Web3Modal/Networking/Explorer/ExplorerAPI.swift +++ b/Sources/Web3Modal/Networking/Explorer/ExplorerAPI.swift @@ -1,21 +1,20 @@ import Foundation struct ExplorerApi { - let getMobileListings: @Sendable (_ projectID: String) async throws -> ListingsResponse + let getListings: @Sendable (_ projectID: String) async throws -> ListingsResponse } extension ExplorerApi { static func live(httpService: HttpService = .live) -> Self { .init( - getMobileListings: { projectId in + getListings: { projectId in let endpoint = Endpoint.bare( - path: "/w3m/v1/getMobileListings", + path: "/w3m/v1/getiOSListings", queryItems: [ .init(name: "projectId", value: projectId), .init(name: "page", value: "1"), .init(name: "entries", value: "9"), - .init(name: "platforms", value: "ios,mac"), ], method: .GET, host: "explorer-api.walletconnect.com" diff --git a/Sources/Web3Modal/Networking/Explorer/ListingsResponse.swift b/Sources/Web3Modal/Networking/Explorer/ListingsResponse.swift index 91160ee85..884943e44 100644 --- a/Sources/Web3Modal/Networking/Explorer/ListingsResponse.swift +++ b/Sources/Web3Modal/Networking/Explorer/ListingsResponse.swift @@ -8,6 +8,7 @@ struct Listing: Codable, Hashable, Identifiable { let id: String let name: String let homepage: String + let order: Int let imageId: String let app: App let mobile: Mobile @@ -16,6 +17,7 @@ struct Listing: Codable, Hashable, Identifiable { case id case name case homepage + case order case imageId = "image_id" case app case mobile diff --git a/Sources/Web3Modal/Resources/Asset.swift b/Sources/Web3Modal/Resources/Asset.swift index c5d987646..2cbc6a7cb 100644 --- a/Sources/Web3Modal/Resources/Asset.swift +++ b/Sources/Web3Modal/Resources/Asset.swift @@ -4,21 +4,21 @@ import UIKit enum Asset: String { - // Icons + /// Icons case close case external_link case help case wallet - // large + /// large case copy_large case qr_large - // Images + /// Images case walletconnect_logo case wc_logo - // Help + /// Help case Browser case DAO case DeFi diff --git a/Sources/Web3Modal/UI/AsyncImage.swift b/Sources/Web3Modal/UI/AsyncImage.swift index 4eca1aba1..c5a2972ab 100644 --- a/Sources/Web3Modal/UI/AsyncImage.swift +++ b/Sources/Web3Modal/UI/AsyncImage.swift @@ -9,6 +9,7 @@ struct AsyncImage: View where Content: View { init(_ url: URL?) { guard let url = url else { return } + URLSession.shared.dataTaskPublisher(for: url) .map(\.data) .map { $0 as Data? } From 68051e00ea25e6fb73456ae3532f5b501737723d Mon Sep 17 00:00:00 2001 From: Radek Novak Date: Thu, 18 May 2023 10:03:18 +0200 Subject: [PATCH 04/85] Fix AsyncImage animations --- Example/ExampleApp.xcodeproj/project.pbxproj | 7 ++++-- .../Configurator/AppearanceConfigurator.swift | 2 +- Sources/Web3Modal/Modal/ModalSheet.swift | 22 +++++++++++-------- Sources/Web3Modal/Modal/ModalViewModel.swift | 5 ++++- Sources/Web3Modal/UI/AsyncImage.swift | 12 ++++++++-- 5 files changed, 33 insertions(+), 15 deletions(-) diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index 2b1dec9c9..856869be0 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -2642,9 +2642,11 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 7; - DEVELOPMENT_TEAM = W5R8AG9K22; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = W5R8AG9K22; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Showcase/Other/Info.plist; INFOPLIST_KEY_NSCameraUsageDescription = "Allow the app to scan for QR codes"; @@ -2662,6 +2664,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.walletconnect.chat; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match Development com.walletconnect.chat"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/Example/Showcase/Classes/ApplicationLayer/Configurator/AppearanceConfigurator.swift b/Example/Showcase/Classes/ApplicationLayer/Configurator/AppearanceConfigurator.swift index d6bb58b30..5ad69dfb5 100644 --- a/Example/Showcase/Classes/ApplicationLayer/Configurator/AppearanceConfigurator.swift +++ b/Example/Showcase/Classes/ApplicationLayer/Configurator/AppearanceConfigurator.swift @@ -14,6 +14,6 @@ struct AppearanceConfigurator: Configurator { UINavigationBar.appearance().scrollEdgeAppearance = appearance UINavigationBar.appearance().compactAppearance = appearance -// UIApplication.currentWindow.overrideUserInterfaceStyle = .dark + UIApplication.currentWindow.overrideUserInterfaceStyle = .dark } } diff --git a/Sources/Web3Modal/Modal/ModalSheet.swift b/Sources/Web3Modal/Modal/ModalSheet.swift index 6a517ce55..06ec57a84 100644 --- a/Sources/Web3Modal/Modal/ModalSheet.swift +++ b/Sources/Web3Modal/Modal/ModalSheet.swift @@ -1,6 +1,7 @@ import SwiftUI public struct ModalSheet: View { + @ObservedObject var viewModel: ModalViewModel public var body: some View { @@ -73,10 +74,12 @@ public struct ModalSheet: View { .foregroundColor(.accent) .frame(height: 60) .overlay( - Text(viewModel.destination.contentTitle) - .font(.system(size: 20).weight(.semibold)) - .foregroundColor(.foreground1) - .padding(.horizontal, 50) + VStack { + Text(viewModel.destination.contentTitle) + .font(.system(size: 20).weight(.semibold)) + .foregroundColor(.foreground1) + .padding(.horizontal, 50) + } ) } @@ -116,18 +119,19 @@ public struct ModalSheet: View { @ViewBuilder private func gridItem(for index: Int) -> some View { let wallet: Listing? = viewModel.wallets.indices.contains(index) ? viewModel.wallets[index] : nil - - if #available(iOS 14.0, *) { + let walletUrl: URL? = wallet != nil ? viewModel.imageUrl(for: wallet!) : nil + + if #available(iOS 15.0, *) { VStack { - AsyncImage(url: wallet != nil ? viewModel.imageUrl(for: wallet!) : nil) { image in + AsyncImage(url: walletUrl) { image in image .resizable() .scaledToFit() } placeholder: { - Color.foreground3 + Color + .foreground3 .frame(width: 60, height: 60) } - .animation(.default) .cornerRadius(8) .overlay( RoundedRectangle(cornerRadius: 8) diff --git a/Sources/Web3Modal/Modal/ModalViewModel.swift b/Sources/Web3Modal/Modal/ModalViewModel.swift index 5cfd93f76..3f6e32b25 100644 --- a/Sources/Web3Modal/Modal/ModalViewModel.swift +++ b/Sources/Web3Modal/Modal/ModalViewModel.swift @@ -53,7 +53,10 @@ extension ModalSheet { let wallets = try await interactor.getListings() // Small deliberate delay to ensure animations execute properly try await Task.sleep(nanoseconds: 500_000_000) - self.wallets = wallets.sorted { $0.order < $1.order } + + withAnimation { + self.wallets = wallets.sorted { $0.order < $1.order } + } } catch { print(error) errorMessage = error.localizedDescription diff --git a/Sources/Web3Modal/UI/AsyncImage.swift b/Sources/Web3Modal/UI/AsyncImage.swift index c5a2972ab..3ce9283c4 100644 --- a/Sources/Web3Modal/UI/AsyncImage.swift +++ b/Sources/Web3Modal/UI/AsyncImage.swift @@ -15,7 +15,11 @@ struct AsyncImage: View where Content: View { .map { $0 as Data? } .replaceError(with: nil) .receive(on: RunLoop.main) - .assign(to: \.data, on: self) + .sink(receiveValue: { data in + withAnimation { + self.data = data + } + }) .store(in: &cancellables) } } @@ -28,7 +32,11 @@ struct AsyncImage: View where Content: View { self.conditionalContent = nil } - init(url: URL?, @ViewBuilder content: @escaping (Image) -> I, @ViewBuilder placeholder: @escaping () -> P) where Content == _ConditionalContent, I: View, P: View { + init( + url: URL?, + @ViewBuilder content: @escaping (Image) -> I, + @ViewBuilder placeholder: @escaping () -> P + ) where Content == _ConditionalContent, I: View, P: View { self.imageLoader = Loader(url) self.conditionalContent = { image in if let image = image { From 3b632a0d330141523f218e06bb737b09d60244be Mon Sep 17 00:00:00 2001 From: Radek Novak Date: Thu, 18 May 2023 18:13:38 +0200 Subject: [PATCH 05/85] Add ViewAll + fix QR code paddings and darkmode --- .../Configurator/AppearanceConfigurator.swift | 2 +- Sources/Web3Modal/Modal/ModalInteractor.swift | 35 ++++++++ Sources/Web3Modal/Modal/ModalSheet.swift | 70 ++++++++++++++- Sources/Web3Modal/Modal/ModalViewModel.swift | 2 +- .../Web3Modal/Modal/Screens/QRCodeView.swift | 87 ++++++++++++++----- .../Networking/Explorer/ExplorerAPI.swift | 2 +- 6 files changed, 168 insertions(+), 30 deletions(-) diff --git a/Example/Showcase/Classes/ApplicationLayer/Configurator/AppearanceConfigurator.swift b/Example/Showcase/Classes/ApplicationLayer/Configurator/AppearanceConfigurator.swift index 5ad69dfb5..d6bb58b30 100644 --- a/Example/Showcase/Classes/ApplicationLayer/Configurator/AppearanceConfigurator.swift +++ b/Example/Showcase/Classes/ApplicationLayer/Configurator/AppearanceConfigurator.swift @@ -14,6 +14,6 @@ struct AppearanceConfigurator: Configurator { UINavigationBar.appearance().scrollEdgeAppearance = appearance UINavigationBar.appearance().compactAppearance = appearance - UIApplication.currentWindow.overrideUserInterfaceStyle = .dark +// UIApplication.currentWindow.overrideUserInterfaceStyle = .dark } } diff --git a/Sources/Web3Modal/Modal/ModalInteractor.swift b/Sources/Web3Modal/Modal/ModalInteractor.swift index e305b6968..e17255489 100644 --- a/Sources/Web3Modal/Modal/ModalInteractor.swift +++ b/Sources/Web3Modal/Modal/ModalInteractor.swift @@ -1,3 +1,5 @@ + +import Foundation import WalletConnectPairing import WalletConnectSign import Combine @@ -5,6 +7,8 @@ import WalletConnectNetworking extension ModalSheet { final class Interactor { + var disposeBag = Set() + let projectId: String let metadata: AppMetadata let socketFactory: WebSocketFactory @@ -18,6 +22,36 @@ extension ModalSheet { Pair.configure(metadata: metadata) Networking.configure(projectId: projectId, socketFactory: socketFactory) + + sessionsPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] sessions in + print(sessions) + + self?.personal_sign(session: sessions.first!) + } + .store(in: &disposeBag) + } + + func personal_sign(session: Session) { + + let method = "personal_sign" + let account = session.namespaces.first!.value.accounts.first!.absoluteString + let requestParams = AnyCodable( + ["0x4d7920656d61696c206973206a6f686e40646f652e636f6d202d2031363533333933373535313531", account] + ) + + let request = Request( + topic: session.topic, + method: method, + params: requestParams, + chainId: Blockchain("eip155:1")! + ) + + Task { + + try? await Sign.instance.request(params: request) + } } func getListings() async throws -> [Listing] { @@ -45,3 +79,4 @@ extension ModalSheet { } } } + diff --git a/Sources/Web3Modal/Modal/ModalSheet.swift b/Sources/Web3Modal/Modal/ModalSheet.swift index 06ec57a84..cd9a67095 100644 --- a/Sources/Web3Modal/Modal/ModalSheet.swift +++ b/Sources/Web3Modal/Modal/ModalSheet.swift @@ -98,6 +98,8 @@ public struct ModalSheet: View { ForEach(4..<7) { wallet in gridItem(for: wallet) } + + viewAllItem() } } @@ -117,11 +119,70 @@ public struct ModalSheet: View { } @ViewBuilder - private func gridItem(for index: Int) -> some View { + func viewAllItem() -> some View { + VStack { + VStack(spacing: 3) { + HStack(spacing: 3) { + ForEach(7..<9) { index in + imageForWallet(at: index) + .cornerRadius(8) + .aspectRatio(1, contentMode: .fit) + } + } + .padding(.horizontal, 5) + + HStack(spacing: 3) { + ForEach(9..<11) { index in + imageForWallet(at: index) + .cornerRadius(8) + .aspectRatio(1, contentMode: .fit) + } + } + .padding(.horizontal, 5) + } + .padding(.vertical, 3) + .frame(width: 60, height: 60) + .background(Color.background2) + .cornerRadius(8) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(.gray.opacity(0.4), lineWidth: 1) + ) + + + Text("View All") + .font(.system(size: 12)) + .foregroundColor(.foreground1) + .padding(.horizontal, 12) + .fixedSize(horizontal: true, vertical: true) + + Spacer() + } + .frame(maxWidth: 80, maxHeight: 96) + } + + @ViewBuilder + func imageForWallet(at index: Int) -> some View { + + let wallet: Listing? = viewModel.wallets.indices.contains(index) ? viewModel.wallets[index] : nil + let walletUrl: URL? = wallet != nil ? viewModel.imageUrl(for: wallet!) : nil + + AsyncImage(url: walletUrl) { image in + image + .resizable() + .scaledToFit() + } placeholder: { + Color.foreground3 + } + } + + + @ViewBuilder + func gridItem(for index: Int) -> some View { let wallet: Listing? = viewModel.wallets.indices.contains(index) ? viewModel.wallets[index] : nil let walletUrl: URL? = wallet != nil ? viewModel.imageUrl(for: wallet!) : nil - if #available(iOS 15.0, *) { + if #available(iOS 14.0, *) { VStack { AsyncImage(url: walletUrl) { image in image @@ -130,8 +191,9 @@ public struct ModalSheet: View { } placeholder: { Color .foreground3 - .frame(width: 60, height: 60) + } + .frame(width: 60, height: 60) .cornerRadius(8) .overlay( RoundedRectangle(cornerRadius: 8) @@ -145,7 +207,7 @@ public struct ModalSheet: View { .fixedSize(horizontal: true, vertical: true) Text("RECENT") - .opacity(0) + .opacity(Double(Int.random(in: 0...1))) .font(.system(size: 10)) .foregroundColor(.foreground3) .padding(.horizontal, 12) diff --git a/Sources/Web3Modal/Modal/ModalViewModel.swift b/Sources/Web3Modal/Modal/ModalViewModel.swift index 3f6e32b25..a3d3f5149 100644 --- a/Sources/Web3Modal/Modal/ModalViewModel.swift +++ b/Sources/Web3Modal/Modal/ModalViewModel.swift @@ -29,7 +29,7 @@ extension ModalSheet { @Published var isShown: Binding @Published var uri: String? - @Published var destination: Destination = .wallets + @Published var destination: Destination = .qr @Published var errorMessage: String? @Published var wallets: [Listing] = [] diff --git a/Sources/Web3Modal/Modal/Screens/QRCodeView.swift b/Sources/Web3Modal/Modal/Screens/QRCodeView.swift index 8ca2a6ca6..17f98f3d6 100644 --- a/Sources/Web3Modal/Modal/Screens/QRCodeView.swift +++ b/Sources/Web3Modal/Modal/Screens/QRCodeView.swift @@ -1,41 +1,82 @@ -import SwiftUI import QRCode +import SwiftUI struct QRCodeView: View { - - @State var doc: QRCode.Document! - @Environment(\.colorScheme) var colorScheme: ColorScheme @State var uri: String + @State var index: Int = 0 + + var foreground1: UIColor { + UIColor(.foreground1).resolvedColor( + with: UITraitCollection( + userInterfaceStyle: colorScheme == .dark ? .dark : .light + ) + ) + } + + var background1: UIColor { + UIColor(.background1).resolvedColor( + with: UITraitCollection( + userInterfaceStyle: colorScheme == .dark ? .dark : .light + ) + ) + } + var body: some View { - QRCodeViewUI( + render( content: uri, - errorCorrection: .quantize, - foregroundColor: AssetColor.background1.uiColor.cgColor, - backgroundColor: AssetColor.foreground1.uiColor.cgColor, - pixelStyle: QRCode.PixelShape.Vertical( - insetFraction: 0.2, - cornerRadiusFraction: 1 - ), - eyeStyle: QRCode.EyeShape.Squircle(), - logoTemplate: QRCode.LogoTemplate( - image: Asset.wc_logo.uiImage.cgImage!, - path: CGPath( - rect: CGRect(x: 0.35, y: 0.3875, width: 0.30, height: 0.225), - transform: nil - ) + size: .init( + width: UIScreen.main.bounds.width - 40, + height: UIScreen.main.bounds.width - 40 + ) + ) + .colorScheme(.dark) + } + + private func render(content: String, size: CGSize) -> Image { + let doc = QRCode.Document( + utf8String: content, + errorCorrection: .quantize + ) + doc.design.shape.eye = QRCode.EyeShape.Squircle() + doc.design.shape.onPixels = QRCode.PixelShape.Vertical( + insetFraction: 0.2, + cornerRadiusFraction: 1 + ) + + doc.design.style.eye = QRCode.FillStyle.Solid(foreground1.cgColor) + doc.design.style.pupil = QRCode.FillStyle.Solid(foreground1.cgColor) + doc.design.style.onPixels = QRCode.FillStyle.Solid(foreground1.cgColor) + doc.design.style.background = QRCode.FillStyle.Solid(background1.cgColor) + + doc.logoTemplate = QRCode.LogoTemplate( + image: Asset.wc_logo.uiImage.cgImage!, + path: CGPath( + rect: CGRect(x: 0.35, y: 0.3875, width: 0.30, height: 0.225), + transform: nil ) ) - .frame(height: UIScreen.main.bounds.width) + + return doc.imageUI( + size, label: Text("QR code with URI") + )! + } +} + +extension UIColor { + func image(_ size: CGSize = CGSize(width: 1, height: 1)) -> UIImage { + return UIGraphicsImageRenderer(size: size).image { rendererContext in + self.setFill() + rendererContext.fill(CGRect(origin: .zero, size: size)) + } } } struct QRCodeView_Previews: PreviewProvider { - - static let stubUri: String = Array(repeating: ["a", "b", "c", "1", "2", "3"], count: 50) - .flatMap({ $0 }) + static let stubUri: String = Array(repeating: ["a", "b", "c", "1", "2", "3"], count: 10) + .flatMap { $0 } .shuffled() .joined() diff --git a/Sources/Web3Modal/Networking/Explorer/ExplorerAPI.swift b/Sources/Web3Modal/Networking/Explorer/ExplorerAPI.swift index 49d977972..ec1062361 100644 --- a/Sources/Web3Modal/Networking/Explorer/ExplorerAPI.swift +++ b/Sources/Web3Modal/Networking/Explorer/ExplorerAPI.swift @@ -14,7 +14,7 @@ extension ExplorerApi { queryItems: [ .init(name: "projectId", value: projectId), .init(name: "page", value: "1"), - .init(name: "entries", value: "9"), + .init(name: "entries", value: "11"), ], method: .GET, host: "explorer-api.walletconnect.com" From 56cf2069f3851a9021b4929a7a9c88d5e5ec23b9 Mon Sep 17 00:00:00 2001 From: Radek Novak Date: Mon, 29 May 2023 17:42:04 +0200 Subject: [PATCH 06/85] Add view all and get wallet --- .../Sign/Connect/ConnectViewController.swift | 2 +- .../ApplicationLayer/SceneDelegate.swift | 36 ++-- .../Web3Modal/Extensions/View+Backport.swift | 34 ++++ .../Web3Modal/Modal/ModalContainerView.swift | 9 +- Sources/Web3Modal/Modal/ModalInteractor.swift | 84 ++++----- .../Web3Modal/Modal/ModalSheet+Previews.swift | 1 + Sources/Web3Modal/Modal/ModalSheet.swift | 161 ++++------------- Sources/Web3Modal/Modal/ModalViewModel.swift | 166 ++++++++++-------- Sources/Web3Modal/Modal/Screens/File.swift | 40 +++++ .../Web3Modal/Modal/Screens/WalletList.swift | 158 +++++++++++++++++ .../Modal/Screens/WhatIsWalletView.swift | 12 +- .../Networking/Explorer/ExplorerAPI.swift | 10 +- .../Explorer/ListingsResponse.swift | 2 +- .../UI/{ => Common}/ActivityIndicator.swift | 0 .../UI/{ => Common}/AsyncImage.swift | 0 Sources/Web3Modal/UI/WalletImage.swift | 40 +++++ .../Web3ModalSheetController.swift | 1 + 17 files changed, 481 insertions(+), 275 deletions(-) create mode 100644 Sources/Web3Modal/Modal/Screens/File.swift create mode 100644 Sources/Web3Modal/Modal/Screens/WalletList.swift rename Sources/Web3Modal/UI/{ => Common}/ActivityIndicator.swift (100%) rename Sources/Web3Modal/UI/{ => Common}/AsyncImage.swift (100%) create mode 100644 Sources/Web3Modal/UI/WalletImage.swift diff --git a/Example/DApp/Sign/Connect/ConnectViewController.swift b/Example/DApp/Sign/Connect/ConnectViewController.swift index 02c9020f7..ccb0d6fa2 100644 --- a/Example/DApp/Sign/Connect/ConnectViewController.swift +++ b/Example/DApp/Sign/Connect/ConnectViewController.swift @@ -63,7 +63,7 @@ class ConnectViewController: UIViewController, UITableViewDataSource, UITableVie } @objc func connectWithExampleWallet() { - let url = URL(string: "https://walletconnect.com/wc?uri=\(uri.absoluteString)")! + let url = URL(string: "walletapp://wc?uri=\(uri.deeplinkUri)")! DispatchQueue.main.async { UIApplication.shared.open(url, options: [:]) { [weak self] _ in self?.dismiss(animated: true, completion: nil) diff --git a/Example/WalletApp/ApplicationLayer/SceneDelegate.swift b/Example/WalletApp/ApplicationLayer/SceneDelegate.swift index 999164f85..488489e74 100644 --- a/Example/WalletApp/ApplicationLayer/SceneDelegate.swift +++ b/Example/WalletApp/ApplicationLayer/SceneDelegate.swift @@ -1,12 +1,10 @@ -import UIKit import Auth +import UIKit import WalletConnectPairing - final class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? - private let app = Application() private var configurators: [Configurator] { @@ -17,19 +15,19 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { AppearanceConfigurator() ] } - + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { - let sceneConfig: UISceneConfiguration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role) - sceneConfig.delegateClass = SceneDelegate.self - return sceneConfig - } + let sceneConfig = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role) + sceneConfig.delegateClass = SceneDelegate.self + return sceneConfig + } func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let windowScene = (scene as? UIWindowScene) else { return } window = UIWindow(windowScene: windowScene) window?.makeKeyAndVisible() - + app.uri = connectionOptions.urlContexts.first?.url.absoluteString.replacingOccurrences(of: "walletapp://wc?uri=", with: "") configurators.configure() @@ -39,10 +37,24 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { guard let context = URLContexts.first else { return } - let uri = context.url.absoluteString.replacingOccurrences(of: "walletapp://wc?uri=", with: "") - Task { - try await Pair.instance.pair(uri: WalletConnectURI(string: uri)!) + let queryParams = context.url.queryParameters + + if let uri = queryParams["uri"] as? String { + Task { + try await Pair.instance.pair(uri: WalletConnectURI(string: uri)!) + } } + } +} +extension URL { + var queryParameters: [AnyHashable: Any] { + let urlComponents = URLComponents(url: self, resolvingAgainstBaseURL: false) + guard let queryItems = urlComponents?.queryItems else { return [:] } + var queryParams: [AnyHashable: Any] = [:] + queryItems.forEach { + queryParams[$0.name] = $0.value + } + return queryParams } } diff --git a/Sources/Web3Modal/Extensions/View+Backport.swift b/Sources/Web3Modal/Extensions/View+Backport.swift index 1946cd60c..e178a1246 100644 --- a/Sources/Web3Modal/Extensions/View+Backport.swift +++ b/Sources/Web3Modal/Extensions/View+Backport.swift @@ -1,6 +1,38 @@ import SwiftUI import Combine +struct Backport { + let content: Content +} + +extension View { + var backport: Backport { Backport(content: self) } +} + +extension Backport where Content: View { + + enum Visibility { + case automatic + case visible + case hidden + } + + @ViewBuilder func scrollContentBackground(_ visibility: Backport.Visibility) -> some View { + if #available(iOS 16, *) { + switch visibility { + case .automatic: + content.scrollContentBackground(.automatic) + case .hidden: + content.scrollContentBackground(.hidden) + case .visible: + content.scrollContentBackground(.visible) + } + } else { + content + } + } +} + extension View { /// A backwards compatible wrapper for iOS 14 `onChange` @ViewBuilder func onChangeBackported(of value: T, perform: @escaping (T) -> Void) -> some View { @@ -13,3 +45,5 @@ extension View { } } } + + diff --git a/Sources/Web3Modal/Modal/ModalContainerView.swift b/Sources/Web3Modal/Modal/ModalContainerView.swift index 312120dbf..13fce4e77 100644 --- a/Sources/Web3Modal/Modal/ModalContainerView.swift +++ b/Sources/Web3Modal/Modal/ModalContainerView.swift @@ -1,6 +1,7 @@ import SwiftUI import WalletConnectPairing +@available(iOS 14.0, *) public struct ModalContainerView: View { @Environment(\.presentationMode) var presentationMode @@ -35,9 +36,11 @@ public struct ModalContainerView: View { isShown: $showModal, projectId: projectId, interactor: .init(projectId: projectId, metadata: metadata, webSocketFactory: webSocketFactory) - )) - .transition(.move(edge: .bottom)) - .animation(.spring(), value: showModal) + ) + ) + .environment(\.projectId, projectId) + .transition(.move(edge: .bottom)) + .animation(.spring(), value: showModal) } } .edgesIgnoringSafeArea(.all) diff --git a/Sources/Web3Modal/Modal/ModalInteractor.swift b/Sources/Web3Modal/Modal/ModalInteractor.swift index 3ffd372e0..d0d73e0fc 100644 --- a/Sources/Web3Modal/Modal/ModalInteractor.swift +++ b/Sources/Web3Modal/Modal/ModalInteractor.swift @@ -1,66 +1,52 @@ +import Combine import Foundation import WalletConnectPairing import WalletConnectSign -import Combine -extension ModalSheet { - final class Interactor { - var disposeBag = Set() +final class Interactor { + var disposeBag = Set() - let projectId: String - let metadata: AppMetadata - let socketFactory: WebSocketFactory + let projectId: String + let metadata: AppMetadata + let socketFactory: WebSocketFactory - lazy var sessionsPublisher: AnyPublisher<[Session], Never> = Sign.instance.sessionsPublisher + lazy var sessionsPublisher: AnyPublisher<[Session], Never> = Sign.instance.sessionsPublisher - init(projectId: String, metadata: AppMetadata, webSocketFactory: WebSocketFactory) { - self.projectId = projectId - self.metadata = metadata - self.socketFactory = webSocketFactory - - Pair.configure(metadata: metadata) - Networking.configure(projectId: projectId, socketFactory: socketFactory) + init(projectId: String, metadata: AppMetadata, webSocketFactory: WebSocketFactory) { + self.projectId = projectId + self.metadata = metadata + self.socketFactory = webSocketFactory - sessionsPublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] sessions in - print(sessions) - - self?.personal_sign(session: sessions.first!) - } - .store(in: &disposeBag) - } + Pair.configure(metadata: metadata) + Networking.configure(projectId: projectId, socketFactory: socketFactory) + } - func getListings() async throws -> [Listing] { - - let httpClient = HTTPNetworkClient(host: "explorer-api.walletconnect.com") - let response = try await httpClient.request( - ListingsResponse.self, - at: ExplorerAPI.getListings(projectId: projectId) - ) + func getListings() async throws -> [Listing] { + let httpClient = HTTPNetworkClient(host: "explorer-api.walletconnect.com") + let response = try await httpClient.request( + ListingsResponse.self, + at: ExplorerAPI.getListings(projectId: projectId) + ) - return response.listings.values.compactMap { $0 } - } + return response.listings.values.compactMap { $0 } + } - func connect() async throws -> WalletConnectURI { - - let uri = try await Pair.instance.create() + func connect() async throws -> WalletConnectURI { + let uri = try await Pair.instance.create() - let methods: Set = ["eth_sendTransaction", "personal_sign", "eth_signTypedData"] - let blockchains: Set = [Blockchain("eip155:1")!, Blockchain("eip155:137")!] - let namespaces: [String: ProposalNamespace] = [ - "eip155": ProposalNamespace( - chains: blockchains, - methods: methods, - events: [] - ) - ] + let methods: Set = ["eth_sendTransaction", "personal_sign", "eth_signTypedData"] + let blockchains: Set = [Blockchain("eip155:1")!, Blockchain("eip155:137")!] + let namespaces: [String: ProposalNamespace] = [ + "eip155": ProposalNamespace( + chains: blockchains, + methods: methods, + events: [] + ) + ] - try await Sign.instance.connect(requiredNamespaces: namespaces, topic: uri.topic) + try await Sign.instance.connect(requiredNamespaces: namespaces, topic: uri.topic) - return uri - } + return uri } } - diff --git a/Sources/Web3Modal/Modal/ModalSheet+Previews.swift b/Sources/Web3Modal/Modal/ModalSheet+Previews.swift index f4be2b253..c15a71e4f 100644 --- a/Sources/Web3Modal/Modal/ModalSheet+Previews.swift +++ b/Sources/Web3Modal/Modal/ModalSheet+Previews.swift @@ -23,6 +23,7 @@ class WebSocketFactoryMock: WebSocketFactory { } } +@available(iOS 14.0, *) struct ModalSheet_Previews: PreviewProvider { static let projectId = "9bfe94c9cbf74aaa0597094ef561f703" static let metadata = AppMetadata( diff --git a/Sources/Web3Modal/Modal/ModalSheet.swift b/Sources/Web3Modal/Modal/ModalSheet.swift index cd9a67095..2b1850db0 100644 --- a/Sources/Web3Modal/Modal/ModalSheet.swift +++ b/Sources/Web3Modal/Modal/ModalSheet.swift @@ -1,7 +1,6 @@ import SwiftUI public struct ModalSheet: View { - @ObservedObject var viewModel: ModalViewModel public var body: some View { @@ -17,10 +16,11 @@ public struct ModalSheet: View { .cornerRadius(30, corners: [.topLeft, .topRight]) } .padding(.bottom, 40) + .edgesIgnoringSafeArea(.bottom) .onAppear { Task { - await viewModel.createURI() await viewModel.fetchWallets() + await viewModel.createURI() } } .background( @@ -55,16 +55,16 @@ public struct ModalSheet: View { private func contentHeader() -> some View { HStack(spacing: 0) { - if viewModel.destination != .wallets { + if viewModel.destination != .welcome { backButton() } Spacer() switch viewModel.destination { - case .wallets: + case .welcome: qrButton() - case .qr: + case .qr, .walletDetail: copyButton() default: EmptyView() @@ -84,139 +84,48 @@ public struct ModalSheet: View { } @ViewBuilder - private func content() -> some View { - switch viewModel.destination { - case .wallets: - ZStack { - VStack { - HStack { - ForEach(0..<4) { wallet in - gridItem(for: wallet) - } - } - HStack { - ForEach(4..<7) { wallet in - gridItem(for: wallet) - } - - viewAllItem() - } - } - - Spacer().frame(height: 200) - } - case .help: - WhatIsWalletView() - case .qr: - VStack { - if let uri = viewModel.uri { - QRCodeView(uri: uri) - } else { - ActivityIndicator(isAnimating: .constant(true), style: .large) - } - } + private func welcome() -> some View { + if #available(iOS 14.0, *) { + WalletList( + wallets: $viewModel.wallets, + destination: .init(get: { + viewModel.destination + }, set: { _ in }), + navigateTo: viewModel.navigateTo(_:) + ) + } else { + EmptyView() } } - @ViewBuilder - func viewAllItem() -> some View { + private func qrCode() -> some View { VStack { - VStack(spacing: 3) { - HStack(spacing: 3) { - ForEach(7..<9) { index in - imageForWallet(at: index) - .cornerRadius(8) - .aspectRatio(1, contentMode: .fit) - } - } - .padding(.horizontal, 5) - - HStack(spacing: 3) { - ForEach(9..<11) { index in - imageForWallet(at: index) - .cornerRadius(8) - .aspectRatio(1, contentMode: .fit) - } - } - .padding(.horizontal, 5) + if let uri = viewModel.uri { + QRCodeView(uri: uri) + } else { + ActivityIndicator(isAnimating: .constant(true), style: .large) } - .padding(.vertical, 3) - .frame(width: 60, height: 60) - .background(Color.background2) - .cornerRadius(8) - .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(.gray.opacity(0.4), lineWidth: 1) - ) - - - Text("View All") - .font(.system(size: 12)) - .foregroundColor(.foreground1) - .padding(.horizontal, 12) - .fixedSize(horizontal: true, vertical: true) - - Spacer() } - .frame(maxWidth: 80, maxHeight: 96) } @ViewBuilder - func imageForWallet(at index: Int) -> some View { - - let wallet: Listing? = viewModel.wallets.indices.contains(index) ? viewModel.wallets[index] : nil - let walletUrl: URL? = wallet != nil ? viewModel.imageUrl(for: wallet!) : nil - - AsyncImage(url: walletUrl) { image in - image - .resizable() - .scaledToFit() - } placeholder: { - Color.foreground3 + private func content() -> some View { + switch viewModel.destination { + case .welcome, + .walletDetail, + .viewAll: + welcome() + case .help: + WhatIsWalletView(navigateTo: viewModel.navigateTo(_:)) + case .qr: + qrCode() + case .getWallet: + GetAWalletView(wallets: Array(viewModel.wallets.prefix(6))) } } - - - @ViewBuilder - func gridItem(for index: Int) -> some View { - let wallet: Listing? = viewModel.wallets.indices.contains(index) ? viewModel.wallets[index] : nil - let walletUrl: URL? = wallet != nil ? viewModel.imageUrl(for: wallet!) : nil - - if #available(iOS 14.0, *) { - VStack { - AsyncImage(url: walletUrl) { image in - image - .resizable() - .scaledToFit() - } placeholder: { - Color - .foreground3 - - } - .frame(width: 60, height: 60) - .cornerRadius(8) - .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(.gray.opacity(0.4), lineWidth: 1) - ) - - Text(wallet?.name ?? "WalletName") - .font(.system(size: 12)) - .foregroundColor(.foreground1) - .padding(.horizontal, 12) - .fixedSize(horizontal: true, vertical: true) +} - Text("RECENT") - .opacity(Double(Int.random(in: 0...1))) - .font(.system(size: 10)) - .foregroundColor(.foreground3) - .padding(.horizontal, 12) - } - .redacted(reason: wallet == nil ? .placeholder : []) - .frame(maxWidth: 80, maxHeight: 96) - } - } - +extension ModalSheet { private func helpButton() -> some View { Button(action: { withAnimation { diff --git a/Sources/Web3Modal/Modal/ModalViewModel.swift b/Sources/Web3Modal/Modal/ModalViewModel.swift index ebc07ae06..e22395b0d 100644 --- a/Sources/Web3Modal/Modal/ModalViewModel.swift +++ b/Sources/Web3Modal/Modal/ModalViewModel.swift @@ -3,94 +3,110 @@ import Combine import Foundation import SwiftUI -extension ModalSheet { - enum Destination: String, CaseIterable { - case wallets - case help - case qr +enum Destination: Equatable { + case welcome + case viewAll + case help + case qr + case walletDetail(Listing) + case getWallet - var contentTitle: String { - switch self { - case .wallets: - return "Connect your wallet" - case .qr: - return "Scan the code" - case .help: - return "What is a wallet?" - } + var contentTitle: String { + switch self { + case .welcome: + return "Connect your wallet" + case .viewAll: + return "View all" + case .qr: + return "Scan the code" + case .help: + return "What is a wallet?" + case .getWallet: + return "Get a wallet" + case let .walletDetail(wallet): + return wallet.name } } +} + +final class ModalViewModel: ObservableObject { + private var disposeBag = Set() + let interactor: Interactor + let projectId: String - final class ModalViewModel: ObservableObject { - private var disposeBag = Set() - private let interactor: Interactor - private let projectId: String - - @Published var isShown: Binding + @Published var destinationStack: [Destination] = [.welcome] + + var destination: Destination { + destinationStack.last! + } + + @Published var isShown: Binding - @Published var uri: String? - @Published var destination: Destination = .qr - @Published var errorMessage: String? - @Published var wallets: [Listing] = [] + @Published var uri: String? + @Published var errorMessage: String? + @Published var wallets: [Listing] = [] - init(isShown: Binding, projectId: String, interactor: Interactor) { - self.isShown = isShown - self.interactor = interactor - self.projectId = projectId + init(isShown: Binding, projectId: String, interactor: Interactor) { + self.isShown = isShown + self.interactor = interactor + self.projectId = projectId - interactor.sessionsPublisher - .receive(on: DispatchQueue.main) - .sink { sessions in - print(sessions) - isShown.wrappedValue = false - } - .store(in: &disposeBag) - } - - @MainActor - func fetchWallets() async { - do { - let wallets = try await interactor.getListings() - // Small deliberate delay to ensure animations execute properly - try await Task.sleep(nanoseconds: 500_000_000) - - withAnimation { - self.wallets = wallets.sorted { $0.order < $1.order } - } - } catch { - print(error) - errorMessage = error.localizedDescription - } - } - - @MainActor - func createURI() async { - do { - uri = try await interactor.connect().absoluteString - } catch { - print(error) - errorMessage = error.localizedDescription + interactor.sessionsPublisher + .receive(on: DispatchQueue.main) + .sink { sessions in + print(sessions) + isShown.wrappedValue = false } - } + .store(in: &disposeBag) + } - func navigateTo(_ destination: Destination) { - self.destination = destination + @MainActor + func createURI() async { + do { + uri = try await interactor.connect().absoluteString + } catch { + print(error) + errorMessage = error.localizedDescription } + } - func onBackButton() { - destination = .wallets - } + func navigateTo(_ destination: Destination) { + guard self.destination != destination else { return } + destinationStack.append(destination) + } - func onCopyButton() { - UIPasteboard.general.string = uri - } + func onBackButton() { + guard destinationStack.count != 1 else { return } + _ = destinationStack.popLast() + } - func imageUrl(for listing: Listing?) -> URL? { - guard let listing = listing else { return nil } - - let urlString = "https://explorer-api.walletconnect.com/v3/logo/md/\(listing.imageId)?projectId=\(projectId)" - - return URL(string: urlString) + func onCopyButton() { + UIPasteboard.general.string = uri + } + + + @MainActor + func fetchWallets() async { + do { + let wallets = try await interactor.getListings() + // Small deliberate delay to ensure animations execute properly + try await Task.sleep(nanoseconds: 500_000_000) + + withAnimation { + self.wallets = wallets.sorted { + guard let lhs = $0.order else { + return false + } + + guard let rhs = $1.order else { + return true + } + + return lhs < rhs + } + } + } catch { + print(error) } } } diff --git a/Sources/Web3Modal/Modal/Screens/File.swift b/Sources/Web3Modal/Modal/Screens/File.swift new file mode 100644 index 000000000..5b9e7a968 --- /dev/null +++ b/Sources/Web3Modal/Modal/Screens/File.swift @@ -0,0 +1,40 @@ +import SwiftUI + +struct GetAWalletView: View { + + let wallets: [Listing] + + init(wallets: [Listing]) { + self.wallets = wallets + + UITableView.appearance().backgroundColor = .clear // tableview background + UITableViewCell.appearance().backgroundColor = .clear // cell background + } + + var body: some View { + List { + ForEach(wallets) { wallet in + Button { + print("foo") + } label: { + + HStack { + WalletImage(wallet: wallet) + .frame(width: 40, height: 40) + + Text(wallet.name) + .font(.system(size: 16, weight: .medium)) + .padding(.horizontal) + + Spacer() + + Image(systemName: "chevron.right") + .font(.system(.footnote).weight(.semibold)) + } + } + } + } + .frame(height: 500) + .listStyle(.plain) + } +} diff --git a/Sources/Web3Modal/Modal/Screens/WalletList.swift b/Sources/Web3Modal/Modal/Screens/WalletList.swift new file mode 100644 index 000000000..b60b29800 --- /dev/null +++ b/Sources/Web3Modal/Modal/Screens/WalletList.swift @@ -0,0 +1,158 @@ +import SwiftUI + +@available(iOS 14.0, *) +struct WalletList: View { + @Namespace var namespace + + @Binding var wallets: [Listing] + @Binding var destination: Destination + + var navigateTo: (Destination) -> Void + + var body: some View { + content() + } + + @ViewBuilder + private func content() -> some View { + switch destination { + case .welcome: + initialList() + case .viewAll: + viewAll() + case let .walletDetail(wallet): + walletDetail(wallet) + default: + EmptyView() + } + } + + private func initialList() -> some View { + ZStack { + VStack { + HStack { + ForEach(0..<4) { wallet in + gridItem(for: wallet) + } + } + HStack { + ForEach(4..<7) { wallet in + gridItem(for: wallet) + } + + viewAllItem() + .onTapGesture { + navigateTo(.viewAll) + } + } + } + + Spacer().frame(height: 200) + } + } + + private func viewAll() -> some View { + ScrollView(.vertical) { + VStack(alignment: .leading) { + ForEach(Array(stride(from: 0, to: wallets.count, by: 4)), id: \.self) { row in + HStack { + ForEach(row..<(row + 4), id: \.self) { index in + if wallets.indices.contains(index) { + gridItem(for: index) + } + } + } + } + } + } + } + + @ViewBuilder + func viewAllItem() -> some View { + VStack { + VStack(spacing: 3) { + HStack(spacing: 3) { + ForEach(7..<9) { index in + WalletImage(wallet: wallets[safe: index]) + .cornerRadius(8) + .aspectRatio(1, contentMode: .fit) + } + } + .padding(.horizontal, 5) + + HStack(spacing: 3) { + ForEach(9..<11) { index in + WalletImage(wallet: wallets[safe: index]) + .cornerRadius(8) + .aspectRatio(1, contentMode: .fit) + } + } + .padding(.horizontal, 5) + } + .padding(.vertical, 3) + .frame(width: 60, height: 60) + .background(Color.background2) + .cornerRadius(8) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(.gray.opacity(0.4), lineWidth: 1) + ) + + Text("View All") + .font(.system(size: 12)) + .foregroundColor(.foreground1) + .padding(.horizontal, 12) + .fixedSize(horizontal: true, vertical: true) + + Spacer() + } + .frame(maxWidth: 80, maxHeight: 96) + } + + @ViewBuilder + func gridItem(for index: Int) -> some View { + let wallet: Listing? = wallets[safe: index] + + VStack { + WalletImage(wallet: wallet) + .frame(width: 60, height: 60) + .matchedGeometryEffect(id: index, in: namespace) + + Text(wallet?.name ?? "WalletName") + .font(.system(size: 12)) + .foregroundColor(.foreground1) + .padding(.horizontal, 12) + .multilineTextAlignment(.center) + .minimumScaleFactor(0.4) + + Text("RECENT") + .opacity(0) + .font(.system(size: 10)) + .foregroundColor(.foreground3) + .padding(.horizontal, 12) + } + .redacted(reason: wallet == nil ? .placeholder : []) + .frame(maxWidth: 80, maxHeight: 96) + .onTapGesture { + guard let wallet else { return } + + navigateTo(.walletDetail(wallet)) + } + } + + private func walletDetail(_ wallet: Listing) -> some View { + VStack { + WalletImage(wallet: wallet, size: .large) + .frame(maxWidth: 96, maxHeight: 96) + + Text("Continue in \(wallet.name)...") + .font(.system(size: 16, weight: .medium)) + .foregroundColor(.foreground1) + + Text("Accept connection request in the app") + .font(.system(size: 14)) + .foregroundColor(.foreground3) + } + .padding() + } +} diff --git a/Sources/Web3Modal/Modal/Screens/WhatIsWalletView.swift b/Sources/Web3Modal/Modal/Screens/WhatIsWalletView.swift index 1119fc8eb..7c75d549b 100644 --- a/Sources/Web3Modal/Modal/Screens/WhatIsWalletView.swift +++ b/Sources/Web3Modal/Modal/Screens/WhatIsWalletView.swift @@ -2,6 +2,8 @@ import SwiftUI struct WhatIsWalletView: View { + var navigateTo: (Destination) -> Void + var body: some View { VStack(spacing: 10) { @@ -22,13 +24,17 @@ struct WhatIsWalletView: View { ) HStack { - Button(action: {}) { + Button(action: { + navigateTo(.getWallet) + }) { HStack { Image("wallet", bundle: .module) Text("Get a Wallet") } } - Button(action: {}) { + Button(action: { + + }) { HStack { Text("Learn More") Image("external_link", bundle: .module) @@ -75,6 +81,6 @@ struct WhatIsWalletView_Previews: PreviewProvider { static var previews: some View { - WhatIsWalletView() + WhatIsWalletView(navigateTo: { _ in}) } } diff --git a/Sources/Web3Modal/Networking/Explorer/ExplorerAPI.swift b/Sources/Web3Modal/Networking/Explorer/ExplorerAPI.swift index b26d84662..1f37011b3 100644 --- a/Sources/Web3Modal/Networking/Explorer/ExplorerAPI.swift +++ b/Sources/Web3Modal/Networking/Explorer/ExplorerAPI.swift @@ -3,19 +3,19 @@ import HTTPClient enum ExplorerAPI: HTTPService { case getListings(projectId: String) - + var path: String { switch self { case .getListings: return "/w3m/v1/getiOSListings" } } - + var method: HTTPMethod { switch self { case .getListings: return .get } } - + var body: Data? { nil } @@ -26,11 +26,11 @@ enum ExplorerAPI: HTTPService { return [ "projectId": projectId, "page": "1", - "entries": "9", + "entries": "300", ] } } - + var scheme: String { return "https" } diff --git a/Sources/Web3Modal/Networking/Explorer/ListingsResponse.swift b/Sources/Web3Modal/Networking/Explorer/ListingsResponse.swift index 884943e44..8c08458a5 100644 --- a/Sources/Web3Modal/Networking/Explorer/ListingsResponse.swift +++ b/Sources/Web3Modal/Networking/Explorer/ListingsResponse.swift @@ -8,7 +8,7 @@ struct Listing: Codable, Hashable, Identifiable { let id: String let name: String let homepage: String - let order: Int + let order: Int? let imageId: String let app: App let mobile: Mobile diff --git a/Sources/Web3Modal/UI/ActivityIndicator.swift b/Sources/Web3Modal/UI/Common/ActivityIndicator.swift similarity index 100% rename from Sources/Web3Modal/UI/ActivityIndicator.swift rename to Sources/Web3Modal/UI/Common/ActivityIndicator.swift diff --git a/Sources/Web3Modal/UI/AsyncImage.swift b/Sources/Web3Modal/UI/Common/AsyncImage.swift similarity index 100% rename from Sources/Web3Modal/UI/AsyncImage.swift rename to Sources/Web3Modal/UI/Common/AsyncImage.swift diff --git a/Sources/Web3Modal/UI/WalletImage.swift b/Sources/Web3Modal/UI/WalletImage.swift new file mode 100644 index 000000000..f14614bd5 --- /dev/null +++ b/Sources/Web3Modal/UI/WalletImage.swift @@ -0,0 +1,40 @@ +import SwiftUI + +struct WalletImage: View { + + enum Size: String { + case small = "sm" + case medium = "md" + case large = "lg" + } + + @Environment(\.projectId) var projectId + + var wallet: Listing? + var size: Size = .medium + + var body: some View { + + AsyncImage(url: imageURL(for: wallet)) { image in + image + .resizable() + .scaledToFit() + } placeholder: { + Color.foreground3 + } + .cornerRadius(8) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(.gray.opacity(0.4), lineWidth: 1) + ) + } + + private func imageURL(for wallet: Listing?) -> URL? { + + guard let wallet else { return nil } + + let urlString = "https://explorer-api.walletconnect.com/v3/logo/\(size.rawValue)/\(wallet.imageId)?projectId=\(projectId)" + + return URL(string: urlString) + } +} diff --git a/Sources/Web3Modal/UIKitSupport/Web3ModalSheetController.swift b/Sources/Web3Modal/UIKitSupport/Web3ModalSheetController.swift index 9890a66d2..66fec9da1 100644 --- a/Sources/Web3Modal/UIKitSupport/Web3ModalSheetController.swift +++ b/Sources/Web3Modal/UIKitSupport/Web3ModalSheetController.swift @@ -2,6 +2,7 @@ import SwiftUI import WalletConnectNetworking import WalletConnectPairing +@available(iOS 14.0, *) public class Web3ModalSheetController: UIHostingController { @MainActor dynamic required init?(coder aDecoder: NSCoder) { From 558e295965537914e149afaaf11b40394c80baba Mon Sep 17 00:00:00 2001 From: Radek Novak Date: Fri, 2 Jun 2023 17:23:14 +0200 Subject: [PATCH 07/85] Cleanup API & Integrate with sample Dapp --- Example/DApp/SceneDelegate.swift | 10 ++ .../Sign/Connect/ConnectViewController.swift | 3 +- .../SelectChainViewController.swift | 19 ++- Example/DApp/Sign/SignCoordinator.swift | 3 +- Example/ExampleApp.xcodeproj/project.pbxproj | 7 + .../Configurator/ThirdPartyConfigurator.swift | 18 +- .../Chat/Import/ImportRouter.swift | 11 +- .../WalletConnectPairing/PairingClient.swift | 2 +- .../PairingInteracting.swift | 2 +- .../Sign/SignClientProtocol.swift | 3 + .../Web3Modal/Modal/ModalContainerView.swift | 22 +-- Sources/Web3Modal/Modal/ModalInteractor.swift | 41 +---- .../Web3Modal/Modal/ModalSheet+Previews.swift | 8 +- Sources/Web3Modal/Modal/ModalViewModel.swift | 18 +- .../Web3ModalSheetController.swift | 12 +- Sources/Web3Modal/Web3Modal.swift | 117 +++++++++++++ Sources/Web3Modal/Web3ModalClient.swift | 160 ++++++++++++++++++ Sources/Web3Modal/Web3ModalImports.swift | 4 + 18 files changed, 357 insertions(+), 103 deletions(-) create mode 100644 Sources/Web3Modal/Web3Modal.swift create mode 100644 Sources/Web3Modal/Web3ModalClient.swift create mode 100644 Sources/Web3Modal/Web3ModalImports.swift diff --git a/Example/DApp/SceneDelegate.swift b/Example/DApp/SceneDelegate.swift index f6b051abc..fc931226d 100644 --- a/Example/DApp/SceneDelegate.swift +++ b/Example/DApp/SceneDelegate.swift @@ -2,6 +2,7 @@ import UIKit import Auth import WalletConnectRelay import WalletConnectNetworking +import Web3Modal class SceneDelegate: UIResponder, UIWindowSceneDelegate { @@ -13,6 +14,15 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { Networking.configure(projectId: InputConfig.projectId, socketFactory: DefaultSocketFactory()) Auth.configure(crypto: DefaultCryptoProvider()) + + let metadata = AppMetadata( + name: "Swift Dapp", + description: "WalletConnect DApp sample", + url: "wallet.connect", + icons: ["https://avatars.githubusercontent.com/u/37784886"] + ) + + Web3Modal.configure(projectId: InputConfig.projectId, metadata: metadata) setupWindow(scene: scene) } diff --git a/Example/DApp/Sign/Connect/ConnectViewController.swift b/Example/DApp/Sign/Connect/ConnectViewController.swift index ccb0d6fa2..094b82b6e 100644 --- a/Example/DApp/Sign/Connect/ConnectViewController.swift +++ b/Example/DApp/Sign/Connect/ConnectViewController.swift @@ -1,7 +1,6 @@ import Foundation import UIKit -import WalletConnectSign -import WalletConnectPairing +import Web3Modal class ConnectViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { let uri: WalletConnectURI diff --git a/Example/DApp/Sign/SelectChain/SelectChainViewController.swift b/Example/DApp/Sign/SelectChain/SelectChainViewController.swift index 5c456a453..ea95acab9 100644 --- a/Example/DApp/Sign/SelectChain/SelectChainViewController.swift +++ b/Example/DApp/Sign/SelectChain/SelectChainViewController.swift @@ -1,6 +1,5 @@ import Foundation -import WalletConnectSign -import WalletConnectPairing +import Web3Modal import UIKit import Combine @@ -70,16 +69,22 @@ class SelectChainViewController: UIViewController, UITableViewDataSource { let sessionProperties: [String: String] = [ "caip154-mandatory": "true" ] + Task { - let uri = try await Pair.instance.create() - try await Sign.instance.connect( + + Web3Modal.set(sessionParams: .init( requiredNamespaces: namespaces, optionalNamespaces: optionalNamespaces, - sessionProperties: sessionProperties, - topic: uri.topic + sessionProperties: sessionProperties + )) + + let uri = try await Web3Modal.instance.createPairingAndConnect( + ) - showConnectScreen(uri: uri) } + + + Web3Modal.present(from: self) } @objc diff --git a/Example/DApp/Sign/SignCoordinator.swift b/Example/DApp/Sign/SignCoordinator.swift index 55344a902..fa527a6e6 100644 --- a/Example/DApp/Sign/SignCoordinator.swift +++ b/Example/DApp/Sign/SignCoordinator.swift @@ -24,7 +24,8 @@ final class SignCoordinator { name: "Swift Dapp", description: "WalletConnect DApp sample", url: "wallet.connect", - icons: ["https://avatars.githubusercontent.com/u/37784886"]) + icons: ["https://avatars.githubusercontent.com/u/37784886"] + ) Pair.configure(metadata: metadata) #if DEBUG diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index 35ab930db..b6836928f 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -268,6 +268,7 @@ C5F32A322954816C00A6476E /* ConnectionDetailsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5F32A312954816C00A6476E /* ConnectionDetailsPresenter.swift */; }; C5F32A342954817600A6476E /* ConnectionDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5F32A332954817600A6476E /* ConnectionDetailsView.swift */; }; C5F32A362954FE3C00A6476E /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C5F32A352954FE3C00A6476E /* Colors.xcassets */; }; + CF140F2D2A2A288D00BEB791 /* Web3Modal in Frameworks */ = {isa = PBXBuildFile; productRef = CF140F2C2A2A288D00BEB791 /* Web3Modal */; }; CF1A594529E5876600AAC16B /* XCUIElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF1A593A29E5876600AAC16B /* XCUIElement.swift */; }; CF1A594629E5876600AAC16B /* PushNotificationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF1A593C29E5876600AAC16B /* PushNotificationTests.swift */; }; CF1A594829E5876600AAC16B /* Engine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF1A593F29E5876600AAC16B /* Engine.swift */; }; @@ -604,6 +605,7 @@ 8448F1D427E4726F0000B866 /* WalletConnect in Frameworks */, A54195A52934E83F0035AD19 /* Web3 in Frameworks */, 84E6B8652981720400428BAF /* WalletConnectPush in Frameworks */, + CF140F2D2A2A288D00BEB791 /* Web3Modal in Frameworks */, A5D85228286333E300DAF5C3 /* Starscream in Frameworks */, A5BB7FA328B6A50400707FC6 /* WalletConnectAuth in Frameworks */, ); @@ -1730,6 +1732,7 @@ A5BB7FA228B6A50400707FC6 /* WalletConnectAuth */, A54195A42934E83F0035AD19 /* Web3 */, 84E6B8642981720400428BAF /* WalletConnectPush */, + CF140F2C2A2A288D00BEB791 /* Web3Modal */, ); productName = DApp; productReference = 84CE641C27981DED00142511 /* DApp.app */; @@ -3143,6 +3146,10 @@ isa = XCSwiftPackageProductDependency; productName = Web3Wallet; }; + CF140F2C2A2A288D00BEB791 /* Web3Modal */ = { + isa = XCSwiftPackageProductDependency; + productName = Web3Modal; + }; CF9C7E492A01802F0037C006 /* Web3Modal */ = { isa = XCSwiftPackageProductDependency; productName = Web3Modal; diff --git a/Example/Showcase/Classes/ApplicationLayer/Configurator/ThirdPartyConfigurator.swift b/Example/Showcase/Classes/ApplicationLayer/Configurator/ThirdPartyConfigurator.swift index fbd9ee1a7..c7734a2f1 100644 --- a/Example/Showcase/Classes/ApplicationLayer/Configurator/ThirdPartyConfigurator.swift +++ b/Example/Showcase/Classes/ApplicationLayer/Configurator/ThirdPartyConfigurator.swift @@ -1,19 +1,21 @@ import WalletConnectNetworking import WalletConnectPairing import Auth +import Web3Modal struct ThirdPartyConfigurator: Configurator { func configure() { + + let metadata = AppMetadata( + name: "Showcase App", + description: "Showcase description", + url: "example.wallet", + icons: ["https://avatars.githubusercontent.com/u/37784886"] + ) + Networking.configure(projectId: InputConfig.projectId, socketFactory: DefaultSocketFactory()) - Pair.configure( - metadata: AppMetadata( - name: "Showcase App", - description: "Showcase description", - url: "example.wallet", - icons: ["https://avatars.githubusercontent.com/u/37784886"] - )) - Auth.configure(crypto: DefaultCryptoProvider()) + Web3Modal.configure(projectId: InputConfig.projectId, metadata: metadata) } } diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportRouter.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportRouter.swift index 512dc95da..57b880cfc 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportRouter.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportRouter.swift @@ -13,16 +13,7 @@ final class ImportRouter { } func presentWeb3Modal() { - Web3ModalSheetController( - projectId: InputConfig.projectId, - metadata: AppMetadata( - name: "Showcase App", - description: "Showcase description", - url: "example.wallet", - icons: ["https://avatars.githubusercontent.com/u/37784886"] - ), - webSocketFactory: DefaultSocketFactory() - ).present(from: viewController) + Web3ModalSheetController().present(from: viewController) } func presentChat(importAccount: ImportAccount) { diff --git a/Sources/WalletConnectPairing/PairingClient.swift b/Sources/WalletConnectPairing/PairingClient.swift index 389c11354..257b90ae7 100644 --- a/Sources/WalletConnectPairing/PairingClient.swift +++ b/Sources/WalletConnectPairing/PairingClient.swift @@ -74,7 +74,7 @@ public class PairingClient: PairingRegisterer, PairingInteracting, PairingClient try await walletPairService.pair(uri) } - public func create() async throws -> WalletConnectURI { + public func create() async throws -> WalletConnectURI { return try await appPairService.create() } diff --git a/Sources/WalletConnectPairing/PairingInteracting.swift b/Sources/WalletConnectPairing/PairingInteracting.swift index b0eb2cd38..c71d8c9bc 100644 --- a/Sources/WalletConnectPairing/PairingInteracting.swift +++ b/Sources/WalletConnectPairing/PairingInteracting.swift @@ -3,7 +3,7 @@ import Foundation public protocol PairingInteracting { func pair(uri: WalletConnectURI) async throws - func create() async throws -> WalletConnectURI + func create() async throws -> WalletConnectURI func getPairings() -> [Pairing] diff --git a/Sources/WalletConnectSign/Sign/SignClientProtocol.swift b/Sources/WalletConnectSign/Sign/SignClientProtocol.swift index a1dcfc3f3..397a59bf4 100644 --- a/Sources/WalletConnectSign/Sign/SignClientProtocol.swift +++ b/Sources/WalletConnectSign/Sign/SignClientProtocol.swift @@ -9,7 +9,10 @@ public protocol SignClientProtocol { var sessionSettlePublisher: AnyPublisher { get } var sessionDeletePublisher: AnyPublisher<(String, Reason), Never> { get } var sessionResponsePublisher: AnyPublisher { get } + var sessionRejectionPublisher: AnyPublisher<(Session.Proposal, Reason), Never> { get } + func connect(requiredNamespaces: [String: ProposalNamespace], optionalNamespaces: [String: ProposalNamespace]?, sessionProperties: [String: String]?, topic: String) async throws + func request(params: Request) async throws func approve(proposalId: String, namespaces: [String: SessionNamespace]) async throws func reject(proposalId: String, reason: RejectionReason) async throws func update(topic: String, namespaces: [String: SessionNamespace]) async throws diff --git a/Sources/Web3Modal/Modal/ModalContainerView.swift b/Sources/Web3Modal/Modal/ModalContainerView.swift index 0d0463f4f..799fe13e5 100644 --- a/Sources/Web3Modal/Modal/ModalContainerView.swift +++ b/Sources/Web3Modal/Modal/ModalContainerView.swift @@ -1,5 +1,4 @@ import SwiftUI -import WalletConnectPairing @available(iOS 14.0, *) public struct ModalContainerView: View { @@ -7,17 +6,7 @@ public struct ModalContainerView: View { @Environment(\.presentationMode) var presentationMode @State var showModal: Bool = false - - let projectId: String - let metadata: AppMetadata - let webSocketFactory: WebSocketFactory - - public init(projectId: String, metadata: AppMetadata, webSocketFactory: WebSocketFactory) { - self.projectId = projectId - self.metadata = metadata - self.webSocketFactory = webSocketFactory - } - + public var body: some View { VStack(spacing: 0) { @@ -34,15 +23,10 @@ public struct ModalContainerView: View { ModalSheet( viewModel: .init( isShown: $showModal, - projectId: projectId, - interactor: DefaultModalSheetInteractor( - projectId: projectId, - metadata: metadata, - webSocketFactory: webSocketFactory - ) + interactor: DefaultModalSheetInteractor() ) ) - .environment(\.projectId, projectId) + .environment(\.projectId, Web3Modal.config.projectId) .transition(.move(edge: .bottom)) .animation(.spring(), value: showModal) } diff --git a/Sources/Web3Modal/Modal/ModalInteractor.swift b/Sources/Web3Modal/Modal/ModalInteractor.swift index c44b539da..69f8671a0 100644 --- a/Sources/Web3Modal/Modal/ModalInteractor.swift +++ b/Sources/Web3Modal/Modal/ModalInteractor.swift @@ -1,59 +1,32 @@ import Combine import Foundation -import WalletConnectPairing -import WalletConnectSign protocol ModalSheetInteractor { func getListings() async throws -> [Listing] - func connect() async throws -> WalletConnectURI + func createPairingAndConnect() async throws -> WalletConnectURI var sessionSettlePublisher: AnyPublisher { get } + var sessionRejectionPublisher: AnyPublisher<(Session.Proposal, Reason), Never> { get } } final class DefaultModalSheetInteractor: ModalSheetInteractor { - let projectId: String - let metadata: AppMetadata - let socketFactory: WebSocketFactory - lazy var sessionSettlePublisher: AnyPublisher = Sign.instance.sessionSettlePublisher - - init(projectId: String, metadata: AppMetadata, webSocketFactory: WebSocketFactory) { - self.projectId = projectId - self.metadata = metadata - self.socketFactory = webSocketFactory - - Pair.configure(metadata: metadata) - Networking.configure(projectId: projectId, socketFactory: socketFactory) - } + lazy var sessionSettlePublisher: AnyPublisher = Web3Modal.instance.sessionSettlePublisher + lazy var sessionRejectionPublisher: AnyPublisher<(Session.Proposal, Reason), Never> = Web3Modal.instance.sessionRejectionPublisher func getListings() async throws -> [Listing] { let httpClient = HTTPNetworkClient(host: "explorer-api.walletconnect.com") let response = try await httpClient.request( ListingsResponse.self, - at: ExplorerAPI.getListings(projectId: projectId) + at: ExplorerAPI.getListings(projectId: Web3Modal.config.projectId) ) return response.listings.values.compactMap { $0 } } - func connect() async throws -> WalletConnectURI { - - let uri = try await Pair.instance.create() - - let methods: Set = ["eth_sendTransaction", "personal_sign", "eth_signTypedData"] - let blockchains: Set = [Blockchain("eip155:1")!] - let namespaces: [String: ProposalNamespace] = [ - "eip155": ProposalNamespace( - chains: blockchains, - methods: methods, - events: [] - ) - ] - - try await Sign.instance.connect(requiredNamespaces: namespaces, topic: uri.topic) - - return uri + func createPairingAndConnect() async throws -> WalletConnectURI { + try await Web3Modal.instance.createPairingAndConnect() } } diff --git a/Sources/Web3Modal/Modal/ModalSheet+Previews.swift b/Sources/Web3Modal/Modal/ModalSheet+Previews.swift index b21358a97..0ac74e2f4 100644 --- a/Sources/Web3Modal/Modal/ModalSheet+Previews.swift +++ b/Sources/Web3Modal/Modal/ModalSheet+Previews.swift @@ -1,7 +1,6 @@ #if DEBUG import SwiftUI -import WalletConnectPairing class WebSocketMock: WebSocketConnecting { var request: URLRequest = .init(url: URL(string: "wss://relay.walletconnect.com")!) @@ -37,12 +36,7 @@ struct ModalSheet_Previews: PreviewProvider { ModalSheet( viewModel: .init( isShown: .constant(true), - projectId: projectId, - interactor: DefaultModalSheetInteractor( - projectId: projectId, - metadata: metadata, - webSocketFactory: WebSocketFactoryMock() - ) + interactor: DefaultModalSheetInteractor() ) ) .previewLayout(.sizeThatFits) diff --git a/Sources/Web3Modal/Modal/ModalViewModel.swift b/Sources/Web3Modal/Modal/ModalViewModel.swift index 9e46705aa..e58658085 100644 --- a/Sources/Web3Modal/Modal/ModalViewModel.swift +++ b/Sources/Web3Modal/Modal/ModalViewModel.swift @@ -34,7 +34,6 @@ final class ModalViewModel: ObservableObject { var isShown: Binding let interactor: ModalSheetInteractor - let projectId: String let uiApplicationWrapper: UIApplicationWrapper @@ -52,13 +51,11 @@ final class ModalViewModel: ObservableObject { init( isShown: Binding, - projectId: String, interactor: ModalSheetInteractor, uiApplicationWrapper: UIApplicationWrapper = .live ) { self.isShown = isShown self.interactor = interactor - self.projectId = projectId self.uiApplicationWrapper = uiApplicationWrapper interactor.sessionSettlePublisher @@ -68,12 +65,25 @@ final class ModalViewModel: ObservableObject { isShown.wrappedValue = false } .store(in: &disposeBag) + + interactor.sessionRejectionPublisher + .receive(on: DispatchQueue.main) + .sink { (proposal, reason) in + + print(reason) + self.errorMessage = reason.message + + Task { + await self.createURI() + } + } + .store(in: &disposeBag) } @MainActor func createURI() async { do { - let wcUri = try await interactor.connect() + let wcUri = try await interactor.createPairingAndConnect() uri = wcUri.absoluteString deeplinkUri = wcUri.deeplinkUri } catch { diff --git a/Sources/Web3Modal/UIKitSupport/Web3ModalSheetController.swift b/Sources/Web3Modal/UIKitSupport/Web3ModalSheetController.swift index 66fec9da1..247a4a561 100644 --- a/Sources/Web3Modal/UIKitSupport/Web3ModalSheetController.swift +++ b/Sources/Web3Modal/UIKitSupport/Web3ModalSheetController.swift @@ -1,20 +1,14 @@ import SwiftUI -import WalletConnectNetworking -import WalletConnectPairing @available(iOS 14.0, *) -public class Web3ModalSheetController: UIHostingController { +public class Web3ModalSheetController: UIHostingController { @MainActor dynamic required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - public init(projectId: String, metadata: AppMetadata, webSocketFactory: WebSocketFactory) { - let view = AnyView( - ModalContainerView(projectId: projectId, metadata: metadata, webSocketFactory: webSocketFactory) - ) - - super.init(rootView: view) + public init() { + super.init(rootView: ModalContainerView()) self.modalTransitionStyle = .crossDissolve self.modalPresentationStyle = .overFullScreen } diff --git a/Sources/Web3Modal/Web3Modal.swift b/Sources/Web3Modal/Web3Modal.swift new file mode 100644 index 000000000..286e2473c --- /dev/null +++ b/Sources/Web3Modal/Web3Modal.swift @@ -0,0 +1,117 @@ +import Foundation + +import UIKit + +#if SWIFT_PACKAGE +public typealias VerifyContext = WalletConnectVerify.VerifyContext +#endif + +/// Web3Modal instance wrapper +/// +/// ```Swift +/// let metadata = AppMetadata( +/// name: "Swift dapp", +/// description: "dapp", +/// url: "dapp.wallet.connect", +/// icons: ["https://my_icon.com/1"] +/// ) +/// Web3Modal.configure(metadata: metadata) +/// Web3Modal.instance.getSessions() +/// ``` +public class Web3Modal { + /// Web3Modalt client instance + public static var instance: Web3ModalClient = { + guard let config = Web3Modal.config else { + fatalError("Error - you must call Web3Modal.configure(_:) before accessing the shared instance.") + } + return Web3ModalClient( + signClient: Sign.instance, + pairingClient: Pair.instance as! (PairingClientProtocol & PairingInteracting) + ) + }() + + struct Config { + let projectId: String + var sessionParams: SessionParams + } + + private(set) static var config: Config! + + private init() {} + + /// Wallet instance wallet config method. + /// - Parameters: + /// - metadata: App metadata + public static func configure( + projectId: String, + metadata: AppMetadata, + sessionParams: SessionParams = .default + ) { + Pair.configure(metadata: metadata) + Web3Modal.config = Web3Modal.Config(projectId: projectId, sessionParams: sessionParams) + } + + public static func set(sessionParams: SessionParams) { + Web3Modal.config.sessionParams = sessionParams + } + + public static func present(from presentingViewController: UIViewController? = nil) { + let vc = presentingViewController ?? topViewController() + + if #available(iOS 14.0, *) { + let modal = Web3ModalSheetController() + vc.present(modal, animated: true) + } + } + + static func topViewController( + _ base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController + ) -> UIViewController { + if let nav = base as? UINavigationController { + return topViewController(nav.visibleViewController) + } + + if let tab = base as? UITabBarController { + if let selected = tab.selectedViewController { + return topViewController(selected) + } + } + + if let presented = base?.presentedViewController { + return topViewController(presented) + } + + return base! + } +} + +public struct SessionParams { + public let requiredNamespaces: [String: ProposalNamespace] + public let optionalNamespaces: [String: ProposalNamespace]? + public let sessionProperties: [String: String]? + + public init(requiredNamespaces: [String : ProposalNamespace], optionalNamespaces: [String : ProposalNamespace]? = nil, sessionProperties: [String : String]? = nil) { + self.requiredNamespaces = requiredNamespaces + self.optionalNamespaces = optionalNamespaces + self.sessionProperties = sessionProperties + } + + public static let `default`: Self = { + let methods: Set = ["eth_sendTransaction", "personal_sign", "eth_signTypedData"] + let events: Set = ["chainChanged", "accountsChanged"] + let blockchains: Set = [Blockchain("eip155:1")!] + let namespaces: [String: ProposalNamespace] = [ + "eip155": ProposalNamespace( + chains: blockchains, + methods: methods, + events: events + ) + ] + + return SessionParams( + requiredNamespaces: namespaces, + optionalNamespaces: nil, + sessionProperties: nil + ) + }() +} diff --git a/Sources/Web3Modal/Web3ModalClient.swift b/Sources/Web3Modal/Web3ModalClient.swift new file mode 100644 index 000000000..f2f41506a --- /dev/null +++ b/Sources/Web3Modal/Web3ModalClient.swift @@ -0,0 +1,160 @@ +import Combine + +// Web3 Modal Client +/// +/// Cannot be instantiated outside of the SDK +/// +/// Access via `Web3Modal.instance` +public class Web3ModalClient { + // MARK: - Public Properties + + /// Publisher that sends sessions on every sessions update + /// + /// Event will be emited on controller and non-controller clients. + public var sessionsPublisher: AnyPublisher<[Session], Never> { + signClient.sessionsPublisher.eraseToAnyPublisher() + } + + /// Publisher that sends web socket connection status + public var socketConnectionStatusPublisher: AnyPublisher { + signClient.socketConnectionStatusPublisher.eraseToAnyPublisher() + } + + /// Publisher that sends session when one is settled + /// + /// Event is emited on proposer and responder client when both communicating peers have successfully established a session. + public var sessionSettlePublisher: AnyPublisher { + signClient.sessionSettlePublisher.eraseToAnyPublisher() + } + + /// Publisher that sends session proposal that has been rejected + /// + /// Event will be emited on dApp client only. + public var sessionRejectionPublisher: AnyPublisher<(Session.Proposal, Reason), Never> { + signClient.sessionRejectionPublisher.eraseToAnyPublisher() + } + + /// Publisher that sends deleted session topic + /// + /// Event can be emited on any type of the client. + public var sessionDeletePublisher: AnyPublisher<(String, Reason), Never> { + signClient.sessionDeletePublisher.eraseToAnyPublisher() + } + + /// Publisher that sends response for session request + /// + /// In most cases that event will be emited on dApp client. + public var sessionResponsePublisher: AnyPublisher { + signClient.sessionResponsePublisher.eraseToAnyPublisher() + } + + // MARK: - Private Properties + private let signClient: SignClientProtocol + private let pairingClient: (PairingClientProtocol & PairingInteracting) + + init( + signClient: SignClientProtocol, + pairingClient: (PairingClientProtocol & PairingInteracting) + ) { + self.signClient = signClient + self.pairingClient = pairingClient + } + + /// For creating new pairing URI + public func createPairing() async throws -> WalletConnectURI { + try await pairingClient.create() + } + + /// For proposing a session to a wallet. + /// Function will propose a session on existing pairing. + /// - Parameter topic: topic from existing pairing. + public func connect(on topic: String) async throws { + try await signClient.connect( + requiredNamespaces: Web3Modal.config.sessionParams.requiredNamespaces, + optionalNamespaces: Web3Modal.config.sessionParams.optionalNamespaces, + sessionProperties: Web3Modal.config.sessionParams.sessionProperties, + topic: topic + ) + } + + /// For proposing a session to a wallet. + /// Function will propose a session on newly created pairing. + public func createPairingAndConnect() async throws -> WalletConnectURI { + let uri = try await createPairing() + + try await signClient.connect( + requiredNamespaces: Web3Modal.config.sessionParams.requiredNamespaces, + optionalNamespaces: Web3Modal.config.sessionParams.optionalNamespaces, + sessionProperties: Web3Modal.config.sessionParams.sessionProperties, + topic: uri.topic + ) + + return uri + } + + /// For proposing a session to a wallet. + /// Function will propose a session on existing pairing. + /// - Parameters: + /// - requiredNamespaces: required namespaces for a session + /// - topic: pairing topic + public func connect( + requiredNamespaces: [String: ProposalNamespace], + optionalNamespaces: [String: ProposalNamespace]? = nil, + sessionProperties: [String: String]? = nil, + topic: String + ) async throws { + try await signClient.connect( + requiredNamespaces: requiredNamespaces, + optionalNamespaces: optionalNamespaces, + sessionProperties: sessionProperties, + topic: topic + ) + } + + /// Ping method allows to check if peer client is online and is subscribing for given topic + /// + /// Should Error: + /// - When the session topic is not found + /// + /// - Parameters: + /// - topic: Topic of a session + public func ping(topic: String) async throws { + try await pairingClient.ping(topic: topic) + } + + /// For sending JSON-RPC requests to wallet. + /// - Parameters: + /// - params: Parameters defining request and related session + public func request(params: Request) async throws { + try await signClient.request(params: params) + } + + /// For a terminating a session + /// + /// Should Error: + /// - When the session topic is not found + /// - Parameters: + /// - topic: Session topic that you want to delete + public func disconnect(topic: String) async throws { + try await signClient.disconnect(topic: topic) + } + + /// Query sessions + /// - Returns: All sessions + public func getSessions() -> [Session] { + signClient.getSessions() + } + + /// Query pairings + /// - Returns: All pairings + public func getPairings() -> [Pairing] { + pairingClient.getPairings() + } + + /// Delete all stored data such as: pairings, sessions, keys + /// + /// - Note: Will unsubscribe from all topics + public func cleanup() async throws { + try await signClient.cleanup() + } +} diff --git a/Sources/Web3Modal/Web3ModalImports.swift b/Sources/Web3Modal/Web3ModalImports.swift new file mode 100644 index 000000000..e0ddb7a79 --- /dev/null +++ b/Sources/Web3Modal/Web3ModalImports.swift @@ -0,0 +1,4 @@ +#if !CocoaPods +@_exported import WalletConnectSign +@_exported import WalletConnectPairing +#endif From 7545e3166f55663b6f7386fb3378d708ad845a56 Mon Sep 17 00:00:00 2001 From: Radek Novak Date: Mon, 5 Jun 2023 14:02:53 +0200 Subject: [PATCH 08/85] Add wallet related screens; Detail, Get a Wallet, View All --- .../Common/Helpers/QueryParameters.swift | 24 +- Sources/Web3Modal/Environment+ProjectId.swift | 12 + .../Web3Modal/Extensions/View+Backport.swift | 34 +++ .../Web3Modal/Modal/ModalContainerView.swift | 15 +- Sources/Web3Modal/Modal/ModalInteractor.swift | 4 +- .../Web3Modal/Modal/ModalSheet+Previews.swift | 1 + Sources/Web3Modal/Modal/ModalSheet.swift | 117 ++++------ Sources/Web3Modal/Modal/ModalViewModel.swift | 219 ++++++++++-------- .../Modal/Screens/GetAWalletView.swift | 34 +++ .../Web3Modal/Modal/Screens/QRCodeView.swift | 87 +++++-- .../Web3Modal/Modal/Screens/WalletList.swift | 163 +++++++++++++ .../Modal/Screens/WhatIsWalletView.swift | 12 +- .../Networking/Common/Endpoint.swift | 113 +++++++++ .../Networking/Common/HttpService.swift | 58 +++++ .../Networking/Explorer/ExplorerAPI.swift | 10 +- .../Explorer/ListingsResponse.swift | 2 +- .../UI/{ => Common}/ActivityIndicator.swift | 0 .../UI/{ => Common}/AsyncImage.swift | 0 Sources/Web3Modal/UI/WalletImage.swift | 40 ++++ .../Web3ModalSheetController.swift | 1 + 20 files changed, 724 insertions(+), 222 deletions(-) create mode 100644 Sources/Web3Modal/Environment+ProjectId.swift create mode 100644 Sources/Web3Modal/Modal/Screens/GetAWalletView.swift create mode 100644 Sources/Web3Modal/Modal/Screens/WalletList.swift create mode 100644 Sources/Web3Modal/Networking/Common/Endpoint.swift create mode 100644 Sources/Web3Modal/Networking/Common/HttpService.swift rename Sources/Web3Modal/UI/{ => Common}/ActivityIndicator.swift (100%) rename Sources/Web3Modal/UI/{ => Common}/AsyncImage.swift (100%) create mode 100644 Sources/Web3Modal/UI/WalletImage.swift diff --git a/Example/WalletApp/Common/Helpers/QueryParameters.swift b/Example/WalletApp/Common/Helpers/QueryParameters.swift index 8e6c0c493..50527700a 100644 --- a/Example/WalletApp/Common/Helpers/QueryParameters.swift +++ b/Example/WalletApp/Common/Helpers/QueryParameters.swift @@ -1,20 +1,14 @@ -// -// QueryParameters.swift -// WalletApp -// -// Created by Aleksandr Maltsev on 24.05.2023. -// import Foundation extension URL { - var queryParameters: [AnyHashable: Any] { - let urlComponents = URLComponents(url: self, resolvingAgainstBaseURL: false) - guard let queryItems = urlComponents?.queryItems else { return [:] } - var queryParams: [AnyHashable: Any] = [:] - queryItems.forEach { - queryParams[$0.name] = $0.value - } - return queryParams - } + var queryParameters: [AnyHashable: Any] { + let urlComponents = URLComponents(url: self, resolvingAgainstBaseURL: false) + guard let queryItems = urlComponents?.queryItems else { return [:] } + var queryParams: [AnyHashable: Any] = [:] + queryItems.forEach { + queryParams[$0.name] = $0.value + } + return queryParams + } } diff --git a/Sources/Web3Modal/Environment+ProjectId.swift b/Sources/Web3Modal/Environment+ProjectId.swift new file mode 100644 index 000000000..30e272aab --- /dev/null +++ b/Sources/Web3Modal/Environment+ProjectId.swift @@ -0,0 +1,12 @@ +import SwiftUI + +private struct ProjectIdKey: EnvironmentKey { + static let defaultValue: String = "" +} + +extension EnvironmentValues { + var projectId: String { + get { self[ProjectIdKey.self] } + set { self[ProjectIdKey.self] = newValue } + } +} diff --git a/Sources/Web3Modal/Extensions/View+Backport.swift b/Sources/Web3Modal/Extensions/View+Backport.swift index 1946cd60c..e178a1246 100644 --- a/Sources/Web3Modal/Extensions/View+Backport.swift +++ b/Sources/Web3Modal/Extensions/View+Backport.swift @@ -1,6 +1,38 @@ import SwiftUI import Combine +struct Backport { + let content: Content +} + +extension View { + var backport: Backport { Backport(content: self) } +} + +extension Backport where Content: View { + + enum Visibility { + case automatic + case visible + case hidden + } + + @ViewBuilder func scrollContentBackground(_ visibility: Backport.Visibility) -> some View { + if #available(iOS 16, *) { + switch visibility { + case .automatic: + content.scrollContentBackground(.automatic) + case .hidden: + content.scrollContentBackground(.hidden) + case .visible: + content.scrollContentBackground(.visible) + } + } else { + content + } + } +} + extension View { /// A backwards compatible wrapper for iOS 14 `onChange` @ViewBuilder func onChangeBackported(of value: T, perform: @escaping (T) -> Void) -> some View { @@ -13,3 +45,5 @@ extension View { } } } + + diff --git a/Sources/Web3Modal/Modal/ModalContainerView.swift b/Sources/Web3Modal/Modal/ModalContainerView.swift index 5722cdd3d..0d0463f4f 100644 --- a/Sources/Web3Modal/Modal/ModalContainerView.swift +++ b/Sources/Web3Modal/Modal/ModalContainerView.swift @@ -1,6 +1,7 @@ import SwiftUI import WalletConnectPairing +@available(iOS 14.0, *) public struct ModalContainerView: View { @Environment(\.presentationMode) var presentationMode @@ -34,10 +35,16 @@ public struct ModalContainerView: View { viewModel: .init( isShown: $showModal, projectId: projectId, - interactor: DefaultModalSheetInteractor(projectId: projectId, metadata: metadata, webSocketFactory: webSocketFactory) - )) - .transition(.move(edge: .bottom)) - .animation(.spring(), value: showModal) + interactor: DefaultModalSheetInteractor( + projectId: projectId, + metadata: metadata, + webSocketFactory: webSocketFactory + ) + ) + ) + .environment(\.projectId, projectId) + .transition(.move(edge: .bottom)) + .animation(.spring(), value: showModal) } } .edgesIgnoringSafeArea(.all) diff --git a/Sources/Web3Modal/Modal/ModalInteractor.swift b/Sources/Web3Modal/Modal/ModalInteractor.swift index 4d2ba5af6..c44b539da 100644 --- a/Sources/Web3Modal/Modal/ModalInteractor.swift +++ b/Sources/Web3Modal/Modal/ModalInteractor.swift @@ -1,6 +1,8 @@ + +import Combine +import Foundation import WalletConnectPairing import WalletConnectSign -import Combine protocol ModalSheetInteractor { func getListings() async throws -> [Listing] diff --git a/Sources/Web3Modal/Modal/ModalSheet+Previews.swift b/Sources/Web3Modal/Modal/ModalSheet+Previews.swift index 161c15bb0..b21358a97 100644 --- a/Sources/Web3Modal/Modal/ModalSheet+Previews.swift +++ b/Sources/Web3Modal/Modal/ModalSheet+Previews.swift @@ -23,6 +23,7 @@ class WebSocketFactoryMock: WebSocketFactory { } } +@available(iOS 14.0, *) struct ModalSheet_Previews: PreviewProvider { static let projectId = "9bfe94c9cbf74aaa0597094ef561f703" static let metadata = AppMetadata( diff --git a/Sources/Web3Modal/Modal/ModalSheet.swift b/Sources/Web3Modal/Modal/ModalSheet.swift index 4d4f42bf8..aa2d7bdad 100644 --- a/Sources/Web3Modal/Modal/ModalSheet.swift +++ b/Sources/Web3Modal/Modal/ModalSheet.swift @@ -1,7 +1,6 @@ import SwiftUI public struct ModalSheet: View { - @ObservedObject var viewModel: ModalViewModel public var body: some View { @@ -17,10 +16,11 @@ public struct ModalSheet: View { .cornerRadius(30, corners: [.topLeft, .topRight]) } .padding(.bottom, 40) + .edgesIgnoringSafeArea(.bottom) .onAppear { Task { - await viewModel.createURI() await viewModel.fetchWallets() + await viewModel.createURI() } } .background( @@ -55,16 +55,16 @@ public struct ModalSheet: View { private func contentHeader() -> some View { HStack(spacing: 0) { - if viewModel.destination != .wallets { + if viewModel.destination != .welcome { backButton() } Spacer() switch viewModel.destination { - case .wallets: + case .welcome: qrButton() - case .qr: + case .qr, .walletDetail: copyButton() default: EmptyView() @@ -84,81 +84,52 @@ public struct ModalSheet: View { } @ViewBuilder - private func content() -> some View { - switch viewModel.destination { - case .wallets: - ZStack { - VStack { - HStack { - ForEach(0..<4) { wallet in - gridItem(for: wallet) - } - } - HStack { - ForEach(4..<7) { wallet in - gridItem(for: wallet) - } - } - } - - Spacer().frame(height: 200) - } - case .help: - WhatIsWalletView() - case .qr: - VStack { - if let uri = viewModel.uri { - QRCodeView(uri: uri) - } else { - ActivityIndicator(isAnimating: .constant(true), style: .large) - } - } + private func welcome() -> some View { + if #available(iOS 14.0, *) { + WalletList( + wallets: .init(get: { + viewModel.wallets + }, set: { _ in }), + destination: .init(get: { + viewModel.destination + }, set: { _ in }), + navigateTo: viewModel.navigateTo(_:), + onListingTap: viewModel.onListingTap(_:) + ) } } - @ViewBuilder - private func gridItem(for index: Int) -> some View { - let wallet: Listing? = viewModel.wallets[safe: index] - - if #available(iOS 14.0, *) { - VStack { - AsyncImage(url: viewModel.imageUrl(for: wallet)) { image in - image - .resizable() - .scaledToFit() - } placeholder: { - Color - .foreground3 - .frame(width: 60, height: 60) - } - .cornerRadius(8) - .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(.gray.opacity(0.4), lineWidth: 1) - ) - - Text(wallet?.name ?? "WalletName") - .font(.system(size: 12)) - .foregroundColor(.foreground1) - .padding(.horizontal, 12) - .fixedSize(horizontal: true, vertical: true) - - Text("RECENT") - .opacity(0) - .font(.system(size: 10)) - .foregroundColor(.foreground3) - .padding(.horizontal, 12) - } - .redacted(reason: wallet == nil ? .placeholder : []) - .frame(maxWidth: 80, maxHeight: 96) - .onTapGesture { - Task { - await viewModel.onWalletTapped(index: index) - } + private func qrCode() -> some View { + VStack { + if let uri = viewModel.uri { + QRCodeView(uri: uri) + } else { + ActivityIndicator(isAnimating: .constant(true), style: .large) } } } + @ViewBuilder + private func content() -> some View { + switch viewModel.destination { + case .welcome, + .walletDetail, + .viewAll: + welcome() + case .help: + WhatIsWalletView(navigateTo: viewModel.navigateTo(_:)) + case .qr: + qrCode() + case .getWallet: + GetAWalletView( + wallets: Array(viewModel.wallets.prefix(6)), + onTap: viewModel.onGetWalletTap(_:) + ) + } + } +} + +extension ModalSheet { private func helpButton() -> some View { Button(action: { withAnimation { diff --git a/Sources/Web3Modal/Modal/ModalViewModel.swift b/Sources/Web3Modal/Modal/ModalViewModel.swift index 921e9b260..9e46705aa 100644 --- a/Sources/Web3Modal/Modal/ModalViewModel.swift +++ b/Sources/Web3Modal/Modal/ModalViewModel.swift @@ -3,118 +3,143 @@ import Combine import Foundation import SwiftUI -extension ModalSheet { - enum Destination: String, CaseIterable { - case wallets - case help - case qr +enum Destination: Equatable { + case welcome + case viewAll + case help + case qr + case walletDetail(Listing) + case getWallet - var contentTitle: String { - switch self { - case .wallets: - return "Connect your wallet" - case .qr: - return "Scan the code" - case .help: - return "What is a wallet?" - } + var contentTitle: String { + switch self { + case .welcome: + return "Connect your wallet" + case .viewAll: + return "View all" + case .qr: + return "Scan the code" + case .help: + return "What is a wallet?" + case .getWallet: + return "Get a wallet" + case let .walletDetail(wallet): + return wallet.name } } +} + +final class ModalViewModel: ObservableObject { - final class ModalViewModel: ObservableObject { - @Published private(set) var isShown: Binding - private let projectId: String - private let interactor: ModalSheetInteractor - private let uiApplicationWrapper: UIApplicationWrapper - - private var disposeBag = Set() - private var deeplinkUri: String? - - @Published private(set) var uri: String? - @Published private(set) var destination: Destination = .wallets - @Published private(set) var errorMessage: String? - @Published private(set) var wallets: [Listing] = [] - - init( - isShown: Binding, - projectId: String, - interactor: ModalSheetInteractor, - uiApplicationWrapper: UIApplicationWrapper = .live - ) { - self.isShown = isShown - self.interactor = interactor - self.projectId = projectId - self.uiApplicationWrapper = uiApplicationWrapper + + var isShown: Binding + let interactor: ModalSheetInteractor + let projectId: String + let uiApplicationWrapper: UIApplicationWrapper + + + @Published private(set) var destinationStack: [Destination] = [.welcome] + @Published private(set) var uri: String? + @Published private(set) var errorMessage: String? + @Published private(set) var wallets: [Listing] = [] + + var destination: Destination { + destinationStack.last! + } + + private var disposeBag = Set() + private var deeplinkUri: String? + + init( + isShown: Binding, + projectId: String, + interactor: ModalSheetInteractor, + uiApplicationWrapper: UIApplicationWrapper = .live + ) { + self.isShown = isShown + self.interactor = interactor + self.projectId = projectId + self.uiApplicationWrapper = uiApplicationWrapper - interactor.sessionSettlePublisher - .receive(on: DispatchQueue.main) - .sink { sessions in - print(sessions) - isShown.wrappedValue = false - } - .store(in: &disposeBag) - } - - @MainActor - func fetchWallets() async { - do { - let wallets = try await interactor.getListings() - // Small deliberate delay to ensure animations execute properly - try await Task.sleep(nanoseconds: 500_000_000) - - withAnimation { - self.wallets = wallets.sorted { $0.order < $1.order } - } - } catch { - print(error) - errorMessage = error.localizedDescription + interactor.sessionSettlePublisher + .receive(on: DispatchQueue.main) + .sink { sessions in + print(sessions) + isShown.wrappedValue = false } - } - - @MainActor - func createURI() async { - do { - let wcUri = try await interactor.connect() - uri = wcUri.absoluteString - deeplinkUri = wcUri.deeplinkUri - } catch { - print(error) - errorMessage = error.localizedDescription - } - } - - func navigateTo(_ destination: Destination) { - self.destination = destination - } + .store(in: &disposeBag) + } - func onBackButton() { - destination = .wallets + @MainActor + func createURI() async { + do { + let wcUri = try await interactor.connect() + uri = wcUri.absoluteString + deeplinkUri = wcUri.deeplinkUri + } catch { + print(error) + errorMessage = error.localizedDescription } + } - func onCopyButton() { - UIPasteboard.general.string = uri - } + func navigateTo(_ destination: Destination) { + guard self.destination != destination else { return } + destinationStack.append(destination) + } + + func onListingTap(_ listing: Listing) { + navigateToDeepLink( + universalLink: listing.mobile.universal ?? "", + nativeLink: listing.mobile.native ?? "" + ) + } + + func onGetWalletTap(_ listing: Listing) { + guard + let storeLinkString = listing.app.ios, + let storeLink = URL(string: storeLinkString) + else { return } - func onWalletTapped(index: Int) { - guard let wallet = wallets[safe: index] else { return } - - navigateToDeepLink( - universalLink: wallet.mobile.universal ?? "", - nativeLink: wallet.mobile.native ?? "" - ) - } + uiApplicationWrapper.openURL(storeLink) + } + + func onBackButton() { + guard destinationStack.count != 1 else { return } + _ = destinationStack.popLast() + } - func imageUrl(for listing: Listing?) -> URL? { - guard let listing = listing else { return nil } - - let urlString = "https://explorer-api.walletconnect.com/v3/logo/md/\(listing.imageId)?projectId=\(projectId)" - - return URL(string: urlString) + func onCopyButton() { + UIPasteboard.general.string = uri + } + + + @MainActor + func fetchWallets() async { + do { + let wallets = try await interactor.getListings() + // Small deliberate delay to ensure animations execute properly + try await Task.sleep(nanoseconds: 500_000_000) + + withAnimation { + self.wallets = wallets.sorted { + guard let lhs = $0.order else { + return false + } + + guard let rhs = $1.order else { + return true + } + + return lhs < rhs + } + } + } catch { + print(error) } } } -private extension ModalSheet.ModalViewModel { +private extension ModalViewModel { enum Errors: Error { case noWalletLinkFound } diff --git a/Sources/Web3Modal/Modal/Screens/GetAWalletView.swift b/Sources/Web3Modal/Modal/Screens/GetAWalletView.swift new file mode 100644 index 000000000..0dba7c792 --- /dev/null +++ b/Sources/Web3Modal/Modal/Screens/GetAWalletView.swift @@ -0,0 +1,34 @@ +import SwiftUI + +struct GetAWalletView: View { + + let wallets: [Listing] + let onTap: (Listing) -> Void + + var body: some View { + List { + ForEach(wallets) { wallet in + Button { + onTap(wallet) + } label: { + + HStack { + WalletImage(wallet: wallet) + .frame(width: 40, height: 40) + + Text(wallet.name) + .font(.system(size: 16, weight: .medium)) + .padding(.horizontal) + + Spacer() + + Image(systemName: "chevron.right") + .font(.system(.footnote).weight(.semibold)) + } + } + } + } + .frame(height: 500) + .listStyle(.plain) + } +} diff --git a/Sources/Web3Modal/Modal/Screens/QRCodeView.swift b/Sources/Web3Modal/Modal/Screens/QRCodeView.swift index 8ca2a6ca6..17f98f3d6 100644 --- a/Sources/Web3Modal/Modal/Screens/QRCodeView.swift +++ b/Sources/Web3Modal/Modal/Screens/QRCodeView.swift @@ -1,41 +1,82 @@ -import SwiftUI import QRCode +import SwiftUI struct QRCodeView: View { - - @State var doc: QRCode.Document! - @Environment(\.colorScheme) var colorScheme: ColorScheme @State var uri: String + @State var index: Int = 0 + + var foreground1: UIColor { + UIColor(.foreground1).resolvedColor( + with: UITraitCollection( + userInterfaceStyle: colorScheme == .dark ? .dark : .light + ) + ) + } + + var background1: UIColor { + UIColor(.background1).resolvedColor( + with: UITraitCollection( + userInterfaceStyle: colorScheme == .dark ? .dark : .light + ) + ) + } + var body: some View { - QRCodeViewUI( + render( content: uri, - errorCorrection: .quantize, - foregroundColor: AssetColor.background1.uiColor.cgColor, - backgroundColor: AssetColor.foreground1.uiColor.cgColor, - pixelStyle: QRCode.PixelShape.Vertical( - insetFraction: 0.2, - cornerRadiusFraction: 1 - ), - eyeStyle: QRCode.EyeShape.Squircle(), - logoTemplate: QRCode.LogoTemplate( - image: Asset.wc_logo.uiImage.cgImage!, - path: CGPath( - rect: CGRect(x: 0.35, y: 0.3875, width: 0.30, height: 0.225), - transform: nil - ) + size: .init( + width: UIScreen.main.bounds.width - 40, + height: UIScreen.main.bounds.width - 40 + ) + ) + .colorScheme(.dark) + } + + private func render(content: String, size: CGSize) -> Image { + let doc = QRCode.Document( + utf8String: content, + errorCorrection: .quantize + ) + doc.design.shape.eye = QRCode.EyeShape.Squircle() + doc.design.shape.onPixels = QRCode.PixelShape.Vertical( + insetFraction: 0.2, + cornerRadiusFraction: 1 + ) + + doc.design.style.eye = QRCode.FillStyle.Solid(foreground1.cgColor) + doc.design.style.pupil = QRCode.FillStyle.Solid(foreground1.cgColor) + doc.design.style.onPixels = QRCode.FillStyle.Solid(foreground1.cgColor) + doc.design.style.background = QRCode.FillStyle.Solid(background1.cgColor) + + doc.logoTemplate = QRCode.LogoTemplate( + image: Asset.wc_logo.uiImage.cgImage!, + path: CGPath( + rect: CGRect(x: 0.35, y: 0.3875, width: 0.30, height: 0.225), + transform: nil ) ) - .frame(height: UIScreen.main.bounds.width) + + return doc.imageUI( + size, label: Text("QR code with URI") + )! + } +} + +extension UIColor { + func image(_ size: CGSize = CGSize(width: 1, height: 1)) -> UIImage { + return UIGraphicsImageRenderer(size: size).image { rendererContext in + self.setFill() + rendererContext.fill(CGRect(origin: .zero, size: size)) + } } } struct QRCodeView_Previews: PreviewProvider { - - static let stubUri: String = Array(repeating: ["a", "b", "c", "1", "2", "3"], count: 50) - .flatMap({ $0 }) + static let stubUri: String = Array(repeating: ["a", "b", "c", "1", "2", "3"], count: 10) + .flatMap { $0 } .shuffled() .joined() diff --git a/Sources/Web3Modal/Modal/Screens/WalletList.swift b/Sources/Web3Modal/Modal/Screens/WalletList.swift new file mode 100644 index 000000000..2a6b4f6d7 --- /dev/null +++ b/Sources/Web3Modal/Modal/Screens/WalletList.swift @@ -0,0 +1,163 @@ +import SwiftUI + +@available(iOS 14.0, *) +struct WalletList: View { + @Namespace var namespace + + @Binding var wallets: [Listing] + @Binding var destination: Destination + + var navigateTo: (Destination) -> Void + var onListingTap: (Listing) -> Void + + var body: some View { + content() + } + + @ViewBuilder + private func content() -> some View { + switch destination { + case .welcome: + initialList() + case .viewAll: + viewAll() + case let .walletDetail(wallet): + walletDetail(wallet) + default: + EmptyView() + } + } + + private func initialList() -> some View { + ZStack { + VStack { + HStack { + ForEach(0..<4) { wallet in + gridItem(for: wallet) + } + } + HStack { + ForEach(4..<7) { wallet in + gridItem(for: wallet) + } + + viewAllItem() + .onTapGesture { + navigateTo(.viewAll) + } + } + } + + Spacer().frame(height: 200) + } + } + + private func viewAll() -> some View { + ScrollView(.vertical) { + VStack(alignment: .leading) { + ForEach(Array(stride(from: 0, to: wallets.count, by: 4)), id: \.self) { row in + HStack { + ForEach(row..<(row + 4), id: \.self) { index in + if wallets.indices.contains(index) { + gridItem(for: index) + } + } + } + } + } + } + } + + @ViewBuilder + func viewAllItem() -> some View { + VStack { + VStack(spacing: 3) { + HStack(spacing: 3) { + ForEach(7..<9) { index in + WalletImage(wallet: wallets[safe: index]) + .cornerRadius(8) + .aspectRatio(1, contentMode: .fit) + } + } + .padding(.horizontal, 5) + + HStack(spacing: 3) { + ForEach(9..<11) { index in + WalletImage(wallet: wallets[safe: index]) + .cornerRadius(8) + .aspectRatio(1, contentMode: .fit) + } + } + .padding(.horizontal, 5) + } + .padding(.vertical, 3) + .frame(width: 60, height: 60) + .background(Color.background2) + .cornerRadius(8) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(.gray.opacity(0.4), lineWidth: 1) + ) + + Text("View All") + .font(.system(size: 12)) + .foregroundColor(.foreground1) + .padding(.horizontal, 12) + .fixedSize(horizontal: true, vertical: true) + + Spacer() + } + .frame(maxWidth: 80, maxHeight: 96) + } + + @ViewBuilder + func gridItem(for index: Int) -> some View { + let wallet: Listing? = wallets[safe: index] + + VStack { + WalletImage(wallet: wallet) + .frame(width: 60, height: 60) + .matchedGeometryEffect(id: index, in: namespace) + + Text(wallet?.name ?? "WalletName") + .font(.system(size: 12)) + .foregroundColor(.foreground1) + .padding(.horizontal, 12) + .multilineTextAlignment(.center) + .minimumScaleFactor(0.4) + + Text("RECENT") + .opacity(0) + .font(.system(size: 10)) + .foregroundColor(.foreground3) + .padding(.horizontal, 12) + } + .redacted(reason: wallet == nil ? .placeholder : []) + .frame(maxWidth: 80, maxHeight: 96) + .onTapGesture { + guard let wallet else { return } + + navigateTo(.walletDetail(wallet)) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + onListingTap(wallet) + } + } + } + + private func walletDetail(_ wallet: Listing) -> some View { + VStack { + WalletImage(wallet: wallet, size: .large) + .frame(maxWidth: 96, maxHeight: 96) + + Text("Continue in \(wallet.name)...") + .font(.system(size: 16, weight: .medium)) + .foregroundColor(.foreground1) + + Text("Accept connection request in the app") + .font(.system(size: 14)) + .foregroundColor(.foreground3) + } + .padding() + } +} diff --git a/Sources/Web3Modal/Modal/Screens/WhatIsWalletView.swift b/Sources/Web3Modal/Modal/Screens/WhatIsWalletView.swift index 1119fc8eb..7c75d549b 100644 --- a/Sources/Web3Modal/Modal/Screens/WhatIsWalletView.swift +++ b/Sources/Web3Modal/Modal/Screens/WhatIsWalletView.swift @@ -2,6 +2,8 @@ import SwiftUI struct WhatIsWalletView: View { + var navigateTo: (Destination) -> Void + var body: some View { VStack(spacing: 10) { @@ -22,13 +24,17 @@ struct WhatIsWalletView: View { ) HStack { - Button(action: {}) { + Button(action: { + navigateTo(.getWallet) + }) { HStack { Image("wallet", bundle: .module) Text("Get a Wallet") } } - Button(action: {}) { + Button(action: { + + }) { HStack { Text("Learn More") Image("external_link", bundle: .module) @@ -75,6 +81,6 @@ struct WhatIsWalletView_Previews: PreviewProvider { static var previews: some View { - WhatIsWalletView() + WhatIsWalletView(navigateTo: { _ in}) } } diff --git a/Sources/Web3Modal/Networking/Common/Endpoint.swift b/Sources/Web3Modal/Networking/Common/Endpoint.swift new file mode 100644 index 000000000..1962d080b --- /dev/null +++ b/Sources/Web3Modal/Networking/Common/Endpoint.swift @@ -0,0 +1,113 @@ +import Foundation + +struct Endpoint { + let path: String + let queryItems: [URLQueryItem] + let headers: [Headers] + let method: Method + let host: String + let body: Data? + let validResponseCodes: Set + + public enum Method: String { + case GET + case POST + case PUT + case PATCH + case DELETE + } + + enum Headers { + /// Standard headers used for every network call + case standard + + public var makeHeader: [String: String] { + switch self { + case .standard: + return [ + "Content-Type": "application/json", + ] + } + } + } + + var urlRequest: URLRequest { + var urlRequest = URLRequest(url: urlForRequest) + urlRequest.httpMethod = method.rawValue + urlRequest.httpBody = body + urlRequest.allHTTPHeaderFields = makeHTTPHeaders(headers) + return urlRequest + } + + private var urlForRequest: URL { + var components = URLComponents() + components.scheme = "https" + components.host = host + components.path = path + + if !queryItems.isEmpty { + components.queryItems = queryItems + } + + guard + let url = components.url + else { + preconditionFailure( + """ + Failed to construct valid url, if setting up new endpoint + make sure you have prefix / in path such as /v1/users + """ + ) + } + + return url + } + + private func makeHTTPHeaders(_ headers: [Headers]) -> [String: String] { + headers.reduce(into: [String: String]()) { result, nextElement in + result = result.merging(nextElement.makeHeader) { _, new in new } + } + } +} + +extension Endpoint.Headers: Equatable { + static func == (lhs: Endpoint.Headers, rhs: Endpoint.Headers) -> Bool { + lhs.makeHeader == rhs.makeHeader + } +} + +extension Endpoint { + + /// Un-authenticated endpoint. + /// - Parameters: + /// - path: Path for your endpoint` + /// - headers: Specific headers + /// - method: .GET, .POST etc + /// - host: Host url + /// - shouldEncodePath: This setting affects how your url is constructed. + /// - body: If you need to pass parameters, provide them here. + /// - validResponseCodes: This is set to default value `Set(200 ..< 300)` + /// and can be overridden if needed + /// - Returns: Endpoint with URLRequest that gets passed directly to HttpService. + static func bare( + path: String, + queryItems: [URLQueryItem] = [], + headers: [Endpoint.Headers] = [], + method: Endpoint.Method, + host: String, + body: Data? = nil, + validResponseCodes: Set = Set(200 ..< 300) + ) -> Self { + var headers = headers + headers.append(.standard) + return Self( + path: path, + queryItems: queryItems, + headers: headers, + method: method, + host: host, + body: body, + validResponseCodes: validResponseCodes + ) + } +} diff --git a/Sources/Web3Modal/Networking/Common/HttpService.swift b/Sources/Web3Modal/Networking/Common/HttpService.swift new file mode 100644 index 000000000..3b4fe6971 --- /dev/null +++ b/Sources/Web3Modal/Networking/Common/HttpService.swift @@ -0,0 +1,58 @@ +import Foundation + +struct HttpService { + var performRequest: (_ endpoint: Endpoint) async throws -> Result +} + +extension HttpService { + + static var live: Self = .init(performRequest: { endpoint in + + let (data, response) = try await URLSession.shared.data(for: endpoint.urlRequest) + + let error = errorForResponse(response, data, validResponseCodes: endpoint.validResponseCodes) + if let error = error { + return .failure(error) + } else { + return .success(data) + } + }) + + private static func errorForResponse( + _ response: URLResponse?, + _ data: Data?, validResponseCodes: Set + ) -> Error? { + guard let httpResponse = response as? HTTPURLResponse else { + return nil + } + + if !validResponseCodes.contains(httpResponse.statusCode) { + return Errors.badResponseCode( + code: httpResponse.statusCode, + payload: data + ) + } + + return nil + } + + enum Errors: Error, Equatable { + case emptyResponse + case badResponseCode(code: Int, payload: Data?) + + public var properties: [String: String] { + switch self { + case let .badResponseCode(code, _): + return [ + "category": "http_error", + "http_code": "\(String(code))" + ] + case .emptyResponse: + return [ + "category": "payload", + "message": "Failed for empty response" + ] + } + } + } +} diff --git a/Sources/Web3Modal/Networking/Explorer/ExplorerAPI.swift b/Sources/Web3Modal/Networking/Explorer/ExplorerAPI.swift index b26d84662..1f37011b3 100644 --- a/Sources/Web3Modal/Networking/Explorer/ExplorerAPI.swift +++ b/Sources/Web3Modal/Networking/Explorer/ExplorerAPI.swift @@ -3,19 +3,19 @@ import HTTPClient enum ExplorerAPI: HTTPService { case getListings(projectId: String) - + var path: String { switch self { case .getListings: return "/w3m/v1/getiOSListings" } } - + var method: HTTPMethod { switch self { case .getListings: return .get } } - + var body: Data? { nil } @@ -26,11 +26,11 @@ enum ExplorerAPI: HTTPService { return [ "projectId": projectId, "page": "1", - "entries": "9", + "entries": "300", ] } } - + var scheme: String { return "https" } diff --git a/Sources/Web3Modal/Networking/Explorer/ListingsResponse.swift b/Sources/Web3Modal/Networking/Explorer/ListingsResponse.swift index 884943e44..8c08458a5 100644 --- a/Sources/Web3Modal/Networking/Explorer/ListingsResponse.swift +++ b/Sources/Web3Modal/Networking/Explorer/ListingsResponse.swift @@ -8,7 +8,7 @@ struct Listing: Codable, Hashable, Identifiable { let id: String let name: String let homepage: String - let order: Int + let order: Int? let imageId: String let app: App let mobile: Mobile diff --git a/Sources/Web3Modal/UI/ActivityIndicator.swift b/Sources/Web3Modal/UI/Common/ActivityIndicator.swift similarity index 100% rename from Sources/Web3Modal/UI/ActivityIndicator.swift rename to Sources/Web3Modal/UI/Common/ActivityIndicator.swift diff --git a/Sources/Web3Modal/UI/AsyncImage.swift b/Sources/Web3Modal/UI/Common/AsyncImage.swift similarity index 100% rename from Sources/Web3Modal/UI/AsyncImage.swift rename to Sources/Web3Modal/UI/Common/AsyncImage.swift diff --git a/Sources/Web3Modal/UI/WalletImage.swift b/Sources/Web3Modal/UI/WalletImage.swift new file mode 100644 index 000000000..f14614bd5 --- /dev/null +++ b/Sources/Web3Modal/UI/WalletImage.swift @@ -0,0 +1,40 @@ +import SwiftUI + +struct WalletImage: View { + + enum Size: String { + case small = "sm" + case medium = "md" + case large = "lg" + } + + @Environment(\.projectId) var projectId + + var wallet: Listing? + var size: Size = .medium + + var body: some View { + + AsyncImage(url: imageURL(for: wallet)) { image in + image + .resizable() + .scaledToFit() + } placeholder: { + Color.foreground3 + } + .cornerRadius(8) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(.gray.opacity(0.4), lineWidth: 1) + ) + } + + private func imageURL(for wallet: Listing?) -> URL? { + + guard let wallet else { return nil } + + let urlString = "https://explorer-api.walletconnect.com/v3/logo/\(size.rawValue)/\(wallet.imageId)?projectId=\(projectId)" + + return URL(string: urlString) + } +} diff --git a/Sources/Web3Modal/UIKitSupport/Web3ModalSheetController.swift b/Sources/Web3Modal/UIKitSupport/Web3ModalSheetController.swift index 9890a66d2..66fec9da1 100644 --- a/Sources/Web3Modal/UIKitSupport/Web3ModalSheetController.swift +++ b/Sources/Web3Modal/UIKitSupport/Web3ModalSheetController.swift @@ -2,6 +2,7 @@ import SwiftUI import WalletConnectNetworking import WalletConnectPairing +@available(iOS 14.0, *) public class Web3ModalSheetController: UIHostingController { @MainActor dynamic required init?(coder aDecoder: NSCoder) { From d4fc494590ca3555671a2d066c332d218fb8db9b Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Wed, 5 Apr 2023 14:42:32 +0500 Subject: [PATCH 09/85] Sync structure --- .../xcschemes/WalletConnectSync.xcscheme | 67 +++++++++++++++++++ Package.swift | 8 ++- Sources/WalletConnectSync/SyncClient.swift | 45 +++++++++++++ Sources/WalletConnectSync/SyncImports.swift | 3 + .../WalletConnectSync/Types/StoreMap.swift | 6 ++ .../WalletConnectSync/Types/StoreUpdate.swift | 17 +++++ .../WalletConnectSync/Types/SyncUpdate.swift | 6 ++ 7 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/WalletConnectSync.xcscheme create mode 100644 Sources/WalletConnectSync/SyncClient.swift create mode 100644 Sources/WalletConnectSync/SyncImports.swift create mode 100644 Sources/WalletConnectSync/Types/StoreMap.swift create mode 100644 Sources/WalletConnectSync/Types/StoreUpdate.swift create mode 100644 Sources/WalletConnectSync/Types/SyncUpdate.swift diff --git a/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/WalletConnectSync.xcscheme b/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/WalletConnectSync.xcscheme new file mode 100644 index 000000000..4b58c89d1 --- /dev/null +++ b/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/WalletConnectSync.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Package.swift b/Package.swift index e1db6a6fd..189529863 100644 --- a/Package.swift +++ b/Package.swift @@ -37,6 +37,9 @@ let package = Package( .library( name: "WalletConnectNetworking", targets: ["WalletConnectNetworking"]), + .library( + name: "WalletConnectSync", + targets: ["WalletConnectSync"]), .library( name: "WalletConnectVerify", targets: ["WalletConnectVerify"]), @@ -120,10 +123,13 @@ let package = Package( dependencies: []), .target( name: "WalletConnectVerify", - dependencies: ["WalletConnectUtils", "WalletConnectNetworking"]), + dependencies: ["WalletConnectUtils"]), .target( name: "Web3Modal", dependencies: ["QRCode", "WalletConnectSign"]), + .target( + name: "WalletConnectSync", + dependencies: ["WalletConnectNetworking"]), .testTarget( name: "WalletConnectSignTests", dependencies: ["WalletConnectSign", "WalletConnectUtils", "TestingUtils", "WalletConnectVerify"]), diff --git a/Sources/WalletConnectSync/SyncClient.swift b/Sources/WalletConnectSync/SyncClient.swift new file mode 100644 index 000000000..33dc12dfb --- /dev/null +++ b/Sources/WalletConnectSync/SyncClient.swift @@ -0,0 +1,45 @@ +import Foundation +import Combine + +public final class SyncClient { + + private let updateSubject = PassthroughSubject() + + public var updatePublisher: AnyPublisher { + return updateSubject.eraseToAnyPublisher() + } + + public init() { + + } + + /// Get message to sign for an account + public func getMessage(account: Account) async throws -> String { + fatalError() + } + + /// Register an account to sync + public func register(account: Account, signature: CacaoSignature) async throws { + fatalError() + } + + /// Create a store + public func create(account: Account, store: String) async throws { + fatalError() + } + + // Set value to store + public func set(account: Account, store: String, key: String, value: String) async throws { + fatalError() + } + + // Set value from store by key + public func delete(account: Account, store: String, key: String) async throws { + fatalError() + } + + // Get stores + public func getStores(account: Account) -> StoreMap { + fatalError() + } +} diff --git a/Sources/WalletConnectSync/SyncImports.swift b/Sources/WalletConnectSync/SyncImports.swift new file mode 100644 index 000000000..23c1738ef --- /dev/null +++ b/Sources/WalletConnectSync/SyncImports.swift @@ -0,0 +1,3 @@ +#if !CocoaPods +@_exported import WalletConnectNetworking +#endif diff --git a/Sources/WalletConnectSync/Types/StoreMap.swift b/Sources/WalletConnectSync/Types/StoreMap.swift new file mode 100644 index 000000000..fd0725ac4 --- /dev/null +++ b/Sources/WalletConnectSync/Types/StoreMap.swift @@ -0,0 +1,6 @@ +import Foundation + +public struct StoreMap { + public let store: String + public let state: [String: String] +} diff --git a/Sources/WalletConnectSync/Types/StoreUpdate.swift b/Sources/WalletConnectSync/Types/StoreUpdate.swift new file mode 100644 index 000000000..4e2e50c87 --- /dev/null +++ b/Sources/WalletConnectSync/Types/StoreUpdate.swift @@ -0,0 +1,17 @@ +import Foundation + +public enum StoreUpdate: Codable { + case set(StoreSet) + case delete(StoreDelete) +} + +public struct StoreSet: Codable { + public let id: UInt64 + public let key: String + public let value: String +} + +public struct StoreDelete: Codable { + public let id: UInt64 + public let key: String +} diff --git a/Sources/WalletConnectSync/Types/SyncUpdate.swift b/Sources/WalletConnectSync/Types/SyncUpdate.swift new file mode 100644 index 000000000..2a15b1480 --- /dev/null +++ b/Sources/WalletConnectSync/Types/SyncUpdate.swift @@ -0,0 +1,6 @@ +import Foundation + +public struct SyncUpdate { + public let store: String + public let update: StoreUpdate +} From 56c55930680dfde1fd051ccd23f4510fc633e7cc Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Sun, 9 Apr 2023 03:14:08 +0500 Subject: [PATCH 10/85] SyncSetService --- Example/ExampleApp.xcodeproj/project.pbxproj | 36 ++++++++++++++ Example/IntegrationTests/Sync/SyncTests.swift | 32 ++++++++++++ .../WalletConnectSigner/CryptoProvider.swift | 4 ++ .../WalletConnectSync/Extensions/String.swift | 20 ++++++++ .../Services/SyncDerivationService.swift | 20 ++++++++ .../Services/SyncSetService.swift | 45 +++++++++++++++++ .../Services/SyncStorage.swift | 49 +++++++++++++++++++ Sources/WalletConnectSync/SyncClient.swift | 22 +++++---- Sources/WalletConnectSync/SyncImports.swift | 1 + .../Types/Methods/SyncDeleteMethod.swift | 9 ++++ .../Types/Methods/SyncSetMethod.swift | 9 ++++ .../WalletConnectSync/Types/StoreUpdate.swift | 10 ++-- .../WalletConnectSync/Types/SyncRecord.swift | 11 +++++ .../Extensions/Dictionary.swift | 0 .../KeyedDatabase.swift | 14 +++--- 15 files changed, 261 insertions(+), 21 deletions(-) create mode 100644 Example/IntegrationTests/Sync/SyncTests.swift create mode 100644 Sources/WalletConnectSync/Extensions/String.swift create mode 100644 Sources/WalletConnectSync/Services/SyncDerivationService.swift create mode 100644 Sources/WalletConnectSync/Services/SyncSetService.swift create mode 100644 Sources/WalletConnectSync/Services/SyncStorage.swift create mode 100644 Sources/WalletConnectSync/Types/Methods/SyncDeleteMethod.swift create mode 100644 Sources/WalletConnectSync/Types/Methods/SyncSetMethod.swift create mode 100644 Sources/WalletConnectSync/Types/SyncRecord.swift rename Sources/{Chat => WalletConnectUtils}/Extensions/Dictionary.swift (100%) rename Sources/{Chat => WalletConnectUtils}/KeyedDatabase.swift (72%) diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index 35ab930db..d45c90be2 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -88,6 +88,9 @@ A54195A02934BFEF0035AD19 /* EIP1271VerifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A541959C2934BFEF0035AD19 /* EIP1271VerifierTests.swift */; }; A54195A12934BFEF0035AD19 /* EIP191VerifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A541959D2934BFEF0035AD19 /* EIP191VerifierTests.swift */; }; A54195A52934E83F0035AD19 /* Web3 in Frameworks */ = {isa = PBXBuildFile; productRef = A54195A42934E83F0035AD19 /* Web3 */; }; + A561C80029DF32CE00DF540D /* HDWalletKit in Frameworks */ = {isa = PBXBuildFile; productRef = A561C7FF29DF32CE00DF540D /* HDWalletKit */; }; + A561C80329DFCCDC00DF540D /* SyncTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A561C80229DFCCDC00DF540D /* SyncTests.swift */; }; + A561C80529DFCD4500DF540D /* WalletConnectSync in Frameworks */ = {isa = PBXBuildFile; productRef = A561C80429DFCD4500DF540D /* WalletConnectSync */; }; A5629AA92876A23100094373 /* ChatService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5629AA82876A23100094373 /* ChatService.swift */; }; A5629ABD2876CBC000094373 /* ChatListModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5629AB82876CBC000094373 /* ChatListModule.swift */; }; A5629ABE2876CBC000094373 /* ChatListPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5629AB92876CBC000094373 /* ChatListPresenter.swift */; }; @@ -417,6 +420,7 @@ A541959B2934BFEF0035AD19 /* SignerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignerTests.swift; sourceTree = ""; }; A541959C2934BFEF0035AD19 /* EIP1271VerifierTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EIP1271VerifierTests.swift; sourceTree = ""; }; A541959D2934BFEF0035AD19 /* EIP191VerifierTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EIP191VerifierTests.swift; sourceTree = ""; }; + A561C80229DFCCDC00DF540D /* SyncTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncTests.swift; sourceTree = ""; }; A5629AA82876A23100094373 /* ChatService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatService.swift; sourceTree = ""; }; A5629AB82876CBC000094373 /* ChatListModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListModule.swift; sourceTree = ""; }; A5629AB92876CBC000094373 /* ChatListPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListPresenter.swift; sourceTree = ""; }; @@ -624,6 +628,7 @@ CF9C7E4A2A01802F0037C006 /* Web3Modal in Frameworks */, A58EC618299D665A00F3452A /* Web3Inbox in Frameworks */, A5629AEA2877F2D600094373 /* WalletConnectChat in Frameworks */, + A561C80029DF32CE00DF540D /* HDWalletKit in Frameworks */, A59FAEC928B7B93A002BB66F /* Web3 in Frameworks */, A5629AF22877F75100094373 /* Starscream in Frameworks */, A59F877628B5462900A9CD80 /* WalletConnectAuth in Frameworks */, @@ -643,6 +648,7 @@ buildActionMask = 2147483647; files = ( A5E03DFF2864662500888481 /* WalletConnect in Frameworks */, + A561C80529DFCD4500DF540D /* WalletConnectSync in Frameworks */, A5E03DF52864651200888481 /* Starscream in Frameworks */, 847CF3AF28E3141700F1D760 /* WalletConnectPush in Frameworks */, A5C8BE85292FE20B006CC85C /* Web3 in Frameworks */, @@ -944,6 +950,14 @@ path = Signer; sourceTree = ""; }; + A561C80129DFCCD300DF540D /* Sync */ = { + isa = PBXGroup; + children = ( + A561C80229DFCCDC00DF540D /* SyncTests.swift */, + ); + path = Sync; + sourceTree = ""; + }; A5629AA42876A19D00094373 /* DomainLayer */ = { isa = PBXGroup; children = ( @@ -1342,6 +1356,7 @@ A5E03DEE286464DB00888481 /* IntegrationTests */ = { isa = PBXGroup; children = ( + A561C80129DFCCD300DF540D /* Sync */, 849D7A91292E2115006A2BD4 /* Push */, 84CEC64728D8A98900D081A8 /* Pairing */, A5E03E0A28646A8A00888481 /* Stubs */, @@ -1776,6 +1791,7 @@ A58EC610299D57B800F3452A /* AsyncButton */, A58EC617299D665A00F3452A /* Web3Inbox */, CF9C7E492A01802F0037C006 /* Web3Modal */, + A561C7FF29DF32CE00DF540D /* HDWalletKit */, ); productName = Showcase; productReference = A58E7CE828729F550082D443 /* Showcase.app */; @@ -1820,6 +1836,7 @@ 847CF3AE28E3141700F1D760 /* WalletConnectPush */, A5C8BE84292FE20B006CC85C /* Web3 */, C5DD5BE0294E09E3008FD3A4 /* Web3Wallet */, + A561C80429DFCD4500DF540D /* WalletConnectSync */, ); productName = IntegrationTests; productReference = A5E03DED286464DB00888481 /* IntegrationTests.xctest */; @@ -1923,6 +1940,7 @@ A5AE354528A1A2AC0059AE8A /* XCRemoteSwiftPackageReference "Web3" */, A5434021291E6A270068F706 /* XCRemoteSwiftPackageReference "solana-swift" */, A58EC60F299D57B800F3452A /* XCRemoteSwiftPackageReference "swiftui-async-button" */, + A561C7FE29DF32CE00DF540D /* XCRemoteSwiftPackageReference "HDWallet" */, ); productRefGroup = 764E1D3D26F8D3FC00A1FB15 /* Products */; projectDirPath = ""; @@ -2151,6 +2169,7 @@ files = ( A5A0843E29D2F624000B9B17 /* DefaultCryptoProvider.swift in Sources */, 84CEC64628D89D6B00D081A8 /* PairingTests.swift in Sources */, + A561C80329DFCCDC00DF540D /* SyncTests.swift in Sources */, 767DC83528997F8E00080FA9 /* EthSendTransaction.swift in Sources */, 8439CB89293F658E00F2F2E2 /* PushMessage.swift in Sources */, A518B31428E33A6500A2CE93 /* InputConfig.swift in Sources */, @@ -2989,6 +3008,14 @@ kind = branch; }; }; + A561C7FE29DF32CE00DF540D /* XCRemoteSwiftPackageReference "HDWallet" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/WalletConnect/HDWallet"; + requirement = { + branch = develop; + kind = branch; + }; + }; A58EC60F299D57B800F3452A /* XCRemoteSwiftPackageReference "swiftui-async-button" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/lorenzofiamingo/swiftui-async-button"; @@ -3058,6 +3085,15 @@ package = A5AE354528A1A2AC0059AE8A /* XCRemoteSwiftPackageReference "Web3" */; productName = Web3; }; + A561C7FF29DF32CE00DF540D /* HDWalletKit */ = { + isa = XCSwiftPackageProductDependency; + package = A561C7FE29DF32CE00DF540D /* XCRemoteSwiftPackageReference "HDWallet" */; + productName = HDWalletKit; + }; + A561C80429DFCD4500DF540D /* WalletConnectSync */ = { + isa = XCSwiftPackageProductDependency; + productName = WalletConnectSync; + }; A5629AE92877F2D600094373 /* WalletConnectChat */ = { isa = XCSwiftPackageProductDependency; productName = WalletConnectChat; diff --git a/Example/IntegrationTests/Sync/SyncTests.swift b/Example/IntegrationTests/Sync/SyncTests.swift new file mode 100644 index 000000000..035f7bda8 --- /dev/null +++ b/Example/IntegrationTests/Sync/SyncTests.swift @@ -0,0 +1,32 @@ +import Foundation +import XCTest +@testable import WalletConnectSync +@testable import WalletConnectSigner + +final class SyncTests: XCTestCase { + var client: SyncClient! + var store: SyncStorage! + var signer: MessageSigner! + + let account = Account("0x1FF34C90a0850Fe7227fcFA642688b9712477482")! + let privateKey = Data(hex: "99c6f0a7ac44d40d3d7f31083e9f5b045d4bf932fdf9f4a3c241cdd3cbc98045") + + override func setUp() async throws { + client = makeClient() + signer = MessageSignerFactory(signerFactory: DefaultSignerFactory()).create(projectId: InputConfig.projectId) + } + + func makeClient() -> SyncClient { + let syncStorage = SyncStorage(keychain: KeychainStorageMock()) + return SyncClient(syncStorage: syncStorage) + } + + func testSync() async throws { + let message = client.getMessage(account: account) + let signature = try signer.sign(message: message, privateKey: privateKey, type: .eip191) + + try client.register(account: account, signature: signature) + + XCTAssertEqual(try store.getSignature(for: account), signature.s) + } +} diff --git a/Sources/WalletConnectSigner/CryptoProvider.swift b/Sources/WalletConnectSigner/CryptoProvider.swift index a04efbcff..4edc66132 100644 --- a/Sources/WalletConnectSigner/CryptoProvider.swift +++ b/Sources/WalletConnectSigner/CryptoProvider.swift @@ -4,3 +4,7 @@ public protocol CryptoProvider { func recoverPubKey(signature: EthereumSignature, message: Data) throws -> Data func keccak256(_ data: Data) -> Data } + +extension CryptoProvider { + +} diff --git a/Sources/WalletConnectSync/Extensions/String.swift b/Sources/WalletConnectSync/Extensions/String.swift new file mode 100644 index 000000000..a32883da5 --- /dev/null +++ b/Sources/WalletConnectSync/Extensions/String.swift @@ -0,0 +1,20 @@ +import Foundation + +extension String: GenericPasswordConvertible { + + enum Errors: Error { + case notUTF8 + } + + public init(rawRepresentation data: D) throws where D: ContiguousBytes { + let buffer = data.withUnsafeBytes { Data($0) } + guard let string = String(data: buffer, encoding: .utf8) else { + throw Errors.notUTF8 + } + self = string + } + + public var rawRepresentation: Data { + return data(using: .utf8) ?? Data() + } +} diff --git a/Sources/WalletConnectSync/Services/SyncDerivationService.swift b/Sources/WalletConnectSync/Services/SyncDerivationService.swift new file mode 100644 index 000000000..ec62b52e0 --- /dev/null +++ b/Sources/WalletConnectSync/Services/SyncDerivationService.swift @@ -0,0 +1,20 @@ +import Foundation + +final class SyncDerivationService { + + private let crypto: CryptoProvider + private let storage: SyncStorage + private let kms: KeyManagementServiceProtocol + + init(crypto: CryptoProvider, storage: SyncStorage, kms: KeyManagementServiceProtocol) { + self.crypto = crypto + self.storage = storage + self.kms = kms + } + + func deriveTopic(account: Account, store: String) throws -> String { + fatalError() + + // TODO: KMS setSymKey + } +} diff --git a/Sources/WalletConnectSync/Services/SyncSetService.swift b/Sources/WalletConnectSync/Services/SyncSetService.swift new file mode 100644 index 000000000..027bdefb6 --- /dev/null +++ b/Sources/WalletConnectSync/Services/SyncSetService.swift @@ -0,0 +1,45 @@ +import Foundation +import Combine + +final class SyncSetService { + + private var publishers: Set = [] + + private let networkInteractor: NetworkInteracting + private let derivationService: SyncDerivationService + private let storage: SyncStorage + + init(networkInteractor: NetworkInteracting, derivationService: SyncDerivationService, storage: SyncStorage) { + self.networkInteractor = networkInteractor + self.derivationService = derivationService + self.storage = storage + + setupSubscriptions() + } + + func set(account: Account, store: String, key: String, value: String) async throws { + let protocolMethod = SyncSetMethod() + let request = RPCRequest(method: protocolMethod.method, params: ["key": key, "value": value]) + let topic = try derivationService.deriveTopic(account: account, store: store) + try await networkInteractor.request(request, topic: topic, protocolMethod: protocolMethod) + + let set = StoreSet(id: request.id!.integer, key: key, value: value) + storage.set(update: .set(set), topic: topic, store: store, for: account) + } +} + +private extension SyncSetService { + + func setupSubscriptions() { + + } + + func handleSetResponse() { + networkInteractor.requestSubscription(on: SyncSetMethod()) + .sink { [unowned self] (payload: RequestSubscriptionPayload) in + storage.set(update: payload.request, topic: payload.topic, store: <#T##String#>, for: <#T##Account#>) + + } + .store(in: &publishers) + } +} diff --git a/Sources/WalletConnectSync/Services/SyncStorage.swift b/Sources/WalletConnectSync/Services/SyncStorage.swift new file mode 100644 index 000000000..de605b99f --- /dev/null +++ b/Sources/WalletConnectSync/Services/SyncStorage.swift @@ -0,0 +1,49 @@ +import Foundation +import Combine + +final class SyncStorage { + + private var syncUpdateSubject = PassthroughSubject() + + var syncUpdatePublisher: AnyPublisher { + syncUpdateSubject.eraseToAnyPublisher() + } + + private let keychain: KeychainStorageProtocol + private let database: KeyedDatabase + + init(keychain: KeychainStorageProtocol, database: KeyedDatabase) { + self.keychain = keychain + self.database = database + } + + func saveIdentityKey(_ key: String, for account: Account) throws { + try keychain.add(key, forKey: signatureIdentifier(for: account)) + } + + func getSignature(for account: Account) throws -> String { + let identifier = signatureIdentifier(for: account) + + guard let key: String = try? keychain.read(key: identifier) + else { throw Errors.signatureNotFound } + + return key + } + + func set(update: StoreUpdate, topic: String, store: String, for account: Account) { + let record = SyncRecord(topic: topic, store: store, update: update) + database.set(record, for: account.absoluteString) + syncUpdateSubject.send(record.publicRepresentation()) + } +} + +private extension SyncStorage { + + enum Errors: Error { + case signatureNotFound + } + + func signatureIdentifier(for account: Account) -> String { + return "com.walletconnect.sync.signature.\(account.absoluteString)" + } +} diff --git a/Sources/WalletConnectSync/SyncClient.swift b/Sources/WalletConnectSync/SyncClient.swift index 33dc12dfb..8bbdefea5 100644 --- a/Sources/WalletConnectSync/SyncClient.swift +++ b/Sources/WalletConnectSync/SyncClient.swift @@ -3,24 +3,28 @@ import Combine public final class SyncClient { - private let updateSubject = PassthroughSubject() - public var updatePublisher: AnyPublisher { - return updateSubject.eraseToAnyPublisher() + return syncStorage.syncUpdatePublisher } - public init() { - + private let syncStorage: SyncStorage + + init(syncStorage: SyncStorage) { + self.syncStorage = syncStorage } /// Get message to sign for an account - public func getMessage(account: Account) async throws -> String { - fatalError() + public func getMessage(account: Account) -> String { + return """ + I authorize this app to sync my account: \(account.absoluteString) + + Read more about Sync API: https://docs.walletconnect.com/2.0/specs/clients/sync + """ } /// Register an account to sync - public func register(account: Account, signature: CacaoSignature) async throws { - fatalError() + public func register(account: Account, signature: CacaoSignature) throws { + try syncStorage.saveIdentityKey(signature.s, for: account) } /// Create a store diff --git a/Sources/WalletConnectSync/SyncImports.swift b/Sources/WalletConnectSync/SyncImports.swift index 23c1738ef..04c25a62c 100644 --- a/Sources/WalletConnectSync/SyncImports.swift +++ b/Sources/WalletConnectSync/SyncImports.swift @@ -1,3 +1,4 @@ #if !CocoaPods @_exported import WalletConnectNetworking +@_exported import WalletConnectSigner #endif diff --git a/Sources/WalletConnectSync/Types/Methods/SyncDeleteMethod.swift b/Sources/WalletConnectSync/Types/Methods/SyncDeleteMethod.swift new file mode 100644 index 000000000..59a602fd3 --- /dev/null +++ b/Sources/WalletConnectSync/Types/Methods/SyncDeleteMethod.swift @@ -0,0 +1,9 @@ +import Foundation + +struct SyncDeleteMethod: ProtocolMethod { + let method: String = "wc_syncDel" + + let requestConfig = RelayConfig(tag: 5002, prompt: false, ttl: 2592000) + + let responseConfig = RelayConfig(tag: 5003, prompt: false, ttl: 2592000) +} diff --git a/Sources/WalletConnectSync/Types/Methods/SyncSetMethod.swift b/Sources/WalletConnectSync/Types/Methods/SyncSetMethod.swift new file mode 100644 index 000000000..fb69c2ad1 --- /dev/null +++ b/Sources/WalletConnectSync/Types/Methods/SyncSetMethod.swift @@ -0,0 +1,9 @@ +import Foundation + +struct SyncSetMethod: ProtocolMethod { + let method: String = "wc_syncSet" + + let requestConfig = RelayConfig(tag: 5000, prompt: false, ttl: 2592000) + + let responseConfig = RelayConfig(tag: 5001, prompt: false, ttl: 2592000) +} diff --git a/Sources/WalletConnectSync/Types/StoreUpdate.swift b/Sources/WalletConnectSync/Types/StoreUpdate.swift index 4e2e50c87..773cbb7d3 100644 --- a/Sources/WalletConnectSync/Types/StoreUpdate.swift +++ b/Sources/WalletConnectSync/Types/StoreUpdate.swift @@ -1,17 +1,17 @@ import Foundation -public enum StoreUpdate: Codable { +public enum StoreUpdate: Codable, Equatable { case set(StoreSet) case delete(StoreDelete) } -public struct StoreSet: Codable { - public let id: UInt64 +public struct StoreSet: Codable, Equatable { + public let id: Int64 public let key: String public let value: String } -public struct StoreDelete: Codable { - public let id: UInt64 +public struct StoreDelete: Codable, Equatable { + public let id: Int64 public let key: String } diff --git a/Sources/WalletConnectSync/Types/SyncRecord.swift b/Sources/WalletConnectSync/Types/SyncRecord.swift new file mode 100644 index 000000000..d6dec2837 --- /dev/null +++ b/Sources/WalletConnectSync/Types/SyncRecord.swift @@ -0,0 +1,11 @@ +import Foundation + +struct SyncRecord: Codable, Equatable { + let topic: String + let store: String + let update: StoreUpdate + + func publicRepresentation() -> SyncUpdate { + return SyncUpdate(store: store, update: update) + } +} diff --git a/Sources/Chat/Extensions/Dictionary.swift b/Sources/WalletConnectUtils/Extensions/Dictionary.swift similarity index 100% rename from Sources/Chat/Extensions/Dictionary.swift rename to Sources/WalletConnectUtils/Extensions/Dictionary.swift diff --git a/Sources/Chat/KeyedDatabase.swift b/Sources/WalletConnectUtils/KeyedDatabase.swift similarity index 72% rename from Sources/Chat/KeyedDatabase.swift rename to Sources/WalletConnectUtils/KeyedDatabase.swift index 9755542fc..d8a25a5ce 100644 --- a/Sources/Chat/KeyedDatabase.swift +++ b/Sources/WalletConnectUtils/KeyedDatabase.swift @@ -1,6 +1,6 @@ import Foundation -class KeyedDatabase where Element: Codable & Equatable { +public class KeyedDatabase where Element: Codable & Equatable { private var index: [String: [Element]] = [:] { didSet { @@ -13,28 +13,28 @@ class KeyedDatabase where Element: Codable & Equatable { private let storage: KeyValueStorage private let identifier: String - var onUpdate: (() -> Void)? + public var onUpdate: (() -> Void)? - init(storage: KeyValueStorage, identifier: String) { + public init(storage: KeyValueStorage, identifier: String) { self.storage = storage self.identifier = identifier initializeIndex() } - func getAll() -> [Element] { + public func getAll() -> [Element] { return index.values.reduce([], +) } - func getElements(for key: String) -> [Element] { + public func getElements(for key: String) -> [Element] { return index[key] ?? [] } - func set(_ element: Element, for key: String) { + public func set(_ element: Element, for key: String) { index.append(element, for: key) } - func delete(_ element: Element, for key: String) { + public func delete(_ element: Element, for key: String) { index.delete(element, for: key) } } From 4061ce29725a78da4857e21cc42b8dc55e784774 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Mon, 10 Apr 2023 22:28:10 +0500 Subject: [PATCH 11/85] Chat Sync Integration --- Example/ExampleApp.xcodeproj/project.pbxproj | 28 ++++ Example/IntegrationTests/Chat/ChatTests.swift | 9 +- .../Sync/SyncDerivationServiceTests.swift | 26 ++++ Example/IntegrationTests/Sync/SyncTests.swift | 116 ++++++++++++++-- Example/Shared/DefaultCryptoProvider.swift | 18 +++ .../DomainLayer/Chat/ChatService.swift | 4 +- .../Web3Inbox/Web3InboxViewController.swift | 2 +- Package.swift | 4 +- Sources/Chat/Chat.swift | 10 +- Sources/Chat/ChatClient.swift | 10 +- Sources/Chat/ChatClientFactory.swift | 18 ++- Sources/Chat/ChatImports.swift | 1 + Sources/Chat/ChatStorage.swift | 84 ++++++------ Sources/Chat/ChatStorageIdentifiers.swift | 2 - .../Invitee/InvitationHandlingService.swift | 3 +- .../Inviter/InviteService.swift | 6 +- .../Sync/SyncRegisterService.swift | 28 ++++ Sources/Chat/Types/Plain/SentInvite.swift | 7 + Sources/Chat/Types/Plain/Thread.swift | 7 + .../WalletConnectSigner/CryptoProvider.swift | 10 +- .../Services/SyncDerivationService.swift | 53 +++++++- .../Services/SyncService.swift | 86 ++++++++++++ .../Services/SyncSetService.swift | 45 ------- .../Services/SyncStorage.swift | 49 ------- .../Stores/SyncIndexStore.swift | 44 ++++++ .../Stores/SyncObjectStore.swift | 51 +++++++ .../Stores/SyncSignatureStore.swift | 35 +++++ .../WalletConnectSync/Stores/SyncStore.swift | 127 ++++++++++++++++++ .../Stores/SyncStoreFactory.swift | 12 ++ Sources/WalletConnectSync/Sync.swift | 27 ++++ Sources/WalletConnectSync/SyncClient.swift | 31 +++-- .../WalletConnectSync/SyncClientFactory.swift | 29 ++++ Sources/WalletConnectSync/SyncConfig.swift | 7 + Sources/WalletConnectSync/SyncImports.swift | 1 - .../SyncStorageIdentifiers.swift | 6 + .../WalletConnectSync/Types/StoreMap.swift | 5 +- .../WalletConnectSync/Types/StoreUpdate.swift | 12 +- .../WalletConnectSync/Types/SyncRecord.swift | 8 +- .../WalletConnectSync/Types/SyncUpdate.swift | 6 - .../WalletConnectUtils/KeyedDatabase.swift | 23 ++-- .../WalletConnectUtils/NewKeyedDatabase.swift | 53 ++++++++ Sources/Web3Inbox/Web3Inbox.swift | 11 +- Tests/AuthTests/Stubs/MessageSignerMock.swift | 3 + WalletConnectSwiftV2.podspec | 7 +- 44 files changed, 902 insertions(+), 222 deletions(-) create mode 100644 Example/IntegrationTests/Sync/SyncDerivationServiceTests.swift create mode 100644 Sources/Chat/ProtocolServices/Sync/SyncRegisterService.swift create mode 100644 Sources/WalletConnectSync/Services/SyncService.swift delete mode 100644 Sources/WalletConnectSync/Services/SyncSetService.swift delete mode 100644 Sources/WalletConnectSync/Services/SyncStorage.swift create mode 100644 Sources/WalletConnectSync/Stores/SyncIndexStore.swift create mode 100644 Sources/WalletConnectSync/Stores/SyncObjectStore.swift create mode 100644 Sources/WalletConnectSync/Stores/SyncSignatureStore.swift create mode 100644 Sources/WalletConnectSync/Stores/SyncStore.swift create mode 100644 Sources/WalletConnectSync/Stores/SyncStoreFactory.swift create mode 100644 Sources/WalletConnectSync/Sync.swift create mode 100644 Sources/WalletConnectSync/SyncClientFactory.swift create mode 100644 Sources/WalletConnectSync/SyncConfig.swift create mode 100644 Sources/WalletConnectSync/SyncStorageIdentifiers.swift delete mode 100644 Sources/WalletConnectSync/Types/SyncUpdate.swift create mode 100644 Sources/WalletConnectUtils/NewKeyedDatabase.swift diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index d45c90be2..1812f7912 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -111,6 +111,10 @@ 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 */; }; + 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 */; }; + A573C53D29EC366500E3CBFD /* HDWalletKit in Frameworks */ = {isa = PBXBuildFile; productRef = A573C53C29EC366500E3CBFD /* HDWalletKit */; }; A578FA322873036400AA7720 /* InputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A578FA312873036400AA7720 /* InputView.swift */; }; A578FA35287304A300AA7720 /* Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = A578FA34287304A300AA7720 /* Color.swift */; }; A578FA372873D8EE00AA7720 /* UIColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A578FA362873D8EE00AA7720 /* UIColor.swift */; }; @@ -440,6 +444,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 = ""; }; + A573C53629EC34A600E3CBFD /* SyncDerivationServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncDerivationServiceTests.swift; sourceTree = ""; }; A578FA312873036400AA7720 /* InputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputView.swift; sourceTree = ""; }; A578FA34287304A300AA7720 /* Color.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Color.swift; sourceTree = ""; }; A578FA362873D8EE00AA7720 /* UIColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIColor.swift; sourceTree = ""; }; @@ -609,6 +614,7 @@ A54195A52934E83F0035AD19 /* Web3 in Frameworks */, 84E6B8652981720400428BAF /* WalletConnectPush in Frameworks */, A5D85228286333E300DAF5C3 /* Starscream in Frameworks */, + A573C53929EC365000E3CBFD /* HDWalletKit in Frameworks */, A5BB7FA328B6A50400707FC6 /* WalletConnectAuth in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -654,6 +660,7 @@ A5C8BE85292FE20B006CC85C /* Web3 in Frameworks */, 84DDB4ED28ABB663003D66ED /* WalletConnectAuth in Frameworks */, C5DD5BE1294E09E3008FD3A4 /* Web3Wallet in Frameworks */, + A573C53B29EC365800E3CBFD /* HDWalletKit in Frameworks */, A5E03E01286466EA00888481 /* WalletConnectChat in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -662,6 +669,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + A573C53D29EC366500E3CBFD /* HDWalletKit in Frameworks */, C56EE27D293F56F8004840D1 /* WalletConnectChat in Frameworks */, C5133A78294125CC00A8314C /* Web3 in Frameworks */, 84536D7429EEBCF0008EA8DB /* Web3Inbox in Frameworks */, @@ -954,6 +962,7 @@ isa = PBXGroup; children = ( A561C80229DFCCDC00DF540D /* SyncTests.swift */, + A573C53629EC34A600E3CBFD /* SyncDerivationServiceTests.swift */, ); path = Sync; sourceTree = ""; @@ -1745,6 +1754,7 @@ A5BB7FA228B6A50400707FC6 /* WalletConnectAuth */, A54195A42934E83F0035AD19 /* Web3 */, 84E6B8642981720400428BAF /* WalletConnectPush */, + A573C53829EC365000E3CBFD /* HDWalletKit */, ); productName = DApp; productReference = 84CE641C27981DED00142511 /* DApp.app */; @@ -1837,6 +1847,7 @@ A5C8BE84292FE20B006CC85C /* Web3 */, C5DD5BE0294E09E3008FD3A4 /* Web3Wallet */, A561C80429DFCD4500DF540D /* WalletConnectSync */, + A573C53A29EC365800E3CBFD /* HDWalletKit */, ); productName = IntegrationTests; productReference = A5E03DED286464DB00888481 /* IntegrationTests.xctest */; @@ -1866,6 +1877,7 @@ C5B2F7042970573D000DBA0E /* SolanaSwift */, 84E6B85329787AAE00428BAF /* WalletConnectPush */, 84536D7329EEBCF0008EA8DB /* Web3Inbox */, + A573C53C29EC366500E3CBFD /* HDWalletKit */, ); productName = ChatWallet; productReference = C56EE21B293F55ED004840D1 /* WalletApp.app */; @@ -2167,6 +2179,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + A573C53729EC34A600E3CBFD /* SyncDerivationServiceTests.swift in Sources */, A5A0843E29D2F624000B9B17 /* DefaultCryptoProvider.swift in Sources */, 84CEC64628D89D6B00D081A8 /* PairingTests.swift in Sources */, A561C80329DFCCDC00DF540D /* SyncTests.swift in Sources */, @@ -3103,6 +3116,21 @@ package = A5D85224286333D500DAF5C3 /* XCRemoteSwiftPackageReference "Starscream" */; productName = Starscream; }; + A573C53829EC365000E3CBFD /* HDWalletKit */ = { + isa = XCSwiftPackageProductDependency; + package = A561C7FE29DF32CE00DF540D /* XCRemoteSwiftPackageReference "HDWallet" */; + productName = HDWalletKit; + }; + A573C53A29EC365800E3CBFD /* HDWalletKit */ = { + isa = XCSwiftPackageProductDependency; + package = A561C7FE29DF32CE00DF540D /* XCRemoteSwiftPackageReference "HDWallet" */; + productName = HDWalletKit; + }; + A573C53C29EC366500E3CBFD /* HDWalletKit */ = { + isa = XCSwiftPackageProductDependency; + package = A561C7FE29DF32CE00DF540D /* XCRemoteSwiftPackageReference "HDWallet" */; + productName = HDWalletKit; + }; A58EC610299D57B800F3452A /* AsyncButton */ = { isa = XCSwiftPackageProductDependency; package = A58EC60F299D57B800F3452A /* XCRemoteSwiftPackageReference "swiftui-async-button" */; diff --git a/Example/IntegrationTests/Chat/ChatTests.swift b/Example/IntegrationTests/Chat/ChatTests.swift index 2c917aa35..305c3aa70 100644 --- a/Example/IntegrationTests/Chat/ChatTests.swift +++ b/Example/IntegrationTests/Chat/ChatTests.swift @@ -3,6 +3,7 @@ import XCTest @testable import WalletConnectChat import WalletConnectUtils @testable import WalletConnectKMS +@testable import WalletConnectSync import WalletConnectRelay import Combine @@ -36,10 +37,16 @@ final class ChatTests: XCTestCase { keychainStorage: keychain, keyValueStorage: keyValueStorage) + let syncClient = SyncClientFactory.create( + networkInteractor: networkingInteractor, + crypto: DefaultCryptoProvider(), + keychain: keychain + ) + let clientId = try! networkingInteractor.getClientId() logger.debug("My client id is: \(clientId)") - return ChatClientFactory.create(keyserverURL: keyserverURL, relayClient: relayClient, networkingInteractor: networkingInteractor, keychain: keychain, logger: logger, keyValueStorage: keyValueStorage) + return ChatClientFactory.create(keyserverURL: keyserverURL, relayClient: relayClient, networkingInteractor: networkingInteractor, keychain: keychain, logger: logger, keyValueStorage: keyValueStorage, syncClient: syncClient) } func testInvite() async throws { diff --git a/Example/IntegrationTests/Sync/SyncDerivationServiceTests.swift b/Example/IntegrationTests/Sync/SyncDerivationServiceTests.swift new file mode 100644 index 000000000..e697748fe --- /dev/null +++ b/Example/IntegrationTests/Sync/SyncDerivationServiceTests.swift @@ -0,0 +1,26 @@ +import Foundation +import XCTest +@testable import WalletConnectSync +@testable import WalletConnectSigner + +class SyncDerivationServiceTests: XCTestCase { + + func testDerivation() throws { + let account = Account("eip155:1:0x1FF34C90a0850Fe7227fcFA642688b9712477482")! + let signature = "0xc91265eadb1473d90f8d49d31b7016feb7f7761a2a986ca2146a4b8964f3357569869680154927596a5829ceea925f4196b8a853a29c2c1d5915832fc9f1c6a01c" + let keychain = KeychainStorageMock() + let syncStorage = SyncSignatureStore(keychain: keychain) + let kms = KeyManagementService(keychain: keychain) + let derivationService = SyncDerivationService( + syncStorage: syncStorage, + crypto: DefaultCryptoProvider(), + kms: kms + ) + + try syncStorage.saveSignature(signature, for: account) + + let topic = try derivationService.deriveTopic(account: account, store: "my-user-profile") + + XCTAssertEqual(topic, "741f8902d339c4c16f33fa598a6598b63e5ed125d761374511b2e06562b033eb") + } +} diff --git a/Example/IntegrationTests/Sync/SyncTests.swift b/Example/IntegrationTests/Sync/SyncTests.swift index 035f7bda8..229f1821f 100644 --- a/Example/IntegrationTests/Sync/SyncTests.swift +++ b/Example/IntegrationTests/Sync/SyncTests.swift @@ -1,32 +1,128 @@ import Foundation +import Combine import XCTest @testable import WalletConnectSync @testable import WalletConnectSigner final class SyncTests: XCTestCase { - var client: SyncClient! - var store: SyncStorage! + + struct TestObject: SyncObject { + let id: String + let value: String + + var syncId: String { + return id + } + } + + var publishers = Set() + + var client1: SyncClient! + var client2: SyncClient! + + var indexStore1: SyncIndexStore! + var indexStore2: SyncIndexStore! + + var syncStore1: SyncStore! + var syncStore2: SyncStore! + var signer: MessageSigner! - let account = Account("0x1FF34C90a0850Fe7227fcFA642688b9712477482")! + let storeName = "SyncTests_store" + let account = Account("eip155:1:0x1FF34C90a0850Fe7227fcFA642688b9712477482")! let privateKey = Data(hex: "99c6f0a7ac44d40d3d7f31083e9f5b045d4bf932fdf9f4a3c241cdd3cbc98045") override func setUp() async throws { - client = makeClient() + indexStore1 = makeIndexStore() + indexStore2 = makeIndexStore() + client1 = makeClient(indexStore: indexStore1, suffix: "❤️") + client2 = makeClient(indexStore: indexStore2, suffix: "💜") + syncStore1 = makeSyncStore(client: client1, indexStore: indexStore1) + syncStore2 = makeSyncStore(client: client2, indexStore: indexStore2) signer = MessageSignerFactory(signerFactory: DefaultSignerFactory()).create(projectId: InputConfig.projectId) } - func makeClient() -> SyncClient { - let syncStorage = SyncStorage(keychain: KeychainStorageMock()) - return SyncClient(syncStorage: syncStorage) + func makeClient(indexStore: SyncIndexStore, suffix: String) -> SyncClient { + let syncSignatureStore = SyncSignatureStore(keychain: KeychainStorageMock()) + let keychain = KeychainStorageMock() + let kms = KeyManagementService(keychain: keychain) + let derivationService = SyncDerivationService(syncStorage: syncSignatureStore, crypto: DefaultCryptoProvider(), kms: kms) + let logger = ConsoleLogger(suffix: suffix, loggingLevel: .debug) + let relayClient = RelayClient(relayHost: InputConfig.relayHost, projectId: InputConfig.projectId, keychainStorage: keychain, socketFactory: DefaultSocketFactory(), logger: logger) + let networkingInteractor = NetworkingClientFactory.create( + relayClient: relayClient, + logger: logger, + keychainStorage: keychain, + keyValueStorage: RuntimeKeyValueStorage()) + let syncService = SyncService(networkInteractor: networkingInteractor, derivationService: derivationService, signatureStore: syncSignatureStore, indexStore: indexStore, logger: logger) + return SyncClient(syncService: syncService, syncSignatureStore: syncSignatureStore) + } + + func makeIndexStore() -> SyncIndexStore { + let store = CodableStore(defaults: RuntimeKeyValueStorage(), identifier: "indexStore") + return SyncIndexStore(store: store) + } + + func makeSyncStore(client: SyncClient, indexStore: SyncIndexStore) -> SyncStore { + let store = NewKeyedDatabase<[String: TestObject]>(storage: RuntimeKeyValueStorage(), identifier: "objectStore") + let objectStore = SyncObjectStore(store: store) + return SyncStore(name: storeName, syncClient: client, indexStore: indexStore, objectStore: objectStore) } func testSync() async throws { + let setExpectation = expectation(description: "syncSetTest") + let delExpectation = expectation(description: "syncDelTest") + + let object = TestObject(id: "id", value: "value") + + syncStore1.syncUpdatePublisher.sink { (_, update) in + switch update { + case .set: + XCTFail() + case .delete: + delExpectation.fulfill() + } + }.store(in: &publishers) + + syncStore2.syncUpdatePublisher.sink { (_, update) in + switch update { + case .set: + setExpectation.fulfill() + case .delete: + XCTFail() + } + }.store(in: &publishers) + + // Configure clients + + try await registerClient(client: client1) + try await registerClient(client: client2) + + // Testing SyncStore `set` + + try await syncStore1.set(object: object, for: account) + + wait(for: [setExpectation], timeout: InputConfig.defaultTimeout) + + XCTAssertEqual(try syncStore1.getAll(for: account), [object]) + XCTAssertEqual(try syncStore2.getAll(for: account), [object]) + + // Testing SyncStore `delete` + + try await syncStore2.delete(id: object.id, for: account) + + wait(for: [delExpectation], timeout: InputConfig.defaultTimeout) + + XCTAssertEqual(try syncStore1.getAll(for: account), []) + XCTAssertEqual(try syncStore2.getAll(for: account), []) + } + + private func registerClient(client: SyncClient) async throws { let message = client.getMessage(account: account) - let signature = try signer.sign(message: message, privateKey: privateKey, type: .eip191) - try client.register(account: account, signature: signature) + let signature = try signer.sign(message: message, privateKey: privateKey, type: .eip191) - XCTAssertEqual(try store.getSignature(for: account), signature.s) + try await client.register(account: account, signature: signature) + try await client.create(account: account, store: storeName) } } diff --git a/Example/Shared/DefaultCryptoProvider.swift b/Example/Shared/DefaultCryptoProvider.swift index 79a11fe2c..4905855f7 100644 --- a/Example/Shared/DefaultCryptoProvider.swift +++ b/Example/Shared/DefaultCryptoProvider.swift @@ -2,6 +2,7 @@ import Foundation import Auth import Web3 import CryptoSwift +import HDWalletKit struct DefaultCryptoProvider: CryptoProvider { @@ -20,4 +21,21 @@ struct DefaultCryptoProvider: CryptoProvider { let hash = digest.calculate(for: [UInt8](data)) return Data(hash) } + + public func derive(entropy: Data, path: [WalletConnectSigner.DerivationPath]) -> Data { + let mnemonic = Mnemonic.create(entropy: entropy) + let seed = Mnemonic.createSeed(mnemonic: mnemonic) + let privateKey = PrivateKey(seed: seed, coin: .bitcoin) + + let derived = path.reduce(privateKey) { result, path in + switch path { + case .hardened(let index): + return result.derived(at: .hardened(index)) + case .notHardened(let index): + return result.derived(at: .notHardened(index)) + } + } + + return derived.raw + } } diff --git a/Example/Showcase/Classes/DomainLayer/Chat/ChatService.swift b/Example/Showcase/Classes/DomainLayer/Chat/ChatService.swift index a55dc13b7..7a3f7fee3 100644 --- a/Example/Showcase/Classes/DomainLayer/Chat/ChatService.swift +++ b/Example/Showcase/Classes/DomainLayer/Chat/ChatService.swift @@ -8,7 +8,7 @@ typealias Stream = AnyPublisher final class ChatService { private lazy var client: ChatClient = { - Chat.configure() + Chat.configure(crypto: DefaultCryptoProvider()) return Chat.instance }() @@ -66,7 +66,7 @@ final class ChatService { } func setupSubscriptions(account: Account) { - client.setupSubscriptions(account: account) + try! client.setupSubscriptions(account: account) } func sendMessage(topic: String, message: String) async throws { diff --git a/Example/Showcase/Classes/PresentationLayer/Web3Inbox/Web3InboxViewController.swift b/Example/Showcase/Classes/PresentationLayer/Web3Inbox/Web3InboxViewController.swift index 05aef8a73..14b7aca43 100644 --- a/Example/Showcase/Classes/PresentationLayer/Web3Inbox/Web3InboxViewController.swift +++ b/Example/Showcase/Classes/PresentationLayer/Web3Inbox/Web3InboxViewController.swift @@ -18,7 +18,7 @@ final class Web3InboxViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - Web3Inbox.configure(account: importAccount.account, config: [.pushEnabled: false], onSign: onSing, environment: .sandbox) + Web3Inbox.configure(account: importAccount.account, crypto: DefaultCryptoProvider(), config: [.pushEnabled: false], environment: .sandbox, onSign: onSing) edgesForExtendedLayout = [] navigationItem.title = "Web3Inbox SDK" diff --git a/Package.swift b/Package.swift index 189529863..af031fec1 100644 --- a/Package.swift +++ b/Package.swift @@ -61,7 +61,7 @@ let package = Package( path: "Sources/WalletConnectSign"), .target( name: "WalletConnectChat", - dependencies: ["WalletConnectIdentity", "WalletConnectSigner"], + dependencies: ["WalletConnectIdentity", "WalletConnectSync"], path: "Sources/Chat"), .target( name: "Auth", @@ -129,7 +129,7 @@ let package = Package( dependencies: ["QRCode", "WalletConnectSign"]), .target( name: "WalletConnectSync", - dependencies: ["WalletConnectNetworking"]), + dependencies: ["WalletConnectSigner"]), .testTarget( name: "WalletConnectSignTests", dependencies: ["WalletConnectSign", "WalletConnectUtils", "TestingUtils", "WalletConnectVerify"]), diff --git a/Sources/Chat/Chat.swift b/Sources/Chat/Chat.swift index 9937d51ba..f1d2efcc8 100644 --- a/Sources/Chat/Chat.swift +++ b/Sources/Chat/Chat.swift @@ -11,7 +11,8 @@ public class Chat { return ChatClientFactory.create( keyserverUrl: keyserverUrl, relayClient: Relay.instance, - networkingInteractor: Networking.interactor + networkingInteractor: Networking.interactor, + syncClient: Sync.instance ) }() @@ -22,7 +23,12 @@ public class Chat { /// Chat instance config method /// - Parameters: /// - account: Chat initial account - static public func configure(keyserverUrl: String = "https://keys.walletconnect.com") { + /// - crypto: Crypto utils implementation + static public func configure( + keyserverUrl: String = "https://keys.walletconnect.com", + crypto: CryptoProvider + ) { Chat.keyserverUrl = keyserverUrl + Sync.configure(crypto: crypto) } } diff --git a/Sources/Chat/ChatClient.swift b/Sources/Chat/ChatClient.swift index 543bfbe1d..44862c66e 100644 --- a/Sources/Chat/ChatClient.swift +++ b/Sources/Chat/ChatClient.swift @@ -11,6 +11,7 @@ public class ChatClient { private let leaveService: LeaveService private let kms: KeyManagementService private let chatStorage: ChatStorage + private let syncRegisterService: SyncRegisterService public let socketConnectionStatusPublisher: AnyPublisher @@ -64,6 +65,7 @@ public class ChatClient { leaveService: LeaveService, kms: KeyManagementService, chatStorage: ChatStorage, + syncRegisterService: SyncRegisterService, socketConnectionStatusPublisher: AnyPublisher ) { self.identityClient = identityClient @@ -74,6 +76,7 @@ public class ChatClient { self.leaveService = leaveService self.kms = kms self.chatStorage = chatStorage + self.syncRegisterService = syncRegisterService self.socketConnectionStatusPublisher = socketConnectionStatusPublisher } @@ -90,6 +93,8 @@ public class ChatClient { ) async throws -> String { let publicKey = try await identityClient.register(account: account, onSign: onSign) + try await syncRegisterService.register(account: account, onSign: onSign) + guard !isPrivate else { return publicKey } @@ -136,6 +141,7 @@ public class ChatClient { /// - Returns: The public invite key public func goPublic(account: Account) async throws { let inviteKey = try await identityClient.goPublic(account: account) + try await chatStorage.initialize(for: account) try await resubscriptionService.subscribeForInvites(inviteKey: inviteKey) } @@ -189,7 +195,7 @@ public class ChatClient { return chatStorage.getMessages(topic: topic) } - public func setupSubscriptions(account: Account) { - chatStorage.setupSubscriptions(account: account) + public func setupSubscriptions(account: Account) throws { + try chatStorage.setupSubscriptions(account: account) } } diff --git a/Sources/Chat/ChatClientFactory.swift b/Sources/Chat/ChatClientFactory.swift index a36b2cac5..d834ec2ab 100644 --- a/Sources/Chat/ChatClientFactory.swift +++ b/Sources/Chat/ChatClientFactory.swift @@ -2,7 +2,7 @@ import Foundation public struct ChatClientFactory { - static func create(keyserverUrl: String, relayClient: RelayClient, networkingInteractor: NetworkingInteractor) -> ChatClient { + static func create(keyserverUrl: String, relayClient: RelayClient, networkingInteractor: NetworkingInteractor, syncClient: SyncClient) -> ChatClient { let keychain = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk") let keyserverURL = URL(string: keyserverUrl)! return ChatClientFactory.create( @@ -11,7 +11,8 @@ public struct ChatClientFactory { networkingInteractor: networkingInteractor, keychain: keychain, logger: ConsoleLogger(loggingLevel: .debug), - keyValueStorage: UserDefaults.standard + keyValueStorage: UserDefaults.standard, + syncClient: syncClient ) } @@ -21,13 +22,14 @@ public struct ChatClientFactory { networkingInteractor: NetworkingInteractor, keychain: KeychainStorageProtocol, logger: ConsoleLogging, - keyValueStorage: KeyValueStorage + keyValueStorage: KeyValueStorage, + syncClient: SyncClient ) -> ChatClient { let kms = KeyManagementService(keychain: keychain) - let messageStore = KeyedDatabase(storage: keyValueStorage, identifier: ChatStorageIdentifiers.messages.rawValue) - let receivedInviteStore = KeyedDatabase(storage: keyValueStorage, identifier: ChatStorageIdentifiers.receivedInvites.rawValue) - let sentInviteStore = KeyedDatabase(storage: keyValueStorage, identifier: ChatStorageIdentifiers.sentInvites.rawValue) - let threadStore = KeyedDatabase(storage: keyValueStorage, identifier: ChatStorageIdentifiers.threads.rawValue) + let messageStore = KeyedDatabase<[Message]>(storage: keyValueStorage, identifier: ChatStorageIdentifiers.messages.rawValue) + let receivedInviteStore = KeyedDatabase<[ReceivedInvite]>(storage: keyValueStorage, identifier: ChatStorageIdentifiers.receivedInvites.rawValue) + let threadStore: SyncStore = SyncStoreFactory.create(name: "chatThreads", syncClient: syncClient) + let sentInviteStore: SyncStore = SyncStoreFactory.create(name: "chatSentInvites", syncClient: syncClient) let chatStorage = ChatStorage(messageStore: messageStore, receivedInviteStore: receivedInviteStore, sentInviteStore: sentInviteStore, threadStore: threadStore) let resubscriptionService = ResubscriptionService(networkingInteractor: networkingInteractor, kms: kms, chatStorage: chatStorage, logger: logger) let identityClient = IdentityClientFactory.create(keyserver: keyserverURL, keychain: keychain, logger: logger) @@ -35,6 +37,7 @@ public struct ChatClientFactory { let inviteService = InviteService(keyserverURL: keyserverURL, networkingInteractor: networkingInteractor, identityClient: identityClient, kms: kms, chatStorage: chatStorage, logger: logger) let leaveService = LeaveService() let messagingService = MessagingService(keyserverURL: keyserverURL, networkingInteractor: networkingInteractor, identityClient: identityClient, chatStorage: chatStorage, logger: logger) + let syncRegisterService = SyncRegisterService(syncClient: syncClient) let client = ChatClient( identityClient: identityClient, @@ -45,6 +48,7 @@ public struct ChatClientFactory { leaveService: leaveService, kms: kms, chatStorage: chatStorage, + syncRegisterService: syncRegisterService, socketConnectionStatusPublisher: relayClient.socketConnectionStatusPublisher ) diff --git a/Sources/Chat/ChatImports.swift b/Sources/Chat/ChatImports.swift index 9af0db974..24dfe29a5 100644 --- a/Sources/Chat/ChatImports.swift +++ b/Sources/Chat/ChatImports.swift @@ -1,4 +1,5 @@ #if !CocoaPods @_exported import WalletConnectSigner @_exported import WalletConnectIdentity +@_exported import WalletConnectSync #endif diff --git a/Sources/Chat/ChatStorage.swift b/Sources/Chat/ChatStorage.swift index c26ed3e1f..3bf14587b 100644 --- a/Sources/Chat/ChatStorage.swift +++ b/Sources/Chat/ChatStorage.swift @@ -3,16 +3,13 @@ import Combine final class ChatStorage { - private let messageStore: KeyedDatabase - private let receivedInviteStore: KeyedDatabase - private let sentInviteStore: KeyedDatabase - private let threadStore: KeyedDatabase + private let messageStore: KeyedDatabase<[Message]> + private let receivedInviteStore: KeyedDatabase<[ReceivedInvite]> + private let sentInviteStore: SyncStore + private let threadStore: SyncStore private var messagesPublisherSubject = PassthroughSubject<[Message], Never>() private var receivedInvitesPublisherSubject = PassthroughSubject<[ReceivedInvite], Never>() - private var sentInvitesPublisherSubject = PassthroughSubject<[SentInvite], Never>() - private var threadsPublisherSubject = PassthroughSubject<[Thread], Never>() - private var newMessagePublisherSubject = PassthroughSubject() private var newReceivedInvitePublisherSubject = PassthroughSubject() private var newSentInvitePublisherSubject = PassthroughSubject() @@ -30,11 +27,11 @@ final class ChatStorage { } var sentInvitesPublisher: AnyPublisher<[SentInvite], Never> { - sentInvitesPublisherSubject.eraseToAnyPublisher() + sentInviteStore.dataUpdatePublisher } var threadsPublisher: AnyPublisher<[Thread], Never> { - threadsPublisherSubject.eraseToAnyPublisher() + threadStore.dataUpdatePublisher } var newMessagePublisher: AnyPublisher { @@ -62,10 +59,10 @@ final class ChatStorage { } init( - messageStore: KeyedDatabase, - receivedInviteStore: KeyedDatabase, - sentInviteStore: KeyedDatabase, - threadStore: KeyedDatabase + messageStore: KeyedDatabase<[Message]>, + receivedInviteStore: KeyedDatabase<[ReceivedInvite]>, + sentInviteStore: SyncStore, + threadStore: SyncStore ) { self.messageStore = messageStore self.receivedInviteStore = receivedInviteStore @@ -73,19 +70,20 @@ final class ChatStorage { self.threadStore = threadStore } - func setupSubscriptions(account: Account) { + func initialize(for account: Account) async throws { + try await sentInviteStore.initialize(for: account) + try await threadStore.initialize(for: account) + } + + func setupSubscriptions(account: Account) throws { messageStore.onUpdate = { [unowned self] in messagesPublisherSubject.send(getMessages(account: account)) } receivedInviteStore.onUpdate = { [unowned self] in receivedInvitesPublisherSubject.send(getReceivedInvites(account: account)) } - sentInviteStore.onUpdate = { [unowned self] in - sentInvitesPublisherSubject.send(getSentInvites(account: account)) - } - threadStore.onUpdate = { [unowned self] in - threadsPublisherSubject.send(getThreads(account: account)) - } + try threadStore.setupSubscriptions(account: account) + try sentInviteStore.setupSubscriptions(account: account) } // MARK: - Invites @@ -95,8 +93,8 @@ final class ChatStorage { .first(where: { $0.id == id }) } - func getSentInvite(id: Int64) -> SentInvite? { - return sentInviteStore.getAll() + func getSentInvite(id: Int64, account: Account) -> SentInvite? { + return try? sentInviteStore.getAll(for: account) .first(where: { $0.id == id }) } @@ -105,17 +103,22 @@ final class ChatStorage { newReceivedInvitePublisherSubject.send(receivedInvite) } - func set(sentInvite: SentInvite, account: Account) { - sentInviteStore.set(sentInvite, for: account.absoluteString) + func set(sentInvite: SentInvite, account: Account) async throws { + try await sentInviteStore.set(object: sentInvite, for: account) newSentInvitePublisherSubject.send(sentInvite) } func getReceivedInvites(account: Account) -> [ReceivedInvite] { - return receivedInviteStore.getElements(for: account.absoluteString) + return receivedInviteStore.getElements(for: account.absoluteString) ?? [] } func getSentInvites(account: Account) -> [SentInvite] { - return sentInviteStore.getElements(for: account.absoluteString) + do { + return try sentInviteStore.getAll(for: account) + } catch { + // TODO: remove fatalError + fatalError(error.localizedDescription) + } } func accept(receivedInvite: ReceivedInvite, account: Account) { @@ -132,27 +135,27 @@ final class ChatStorage { receivedInviteStore.set(rejected, for: account.absoluteString) } - func accept(sentInviteId: Int64, account: Account, topic: String) { - guard let invite = getSentInvite(id: sentInviteId) + func accept(sentInviteId: Int64, account: Account, topic: String) async throws { + guard let invite = getSentInvite(id: sentInviteId, account: account) else { return } - sentInviteStore.delete(invite, for: account.absoluteString) + try await sentInviteStore.delete(id: invite.syncId, for: account) let approved = SentInvite(invite: invite, status: .approved) - sentInviteStore.set(approved, for: account.absoluteString) + try await sentInviteStore.set(object: approved, for: account) acceptPublisherSubject.send((topic, approved)) } - func reject(sentInviteId: Int64, account: Account) { - guard let invite = getSentInvite(id: sentInviteId) + func reject(sentInviteId: Int64, account: Account) async throws { + guard let invite = getSentInvite(id: sentInviteId, account: account) else { return } - sentInviteStore.delete(invite, for: account.absoluteString) + try await sentInviteStore.delete(id: invite.syncId, for: account) let rejected = SentInvite(invite: invite, status: .rejected) // TODO: Update also for peer invites - sentInviteStore.set(rejected, for: account.absoluteString) + try await sentInviteStore.set(object: rejected, for: account) rejectPublisherSubject.send(rejected) } @@ -164,15 +167,20 @@ final class ChatStorage { } func getThreads(account: Account) -> [Thread] { - return threadStore.getElements(for: account.absoluteString) + do { + return try threadStore.getAll(for: account) + } catch { + // TODO: remove fatalError + fatalError(error.localizedDescription) + } } func getThread(topic: String) -> Thread? { return getAllThreads().first(where: { $0.topic == topic }) } - func set(thread: Thread, account: Account) { - threadStore.set(thread, for: account.absoluteString) + func set(thread: Thread, account: Account) async throws { + try await threadStore.set(object: thread, for: account) newThreadPublisherSubject.send(thread) } @@ -188,6 +196,6 @@ final class ChatStorage { } func getMessages(account: Account) -> [Message] { - return messageStore.getElements(for: account.absoluteString) + return messageStore.getElements(for: account.absoluteString) ?? [] } } diff --git a/Sources/Chat/ChatStorageIdentifiers.swift b/Sources/Chat/ChatStorageIdentifiers.swift index 2194a3986..5f80b413b 100644 --- a/Sources/Chat/ChatStorageIdentifiers.swift +++ b/Sources/Chat/ChatStorageIdentifiers.swift @@ -3,7 +3,5 @@ import Foundation enum ChatStorageIdentifiers: String { case topicToInvitationPubKey = "com.walletconnect.chat.topicToInvitationPubKey" case messages = "com.walletconnect.chat.messages" - case threads = "com.walletconnect.chat.threads" case receivedInvites = "com.walletconnect.chat.receivedInvites" - case sentInvites = "com.walletconnect.chat.sentInvites" } diff --git a/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift b/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift index f35fbd3bd..f51bb90e7 100644 --- a/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift +++ b/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift @@ -69,7 +69,8 @@ class InvitationHandlingService { peerAccount: invite.inviterAccount ) - chatStorage.set(thread: thread, account: invite.inviteeAccount) + try await chatStorage.set(thread: thread, account: invite.inviteeAccount) + chatStorage.accept(receivedInvite: invite, account: invite.inviteeAccount) return thread.topic diff --git a/Sources/Chat/ProtocolServices/Inviter/InviteService.swift b/Sources/Chat/ProtocolServices/Inviter/InviteService.swift index 4a5797d7e..b6e465ee7 100644 --- a/Sources/Chat/ProtocolServices/Inviter/InviteService.swift +++ b/Sources/Chat/ProtocolServices/Inviter/InviteService.swift @@ -70,7 +70,7 @@ class InviteService { timestamp: Date().millisecondsSince1970 ) - chatStorage.set(sentInvite: sentInvite, account: invite.inviterAccount) + try await chatStorage.set(sentInvite: sentInvite, account: invite.inviterAccount) logger.debug("invite sent on topic: \(inviteTopic)") @@ -116,10 +116,10 @@ private extension InviteService { peerAccount: peerAccount ) - chatStorage.set(thread: thread, account: account) + try await chatStorage.set(thread: thread, account: account) // TODO: Implement reject for sentInvite - chatStorage.accept(sentInviteId: sentInviteId, account: account, topic: threadTopic) + try await chatStorage.accept(sentInviteId: sentInviteId, account: account, topic: threadTopic) // TODO - remove symKeyI } diff --git a/Sources/Chat/ProtocolServices/Sync/SyncRegisterService.swift b/Sources/Chat/ProtocolServices/Sync/SyncRegisterService.swift new file mode 100644 index 000000000..bcfe25fc5 --- /dev/null +++ b/Sources/Chat/ProtocolServices/Sync/SyncRegisterService.swift @@ -0,0 +1,28 @@ +import Foundation + +final class SyncRegisterService { + + private let syncClient: SyncClient + + init(syncClient: SyncClient) { + self.syncClient = syncClient + } + + func register(account: Account, onSign: @escaping SigningCallback) async throws { + let message = syncClient.getMessage(account: account) + + switch await onSign(message) { + case .signed(let signature): + try await syncClient.register(account: account, signature: signature) + case .rejected: + throw Errors.signatureRejected + } + } +} + +private extension SyncRegisterService { + + enum Errors: Error { + case signatureRejected + } +} diff --git a/Sources/Chat/Types/Plain/SentInvite.swift b/Sources/Chat/Types/Plain/SentInvite.swift index a1d8f1dcc..549687013 100644 --- a/Sources/Chat/Types/Plain/SentInvite.swift +++ b/Sources/Chat/Types/Plain/SentInvite.swift @@ -36,6 +36,13 @@ public struct SentInvite: Codable, Equatable { } } +extension SentInvite: SyncObject { + + public var syncId: String { + return String(id) + } +} + extension SentInvite { public enum Status: String, Codable, Equatable { diff --git a/Sources/Chat/Types/Plain/Thread.swift b/Sources/Chat/Types/Plain/Thread.swift index ed5ef5ca0..731d1ee78 100644 --- a/Sources/Chat/Types/Plain/Thread.swift +++ b/Sources/Chat/Types/Plain/Thread.swift @@ -5,3 +5,10 @@ public struct Thread: Codable, Equatable { public let selfAccount: Account public let peerAccount: Account } + +extension Thread: SyncObject { + + public var syncId: String { + return topic + } +} diff --git a/Sources/WalletConnectSigner/CryptoProvider.swift b/Sources/WalletConnectSigner/CryptoProvider.swift index 4edc66132..971572363 100644 --- a/Sources/WalletConnectSigner/CryptoProvider.swift +++ b/Sources/WalletConnectSigner/CryptoProvider.swift @@ -1,10 +1,12 @@ import Foundation +public enum DerivationPath { + case hardened(UInt32) + case notHardened(UInt32) +} + public protocol CryptoProvider { func recoverPubKey(signature: EthereumSignature, message: Data) throws -> Data func keccak256(_ data: Data) -> Data -} - -extension CryptoProvider { - + func derive(entropy: Data, path: [DerivationPath]) -> Data } diff --git a/Sources/WalletConnectSync/Services/SyncDerivationService.swift b/Sources/WalletConnectSync/Services/SyncDerivationService.swift index ec62b52e0..954d8e048 100644 --- a/Sources/WalletConnectSync/Services/SyncDerivationService.swift +++ b/Sources/WalletConnectSync/Services/SyncDerivationService.swift @@ -2,19 +2,62 @@ import Foundation final class SyncDerivationService { + private let syncStorage: SyncSignatureStore private let crypto: CryptoProvider - private let storage: SyncStorage private let kms: KeyManagementServiceProtocol - init(crypto: CryptoProvider, storage: SyncStorage, kms: KeyManagementServiceProtocol) { + init( + syncStorage: SyncSignatureStore, + crypto: CryptoProvider, + kms: KeyManagementServiceProtocol + ) { + self.syncStorage = syncStorage self.crypto = crypto - self.storage = storage self.kms = kms } func deriveTopic(account: Account, store: String) throws -> String { - fatalError() + let signature = try syncStorage.getSignature(for: account) - // TODO: KMS setSymKey + guard let signatureData = signature.data(using: .utf8) else { + throw Errors.signatureIsNotUTF8 + } + + let slice = store.components(withMaxLength: 4) + .compactMap { $0.data(using: .utf8) } + .compactMap { UInt32($0.toHexString(), radix: 16) } + + let path: [DerivationPath] = [ + .hardened(77), + .hardened(0), + .notHardened(0) + ] + slice.map { .notHardened($0) } + + let entropy = signatureData.sha256() + let storeKey = crypto.derive(entropy: entropy, path: path) + let topic = storeKey.sha256().toHexString() + + let symmetricKey = try SymmetricKey(rawRepresentation: storeKey) + try kms.setSymmetricKey(symmetricKey, for: topic) + + return topic + } +} + +private extension SyncDerivationService { + + enum Errors: Error { + case signatureIsNotUTF8 + } +} + +fileprivate extension String { + + func components(withMaxLength length: Int) -> [String] { + return stride(from: 0, to: count, by: length).map { + let start = index(startIndex, offsetBy: $0) + let end = index(start, offsetBy: length, limitedBy: endIndex) ?? endIndex + return String(self[start..() + + var updatePublisher: AnyPublisher<(String, StoreUpdate), Never> { + return updateSubject.eraseToAnyPublisher() + } + + private var publishers: Set = [] + + private let networkInteractor: NetworkInteracting + private let derivationService: SyncDerivationService + private let signatureStore: SyncSignatureStore + private let logger: ConsoleLogging + + /// `account` to `Record` keyValue store + private let indexStore: SyncIndexStore + + init(networkInteractor: NetworkInteracting, derivationService: SyncDerivationService, signatureStore: SyncSignatureStore, indexStore: SyncIndexStore, logger: ConsoleLogging) { + self.networkInteractor = networkInteractor + self.derivationService = derivationService + self.signatureStore = signatureStore + self.indexStore = indexStore + self.logger = logger + + setupSubscriptions() + } + + func set(account: Account, store: String, object: Object) async throws { + defer { logger.debug("Did set value for \(store)") } + let protocolMethod = SyncSetMethod() + let params = StoreSet(key: object.syncId, value: object) + let request = RPCRequest(method: protocolMethod.method, params: params) + let record = try indexStore.getRecord(account: account, name: store) + try await networkInteractor.request(request, topic: record.topic, protocolMethod: protocolMethod) + } + + func delete(account: Account, store: String, key: String) async throws { + defer { logger.debug("Did delete value for \(store)") } + let protocolMethod = SyncDeleteMethod() + let request = RPCRequest(method: protocolMethod.method, params: ["key": key]) + let record = try indexStore.getRecord(account: account, name: store) + try await networkInteractor.request(request, topic: record.topic, protocolMethod: protocolMethod) + } + + func create(account: Account, store: String) async throws { + defer { logger.debug("Store \(store) created") } + + let topic = try getTopic(for: account, store: store) + try await networkInteractor.subscribe(topic: topic) + } +} + +private extension SyncService { + + enum Errors: Error { + case recordNotFoundForAccount + } + + func setupSubscriptions() { + networkInteractor.requestSubscription(on: SyncSetMethod()) + .sink { [unowned self] (payload: RequestSubscriptionPayload) in + self.updateSubject.send((payload.topic, .set(payload.request))) + } + .store(in: &publishers) + + networkInteractor.requestSubscription(on: SyncDeleteMethod()) + .sink { [unowned self] (payload: RequestSubscriptionPayload) in + self.updateSubject.send((payload.topic, .delete(payload.request.key))) + } + .store(in: &publishers) + } + + func getTopic(for account: Account, store: String) throws -> String { + if let record = try? indexStore.getRecord(account: account, name: store) { + return record.store + } + + let topic = try derivationService.deriveTopic(account: account, store: store) + indexStore.set(topic: topic, name: store, account: account) + return topic + } +} diff --git a/Sources/WalletConnectSync/Services/SyncSetService.swift b/Sources/WalletConnectSync/Services/SyncSetService.swift deleted file mode 100644 index 027bdefb6..000000000 --- a/Sources/WalletConnectSync/Services/SyncSetService.swift +++ /dev/null @@ -1,45 +0,0 @@ -import Foundation -import Combine - -final class SyncSetService { - - private var publishers: Set = [] - - private let networkInteractor: NetworkInteracting - private let derivationService: SyncDerivationService - private let storage: SyncStorage - - init(networkInteractor: NetworkInteracting, derivationService: SyncDerivationService, storage: SyncStorage) { - self.networkInteractor = networkInteractor - self.derivationService = derivationService - self.storage = storage - - setupSubscriptions() - } - - func set(account: Account, store: String, key: String, value: String) async throws { - let protocolMethod = SyncSetMethod() - let request = RPCRequest(method: protocolMethod.method, params: ["key": key, "value": value]) - let topic = try derivationService.deriveTopic(account: account, store: store) - try await networkInteractor.request(request, topic: topic, protocolMethod: protocolMethod) - - let set = StoreSet(id: request.id!.integer, key: key, value: value) - storage.set(update: .set(set), topic: topic, store: store, for: account) - } -} - -private extension SyncSetService { - - func setupSubscriptions() { - - } - - func handleSetResponse() { - networkInteractor.requestSubscription(on: SyncSetMethod()) - .sink { [unowned self] (payload: RequestSubscriptionPayload) in - storage.set(update: payload.request, topic: payload.topic, store: <#T##String#>, for: <#T##Account#>) - - } - .store(in: &publishers) - } -} diff --git a/Sources/WalletConnectSync/Services/SyncStorage.swift b/Sources/WalletConnectSync/Services/SyncStorage.swift deleted file mode 100644 index de605b99f..000000000 --- a/Sources/WalletConnectSync/Services/SyncStorage.swift +++ /dev/null @@ -1,49 +0,0 @@ -import Foundation -import Combine - -final class SyncStorage { - - private var syncUpdateSubject = PassthroughSubject() - - var syncUpdatePublisher: AnyPublisher { - syncUpdateSubject.eraseToAnyPublisher() - } - - private let keychain: KeychainStorageProtocol - private let database: KeyedDatabase - - init(keychain: KeychainStorageProtocol, database: KeyedDatabase) { - self.keychain = keychain - self.database = database - } - - func saveIdentityKey(_ key: String, for account: Account) throws { - try keychain.add(key, forKey: signatureIdentifier(for: account)) - } - - func getSignature(for account: Account) throws -> String { - let identifier = signatureIdentifier(for: account) - - guard let key: String = try? keychain.read(key: identifier) - else { throw Errors.signatureNotFound } - - return key - } - - func set(update: StoreUpdate, topic: String, store: String, for account: Account) { - let record = SyncRecord(topic: topic, store: store, update: update) - database.set(record, for: account.absoluteString) - syncUpdateSubject.send(record.publicRepresentation()) - } -} - -private extension SyncStorage { - - enum Errors: Error { - case signatureNotFound - } - - func signatureIdentifier(for account: Account) -> String { - return "com.walletconnect.sync.signature.\(account.absoluteString)" - } -} diff --git a/Sources/WalletConnectSync/Stores/SyncIndexStore.swift b/Sources/WalletConnectSync/Stores/SyncIndexStore.swift new file mode 100644 index 000000000..e92def273 --- /dev/null +++ b/Sources/WalletConnectSync/Stores/SyncIndexStore.swift @@ -0,0 +1,44 @@ +import Foundation + +final class SyncIndexStore { + + /// `account-store` to SyncRecord map keyValue store + private let store: CodableStore + + init(store: CodableStore) { + self.store = store + } + + func getRecord(account: Account, name: String) throws -> SyncRecord { + let identifier = identifier(account: account, name: name) + guard let record = try store.get(key: identifier) else { + throw Errors.recordNotFoundForAccount + } + return record + } + + func getRecord(topic: String) throws -> SyncRecord { + guard let record = store.getAll().first(where: { $0.topic == topic }) else { + throw Errors.accountNotFoundForTopic + } + return record + } + + func set(topic: String, name: String, account: Account) { + let identifier = identifier(account: account, name: name) + let record = SyncRecord(topic: topic, store: name, account: account) + store.set(record, forKey: identifier) + } +} + +private extension SyncIndexStore { + + enum Errors: Error { + case recordNotFoundForAccount + case accountNotFoundForTopic + } + + func identifier(account: Account, name: String) -> String { + return "\(account.absoluteString)-\(name)" + } +} diff --git a/Sources/WalletConnectSync/Stores/SyncObjectStore.swift b/Sources/WalletConnectSync/Stores/SyncObjectStore.swift new file mode 100644 index 000000000..9852ee99a --- /dev/null +++ b/Sources/WalletConnectSync/Stores/SyncObjectStore.swift @@ -0,0 +1,51 @@ +import Foundation + +final class SyncObjectStore { + + /// `storeTopic` to [`id`: `Object`] map keyValue store + private let store: NewKeyedDatabase<[String: Object]> + + var onUpdate: (() -> Void)? { + get { + return store.onUpdate + } + set { + store.onUpdate = newValue + } + } + + init(store: NewKeyedDatabase<[String : Object]>) { + self.store = store + } + + func getMap(topic: String) -> [String: Object] { + return store.getElement(for: topic) ?? [:] + } + + func getAll(topic: String) -> [Object] { + let map = getMap(topic: topic) + return Array(map.values) + } + + func getAll() -> [Object] { + return store.index.values.reduce([]) { result, values in + return result + values.values + } + } + + func isExists(topic: String, id: String) -> Bool { + return store.getElement(for: topic)?[id] != nil + } + + func set(object: Object, topic: String) { + var map = getMap(topic: topic) + map[object.syncId] = object + store.set(element: map, for: topic) + } + + func delete(id: String, topic: String) { + var map = getMap(topic: topic) + map[id] = nil + store.set(element: map, for: topic) + } +} diff --git a/Sources/WalletConnectSync/Stores/SyncSignatureStore.swift b/Sources/WalletConnectSync/Stores/SyncSignatureStore.swift new file mode 100644 index 000000000..81e9158f1 --- /dev/null +++ b/Sources/WalletConnectSync/Stores/SyncSignatureStore.swift @@ -0,0 +1,35 @@ +import Foundation +import Combine + +final class SyncSignatureStore { + + private let keychain: KeychainStorageProtocol + + init(keychain: KeychainStorageProtocol) { + self.keychain = keychain + } + + func saveSignature(_ key: String, for account: Account) throws { + try keychain.add(key, forKey: signatureIdentifier(for: account)) + } + + func getSignature(for account: Account) throws -> String { + let identifier = signatureIdentifier(for: account) + + guard let key: String = try? keychain.read(key: identifier) + else { throw Errors.signatureNotFound } + + return key + } +} + +private extension SyncSignatureStore { + + enum Errors: Error { + case signatureNotFound + } + + func signatureIdentifier(for account: Account) -> String { + return "com.walletconnect.sync.signature.\(account.absoluteString)" + } +} diff --git a/Sources/WalletConnectSync/Stores/SyncStore.swift b/Sources/WalletConnectSync/Stores/SyncStore.swift new file mode 100644 index 000000000..3244c194f --- /dev/null +++ b/Sources/WalletConnectSync/Stores/SyncStore.swift @@ -0,0 +1,127 @@ +import Foundation +import Combine + +public protocol SyncObject: Codable & Equatable { + var syncId: String { get } +} + +public final class SyncStore { + + private var publishers = Set() + + private let name: String + private let syncClient: SyncClient + + /// `account` to `Record` keyValue store + private let indexStore: SyncIndexStore + + /// `storeTopic` to [`id`: `Object`] map keyValue store + private let objectStore: SyncObjectStore + + private let dataUpdateSubject = PassthroughSubject<[Object], Never>() + private let syncUpdateSubject = PassthroughSubject<(String, StoreUpdate), Never>() + + public var dataUpdatePublisher: AnyPublisher<[Object], Never> { + return dataUpdateSubject.eraseToAnyPublisher() + } + + public var syncUpdatePublisher: AnyPublisher<(String, StoreUpdate), Never> { + return syncUpdateSubject.eraseToAnyPublisher() + } + + init(name: String, syncClient: SyncClient, indexStore: SyncIndexStore, objectStore: SyncObjectStore) { + self.name = name + self.syncClient = syncClient + self.indexStore = indexStore + self.objectStore = objectStore + + setupSubscriptions() + } + + public func initialize(for account: Account) async throws { + try await syncClient.create(account: account, store: name) + } + + public func getAll(for account: Account) throws -> [Object] { + let record = try indexStore.getRecord(account: account, name: name) + return objectStore.getAll(topic: record.topic) + } + + public func getAll() -> [Object] { + return objectStore.getAll() + } + + public func set(object: Object, for account: Account) async throws { + let record = try indexStore.getRecord(account: account, name: name) + + guard !objectStore.isExists(topic: record.topic, id: object.syncId) else { + return + } + + try await syncClient.set(account: account, store: record.store, object: object) + + objectStore.set(object: object, topic: record.topic) + } + + public func delete(id: String, for account: Account) async throws { + let record = try indexStore.getRecord(account: account, name: name) + + guard objectStore.isExists(topic: record.topic, id: id) else { + return + } + + try await syncClient.delete(account: account, store: record.store, key: id) + + objectStore.delete(id: id, topic: record.topic) + } + + public func setupSubscriptions(account: Account) throws { + let record = try indexStore.getRecord(account: account, name: name) + + objectStore.onUpdate = { [unowned self] in + dataUpdateSubject.send(objectStore.getAll(topic: record.topic)) + } + } +} + +private extension SyncStore { + + func setupSubscriptions() { + syncClient.updatePublisher.sink { [unowned self] (topic, update) in + + let record = try! indexStore.getRecord(topic: topic) + + guard record.topic == name else { return } + + switch update { + case .set(let value): + let decoded = try! value.get(StoreSet.self) + try! setInStore(object: decoded.value, for: record.account) + syncUpdateSubject.send((topic, update)) + case .delete(let key): + try! deleteInStore(id: key, for: record.account) + syncUpdateSubject.send((topic, update)) + } + }.store(in: &publishers) + } + + func setInStore(object: Object, for account: Account) throws { + let record = try indexStore.getRecord(account: account, name: name) + + guard !objectStore.isExists(topic: record.topic, id: object.syncId) else { + return + } + + objectStore.set(object: object, topic: record.topic) + } + + func deleteInStore(id: String, for account: Account) throws { + let record = try indexStore.getRecord(account: account, name: name) + + guard objectStore.isExists(topic: record.topic, id: id) else { + return + } + + objectStore.delete(id: id, topic: record.topic) + } +} diff --git a/Sources/WalletConnectSync/Stores/SyncStoreFactory.swift b/Sources/WalletConnectSync/Stores/SyncStoreFactory.swift new file mode 100644 index 000000000..e102544b0 --- /dev/null +++ b/Sources/WalletConnectSync/Stores/SyncStoreFactory.swift @@ -0,0 +1,12 @@ +import Foundation + +public final class SyncStoreFactory { + + public static func create(name: String, syncClient: SyncClient) -> SyncStore { + let indexDatabase = CodableStore(defaults: UserDefaults.standard, identifier: SyncStorageIdentifiers.index.rawValue) + let indexStore = SyncIndexStore(store: indexDatabase) + let objectDatabase = NewKeyedDatabase<[String: Object]>(storage: UserDefaults.standard, identifier: SyncStorageIdentifiers.object.rawValue) + let objectStore = SyncObjectStore(store: objectDatabase) + return SyncStore(name: name, syncClient: syncClient, indexStore: indexStore, objectStore: objectStore) + } +} diff --git a/Sources/WalletConnectSync/Sync.swift b/Sources/WalletConnectSync/Sync.swift new file mode 100644 index 000000000..d524381c5 --- /dev/null +++ b/Sources/WalletConnectSync/Sync.swift @@ -0,0 +1,27 @@ +import Foundation + +/// Sync instatnce wrapper +public class Sync { + + /// Sync client instance + public static var instance: SyncClient = { + guard let config = config else { + fatalError("Error - you must call Sync.configure(_:) before accessing the shared instance.") + } + return SyncClientFactory.create( + networkInteractor: Networking.interactor, + crypto: config.crypto + ) + }() + + private static var config: Config? + + private init() { } + + /// Auth instance wallet config method. For DApp usage + /// - Parameters: + /// - crypto: Crypto utils implementation + static public func configure(crypto: CryptoProvider) { + Sync.config = Sync.Config(crypto: crypto) + } +} diff --git a/Sources/WalletConnectSync/SyncClient.swift b/Sources/WalletConnectSync/SyncClient.swift index 8bbdefea5..488058a93 100644 --- a/Sources/WalletConnectSync/SyncClient.swift +++ b/Sources/WalletConnectSync/SyncClient.swift @@ -3,14 +3,18 @@ import Combine public final class SyncClient { - public var updatePublisher: AnyPublisher { - return syncStorage.syncUpdatePublisher + public var updatePublisher: AnyPublisher<(String, StoreUpdate), Never> { + return syncService.updatePublisher } - private let syncStorage: SyncStorage + private let syncService: SyncService + private let syncSignatureStore: SyncSignatureStore - init(syncStorage: SyncStorage) { - self.syncStorage = syncStorage + init(syncService: SyncService, syncSignatureStore: SyncSignatureStore) { + self.syncService = syncService + self.syncSignatureStore = syncSignatureStore + + // TODO: Resubscription service } /// Get message to sign for an account @@ -23,23 +27,28 @@ public final class SyncClient { } /// Register an account to sync - public func register(account: Account, signature: CacaoSignature) throws { - try syncStorage.saveIdentityKey(signature.s, for: account) + public func register(account: Account, signature: CacaoSignature) async throws { + // TODO: Signature verify + try syncSignatureStore.saveSignature(signature.s, for: account) } /// Create a store public func create(account: Account, store: String) async throws { - fatalError() + try await syncService.create(account: account, store: store) } // Set value to store - public func set(account: Account, store: String, key: String, value: String) async throws { - fatalError() + public func set( + account: Account, + store: String, + object: Object + ) async throws { + try await syncService.set(account: account, store: store, object: object) } // Set value from store by key public func delete(account: Account, store: String, key: String) async throws { - fatalError() + try await syncService.delete(account: account, store: store, key: key) } // Get stores diff --git a/Sources/WalletConnectSync/SyncClientFactory.swift b/Sources/WalletConnectSync/SyncClientFactory.swift new file mode 100644 index 000000000..6fd79c4c3 --- /dev/null +++ b/Sources/WalletConnectSync/SyncClientFactory.swift @@ -0,0 +1,29 @@ +import Foundation + +final class SyncClientFactory { + + static func create(networkInteractor: NetworkingInteractor, crypto: CryptoProvider) -> SyncClient { + let keychain = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk") + return create(networkInteractor: networkInteractor, crypto: crypto, keychain: keychain) + } + + static func create(networkInteractor: NetworkingInteractor, crypto: CryptoProvider, keychain: KeychainStorageProtocol) -> SyncClient { + let signatureStore = SyncSignatureStore(keychain: keychain) + let kms = KeyManagementService(keychain: keychain) + let deriviationService = SyncDerivationService( + syncStorage: signatureStore, + crypto: crypto, + kms: kms + ) + let indexStore = CodableStore(defaults: UserDefaults.standard, identifier: SyncStorageIdentifiers.index.rawValue) + let syncIndexStore = SyncIndexStore(store: indexStore) + let syncService = SyncService( + networkInteractor: networkInteractor, + derivationService: deriviationService, + signatureStore: signatureStore, + indexStore: syncIndexStore, + logger: ConsoleLogger(loggingLevel: .debug) + ) + return SyncClient(syncService: syncService, syncSignatureStore: signatureStore) + } +} diff --git a/Sources/WalletConnectSync/SyncConfig.swift b/Sources/WalletConnectSync/SyncConfig.swift new file mode 100644 index 000000000..d0587eea7 --- /dev/null +++ b/Sources/WalletConnectSync/SyncConfig.swift @@ -0,0 +1,7 @@ +import Foundation + +extension Sync { + struct Config { + let crypto: CryptoProvider + } +} diff --git a/Sources/WalletConnectSync/SyncImports.swift b/Sources/WalletConnectSync/SyncImports.swift index 04c25a62c..338babe27 100644 --- a/Sources/WalletConnectSync/SyncImports.swift +++ b/Sources/WalletConnectSync/SyncImports.swift @@ -1,4 +1,3 @@ #if !CocoaPods -@_exported import WalletConnectNetworking @_exported import WalletConnectSigner #endif diff --git a/Sources/WalletConnectSync/SyncStorageIdentifiers.swift b/Sources/WalletConnectSync/SyncStorageIdentifiers.swift new file mode 100644 index 000000000..87a5077f1 --- /dev/null +++ b/Sources/WalletConnectSync/SyncStorageIdentifiers.swift @@ -0,0 +1,6 @@ +import Foundation + +enum SyncStorageIdentifiers: String { + case index = "com.walletconnect.sync.index" + case object = "com.walletconnect.sync.object" +} diff --git a/Sources/WalletConnectSync/Types/StoreMap.swift b/Sources/WalletConnectSync/Types/StoreMap.swift index fd0725ac4..62f9ac2c7 100644 --- a/Sources/WalletConnectSync/Types/StoreMap.swift +++ b/Sources/WalletConnectSync/Types/StoreMap.swift @@ -1,6 +1,3 @@ import Foundation -public struct StoreMap { - public let store: String - public let state: [String: String] -} +public typealias StoreMap = Dictionary diff --git a/Sources/WalletConnectSync/Types/StoreUpdate.swift b/Sources/WalletConnectSync/Types/StoreUpdate.swift index 773cbb7d3..6c1f1b942 100644 --- a/Sources/WalletConnectSync/Types/StoreUpdate.swift +++ b/Sources/WalletConnectSync/Types/StoreUpdate.swift @@ -1,17 +1,15 @@ import Foundation -public enum StoreUpdate: Codable, Equatable { - case set(StoreSet) - case delete(StoreDelete) +public enum StoreUpdate { + case set(AnyCodable) + case delete(String) } -public struct StoreSet: Codable, Equatable { - public let id: Int64 +public struct StoreSet: Codable, Equatable { public let key: String - public let value: String + public let value: Object } public struct StoreDelete: Codable, Equatable { - public let id: Int64 public let key: String } diff --git a/Sources/WalletConnectSync/Types/SyncRecord.swift b/Sources/WalletConnectSync/Types/SyncRecord.swift index d6dec2837..ddde0b38b 100644 --- a/Sources/WalletConnectSync/Types/SyncRecord.swift +++ b/Sources/WalletConnectSync/Types/SyncRecord.swift @@ -1,11 +1,7 @@ import Foundation -struct SyncRecord: Codable, Equatable { +struct SyncRecord: Codable & Equatable { let topic: String let store: String - let update: StoreUpdate - - func publicRepresentation() -> SyncUpdate { - return SyncUpdate(store: store, update: update) - } + let account: Account } diff --git a/Sources/WalletConnectSync/Types/SyncUpdate.swift b/Sources/WalletConnectSync/Types/SyncUpdate.swift deleted file mode 100644 index 2a15b1480..000000000 --- a/Sources/WalletConnectSync/Types/SyncUpdate.swift +++ /dev/null @@ -1,6 +0,0 @@ -import Foundation - -public struct SyncUpdate { - public let store: String - public let update: StoreUpdate -} diff --git a/Sources/WalletConnectUtils/KeyedDatabase.swift b/Sources/WalletConnectUtils/KeyedDatabase.swift index d8a25a5ce..b15a87a5b 100644 --- a/Sources/WalletConnectUtils/KeyedDatabase.swift +++ b/Sources/WalletConnectUtils/KeyedDatabase.swift @@ -2,7 +2,7 @@ import Foundation public class KeyedDatabase where Element: Codable & Equatable { - private var index: [String: [Element]] = [:] { + public var index: [String: Element] = [:] { didSet { guard oldValue != index else { return } set(index, for: identifier) @@ -21,22 +21,25 @@ public class KeyedDatabase where Element: Codable & Equatable { initializeIndex() } +} - public func getAll() -> [Element] { - return index.values.reduce([], +) - } +extension KeyedDatabase where Element: RangeReplaceableCollection, Element.Element: Equatable { - public func getElements(for key: String) -> [Element] { - return index[key] ?? [] + public func getAll() -> [Element.Element] { + return index.values.reduce([], +) } - public func set(_ element: Element, for key: String) { + public func set(_ element: Element.Element, for key: String) { index.append(element, for: key) } - public func delete(_ element: Element, for key: String) { + public func delete(_ element: Element.Element, for key: String) { index.delete(element, for: key) } + + public func getElements(for key: String) -> Element? { + return index[key] + } } private extension KeyedDatabase { @@ -44,13 +47,13 @@ private extension KeyedDatabase { func initializeIndex() { guard let data = storage.object(forKey: identifier) as? Data, - let decoded = try? JSONDecoder().decode([String: [Element]].self, from: data) + let decoded = try? JSONDecoder().decode([String: Element].self, from: data) else { return } index = decoded } - func set(_ value: [String: [Element]], for key: String) { + func set(_ value: [String: Element], for key: String) { let data = try! JSONEncoder().encode(value) storage.set(data, forKey: key) } diff --git a/Sources/WalletConnectUtils/NewKeyedDatabase.swift b/Sources/WalletConnectUtils/NewKeyedDatabase.swift new file mode 100644 index 000000000..844b92ef3 --- /dev/null +++ b/Sources/WalletConnectUtils/NewKeyedDatabase.swift @@ -0,0 +1,53 @@ +import Foundation + +public class NewKeyedDatabase where Element: Codable & Equatable { + + public var index: [String: Element] = [:] { + didSet { + guard oldValue != index else { return } + set(index, for: identifier) + onUpdate?() + } + } + + private let storage: KeyValueStorage + private let identifier: String + + public var onUpdate: (() -> Void)? + + public init(storage: KeyValueStorage, identifier: String) { + self.storage = storage + self.identifier = identifier + + initializeIndex() + } + + public func getAll() -> [Element] { + return Array(index.values) + } + + public func getElement(for key: String) -> Element? { + return index[key] + } + + public func set(element: Element, for key: String) { + index[key] = element + } +} + +private extension NewKeyedDatabase { + + func initializeIndex() { + guard + let data = storage.object(forKey: identifier) as? Data, + let decoded = try? JSONDecoder().decode([String: Element].self, from: data) + else { return } + + index = decoded + } + + func set(_ value: [String: Element], for key: String) { + let data = try! JSONEncoder().encode(value) + storage.set(data, forKey: key) + } +} diff --git a/Sources/Web3Inbox/Web3Inbox.swift b/Sources/Web3Inbox/Web3Inbox.swift index 32a1a87fb..125971c51 100644 --- a/Sources/Web3Inbox/Web3Inbox.swift +++ b/Sources/Web3Inbox/Web3Inbox.swift @@ -19,11 +19,18 @@ public final class Web3Inbox { /// Web3Inbox instance config method /// - Parameters: /// - account: Web3Inbox initial account - static public func configure(account: Account, config: [ConfigParam: Bool] = [:], onSign: @escaping SigningCallback, environment: APNSEnvironment) { + /// - crypto: Crypto utils implementation + static public func configure( + account: Account, + crypto: CryptoProvider, + config: [ConfigParam: Bool] = [:], + environment: APNSEnvironment, + onSign: @escaping SigningCallback, + ) { Web3Inbox.account = account Web3Inbox.config = config Web3Inbox.onSign = onSign - Chat.configure() + Chat.configure(crypto: crypto) Push.configure(environment: environment) } } diff --git a/Tests/AuthTests/Stubs/MessageSignerMock.swift b/Tests/AuthTests/Stubs/MessageSignerMock.swift index 3809c2dbb..45e77cd12 100644 --- a/Tests/AuthTests/Stubs/MessageSignerMock.swift +++ b/Tests/AuthTests/Stubs/MessageSignerMock.swift @@ -15,6 +15,9 @@ extension MessageVerifier { } struct Crypto: CryptoProvider { + func derive(entropy: Data, path: [WalletConnectSigner.DerivationPath]) -> Data { + return Data() + } func keccak256(_ data: Data) -> Data { return Data() diff --git a/WalletConnectSwiftV2.podspec b/WalletConnectSwiftV2.podspec index c224bae47..ed2856704 100644 --- a/WalletConnectSwiftV2.podspec +++ b/WalletConnectSwiftV2.podspec @@ -103,10 +103,15 @@ Pod::Spec.new do |spec| spec.subspec 'WalletConnectChat' do |ss| ss.source_files = 'Sources/Chat/**/*.{h,m,swift}' - ss.dependency 'WalletConnectSwiftV2/WalletConnectSigner' + ss.dependency 'WalletConnectSwiftV2/WalletConnectSync' ss.dependency 'WalletConnectSwiftV2/WalletConnectIdentity' end + spec.subspec 'WalletConnectSync' do |ss| + ss.source_files = 'Sources/WalletConnectSync/**/*.{h,m,swift}' + ss.dependency 'WalletConnectSwiftV2/WalletConnectSigner' + end + spec.subspec 'WalletConnectSigner' do |ss| ss.source_files = 'Sources/WalletConnectSigner/**/*.{h,m,swift}' ss.dependency 'WalletConnectSwiftV2/WalletConnectNetworking' From 3f2c8e0c6179fd52799528eeae2ffe0f8cb04ea4 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Sat, 22 Apr 2023 12:24:09 -0100 Subject: [PATCH 12/85] Testing session --- .../Classes/DomainLayer/Chat/ImportAccount.swift | 2 +- Sources/WalletConnectSync/Services/SyncService.swift | 12 +++++++----- Sources/WalletConnectSync/Stores/SyncStore.swift | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Example/Showcase/Classes/DomainLayer/Chat/ImportAccount.swift b/Example/Showcase/Classes/DomainLayer/Chat/ImportAccount.swift index 2c8ccffa0..54d43eb1d 100644 --- a/Example/Showcase/Classes/DomainLayer/Chat/ImportAccount.swift +++ b/Example/Showcase/Classes/DomainLayer/Chat/ImportAccount.swift @@ -48,7 +48,7 @@ enum ImportAccount { case .js: return Account("eip155:1:0x7ABa5B1F436e42f6d4A579FB3Ad6D204F6A91863")! case .custom(let privateKey): - let address = try! EthereumPrivateKey(hexPrivateKey: "0x" + privateKey, ctx: nil).address.hex(eip55: false) + let address = try! EthereumPrivateKey(hexPrivateKey: "0x" + privateKey, ctx: nil).address.hex(eip55: true) return Account("eip155:1:\(address)")! } } diff --git a/Sources/WalletConnectSync/Services/SyncService.swift b/Sources/WalletConnectSync/Services/SyncService.swift index 293417381..5bb5a64c8 100644 --- a/Sources/WalletConnectSync/Services/SyncService.swift +++ b/Sources/WalletConnectSync/Services/SyncService.swift @@ -30,27 +30,29 @@ final class SyncService { } func set(account: Account, store: String, object: Object) async throws { - defer { logger.debug("Did set value for \(store)") } let protocolMethod = SyncSetMethod() let params = StoreSet(key: object.syncId, value: object) let request = RPCRequest(method: protocolMethod.method, params: params) let record = try indexStore.getRecord(account: account, name: store) try await networkInteractor.request(request, topic: record.topic, protocolMethod: protocolMethod) + + logger.debug("Did set value for \(store). Sent on \(record.topic)") } func delete(account: Account, store: String, key: String) async throws { - defer { logger.debug("Did delete value for \(store)") } let protocolMethod = SyncDeleteMethod() let request = RPCRequest(method: protocolMethod.method, params: ["key": key]) let record = try indexStore.getRecord(account: account, name: store) try await networkInteractor.request(request, topic: record.topic, protocolMethod: protocolMethod) + + logger.debug("Did delete value for \(store). Sent on: \(record.topic)") } func create(account: Account, store: String) async throws { - defer { logger.debug("Store \(store) created") } - let topic = try getTopic(for: account, store: store) try await networkInteractor.subscribe(topic: topic) + + logger.debug("Store \(store) created. Subscribed on: \(topic)") } } @@ -76,7 +78,7 @@ private extension SyncService { func getTopic(for account: Account, store: String) throws -> String { if let record = try? indexStore.getRecord(account: account, name: store) { - return record.store + return record.topic } let topic = try derivationService.deriveTopic(account: account, store: store) diff --git a/Sources/WalletConnectSync/Stores/SyncStore.swift b/Sources/WalletConnectSync/Stores/SyncStore.swift index 3244c194f..43f48f85e 100644 --- a/Sources/WalletConnectSync/Stores/SyncStore.swift +++ b/Sources/WalletConnectSync/Stores/SyncStore.swift @@ -91,7 +91,7 @@ private extension SyncStore { let record = try! indexStore.getRecord(topic: topic) - guard record.topic == name else { return } + guard record.store == name else { return } switch update { case .set(let value): From 3907837110f90a4dbb101cf7c793fcc9f77b4669 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Sat, 22 Apr 2023 13:17:48 -0100 Subject: [PATCH 13/85] Reject sentInvite --- Sources/Chat/ChatStorage.swift | 14 ++++++++------ .../ProtocolServices/Inviter/InviteService.swift | 12 +++++++++--- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/Sources/Chat/ChatStorage.swift b/Sources/Chat/ChatStorage.swift index 3bf14587b..541b7bed5 100644 --- a/Sources/Chat/ChatStorage.swift +++ b/Sources/Chat/ChatStorage.swift @@ -93,8 +93,8 @@ final class ChatStorage { .first(where: { $0.id == id }) } - func getSentInvite(id: Int64, account: Account) -> SentInvite? { - return try? sentInviteStore.getAll(for: account) + func getSentInvite(id: Int64) -> SentInvite? { + return sentInviteStore.getAll() .first(where: { $0.id == id }) } @@ -135,10 +135,11 @@ final class ChatStorage { receivedInviteStore.set(rejected, for: account.absoluteString) } - func accept(sentInviteId: Int64, account: Account, topic: String) async throws { - guard let invite = getSentInvite(id: sentInviteId, account: account) + func accept(sentInviteId: Int64, topic: String) async throws { + guard let invite = getSentInvite(id: sentInviteId) else { return } + let account = invite.inviterAccount try await sentInviteStore.delete(id: invite.syncId, for: account) let approved = SentInvite(invite: invite, status: .approved) @@ -147,10 +148,11 @@ final class ChatStorage { acceptPublisherSubject.send((topic, approved)) } - func reject(sentInviteId: Int64, account: Account) async throws { - guard let invite = getSentInvite(id: sentInviteId, account: account) + func reject(sentInviteId: Int64) async throws { + guard let invite = getSentInvite(id: sentInviteId) else { return } + let account = invite.inviterAccount try await sentInviteStore.delete(id: invite.syncId, for: account) let rejected = SentInvite(invite: invite, status: .rejected) diff --git a/Sources/Chat/ProtocolServices/Inviter/InviteService.swift b/Sources/Chat/ProtocolServices/Inviter/InviteService.swift index b6e465ee7..a0eb30c1d 100644 --- a/Sources/Chat/ProtocolServices/Inviter/InviteService.swift +++ b/Sources/Chat/ProtocolServices/Inviter/InviteService.swift @@ -100,6 +100,14 @@ private extension InviteService { ) } }.store(in: &publishers) + + networkingInteractor.responseErrorSubscription(on: ChatInviteProtocolMethod()) + .sink { [unowned self] (payload: ResponseSubscriptionErrorPayload) in + + Task(priority: .high) { + try await chatStorage.reject(sentInviteId: payload.id.integer) + } + }.store(in: &publishers) } func createThread(sentInviteId: Int64, selfPubKeyHex: String, peerPubKey: DIDKey, account: Account, peerAccount: Account) async throws { @@ -117,9 +125,7 @@ private extension InviteService { ) try await chatStorage.set(thread: thread, account: account) - - // TODO: Implement reject for sentInvite - try await chatStorage.accept(sentInviteId: sentInviteId, account: account, topic: threadTopic) + try await chatStorage.accept(sentInviteId: sentInviteId, topic: threadTopic) // TODO - remove symKeyI } From 7d91d7f037ffdcd785be2f2a9c3a55095ac9bb5e Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Sat, 22 Apr 2023 14:39:41 -0100 Subject: [PATCH 14/85] Random account UI --- Example/ExampleApp.xcodeproj/project.pbxproj | 4 ++++ .../DomainLayer/Chat/ImportAccount.swift | 5 +++++ .../Chat/ChatList/ChatListInteractor.swift | 4 ++++ .../Chat/ChatList/ChatListPresenter.swift | 10 ++++++++-- .../Chat/ChatList/ChatListView.swift | 8 ++++++++ .../Chat/ChatList/Models/AlertError.swift | 9 +++++++++ .../Chat/Import/ImportPresenter.swift | 18 ++++++++++++++---- .../Chat/Import/ImportView.swift | 8 ++++++++ 8 files changed, 60 insertions(+), 6 deletions(-) create mode 100644 Example/Showcase/Classes/PresentationLayer/Chat/ChatList/Models/AlertError.swift diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index 1812f7912..a4a33494b 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -200,6 +200,7 @@ A5E22D242840C8DB00E36487 /* SafariEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E22D232840C8DB00E36487 /* SafariEngine.swift */; }; A5E22D2C2840EAC300E36487 /* XCUIElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E22D2B2840EAC300E36487 /* XCUIElement.swift */; }; A74D32BA2A1E25AD00CB8536 /* QueryParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = A74D32B92A1E25AD00CB8536 /* QueryParameters.swift */; }; + A5E776BA29F4362D00172091 /* AlertError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E776B929F4362D00172091 /* AlertError.swift */; }; C5133A78294125CC00A8314C /* Web3 in Frameworks */ = {isa = PBXBuildFile; productRef = C5133A77294125CC00A8314C /* Web3 */; }; C53AA4362941251C008EA57C /* DefaultSignerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59CF4F5292F83D50031A42F /* DefaultSignerFactory.swift */; }; C55D347F295DD7140004314A /* AuthRequestModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = C55D347A295DD7140004314A /* AuthRequestModule.swift */; }; @@ -517,6 +518,7 @@ A5E22D212840C8D300E36487 /* WalletEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletEngine.swift; sourceTree = ""; }; A5E22D232840C8DB00E36487 /* SafariEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariEngine.swift; sourceTree = ""; }; A5E22D2B2840EAC300E36487 /* XCUIElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCUIElement.swift; sourceTree = ""; }; + A5E776B929F4362D00172091 /* AlertError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertError.swift; sourceTree = ""; }; A5F48A0528E43D3F0034CBFB /* Configuration.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Configuration.xcconfig; path = ../Configuration.xcconfig; sourceTree = ""; }; A74D32B92A1E25AD00CB8536 /* QueryParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueryParameters.swift; sourceTree = ""; }; C55D347A295DD7140004314A /* AuthRequestModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthRequestModule.swift; sourceTree = ""; }; @@ -1018,6 +1020,7 @@ isa = PBXGroup; children = ( A5629AE32876E6D200094373 /* ThreadViewModel.swift */, + A5E776B929F4362D00172091 /* AlertError.swift */, ); path = Models; sourceTree = ""; @@ -2115,6 +2118,7 @@ A58E7CEB28729F550082D443 /* AppDelegate.swift in Sources */, A578FA35287304A300AA7720 /* Color.swift in Sources */, A5629ADE2876CC6E00094373 /* InviteListModule.swift in Sources */, + A5E776BA29F4362D00172091 /* AlertError.swift in Sources */, A578FA322873036400AA7720 /* InputView.swift in Sources */, A5A0843F29D2F625000B9B17 /* DefaultCryptoProvider.swift in Sources */, A5C2021B287E1FD8007E3188 /* ImportRouter.swift in Sources */, diff --git a/Example/Showcase/Classes/DomainLayer/Chat/ImportAccount.swift b/Example/Showcase/Classes/DomainLayer/Chat/ImportAccount.swift index 54d43eb1d..648271f77 100644 --- a/Example/Showcase/Classes/DomainLayer/Chat/ImportAccount.swift +++ b/Example/Showcase/Classes/DomainLayer/Chat/ImportAccount.swift @@ -65,4 +65,9 @@ enum ImportAccount { return privateKey } } + + static func new() -> ImportAccount { + let key = try! EthereumPrivateKey() + return ImportAccount.custom(privateKey: key.rawPrivateKey.toHexString()) + } } diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListInteractor.swift b/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListInteractor.swift index 758b03fb7..27993fa6f 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListInteractor.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListInteractor.swift @@ -5,6 +5,10 @@ final class ChatListInteractor { private let chatService: ChatService private let accountStorage: AccountStorage + var account: Account? { + return accountStorage.importAccount?.account + } + init(chatService: ChatService, accountStorage: AccountStorage) { self.chatService = chatService self.accountStorage = accountStorage diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListPresenter.swift b/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListPresenter.swift index e44e516b8..56a3a0f9c 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListPresenter.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListPresenter.swift @@ -21,14 +21,12 @@ final class ChatListPresenter: ObservableObject { var receivedInviteViewModels: [InviteViewModel] { return receivedInvites - .filter { $0.status == .pending } .sorted(by: { $0.timestamp < $1.timestamp }) .map { InviteViewModel(invite: $0) } } var sentInviteViewModels: [InviteViewModel] { return sentInvites - .filter { $0.status == .pending } .sorted(by: { $0.timestamp < $1.timestamp }) .map { InviteViewModel(invite: $0) } } @@ -66,6 +64,14 @@ final class ChatListPresenter: ObservableObject { router.presentWelcome() } + @MainActor + func didCopyPress() async throws { + guard let account = interactor.account else { return } + UIPasteboard.general.string = account.absoluteString + + throw AlertError(message: "Account copied to clipboard") + } + func didPressNewChat() { presentInvite() } diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListView.swift b/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListView.swift index 3dc52a15f..979d1fe6d 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListView.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListView.swift @@ -36,6 +36,14 @@ struct ChatListView: View { } } + PlainButton { + try await presenter.didCopyPress() + } label: { + Text("Copy account") + .foregroundColor(.white) + } + .padding(.bottom, 16) + PlainButton { try await presenter.didLogoutPress() } label: { diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/Models/AlertError.swift b/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/Models/AlertError.swift new file mode 100644 index 000000000..c7cc4f270 --- /dev/null +++ b/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/Models/AlertError.swift @@ -0,0 +1,9 @@ +import Foundation + +struct AlertError: Error, LocalizedError { + let message: String + + var errorDescription: String? { + return message + } +} diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportPresenter.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportPresenter.swift index 9987116c2..a3fb26567 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportPresenter.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportPresenter.swift @@ -22,12 +22,15 @@ final class ImportPresenter: ObservableObject { @MainActor func didPressImport() async throws { - guard let importAccount = ImportAccount(input: input) + guard let account = ImportAccount(input: input) else { return input = .empty } + try await importAccount(account) + } - interactor.save(importAccount: importAccount) - try await interactor.register(importAccount: importAccount) - router.presentChat(importAccount: importAccount) + + func didPressRandom() async throws { + let account = ImportAccount.new() + try await importAccount(account) } } @@ -51,4 +54,11 @@ private extension ImportPresenter { func setupInitialState() { } + + @MainActor + func importAccount(_ importAccount: ImportAccount) async throws { + interactor.save(importAccount: importAccount) + try await interactor.register(importAccount: importAccount) + router.presentChat(importAccount: importAccount) + } } diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportView.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportView.swift index 9de69d9f3..93954d6b5 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportView.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportView.swift @@ -26,6 +26,14 @@ struct ImportView: View { } } .padding(16.0) + + PlainButton { + try await presenter.didPressRandom() + } label: { + Text("Create new account") + .foregroundColor(.white) + } + .padding(.bottom, 16) } } } From ca823e8f4d5b6d65811502ca2cd5b66b967e67fc Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Tue, 9 May 2023 16:11:05 +0300 Subject: [PATCH 15/85] SentInvites fixed --- Sources/Chat/ChatStorage.swift | 11 ++-------- .../Stores/SyncObjectStore.swift | 21 ++++++++++++++---- .../WalletConnectSync/Stores/SyncStore.swift | 22 ------------------- 3 files changed, 19 insertions(+), 35 deletions(-) diff --git a/Sources/Chat/ChatStorage.swift b/Sources/Chat/ChatStorage.swift index 541b7bed5..8a75bb31b 100644 --- a/Sources/Chat/ChatStorage.swift +++ b/Sources/Chat/ChatStorage.swift @@ -139,11 +139,8 @@ final class ChatStorage { guard let invite = getSentInvite(id: sentInviteId) else { return } - let account = invite.inviterAccount - try await sentInviteStore.delete(id: invite.syncId, for: account) - let approved = SentInvite(invite: invite, status: .approved) - try await sentInviteStore.set(object: approved, for: account) + try await sentInviteStore.set(object: approved, for: invite.inviterAccount) acceptPublisherSubject.send((topic, approved)) } @@ -152,12 +149,8 @@ final class ChatStorage { guard let invite = getSentInvite(id: sentInviteId) else { return } - let account = invite.inviterAccount - try await sentInviteStore.delete(id: invite.syncId, for: account) - let rejected = SentInvite(invite: invite, status: .rejected) - // TODO: Update also for peer invites - try await sentInviteStore.set(object: rejected, for: account) + try await sentInviteStore.set(object: rejected, for: invite.inviterAccount) rejectPublisherSubject.send(rejected) } diff --git a/Sources/WalletConnectSync/Stores/SyncObjectStore.swift b/Sources/WalletConnectSync/Stores/SyncObjectStore.swift index 9852ee99a..43e8707b7 100644 --- a/Sources/WalletConnectSync/Stores/SyncObjectStore.swift +++ b/Sources/WalletConnectSync/Stores/SyncObjectStore.swift @@ -33,19 +33,32 @@ final class SyncObjectStore { } } - func isExists(topic: String, id: String) -> Bool { - return store.getElement(for: topic)?[id] != nil - } - func set(object: Object, topic: String) { + guard isChanged(object, topic: topic) else { return } var map = getMap(topic: topic) map[object.syncId] = object store.set(element: map, for: topic) } func delete(id: String, topic: String) { + guard isExists(id: id, topic: topic) else { return } var map = getMap(topic: topic) map[id] = nil store.set(element: map, for: topic) } } + +private extension SyncObjectStore { + + func isExists(id: String, topic: String) -> Bool { + return getElement(id: id, topic: topic) != nil + } + + func getElement(id: String, topic: String) -> Object? { + return store.getElement(for: topic)?[id] + } + + func isChanged(_ object: Object, topic: String) -> Bool { + return object != getElement(id: object.syncId, topic: topic) + } +} diff --git a/Sources/WalletConnectSync/Stores/SyncStore.swift b/Sources/WalletConnectSync/Stores/SyncStore.swift index 43f48f85e..082298d18 100644 --- a/Sources/WalletConnectSync/Stores/SyncStore.swift +++ b/Sources/WalletConnectSync/Stores/SyncStore.swift @@ -53,25 +53,13 @@ public final class SyncStore { public func set(object: Object, for account: Account) async throws { let record = try indexStore.getRecord(account: account, name: name) - - guard !objectStore.isExists(topic: record.topic, id: object.syncId) else { - return - } - try await syncClient.set(account: account, store: record.store, object: object) - objectStore.set(object: object, topic: record.topic) } public func delete(id: String, for account: Account) async throws { let record = try indexStore.getRecord(account: account, name: name) - - guard objectStore.isExists(topic: record.topic, id: id) else { - return - } - try await syncClient.delete(account: account, store: record.store, key: id) - objectStore.delete(id: id, topic: record.topic) } @@ -107,21 +95,11 @@ private extension SyncStore { func setInStore(object: Object, for account: Account) throws { let record = try indexStore.getRecord(account: account, name: name) - - guard !objectStore.isExists(topic: record.topic, id: object.syncId) else { - return - } - objectStore.set(object: object, topic: record.topic) } func deleteInStore(id: String, for account: Account) throws { let record = try indexStore.getRecord(account: account, name: name) - - guard objectStore.isExists(topic: record.topic, id: id) else { - return - } - objectStore.delete(id: id, topic: record.topic) } } From 798896a4845ed73b6edbaf52e61e80a621bc577e Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Wed, 10 May 2023 12:18:25 +0300 Subject: [PATCH 16/85] Subscribe for sync invites --- Sources/Chat/ChatClient.swift | 1 + .../Common/ResubscriptionService.swift | 17 ++++++++ .../Inviter/InviteService.swift | 39 +++++++++++-------- Sources/Chat/Types/Plain/SentInvite.swift | 18 ++++++++- .../CryptoKitWrapper/AgreementCryptoKit.swift | 5 +++ 5 files changed, 63 insertions(+), 17 deletions(-) diff --git a/Sources/Chat/ChatClient.swift b/Sources/Chat/ChatClient.swift index 44862c66e..fff8fc95d 100644 --- a/Sources/Chat/ChatClient.swift +++ b/Sources/Chat/ChatClient.swift @@ -143,6 +143,7 @@ public class ChatClient { let inviteKey = try await identityClient.goPublic(account: account) try await chatStorage.initialize(for: account) try await resubscriptionService.subscribeForInvites(inviteKey: inviteKey) + try await resubscriptionService.subscribeForSyncInvites(account: account) } /// Accepts a chat invite by id from account specified as inviteeAccount in Invite diff --git a/Sources/Chat/ProtocolServices/Common/ResubscriptionService.swift b/Sources/Chat/ProtocolServices/Common/ResubscriptionService.swift index b2f87f7b6..b23eb7f7b 100644 --- a/Sources/Chat/ProtocolServices/Common/ResubscriptionService.swift +++ b/Sources/Chat/ProtocolServices/Common/ResubscriptionService.swift @@ -45,4 +45,21 @@ class ResubscriptionService { kms.deletePublicKey(for: topic) networkingInteractor.unsubscribe(topic: topic) } + + func subscribeForSyncInvites(account: Account) async throws { + let invites = chatStorage.getSentInvites(account: account) + + for invite in invites { + let symmetricKey = try SymmetricKey(hex: invite.symKey) + let agreementPublicKey = try AgreementPublicKey(hex: invite.inviterPubKeyY) + let agreementPrivateKey = try AgreementPrivateKey(hex: invite.inviterPrivKeyY) + + // TODO: Should we set symKey for inviteTopic??? + try kms.setSymmetricKey(symmetricKey, for: invite.responseTopic) + try kms.setPublicKey(publicKey: agreementPublicKey, for: invite.responseTopic) + try kms.setPrivateKey(agreementPrivateKey) + } + + try await networkingInteractor.batchSubscribe(topics: invites.map { $0.responseTopic }) + } } diff --git a/Sources/Chat/ProtocolServices/Inviter/InviteService.swift b/Sources/Chat/ProtocolServices/Inviter/InviteService.swift index a0eb30c1d..3c536129f 100644 --- a/Sources/Chat/ProtocolServices/Inviter/InviteService.swift +++ b/Sources/Chat/ProtocolServices/Inviter/InviteService.swift @@ -30,35 +30,38 @@ class InviteService { @discardableResult func invite(invite: Invite) async throws -> Int64 { - // TODO ad storage let protocolMethod = ChatInviteProtocolMethod() + let selfPubKeyY = try kms.createX25519KeyPair() + let selfPrivKeyY = try kms.getPrivateKey(for: selfPubKeyY)! + let inviteePublicKey = try DIDKey(did: invite.inviteePublicKey) - let symKeyI = try kms.performKeyAgreement(selfPublicKey: selfPubKeyY, peerPublicKey: inviteePublicKey.hexString) - let inviteTopic = try AgreementPublicKey(hex: inviteePublicKey.hexString).rawRepresentation.sha256().toHexString() - // overrides on invite toipic + let symKeyI = try kms.performKeyAgreement( + selfPublicKey: selfPubKeyY, + peerPublicKey: inviteePublicKey.hexString + ) + + let pubKeyX = try AgreementPublicKey(hex: inviteePublicKey.hexString) + let inviteTopic = pubKeyX.rawRepresentation.sha256().toHexString() + let responseTopic = symKeyI.derivedTopic() + try kms.setSymmetricKey(symKeyI.sharedKey, for: inviteTopic) + try kms.setSymmetricKey(symKeyI.sharedKey, for: responseTopic) - let payload = InvitePayload( - keyserver: keyserverURL, - message: invite.message, - inviteeAccount: invite.inviteeAccount, - inviterPublicKey: DIDKey(rawData: selfPubKeyY.rawRepresentation) - ) let wrapper = try identityClient.signAndCreateWrapper( - payload: payload, + payload: InvitePayload( + keyserver: keyserverURL, + message: invite.message, + inviteeAccount: invite.inviteeAccount, + inviterPublicKey: DIDKey(rawData: selfPubKeyY.rawRepresentation) + ), account: invite.inviterAccount ) let inviteId = RPCID() let request = RPCRequest(method: protocolMethod.method, params: wrapper, rpcid: inviteId) - // 2. Proposer subscribes to topic R which is the hash of the derived symKey - let responseTopic = symKeyI.derivedTopic() - - try kms.setSymmetricKey(symKeyI.sharedKey, for: responseTopic) - try await networkingInteractor.subscribe(topic: responseTopic) try await networkingInteractor.request(request, topic: inviteTopic, protocolMethod: protocolMethod, envelopeType: .type1(pubKey: selfPubKeyY.rawRepresentation)) @@ -67,6 +70,10 @@ class InviteService { message: invite.message, inviterAccount: invite.inviterAccount, inviteeAccount: invite.inviteeAccount, + inviterPubKeyY: selfPubKeyY.hexRepresentation, + inviterPrivKeyY: selfPrivKeyY.rawRepresentation.toHexString(), + responseTopic: responseTopic, + symKey: symKeyI.sharedKey.hexRepresentation, timestamp: Date().millisecondsSince1970 ) diff --git a/Sources/Chat/Types/Plain/SentInvite.swift b/Sources/Chat/Types/Plain/SentInvite.swift index 549687013..46db908f5 100644 --- a/Sources/Chat/Types/Plain/SentInvite.swift +++ b/Sources/Chat/Types/Plain/SentInvite.swift @@ -5,6 +5,10 @@ public struct SentInvite: Codable, Equatable { public let message: String public let inviterAccount: Account public let inviteeAccount: Account + public let inviterPubKeyY: String + public let inviterPrivKeyY: String + public let responseTopic: String + public let symKey: String public let timestamp: UInt64 public var status: Status @@ -13,13 +17,21 @@ public struct SentInvite: Codable, Equatable { message: String, inviterAccount: Account, inviteeAccount: Account, + inviterPubKeyY: String, + inviterPrivKeyY: String, + responseTopic: String, + symKey: String, timestamp: UInt64, - status: SentInvite.Status = .pending // TODO: Implement statuses + status: SentInvite.Status = .pending ) { self.id = id self.message = message self.inviterAccount = inviterAccount self.inviteeAccount = inviteeAccount + self.inviterPubKeyY = inviterPubKeyY + self.inviterPrivKeyY = inviterPrivKeyY + self.responseTopic = responseTopic + self.symKey = symKey self.timestamp = timestamp self.status = status } @@ -30,6 +42,10 @@ public struct SentInvite: Codable, Equatable { message: invite.message, inviterAccount: invite.inviterAccount, inviteeAccount: invite.inviteeAccount, + inviterPubKeyY: invite.inviterPubKeyY, + inviterPrivKeyY: invite.inviterPrivKeyY, + responseTopic: invite.responseTopic, + symKey: invite.symKey, timestamp: invite.timestamp, status: status ) diff --git a/Sources/WalletConnectKMS/Crypto/CryptoKitWrapper/AgreementCryptoKit.swift b/Sources/WalletConnectKMS/Crypto/CryptoKitWrapper/AgreementCryptoKit.swift index f5e0f906e..9ee627729 100644 --- a/Sources/WalletConnectKMS/Crypto/CryptoKitWrapper/AgreementCryptoKit.swift +++ b/Sources/WalletConnectKMS/Crypto/CryptoKitWrapper/AgreementCryptoKit.swift @@ -80,6 +80,11 @@ public struct AgreementPrivateKey: GenericPasswordConvertible, Equatable { self.key = Curve25519.KeyAgreement.PrivateKey() } + public init(hex: String) throws { + let data = Data(hex: hex) + try self.init(rawRepresentation: data) + } + public init(rawRepresentation: D) throws where D: ContiguousBytes { self.key = try Curve25519.KeyAgreement.PrivateKey(rawRepresentation: rawRepresentation) } From ac56c7a44b5cb951039da92a53ff95b6a08c1473 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Wed, 10 May 2023 23:48:09 +0300 Subject: [PATCH 17/85] savepoint 1 --- Sources/Chat/ChatClient.swift | 1 - Sources/Chat/ChatClientFactory.swift | 4 +- .../Common/ResubscriptionService.swift | 31 ------------- .../Invitee/InvitationHandlingService.swift | 3 +- .../Inviter/InviteService.swift | 3 +- Sources/Chat/{ => Storage}/ChatStorage.swift | 18 +++++++- .../Storage/SentInviteStoreDelegate.swift | 44 +++++++++++++++++++ .../Chat/Storage/ThreadStoreDelegate.swift | 27 ++++++++++++ Sources/Chat/Types/Plain/SentInvite.swift | 2 +- Sources/Chat/Types/Plain/Thread.swift | 1 + .../CryptoKitWrapper/AgreementCryptoKit.swift | 2 +- .../Stores/SyncObjectStore.swift | 11 +++-- .../WalletConnectSync/Stores/SyncStore.swift | 15 ++++++- .../Stores/SyncStoreFactory.swift | 5 ++- .../WalletConnectSync/SyncClientFactory.swift | 2 +- .../SyncStorageIdentifiers.swift | 15 +++++-- 16 files changed, 134 insertions(+), 50 deletions(-) rename Sources/Chat/{ => Storage}/ChatStorage.swift (89%) create mode 100644 Sources/Chat/Storage/SentInviteStoreDelegate.swift create mode 100644 Sources/Chat/Storage/ThreadStoreDelegate.swift diff --git a/Sources/Chat/ChatClient.swift b/Sources/Chat/ChatClient.swift index fff8fc95d..44862c66e 100644 --- a/Sources/Chat/ChatClient.swift +++ b/Sources/Chat/ChatClient.swift @@ -143,7 +143,6 @@ public class ChatClient { let inviteKey = try await identityClient.goPublic(account: account) try await chatStorage.initialize(for: account) try await resubscriptionService.subscribeForInvites(inviteKey: inviteKey) - try await resubscriptionService.subscribeForSyncInvites(account: account) } /// Accepts a chat invite by id from account specified as inviteeAccount in Invite diff --git a/Sources/Chat/ChatClientFactory.swift b/Sources/Chat/ChatClientFactory.swift index d834ec2ab..f72d31aaa 100644 --- a/Sources/Chat/ChatClientFactory.swift +++ b/Sources/Chat/ChatClientFactory.swift @@ -29,8 +29,10 @@ public struct ChatClientFactory { let messageStore = KeyedDatabase<[Message]>(storage: keyValueStorage, identifier: ChatStorageIdentifiers.messages.rawValue) let receivedInviteStore = KeyedDatabase<[ReceivedInvite]>(storage: keyValueStorage, identifier: ChatStorageIdentifiers.receivedInvites.rawValue) let threadStore: SyncStore = SyncStoreFactory.create(name: "chatThreads", syncClient: syncClient) + let sentInviteDelegate = SentInviteStoreDelegate(networkingInteractor: networkingInteractor, kms: kms) + let threadDelegate = ThreadStoreDelegate(networkingInteractor: networkingInteractor, kms: kms) let sentInviteStore: SyncStore = SyncStoreFactory.create(name: "chatSentInvites", syncClient: syncClient) - let chatStorage = ChatStorage(messageStore: messageStore, receivedInviteStore: receivedInviteStore, sentInviteStore: sentInviteStore, threadStore: threadStore) + let chatStorage = ChatStorage(messageStore: messageStore, receivedInviteStore: receivedInviteStore, sentInviteStore: sentInviteStore, threadStore: threadStore, sentInviteStoreDelegate: sentInviteDelegate, threadStoreDelegate: threadDelegate) let resubscriptionService = ResubscriptionService(networkingInteractor: networkingInteractor, kms: kms, chatStorage: chatStorage, logger: logger) let identityClient = IdentityClientFactory.create(keyserver: keyserverURL, keychain: keychain, logger: logger) let invitationHandlingService = InvitationHandlingService(keyserverURL: keyserverURL, networkingInteractor: networkingInteractor, identityClient: identityClient, kms: kms, logger: logger, chatStorage: chatStorage) diff --git a/Sources/Chat/ProtocolServices/Common/ResubscriptionService.swift b/Sources/Chat/ProtocolServices/Common/ResubscriptionService.swift index b23eb7f7b..93d6c865e 100644 --- a/Sources/Chat/ProtocolServices/Common/ResubscriptionService.swift +++ b/Sources/Chat/ProtocolServices/Common/ResubscriptionService.swift @@ -18,20 +18,6 @@ class ResubscriptionService { self.kms = kms self.logger = logger self.chatStorage = chatStorage - - setUpResubscription() - } - - func setUpResubscription() { - networkingInteractor.socketConnectionStatusPublisher - .sink { [unowned self] status in - guard status == .connected else { return } - - Task(priority: .high) { - let topics = chatStorage.getAllThreads().map { $0.topic } - try await networkingInteractor.batchSubscribe(topics: topics) - } - }.store(in: &publishers) } func subscribeForInvites(inviteKey: AgreementPublicKey) async throws { @@ -45,21 +31,4 @@ class ResubscriptionService { kms.deletePublicKey(for: topic) networkingInteractor.unsubscribe(topic: topic) } - - func subscribeForSyncInvites(account: Account) async throws { - let invites = chatStorage.getSentInvites(account: account) - - for invite in invites { - let symmetricKey = try SymmetricKey(hex: invite.symKey) - let agreementPublicKey = try AgreementPublicKey(hex: invite.inviterPubKeyY) - let agreementPrivateKey = try AgreementPrivateKey(hex: invite.inviterPrivKeyY) - - // TODO: Should we set symKey for inviteTopic??? - try kms.setSymmetricKey(symmetricKey, for: invite.responseTopic) - try kms.setPublicKey(publicKey: agreementPublicKey, for: invite.responseTopic) - try kms.setPrivateKey(agreementPrivateKey) - } - - try await networkingInteractor.batchSubscribe(topics: invites.map { $0.responseTopic }) - } } diff --git a/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift b/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift index f51bb90e7..2e3ea3aa1 100644 --- a/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift +++ b/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift @@ -66,7 +66,8 @@ class InvitationHandlingService { let thread = Thread( topic: threadTopic, selfAccount: invite.inviteeAccount, - peerAccount: invite.inviterAccount + peerAccount: invite.inviterAccount, + symKey: threadSymmetricKey.sharedKey.hexRepresentation ) try await chatStorage.set(thread: thread, account: invite.inviteeAccount) diff --git a/Sources/Chat/ProtocolServices/Inviter/InviteService.swift b/Sources/Chat/ProtocolServices/Inviter/InviteService.swift index 3c536129f..f1f09d31f 100644 --- a/Sources/Chat/ProtocolServices/Inviter/InviteService.swift +++ b/Sources/Chat/ProtocolServices/Inviter/InviteService.swift @@ -128,7 +128,8 @@ private extension InviteService { let thread = Thread( topic: threadTopic, selfAccount: account, - peerAccount: peerAccount + peerAccount: peerAccount, + symKey: agreementKeys.sharedKey.hexRepresentation ) try await chatStorage.set(thread: thread, account: account) diff --git a/Sources/Chat/ChatStorage.swift b/Sources/Chat/Storage/ChatStorage.swift similarity index 89% rename from Sources/Chat/ChatStorage.swift rename to Sources/Chat/Storage/ChatStorage.swift index 8a75bb31b..c3c3779f7 100644 --- a/Sources/Chat/ChatStorage.swift +++ b/Sources/Chat/Storage/ChatStorage.swift @@ -8,6 +8,9 @@ final class ChatStorage { private let sentInviteStore: SyncStore private let threadStore: SyncStore + private let sentInviteStoreDelegate: SentInviteStoreDelegate + private let threadStoreDelegate: ThreadStoreDelegate + private var messagesPublisherSubject = PassthroughSubject<[Message], Never>() private var receivedInvitesPublisherSubject = PassthroughSubject<[ReceivedInvite], Never>() private var newMessagePublisherSubject = PassthroughSubject() @@ -62,15 +65,27 @@ final class ChatStorage { messageStore: KeyedDatabase<[Message]>, receivedInviteStore: KeyedDatabase<[ReceivedInvite]>, sentInviteStore: SyncStore, - threadStore: SyncStore + threadStore: SyncStore, + sentInviteStoreDelegate: SentInviteStoreDelegate, + threadStoreDelegate: ThreadStoreDelegate ) { self.messageStore = messageStore self.receivedInviteStore = receivedInviteStore self.sentInviteStore = sentInviteStore self.threadStore = threadStore + self.sentInviteStoreDelegate = sentInviteStoreDelegate + self.threadStoreDelegate = threadStoreDelegate } func initialize(for account: Account) async throws { + sentInviteStore.onInitialization = sentInviteStoreDelegate.onInitialization + sentInviteStore.onUpdate = sentInviteStoreDelegate.onUpdate + sentInviteStore.onDelete = sentInviteStoreDelegate.onDelete + + threadStore.onInitialization = threadStoreDelegate.onInitialization + threadStore.onUpdate = threadStoreDelegate.onUpdate + threadStore.onDelete = threadStoreDelegate.onDelete + try await sentInviteStore.initialize(for: account) try await threadStore.initialize(for: account) } @@ -82,6 +97,7 @@ final class ChatStorage { receivedInviteStore.onUpdate = { [unowned self] in receivedInvitesPublisherSubject.send(getReceivedInvites(account: account)) } + try threadStore.setupSubscriptions(account: account) try sentInviteStore.setupSubscriptions(account: account) } diff --git a/Sources/Chat/Storage/SentInviteStoreDelegate.swift b/Sources/Chat/Storage/SentInviteStoreDelegate.swift new file mode 100644 index 000000000..ea55bf8c1 --- /dev/null +++ b/Sources/Chat/Storage/SentInviteStoreDelegate.swift @@ -0,0 +1,44 @@ +import Foundation + +final class SentInviteStoreDelegate { + + private let networkingInteractor: NetworkInteracting + private let kms: KeyManagementServiceProtocol + + init(networkingInteractor: NetworkInteracting, kms: KeyManagementServiceProtocol) { + self.networkingInteractor = networkingInteractor + self.kms = kms + } + + func onInitialization(_ objects: [SentInvite]) async throws { + for invite in objects { + try syncKeychain(invite: invite) + } + + let topics = objects.map { $0.responseTopic } + try await networkingInteractor.batchSubscribe(topics: topics) + } + + func onUpdate(_ object: SentInvite) async throws { + try syncKeychain(invite: object) + try await networkingInteractor.subscribe(topic: object.responseTopic) + } + + func onDelete(_ id: String) async throws { + // TODO: Implement unsubscribe + } +} + +private extension SentInviteStoreDelegate { + + func syncKeychain(invite: SentInvite) throws { +// let symmetricKey = try SymmetricKey(hex: invite.symKey) +// let agreementPublicKey = try AgreementPublicKey(hex: invite.inviterPubKeyY) +// let agreementPrivateKey = try AgreementPrivateKey(hex: invite.inviterPrivKeyY) +// +// // TODO: Should we set symKey for inviteTopic??? +// try kms.setSymmetricKey(symmetricKey, for: invite.responseTopic) +// try kms.setPublicKey(publicKey: agreementPublicKey, for: invite.responseTopic) +// try kms.setPrivateKey(agreementPrivateKey) + } +} diff --git a/Sources/Chat/Storage/ThreadStoreDelegate.swift b/Sources/Chat/Storage/ThreadStoreDelegate.swift new file mode 100644 index 000000000..69538b366 --- /dev/null +++ b/Sources/Chat/Storage/ThreadStoreDelegate.swift @@ -0,0 +1,27 @@ +import Foundation + +final class ThreadStoreDelegate { + + private let networkingInteractor: NetworkInteracting + private let kms: KeyManagementServiceProtocol + + init(networkingInteractor: NetworkInteracting, kms: KeyManagementServiceProtocol) { + self.networkingInteractor = networkingInteractor + self.kms = kms + } + + func onInitialization(_ threads: [Thread]) async throws { + let topics = threads.map { $0.topic } + try await networkingInteractor.batchSubscribe(topics: topics) + } + + func onUpdate(_ thread: Thread) async throws { +// let symmetricKey = try SymmetricKey(hex: thread.symKey) +// try kms.setSymmetricKey(symmetricKey, for: thread.topic) +// try await networkingInteractor.subscribe(topic: thread.topic) + } + + func onDelete(_ id: String) async throws { + + } +} diff --git a/Sources/Chat/Types/Plain/SentInvite.swift b/Sources/Chat/Types/Plain/SentInvite.swift index 46db908f5..6109367fa 100644 --- a/Sources/Chat/Types/Plain/SentInvite.swift +++ b/Sources/Chat/Types/Plain/SentInvite.swift @@ -12,7 +12,7 @@ public struct SentInvite: Codable, Equatable { public let timestamp: UInt64 public var status: Status - public init( + init( id: Int64, message: String, inviterAccount: Account, diff --git a/Sources/Chat/Types/Plain/Thread.swift b/Sources/Chat/Types/Plain/Thread.swift index 731d1ee78..1ddd6e7b1 100644 --- a/Sources/Chat/Types/Plain/Thread.swift +++ b/Sources/Chat/Types/Plain/Thread.swift @@ -4,6 +4,7 @@ public struct Thread: Codable, Equatable { public let topic: String public let selfAccount: Account public let peerAccount: Account + public let symKey: String } extension Thread: SyncObject { diff --git a/Sources/WalletConnectKMS/Crypto/CryptoKitWrapper/AgreementCryptoKit.swift b/Sources/WalletConnectKMS/Crypto/CryptoKitWrapper/AgreementCryptoKit.swift index 9ee627729..b61fdc66c 100644 --- a/Sources/WalletConnectKMS/Crypto/CryptoKitWrapper/AgreementCryptoKit.swift +++ b/Sources/WalletConnectKMS/Crypto/CryptoKitWrapper/AgreementCryptoKit.swift @@ -47,7 +47,7 @@ public struct AgreementPublicKey: GenericPasswordConvertible, Equatable { } public var hexRepresentation: String { - key.rawRepresentation.toHexString() + rawRepresentation.toHexString() } public var did: String { diff --git a/Sources/WalletConnectSync/Stores/SyncObjectStore.swift b/Sources/WalletConnectSync/Stores/SyncObjectStore.swift index 43e8707b7..2e7253906 100644 --- a/Sources/WalletConnectSync/Stores/SyncObjectStore.swift +++ b/Sources/WalletConnectSync/Stores/SyncObjectStore.swift @@ -33,18 +33,21 @@ final class SyncObjectStore { } } - func set(object: Object, topic: String) { - guard isChanged(object, topic: topic) else { return } + @discardableResult func set(object: Object, topic: String) -> Bool { + guard isChanged(object, topic: topic) else { return false } var map = getMap(topic: topic) map[object.syncId] = object store.set(element: map, for: topic) + return true } - func delete(id: String, topic: String) { - guard isExists(id: id, topic: topic) else { return } + + @discardableResult func delete(id: String, topic: String) -> Bool { + guard isExists(id: id, topic: topic) else { return false } var map = getMap(topic: topic) map[id] = nil store.set(element: map, for: topic) + return true } } diff --git a/Sources/WalletConnectSync/Stores/SyncStore.swift b/Sources/WalletConnectSync/Stores/SyncStore.swift index 082298d18..9ab2b8830 100644 --- a/Sources/WalletConnectSync/Stores/SyncStore.swift +++ b/Sources/WalletConnectSync/Stores/SyncStore.swift @@ -29,6 +29,10 @@ public final class SyncStore { return syncUpdateSubject.eraseToAnyPublisher() } + public var onInitialization: (([Object]) async throws -> Void)? + public var onUpdate: ((Object) async throws -> Void)? + public var onDelete: ((String) async throws -> Void)? + init(name: String, syncClient: SyncClient, indexStore: SyncIndexStore, objectStore: SyncObjectStore) { self.name = name self.syncClient = syncClient @@ -40,6 +44,7 @@ public final class SyncStore { public func initialize(for account: Account) async throws { try await syncClient.create(account: account, store: name) + try await onInitialization?(getAll()) } public func getAll(for account: Account) throws -> [Object] { @@ -54,13 +59,19 @@ public final class SyncStore { public func set(object: Object, for account: Account) async throws { let record = try indexStore.getRecord(account: account, name: name) try await syncClient.set(account: account, store: record.store, object: object) - objectStore.set(object: object, topic: record.topic) + + if objectStore.set(object: object, topic: record.topic) { + try await onUpdate?(object) + } } public func delete(id: String, for account: Account) async throws { let record = try indexStore.getRecord(account: account, name: name) try await syncClient.delete(account: account, store: record.store, key: id) - objectStore.delete(id: id, topic: record.topic) + + if objectStore.delete(id: id, topic: record.topic) { + try await onDelete?(id) + } } public func setupSubscriptions(account: Account) throws { diff --git a/Sources/WalletConnectSync/Stores/SyncStoreFactory.swift b/Sources/WalletConnectSync/Stores/SyncStoreFactory.swift index e102544b0..17c680413 100644 --- a/Sources/WalletConnectSync/Stores/SyncStoreFactory.swift +++ b/Sources/WalletConnectSync/Stores/SyncStoreFactory.swift @@ -3,9 +3,10 @@ import Foundation public final class SyncStoreFactory { public static func create(name: String, syncClient: SyncClient) -> SyncStore { - let indexDatabase = CodableStore(defaults: UserDefaults.standard, identifier: SyncStorageIdentifiers.index.rawValue) + let indexDatabase = CodableStore(defaults: UserDefaults.standard, identifier: SyncStorageIdentifiers.index.identifier) let indexStore = SyncIndexStore(store: indexDatabase) - let objectDatabase = NewKeyedDatabase<[String: Object]>(storage: UserDefaults.standard, identifier: SyncStorageIdentifiers.object.rawValue) + let objectIdentifier = SyncStorageIdentifiers.object(store: name).identifier + let objectDatabase = NewKeyedDatabase<[String: Object]>(storage: UserDefaults.standard, identifier: objectIdentifier) let objectStore = SyncObjectStore(store: objectDatabase) return SyncStore(name: name, syncClient: syncClient, indexStore: indexStore, objectStore: objectStore) } diff --git a/Sources/WalletConnectSync/SyncClientFactory.swift b/Sources/WalletConnectSync/SyncClientFactory.swift index 6fd79c4c3..6be21ccb1 100644 --- a/Sources/WalletConnectSync/SyncClientFactory.swift +++ b/Sources/WalletConnectSync/SyncClientFactory.swift @@ -15,7 +15,7 @@ final class SyncClientFactory { crypto: crypto, kms: kms ) - let indexStore = CodableStore(defaults: UserDefaults.standard, identifier: SyncStorageIdentifiers.index.rawValue) + let indexStore = CodableStore(defaults: UserDefaults.standard, identifier: SyncStorageIdentifiers.index.identifier) let syncIndexStore = SyncIndexStore(store: indexStore) let syncService = SyncService( networkInteractor: networkInteractor, diff --git a/Sources/WalletConnectSync/SyncStorageIdentifiers.swift b/Sources/WalletConnectSync/SyncStorageIdentifiers.swift index 87a5077f1..a3c757c14 100644 --- a/Sources/WalletConnectSync/SyncStorageIdentifiers.swift +++ b/Sources/WalletConnectSync/SyncStorageIdentifiers.swift @@ -1,6 +1,15 @@ import Foundation -enum SyncStorageIdentifiers: String { - case index = "com.walletconnect.sync.index" - case object = "com.walletconnect.sync.object" +enum SyncStorageIdentifiers { + case index + case object(store: String) + + var identifier: String { + switch self { + case .index: + return "com.walletconnect.sync.index" + case .object(let store): + return "com.walletconnect.sync.object.\(store)" + } + } } From a12ffa7623a60268f7fabfe13034c6466e2fcb38 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Thu, 11 May 2023 19:32:19 +0300 Subject: [PATCH 18/85] savepoint 2 --- .../Storage/SentInviteStoreDelegate.swift | 14 +++++------ .../Chat/Storage/ThreadStoreDelegate.swift | 6 ++--- .../WalletConnectSync/Stores/SyncStore.swift | 23 +++++++++++-------- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/Sources/Chat/Storage/SentInviteStoreDelegate.swift b/Sources/Chat/Storage/SentInviteStoreDelegate.swift index ea55bf8c1..85de84155 100644 --- a/Sources/Chat/Storage/SentInviteStoreDelegate.swift +++ b/Sources/Chat/Storage/SentInviteStoreDelegate.swift @@ -32,13 +32,11 @@ final class SentInviteStoreDelegate { private extension SentInviteStoreDelegate { func syncKeychain(invite: SentInvite) throws { -// let symmetricKey = try SymmetricKey(hex: invite.symKey) -// let agreementPublicKey = try AgreementPublicKey(hex: invite.inviterPubKeyY) -// let agreementPrivateKey = try AgreementPrivateKey(hex: invite.inviterPrivKeyY) -// -// // TODO: Should we set symKey for inviteTopic??? -// try kms.setSymmetricKey(symmetricKey, for: invite.responseTopic) -// try kms.setPublicKey(publicKey: agreementPublicKey, for: invite.responseTopic) -// try kms.setPrivateKey(agreementPrivateKey) + let symmetricKey = try SymmetricKey(hex: invite.symKey) + let agreementPrivateKey = try AgreementPrivateKey(hex: invite.inviterPrivKeyY) + + // TODO: Should we set symKey for inviteTopic??? + try kms.setSymmetricKey(symmetricKey, for: invite.responseTopic) + try kms.setPrivateKey(agreementPrivateKey) } } diff --git a/Sources/Chat/Storage/ThreadStoreDelegate.swift b/Sources/Chat/Storage/ThreadStoreDelegate.swift index 69538b366..ea260f96e 100644 --- a/Sources/Chat/Storage/ThreadStoreDelegate.swift +++ b/Sources/Chat/Storage/ThreadStoreDelegate.swift @@ -16,9 +16,9 @@ final class ThreadStoreDelegate { } func onUpdate(_ thread: Thread) async throws { -// let symmetricKey = try SymmetricKey(hex: thread.symKey) -// try kms.setSymmetricKey(symmetricKey, for: thread.topic) -// try await networkingInteractor.subscribe(topic: thread.topic) + let symmetricKey = try SymmetricKey(hex: thread.symKey) + try kms.setSymmetricKey(symmetricKey, for: thread.topic) + try await networkingInteractor.subscribe(topic: thread.topic) } func onDelete(_ id: String) async throws { diff --git a/Sources/WalletConnectSync/Stores/SyncStore.swift b/Sources/WalletConnectSync/Stores/SyncStore.swift index 9ab2b8830..d1ec08059 100644 --- a/Sources/WalletConnectSync/Stores/SyncStore.swift +++ b/Sources/WalletConnectSync/Stores/SyncStore.swift @@ -60,18 +60,13 @@ public final class SyncStore { let record = try indexStore.getRecord(account: account, name: name) try await syncClient.set(account: account, store: record.store, object: object) - if objectStore.set(object: object, topic: record.topic) { - try await onUpdate?(object) - } + objectStore.set(object: object, topic: record.topic) } public func delete(id: String, for account: Account) async throws { let record = try indexStore.getRecord(account: account, name: name) try await syncClient.delete(account: account, store: record.store, key: id) - - if objectStore.delete(id: id, topic: record.topic) { - try await onDelete?(id) - } + objectStore.delete(id: id, topic: record.topic) } public func setupSubscriptions(account: Account) throws { @@ -106,11 +101,21 @@ private extension SyncStore { func setInStore(object: Object, for account: Account) throws { let record = try indexStore.getRecord(account: account, name: name) - objectStore.set(object: object, topic: record.topic) + + if objectStore.set(object: object, topic: record.topic) { + Task(priority: .high) { + try await onUpdate?(object) // TODO: Replace with syncUpdatePublisher + } + } } func deleteInStore(id: String, for account: Account) throws { let record = try indexStore.getRecord(account: account, name: name) - objectStore.delete(id: id, topic: record.topic) + + if objectStore.delete(id: id, topic: record.topic) { + Task(priority: .high) { + try await onDelete?(id) // TODO: Replace with syncUpdatePublisher + } + } } } From e7c2807c804b125d7ca3b4d8dbb444c87302a694 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Fri, 12 May 2023 12:44:07 +0300 Subject: [PATCH 19/85] Testing session --- .../DomainLayer/Chat/ChatService.swift | 2 +- Sources/Chat/Storage/ChatStorage.swift | 38 +++++++++++++---- .../Storage/SentInviteStoreDelegate.swift | 24 ++++++----- .../Chat/Storage/ThreadStoreDelegate.swift | 20 +++++---- .../WalletConnectSync/Stores/SyncStore.swift | 42 ++++++++----------- Sources/WalletConnectSync/SyncClient.swift | 2 +- 6 files changed, 75 insertions(+), 53 deletions(-) diff --git a/Example/Showcase/Classes/DomainLayer/Chat/ChatService.swift b/Example/Showcase/Classes/DomainLayer/Chat/ChatService.swift index 7a3f7fee3..10177170c 100644 --- a/Example/Showcase/Classes/DomainLayer/Chat/ChatService.swift +++ b/Example/Showcase/Classes/DomainLayer/Chat/ChatService.swift @@ -7,7 +7,7 @@ typealias Stream = AnyPublisher final class ChatService { - private lazy var client: ChatClient = { + private var client: ChatClient = { Chat.configure(crypto: DefaultCryptoProvider()) return Chat.instance }() diff --git a/Sources/Chat/Storage/ChatStorage.swift b/Sources/Chat/Storage/ChatStorage.swift index c3c3779f7..2c64446d3 100644 --- a/Sources/Chat/Storage/ChatStorage.swift +++ b/Sources/Chat/Storage/ChatStorage.swift @@ -3,6 +3,8 @@ import Combine final class ChatStorage { + private var publishers = Set() + private let messageStore: KeyedDatabase<[Message]> private let receivedInviteStore: KeyedDatabase<[ReceivedInvite]> private let sentInviteStore: SyncStore @@ -75,17 +77,11 @@ final class ChatStorage { self.threadStore = threadStore self.sentInviteStoreDelegate = sentInviteStoreDelegate self.threadStoreDelegate = threadStoreDelegate + + setupSyncSubscriptions() } func initialize(for account: Account) async throws { - sentInviteStore.onInitialization = sentInviteStoreDelegate.onInitialization - sentInviteStore.onUpdate = sentInviteStoreDelegate.onUpdate - sentInviteStore.onDelete = sentInviteStoreDelegate.onDelete - - threadStore.onInitialization = threadStoreDelegate.onInitialization - threadStore.onUpdate = threadStoreDelegate.onUpdate - threadStore.onDelete = threadStoreDelegate.onDelete - try await sentInviteStore.initialize(for: account) try await threadStore.initialize(for: account) } @@ -210,3 +206,29 @@ final class ChatStorage { return messageStore.getElements(for: account.absoluteString) ?? [] } } + +private extension ChatStorage { + + func setupSyncSubscriptions() { + sentInviteStoreDelegate.onInitialization(sentInviteStore.getAll()) + threadStoreDelegate.onInitialization(threadStore.getAll()) + + sentInviteStore.syncUpdatePublisher.sink { [unowned self] topic, update in + switch update { + case .set(let object): + self.sentInviteStoreDelegate.onUpdate(object) + case .delete(let id): + self.sentInviteStoreDelegate.onDelete(id) + } + }.store(in: &publishers) + + threadStore.syncUpdatePublisher.sink { [unowned self] topic, update in + switch update { + case .set(let object): + self.threadStoreDelegate.onUpdate(object) + case .delete(let id): + self.threadStoreDelegate.onDelete(id) + } + }.store(in: &publishers) + } +} diff --git a/Sources/Chat/Storage/SentInviteStoreDelegate.swift b/Sources/Chat/Storage/SentInviteStoreDelegate.swift index 85de84155..081e75135 100644 --- a/Sources/Chat/Storage/SentInviteStoreDelegate.swift +++ b/Sources/Chat/Storage/SentInviteStoreDelegate.swift @@ -10,21 +10,25 @@ final class SentInviteStoreDelegate { self.kms = kms } - func onInitialization(_ objects: [SentInvite]) async throws { - for invite in objects { - try syncKeychain(invite: invite) + func onInitialization(_ objects: [SentInvite]) { + Task(priority: .high) { + for invite in objects { + try syncKeychain(invite: invite) + } + + let topics = objects.map { $0.responseTopic } + try await networkingInteractor.batchSubscribe(topics: topics) } - - let topics = objects.map { $0.responseTopic } - try await networkingInteractor.batchSubscribe(topics: topics) } - func onUpdate(_ object: SentInvite) async throws { - try syncKeychain(invite: object) - try await networkingInteractor.subscribe(topic: object.responseTopic) + func onUpdate(_ object: SentInvite) { + Task(priority: .high) { + try syncKeychain(invite: object) + try await networkingInteractor.subscribe(topic: object.responseTopic) + } } - func onDelete(_ id: String) async throws { + func onDelete(_ id: String) { // TODO: Implement unsubscribe } } diff --git a/Sources/Chat/Storage/ThreadStoreDelegate.swift b/Sources/Chat/Storage/ThreadStoreDelegate.swift index ea260f96e..3ec14b0eb 100644 --- a/Sources/Chat/Storage/ThreadStoreDelegate.swift +++ b/Sources/Chat/Storage/ThreadStoreDelegate.swift @@ -10,18 +10,22 @@ final class ThreadStoreDelegate { self.kms = kms } - func onInitialization(_ threads: [Thread]) async throws { - let topics = threads.map { $0.topic } - try await networkingInteractor.batchSubscribe(topics: topics) + func onInitialization(_ threads: [Thread]) { + Task(priority: .high) { + let topics = threads.map { $0.topic } + try await networkingInteractor.batchSubscribe(topics: topics) + } } - func onUpdate(_ thread: Thread) async throws { - let symmetricKey = try SymmetricKey(hex: thread.symKey) - try kms.setSymmetricKey(symmetricKey, for: thread.topic) - try await networkingInteractor.subscribe(topic: thread.topic) + func onUpdate(_ thread: Thread) { + Task(priority: .high) { + let symmetricKey = try SymmetricKey(hex: thread.symKey) + try kms.setSymmetricKey(symmetricKey, for: thread.topic) + try await networkingInteractor.subscribe(topic: thread.topic) + } } - func onDelete(_ id: String) async throws { + func onDelete(_ id: String) { } } diff --git a/Sources/WalletConnectSync/Stores/SyncStore.swift b/Sources/WalletConnectSync/Stores/SyncStore.swift index d1ec08059..521e96ff2 100644 --- a/Sources/WalletConnectSync/Stores/SyncStore.swift +++ b/Sources/WalletConnectSync/Stores/SyncStore.swift @@ -5,6 +5,11 @@ public protocol SyncObject: Codable & Equatable { var syncId: String { get } } +public enum SyncUpdate { + case set(object: Object) + case delete(id: String) +} + public final class SyncStore { private var publishers = Set() @@ -19,20 +24,16 @@ public final class SyncStore { private let objectStore: SyncObjectStore private let dataUpdateSubject = PassthroughSubject<[Object], Never>() - private let syncUpdateSubject = PassthroughSubject<(String, StoreUpdate), Never>() + private let syncUpdateSubject = PassthroughSubject<(String, SyncUpdate), Never>() public var dataUpdatePublisher: AnyPublisher<[Object], Never> { return dataUpdateSubject.eraseToAnyPublisher() } - public var syncUpdatePublisher: AnyPublisher<(String, StoreUpdate), Never> { + public var syncUpdatePublisher: AnyPublisher<(String, SyncUpdate), Never> { return syncUpdateSubject.eraseToAnyPublisher() } - public var onInitialization: (([Object]) async throws -> Void)? - public var onUpdate: ((Object) async throws -> Void)? - public var onDelete: ((String) async throws -> Void)? - init(name: String, syncClient: SyncClient, indexStore: SyncIndexStore, objectStore: SyncObjectStore) { self.name = name self.syncClient = syncClient @@ -44,7 +45,6 @@ public final class SyncStore { public func initialize(for account: Account) async throws { try await syncClient.create(account: account, store: name) - try await onInitialization?(getAll()) } public func getAll(for account: Account) throws -> [Object] { @@ -90,32 +90,24 @@ private extension SyncStore { switch update { case .set(let value): let decoded = try! value.get(StoreSet.self) - try! setInStore(object: decoded.value, for: record.account) - syncUpdateSubject.send((topic, update)) + if try! setInStore(object: decoded.value, for: record.account) { + syncUpdateSubject.send((topic, .set(object: decoded.value))) + } case .delete(let key): - try! deleteInStore(id: key, for: record.account) - syncUpdateSubject.send((topic, update)) + if try! deleteInStore(id: key, for: record.account) { + syncUpdateSubject.send((topic, .delete(id: key))) + } } }.store(in: &publishers) } - func setInStore(object: Object, for account: Account) throws { + func setInStore(object: Object, for account: Account) throws -> Bool { let record = try indexStore.getRecord(account: account, name: name) - - if objectStore.set(object: object, topic: record.topic) { - Task(priority: .high) { - try await onUpdate?(object) // TODO: Replace with syncUpdatePublisher - } - } + return objectStore.set(object: object, topic: record.topic) } - func deleteInStore(id: String, for account: Account) throws { + func deleteInStore(id: String, for account: Account) throws -> Bool { let record = try indexStore.getRecord(account: account, name: name) - - if objectStore.delete(id: id, topic: record.topic) { - Task(priority: .high) { - try await onDelete?(id) // TODO: Replace with syncUpdatePublisher - } - } + return objectStore.delete(id: id, topic: record.topic) } } diff --git a/Sources/WalletConnectSync/SyncClient.swift b/Sources/WalletConnectSync/SyncClient.swift index 488058a93..4f8cb3819 100644 --- a/Sources/WalletConnectSync/SyncClient.swift +++ b/Sources/WalletConnectSync/SyncClient.swift @@ -22,7 +22,7 @@ public final class SyncClient { return """ I authorize this app to sync my account: \(account.absoluteString) - Read more about Sync API: https://docs.walletconnect.com/2.0/specs/clients/sync + Read more about Sync API: https://docs.walletconnect.com/2.0/specs/clients/core/sync """ } From ec3974de5ac3e66c7bfa7f74a4ec88caac2be1c1 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Mon, 15 May 2023 16:54:09 +0300 Subject: [PATCH 20/85] Sync invite keys --- Sources/Chat/ChatClient.swift | 5 +- Sources/Chat/ChatClientFactory.swift | 10 ++-- Sources/Chat/ChatStorageIdentifiers.swift | 3 + .../Common/ResubscriptionService.swift | 12 ---- Sources/Chat/Storage/ChatStorage.swift | 39 ++++++++++++- Sources/Chat/Storage/InviteKeyDelegate.swift | 58 +++++++++++++++++++ Sources/Chat/Types/Plain/InviteKey.swift | 14 +++++ .../IdentityClient.swift | 8 +++ .../IdentityStorage.swift | 4 +- .../Services/SyncService.swift | 4 +- .../WalletConnectSync/Stores/SyncStore.swift | 8 +-- Sources/WalletConnectSync/SyncClient.swift | 2 - 12 files changed, 135 insertions(+), 32 deletions(-) create mode 100644 Sources/Chat/Storage/InviteKeyDelegate.swift create mode 100644 Sources/Chat/Types/Plain/InviteKey.swift diff --git a/Sources/Chat/ChatClient.swift b/Sources/Chat/ChatClient.swift index 44862c66e..63cbc0df5 100644 --- a/Sources/Chat/ChatClient.swift +++ b/Sources/Chat/ChatClient.swift @@ -131,8 +131,7 @@ public class ChatClient { /// Stops listening for invites /// - Parameter account: CAIP10 blockachain account public func goPrivate(account: Account) async throws { - let inviteKey = try await identityClient.goPrivate(account: account) - resubscriptionService.unsubscribeFromInvites(inviteKey: inviteKey) + _ = try await identityClient.goPrivate(account: account) } /// Registers an invite key if not yet registered on this client from keyserver @@ -142,7 +141,7 @@ public class ChatClient { public func goPublic(account: Account) async throws { let inviteKey = try await identityClient.goPublic(account: account) try await chatStorage.initialize(for: account) - try await resubscriptionService.subscribeForInvites(inviteKey: inviteKey) + try await chatStorage.syncInviteKey(inviteKey, account: account) } /// Accepts a chat invite by id from account specified as inviteeAccount in Invite diff --git a/Sources/Chat/ChatClientFactory.swift b/Sources/Chat/ChatClientFactory.swift index f72d31aaa..797163577 100644 --- a/Sources/Chat/ChatClientFactory.swift +++ b/Sources/Chat/ChatClientFactory.swift @@ -28,13 +28,15 @@ public struct ChatClientFactory { let kms = KeyManagementService(keychain: keychain) let messageStore = KeyedDatabase<[Message]>(storage: keyValueStorage, identifier: ChatStorageIdentifiers.messages.rawValue) let receivedInviteStore = KeyedDatabase<[ReceivedInvite]>(storage: keyValueStorage, identifier: ChatStorageIdentifiers.receivedInvites.rawValue) - let threadStore: SyncStore = SyncStoreFactory.create(name: "chatThreads", syncClient: syncClient) + let threadStore: SyncStore = SyncStoreFactory.create(name: ChatStorageIdentifiers.thread.rawValue, syncClient: syncClient) + let identityClient = IdentityClientFactory.create(keyserver: keyserverURL, keychain: keychain, logger: logger) + let inviteKeyDelegate = InviteKeyDelegate(networkingInteractor: networkingInteractor, kms: kms, identityClient: identityClient) let sentInviteDelegate = SentInviteStoreDelegate(networkingInteractor: networkingInteractor, kms: kms) let threadDelegate = ThreadStoreDelegate(networkingInteractor: networkingInteractor, kms: kms) - let sentInviteStore: SyncStore = SyncStoreFactory.create(name: "chatSentInvites", syncClient: syncClient) - let chatStorage = ChatStorage(messageStore: messageStore, receivedInviteStore: receivedInviteStore, sentInviteStore: sentInviteStore, threadStore: threadStore, sentInviteStoreDelegate: sentInviteDelegate, threadStoreDelegate: threadDelegate) + let sentInviteStore: SyncStore = SyncStoreFactory.create(name: ChatStorageIdentifiers.sentInvite.rawValue, syncClient: syncClient) + let inviteKeyStore: SyncStore = SyncStoreFactory.create(name: ChatStorageIdentifiers.inviteKey.rawValue, syncClient: syncClient) + let chatStorage = ChatStorage(kms: kms, messageStore: messageStore, receivedInviteStore: receivedInviteStore, sentInviteStore: sentInviteStore, threadStore: threadStore, inviteKeyStore: inviteKeyStore, sentInviteStoreDelegate: sentInviteDelegate, threadStoreDelegate: threadDelegate, inviteKeyDelegate: inviteKeyDelegate) let resubscriptionService = ResubscriptionService(networkingInteractor: networkingInteractor, kms: kms, chatStorage: chatStorage, logger: logger) - let identityClient = IdentityClientFactory.create(keyserver: keyserverURL, keychain: keychain, logger: logger) let invitationHandlingService = InvitationHandlingService(keyserverURL: keyserverURL, networkingInteractor: networkingInteractor, identityClient: identityClient, kms: kms, logger: logger, chatStorage: chatStorage) let inviteService = InviteService(keyserverURL: keyserverURL, networkingInteractor: networkingInteractor, identityClient: identityClient, kms: kms, chatStorage: chatStorage, logger: logger) let leaveService = LeaveService() diff --git a/Sources/Chat/ChatStorageIdentifiers.swift b/Sources/Chat/ChatStorageIdentifiers.swift index 5f80b413b..568e3718e 100644 --- a/Sources/Chat/ChatStorageIdentifiers.swift +++ b/Sources/Chat/ChatStorageIdentifiers.swift @@ -4,4 +4,7 @@ enum ChatStorageIdentifiers: String { case topicToInvitationPubKey = "com.walletconnect.chat.topicToInvitationPubKey" case messages = "com.walletconnect.chat.messages" case receivedInvites = "com.walletconnect.chat.receivedInvites" + case thread = "com.walletconnect.web3inbox.chatThreads" + case sentInvite = "com.walletconnect.web3inbox.chatSentInvites" + case inviteKey = "com.walletconnect.web3inbox.chatInviteKeys" } diff --git a/Sources/Chat/ProtocolServices/Common/ResubscriptionService.swift b/Sources/Chat/ProtocolServices/Common/ResubscriptionService.swift index 93d6c865e..1ebeb1e31 100644 --- a/Sources/Chat/ProtocolServices/Common/ResubscriptionService.swift +++ b/Sources/Chat/ProtocolServices/Common/ResubscriptionService.swift @@ -19,16 +19,4 @@ class ResubscriptionService { self.logger = logger self.chatStorage = chatStorage } - - func subscribeForInvites(inviteKey: AgreementPublicKey) async throws { - let topic = inviteKey.rawRepresentation.sha256().toHexString() - try kms.setPublicKey(publicKey: inviteKey, for: topic) - try await networkingInteractor.subscribe(topic: topic) - } - - func unsubscribeFromInvites(inviteKey: AgreementPublicKey) { - let topic = inviteKey.rawRepresentation.sha256().toHexString() - kms.deletePublicKey(for: topic) - networkingInteractor.unsubscribe(topic: topic) - } } diff --git a/Sources/Chat/Storage/ChatStorage.swift b/Sources/Chat/Storage/ChatStorage.swift index 2c64446d3..419a2a945 100644 --- a/Sources/Chat/Storage/ChatStorage.swift +++ b/Sources/Chat/Storage/ChatStorage.swift @@ -5,13 +5,16 @@ final class ChatStorage { private var publishers = Set() + private let kms: KeyManagementServiceProtocol private let messageStore: KeyedDatabase<[Message]> private let receivedInviteStore: KeyedDatabase<[ReceivedInvite]> private let sentInviteStore: SyncStore private let threadStore: SyncStore + private let inviteKeyStore: SyncStore private let sentInviteStoreDelegate: SentInviteStoreDelegate private let threadStoreDelegate: ThreadStoreDelegate + private let inviteKeyDelegate: InviteKeyDelegate private var messagesPublisherSubject = PassthroughSubject<[Message], Never>() private var receivedInvitesPublisherSubject = PassthroughSubject<[ReceivedInvite], Never>() @@ -64,19 +67,25 @@ final class ChatStorage { } init( + kms: KeyManagementServiceProtocol, messageStore: KeyedDatabase<[Message]>, receivedInviteStore: KeyedDatabase<[ReceivedInvite]>, sentInviteStore: SyncStore, threadStore: SyncStore, + inviteKeyStore: SyncStore, sentInviteStoreDelegate: SentInviteStoreDelegate, - threadStoreDelegate: ThreadStoreDelegate + threadStoreDelegate: ThreadStoreDelegate, + inviteKeyDelegate: InviteKeyDelegate ) { + self.kms = kms self.messageStore = messageStore self.receivedInviteStore = receivedInviteStore self.sentInviteStore = sentInviteStore self.threadStore = threadStore + self.inviteKeyStore = inviteKeyStore self.sentInviteStoreDelegate = sentInviteStoreDelegate self.threadStoreDelegate = threadStoreDelegate + self.inviteKeyDelegate = inviteKeyDelegate setupSyncSubscriptions() } @@ -84,6 +93,7 @@ final class ChatStorage { func initialize(for account: Account) async throws { try await sentInviteStore.initialize(for: account) try await threadStore.initialize(for: account) + try await inviteKeyStore.initialize(for: account) } func setupSubscriptions(account: Account) throws { @@ -96,6 +106,7 @@ final class ChatStorage { try threadStore.setupSubscriptions(account: account) try sentInviteStore.setupSubscriptions(account: account) + try sentInviteStore.setupSubscriptions(account: account) } // MARK: - Invites @@ -167,6 +178,18 @@ final class ChatStorage { rejectPublisherSubject.send(rejected) } + // MARK: InviteKeys + + func syncInviteKey(_ inviteKey: AgreementPublicKey, account: Account) async throws { + if let privateKey = try kms.getPrivateKey(for: inviteKey) { + let key = InviteKey( + publicKey: inviteKey.hexRepresentation, + privateKey: privateKey.rawRepresentation.toHexString() + ) + try await inviteKeyStore.set(object: key, for: account) + } + } + // MARK: - Threads func getAllThreads() -> [Thread] { @@ -212,8 +235,9 @@ private extension ChatStorage { func setupSyncSubscriptions() { sentInviteStoreDelegate.onInitialization(sentInviteStore.getAll()) threadStoreDelegate.onInitialization(threadStore.getAll()) + inviteKeyDelegate.onInitialization(inviteKeyStore.getAll()) - sentInviteStore.syncUpdatePublisher.sink { [unowned self] topic, update in + sentInviteStore.syncUpdatePublisher.sink { [unowned self] topic, account, update in switch update { case .set(let object): self.sentInviteStoreDelegate.onUpdate(object) @@ -222,7 +246,7 @@ private extension ChatStorage { } }.store(in: &publishers) - threadStore.syncUpdatePublisher.sink { [unowned self] topic, update in + threadStore.syncUpdatePublisher.sink { [unowned self] topic, account, update in switch update { case .set(let object): self.threadStoreDelegate.onUpdate(object) @@ -230,5 +254,14 @@ private extension ChatStorage { self.threadStoreDelegate.onDelete(id) } }.store(in: &publishers) + + inviteKeyStore.syncUpdatePublisher.sink { [unowned self] topic, account, update in + switch update { + case .set(let object): + self.inviteKeyDelegate.onUpdate(object, account: account) + case .delete(let id): + self.inviteKeyDelegate.onDelete(id) + } + }.store(in: &publishers) } } diff --git a/Sources/Chat/Storage/InviteKeyDelegate.swift b/Sources/Chat/Storage/InviteKeyDelegate.swift new file mode 100644 index 000000000..725bf459e --- /dev/null +++ b/Sources/Chat/Storage/InviteKeyDelegate.swift @@ -0,0 +1,58 @@ +import Foundation + +final class InviteKeyDelegate { + + private let networkingInteractor: NetworkInteracting + private let kms: KeyManagementServiceProtocol + private let identityClient: IdentityClient + + init(networkingInteractor: NetworkInteracting, kms: KeyManagementServiceProtocol, identityClient: IdentityClient) { + self.networkingInteractor = networkingInteractor + self.kms = kms + self.identityClient = identityClient + } + + func onInitialization(_ keys: [InviteKey]) { + Task(priority: .high) { + for key in keys { + try syncKms(key: key) + } + + let topics = keys.map { $0.topic } + try await networkingInteractor.batchSubscribe(topics: topics) + } + } + + func onUpdate(_ key: InviteKey, account: Account) { + Task(priority: .high) { + try syncKms(key: key) + try syncIdentityStorage(key: key, account: account) + try await networkingInteractor.subscribe(topic: key.topic) + } + } + + func onDelete(_ id: String) { + Task(priority: .high) { + let inviteKey = try AgreementPublicKey(hex: id) // InviteKey id is pubKey hex + let topic = inviteKey.rawRepresentation.sha256().toHexString() + kms.deletePublicKey(for: topic) + networkingInteractor.unsubscribe(topic: topic) + } + } +} + +private extension InviteKeyDelegate { + + func syncKms(key: InviteKey) throws { + let inviteKey = try AgreementPublicKey(hex: key.publicKey) + let privateKey = try AgreementPrivateKey(hex: key.privateKey) + try kms.setPublicKey(publicKey: inviteKey, for: key.topic) + try kms.setPrivateKey(privateKey) + } + + func syncIdentityStorage(key: InviteKey, account: Account) throws { + let inviteKey = try AgreementPublicKey(hex: key.publicKey) + try identityClient.setInviteKey(inviteKey, account: account) + } +} + diff --git a/Sources/Chat/Types/Plain/InviteKey.swift b/Sources/Chat/Types/Plain/InviteKey.swift new file mode 100644 index 000000000..f5dbffe01 --- /dev/null +++ b/Sources/Chat/Types/Plain/InviteKey.swift @@ -0,0 +1,14 @@ +import Foundation + +struct InviteKey: SyncObject { + let publicKey: String + let privateKey: String + + var topic: String { + return Data(hex: publicKey).sha256().toHexString() + } + + var syncId: String { + return publicKey + } +} diff --git a/Sources/WalletConnectIdentity/IdentityClient.swift b/Sources/WalletConnectIdentity/IdentityClient.swift index bbccb14b5..8ec6d2943 100644 --- a/Sources/WalletConnectIdentity/IdentityClient.swift +++ b/Sources/WalletConnectIdentity/IdentityClient.swift @@ -45,6 +45,14 @@ public final class IdentityClient { return inviteKey } + public func setInviteKey(_ inviteKey: AgreementPublicKey, account: Account) throws { + try identityStorage.saveInviteKey(inviteKey, for: account) + } + + public func deleteInviteKey(account: Account) throws { + try identityStorage.removeInviteKey(for: account) + } + public func resolveInvite(account: Account) async throws -> String { return try await identityService.resolveInvite(account: account) } diff --git a/Sources/WalletConnectIdentity/IdentityStorage.swift b/Sources/WalletConnectIdentity/IdentityStorage.swift index 0e591f097..0988cd045 100644 --- a/Sources/WalletConnectIdentity/IdentityStorage.swift +++ b/Sources/WalletConnectIdentity/IdentityStorage.swift @@ -18,7 +18,7 @@ public final class IdentityStorage { } @discardableResult - func saveInviteKey( + public func saveInviteKey( _ key: AgreementPublicKey, for account: Account ) throws -> AgreementPublicKey { @@ -30,7 +30,7 @@ public final class IdentityStorage { try keychain.delete(key: identityKeyIdentifier(for: account)) } - func removeInviteKey(for account: Account) throws { + public func removeInviteKey(for account: Account) throws { try keychain.delete(key: inviteKeyIdentifier(for: account)) } diff --git a/Sources/WalletConnectSync/Services/SyncService.swift b/Sources/WalletConnectSync/Services/SyncService.swift index 5bb5a64c8..3c7365f8a 100644 --- a/Sources/WalletConnectSync/Services/SyncService.swift +++ b/Sources/WalletConnectSync/Services/SyncService.swift @@ -36,7 +36,7 @@ final class SyncService { let record = try indexStore.getRecord(account: account, name: store) try await networkInteractor.request(request, topic: record.topic, protocolMethod: protocolMethod) - logger.debug("Did set value for \(store). Sent on \(record.topic)") + logger.debug("Did set value for \(store). Sent on \(record.topic). Object: \n\(object)\n") } func delete(account: Account, store: String, key: String) async throws { @@ -45,7 +45,7 @@ final class SyncService { let record = try indexStore.getRecord(account: account, name: store) try await networkInteractor.request(request, topic: record.topic, protocolMethod: protocolMethod) - logger.debug("Did delete value for \(store). Sent on: \(record.topic)") + logger.debug("Did delete value for \(store). Sent on: \(record.topic). Key: \n\(key)\n") } func create(account: Account, store: String) async throws { diff --git a/Sources/WalletConnectSync/Stores/SyncStore.swift b/Sources/WalletConnectSync/Stores/SyncStore.swift index 521e96ff2..fb66471e9 100644 --- a/Sources/WalletConnectSync/Stores/SyncStore.swift +++ b/Sources/WalletConnectSync/Stores/SyncStore.swift @@ -24,13 +24,13 @@ public final class SyncStore { private let objectStore: SyncObjectStore private let dataUpdateSubject = PassthroughSubject<[Object], Never>() - private let syncUpdateSubject = PassthroughSubject<(String, SyncUpdate), Never>() + private let syncUpdateSubject = PassthroughSubject<(String, Account, SyncUpdate), Never>() public var dataUpdatePublisher: AnyPublisher<[Object], Never> { return dataUpdateSubject.eraseToAnyPublisher() } - public var syncUpdatePublisher: AnyPublisher<(String, SyncUpdate), Never> { + public var syncUpdatePublisher: AnyPublisher<(String, Account, SyncUpdate), Never> { return syncUpdateSubject.eraseToAnyPublisher() } @@ -91,11 +91,11 @@ private extension SyncStore { case .set(let value): let decoded = try! value.get(StoreSet.self) if try! setInStore(object: decoded.value, for: record.account) { - syncUpdateSubject.send((topic, .set(object: decoded.value))) + syncUpdateSubject.send((topic, record.account, .set(object: decoded.value))) } case .delete(let key): if try! deleteInStore(id: key, for: record.account) { - syncUpdateSubject.send((topic, .delete(id: key))) + syncUpdateSubject.send((topic, record.account, .delete(id: key))) } } }.store(in: &publishers) diff --git a/Sources/WalletConnectSync/SyncClient.swift b/Sources/WalletConnectSync/SyncClient.swift index 4f8cb3819..e82867e85 100644 --- a/Sources/WalletConnectSync/SyncClient.swift +++ b/Sources/WalletConnectSync/SyncClient.swift @@ -13,8 +13,6 @@ public final class SyncClient { init(syncService: SyncService, syncSignatureStore: SyncSignatureStore) { self.syncService = syncService self.syncSignatureStore = syncSignatureStore - - // TODO: Resubscription service } /// Get message to sign for an account From 7b22e7ad43aba119a11ce38b50ba3f5a521742f3 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Wed, 17 May 2023 01:41:42 +0300 Subject: [PATCH 21/85] Chat+Sync integration tests --- Example/IntegrationTests/Chat/ChatTests.swift | 85 ++++++++++++------- Example/IntegrationTests/Sync/SyncTests.swift | 4 +- Sources/Chat/ChatClient.swift | 9 +- Sources/Chat/ChatClientFactory.swift | 14 +-- Sources/Chat/ChatStorageIdentifiers.swift | 6 +- .../Common/ResubscriptionService.swift | 12 +++ Sources/Chat/Storage/ChatStorage.swift | 27 +++--- Sources/Chat/Storage/InviteKeyDelegate.swift | 14 ++- .../Storage/SentInviteStoreDelegate.swift | 14 ++- .../Chat/Storage/ThreadStoreDelegate.swift | 8 +- .../Services/SyncService.swift | 2 - .../WalletConnectSync/Stores/SyncStore.swift | 16 ++-- .../Stores/SyncStoreFactory.swift | 4 +- .../WalletConnectSync/SyncClientFactory.swift | 4 +- 14 files changed, 124 insertions(+), 95 deletions(-) diff --git a/Example/IntegrationTests/Chat/ChatTests.swift b/Example/IntegrationTests/Chat/ChatTests.swift index 305c3aa70..8d6f0f0c5 100644 --- a/Example/IntegrationTests/Chat/ChatTests.swift +++ b/Example/IntegrationTests/Chat/ChatTests.swift @@ -6,23 +6,43 @@ import WalletConnectUtils @testable import WalletConnectSync import WalletConnectRelay import Combine +import Web3 final class ChatTests: XCTestCase { - var invitee: ChatClient! - var inviter: ChatClient! + var invitee1: ChatClient! + var inviter1: ChatClient! + var invitee2: ChatClient! + var inviter2: ChatClient! private var publishers = [AnyCancellable]() - let inviteeAccount = Account("eip155:1:0x15bca56b6e2728aec2532df9d436bd1600e86688")! - let inviterAccount = Account("eip155:2:0x15bca56b6e2728aec2532df9d436bd1600e86688")! + var inviteeAccount: Account { + return Account("eip155:1:" + pk1.address.hex(eip55: true))! + } - let privateKey = Data(hex: "305c6cde3846927892cd32762f6120539f3ec74c9e3a16b9b798b1e85351ae2a") + var inviterAccount: Account { + return Account("eip155:1:" + pk2.address.hex(eip55: true))! + } - override func setUp() async throws { - invitee = makeClient(prefix: "🦖 Invitee", account: inviteeAccount) - inviter = makeClient(prefix: "🍄 Inviter", account: inviterAccount) + let pk1 = try! EthereumPrivateKey() + let pk2 = try! EthereumPrivateKey() - try await invitee.register(account: inviteeAccount, onSign: sign) - try await inviter.register(account: inviterAccount, onSign: sign) + var privateKey1: Data { + return Data(pk1.rawPrivateKey) + } + var privateKey2: Data { + return Data(pk2.rawPrivateKey) + } + + override func setUp() async throws { + invitee1 = makeClient(prefix: "🦖 Invitee", account: inviteeAccount) + inviter1 = makeClient(prefix: "🍄 Inviter", account: inviterAccount) + + try await invitee1.register(account: inviteeAccount) { message in + return self.sign(message, privateKey: self.privateKey1) + } + try await inviter1.register(account: inviterAccount) { message in + return self.sign(message, privateKey: self.privateKey2) + } } func makeClient(prefix: String, account: Account) -> ChatClient { @@ -46,24 +66,24 @@ final class ChatTests: XCTestCase { let clientId = try! networkingInteractor.getClientId() logger.debug("My client id is: \(clientId)") - return ChatClientFactory.create(keyserverURL: keyserverURL, relayClient: relayClient, networkingInteractor: networkingInteractor, keychain: keychain, logger: logger, keyValueStorage: keyValueStorage, syncClient: syncClient) + return ChatClientFactory.create(keyserverURL: keyserverURL, relayClient: relayClient, networkingInteractor: networkingInteractor, keychain: keychain, logger: logger, storage: keyValueStorage, syncClient: syncClient) } func testInvite() async throws { let inviteExpectation = expectation(description: "invitation expectation") inviteExpectation.expectedFulfillmentCount = 2 - invitee.newReceivedInvitePublisher.sink { _ in + invitee1.newReceivedInvitePublisher.sink { _ in inviteExpectation.fulfill() }.store(in: &publishers) - inviter.newSentInvitePublisher.sink { _ in + inviter1.newSentInvitePublisher.sink { _ in inviteExpectation.fulfill() }.store(in: &publishers) - let inviteePublicKey = try await inviter.resolve(account: inviteeAccount) + let inviteePublicKey = try await inviter1.resolve(account: inviteeAccount) let invite = Invite(message: "", inviterAccount: inviterAccount, inviteeAccount: inviteeAccount, inviteePublicKey: inviteePublicKey) - _ = try await inviter.invite(invite: invite) + _ = try await inviter1.invite(invite: invite) wait(for: [inviteExpectation], timeout: InputConfig.defaultTimeout) } @@ -72,21 +92,21 @@ final class ChatTests: XCTestCase { let newThreadInviterExpectation = expectation(description: "new thread on inviting client expectation") let newThreadinviteeExpectation = expectation(description: "new thread on invitee client expectation") - invitee.newReceivedInvitePublisher.sink { [unowned self] invite in - Task { try! await invitee.accept(inviteId: invite.id) } + invitee1.newReceivedInvitePublisher.sink { [unowned self] invite in + Task { try! await invitee1.accept(inviteId: invite.id) } }.store(in: &publishers) - invitee.newThreadPublisher.sink { _ in + invitee1.newThreadPublisher.sink { _ in newThreadinviteeExpectation.fulfill() }.store(in: &publishers) - inviter.newThreadPublisher.sink { _ in + inviter1.newThreadPublisher.sink { _ in newThreadInviterExpectation.fulfill() }.store(in: &publishers) - let inviteePublicKey = try await inviter.resolve(account: inviteeAccount) + let inviteePublicKey = try await inviter1.resolve(account: inviteeAccount) let invite = Invite(message: "", inviterAccount: inviterAccount, inviteeAccount: inviteeAccount, inviteePublicKey: inviteePublicKey) - try await inviter.invite(invite: invite) + try await inviter1.invite(invite: invite) wait(for: [newThreadinviteeExpectation, newThreadInviterExpectation], timeout: InputConfig.defaultTimeout) } @@ -95,19 +115,19 @@ final class ChatTests: XCTestCase { let messageExpectation = expectation(description: "message received") messageExpectation.expectedFulfillmentCount = 4 - invitee.newReceivedInvitePublisher.sink { [unowned self] invite in - Task { try! await invitee.accept(inviteId: invite.id) } + invitee1.newReceivedInvitePublisher.sink { [unowned self] invite in + Task { try! await invitee1.accept(inviteId: invite.id) } }.store(in: &publishers) - invitee.newThreadPublisher.sink { [unowned self] thread in - Task { try! await invitee.message(topic: thread.topic, message: "message1") } + invitee1.newThreadPublisher.sink { [unowned self] thread in + Task { try! await invitee1.message(topic: thread.topic, message: "message1") } }.store(in: &publishers) - inviter.newThreadPublisher.sink { [unowned self] thread in - Task { try! await inviter.message(topic: thread.topic, message: "message2") } + inviter1.newThreadPublisher.sink { [unowned self] thread in + Task { try! await inviter1.message(topic: thread.topic, message: "message2") } }.store(in: &publishers) - inviter.newMessagePublisher.sink { message in + inviter1.newMessagePublisher.sink { message in if message.authorAccount == self.inviterAccount { XCTAssertEqual(message.message, "message2") } else { @@ -116,7 +136,7 @@ final class ChatTests: XCTestCase { messageExpectation.fulfill() }.store(in: &publishers) - invitee.newMessagePublisher.sink { message in + invitee1.newMessagePublisher.sink { message in if message.authorAccount == self.inviteeAccount { XCTAssertEqual(message.message, "message1") } else { @@ -125,15 +145,14 @@ final class ChatTests: XCTestCase { messageExpectation.fulfill() }.store(in: &publishers) - let inviteePublicKey = try await inviter.resolve(account: inviteeAccount) + let inviteePublicKey = try await inviter1.resolve(account: inviteeAccount) let invite = Invite(message: "", inviterAccount: inviterAccount, inviteeAccount: inviteeAccount, inviteePublicKey: inviteePublicKey) - try await inviter.invite(invite: invite) + try await inviter1.invite(invite: invite) wait(for: [messageExpectation], timeout: InputConfig.defaultTimeout) } - private func sign(_ message: String) -> SigningResult { - let privateKey = Data(hex: "305c6cde3846927892cd32762f6120539f3ec74c9e3a16b9b798b1e85351ae2a") + private func sign(_ message: String, privateKey: Data) -> SigningResult { let signer = MessageSignerFactory(signerFactory: DefaultSignerFactory()).create(projectId: InputConfig.projectId) return .signed(try! signer.sign(message: message, privateKey: privateKey, type: .eip191)) } diff --git a/Example/IntegrationTests/Sync/SyncTests.swift b/Example/IntegrationTests/Sync/SyncTests.swift index 229f1821f..eda8af1d2 100644 --- a/Example/IntegrationTests/Sync/SyncTests.swift +++ b/Example/IntegrationTests/Sync/SyncTests.swift @@ -75,7 +75,7 @@ final class SyncTests: XCTestCase { let object = TestObject(id: "id", value: "value") - syncStore1.syncUpdatePublisher.sink { (_, update) in + syncStore1.syncUpdatePublisher.sink { (_, _, update) in switch update { case .set: XCTFail() @@ -84,7 +84,7 @@ final class SyncTests: XCTestCase { } }.store(in: &publishers) - syncStore2.syncUpdatePublisher.sink { (_, update) in + syncStore2.syncUpdatePublisher.sink { (_, _, update) in switch update { case .set: setExpectation.fulfill() diff --git a/Sources/Chat/ChatClient.swift b/Sources/Chat/ChatClient.swift index 63cbc0df5..a194c6b66 100644 --- a/Sources/Chat/ChatClient.swift +++ b/Sources/Chat/ChatClient.swift @@ -131,7 +131,8 @@ public class ChatClient { /// Stops listening for invites /// - Parameter account: CAIP10 blockachain account public func goPrivate(account: Account) async throws { - _ = try await identityClient.goPrivate(account: account) + let inviteKey = try await identityClient.goPrivate(account: account) + resubscriptionService.unsubscribeFromInvites(inviteKey: inviteKey) } /// Registers an invite key if not yet registered on this client from keyserver @@ -140,8 +141,10 @@ public class ChatClient { /// - Returns: The public invite key public func goPublic(account: Account) async throws { let inviteKey = try await identityClient.goPublic(account: account) - try await chatStorage.initialize(for: account) - try await chatStorage.syncInviteKey(inviteKey, account: account) + try await resubscriptionService.subscribeForInvites(inviteKey: inviteKey) + try await chatStorage.initializeStores(for: account) + try await chatStorage.initializeDelegates() + try await chatStorage.setInviteKey(inviteKey, account: account) } /// Accepts a chat invite by id from account specified as inviteeAccount in Invite diff --git a/Sources/Chat/ChatClientFactory.swift b/Sources/Chat/ChatClientFactory.swift index 797163577..12b9f83de 100644 --- a/Sources/Chat/ChatClientFactory.swift +++ b/Sources/Chat/ChatClientFactory.swift @@ -11,7 +11,7 @@ public struct ChatClientFactory { networkingInteractor: networkingInteractor, keychain: keychain, logger: ConsoleLogger(loggingLevel: .debug), - keyValueStorage: UserDefaults.standard, + storage: UserDefaults.standard, syncClient: syncClient ) } @@ -22,19 +22,19 @@ public struct ChatClientFactory { networkingInteractor: NetworkingInteractor, keychain: KeychainStorageProtocol, logger: ConsoleLogging, - keyValueStorage: KeyValueStorage, + storage: KeyValueStorage, syncClient: SyncClient ) -> ChatClient { let kms = KeyManagementService(keychain: keychain) - let messageStore = KeyedDatabase<[Message]>(storage: keyValueStorage, identifier: ChatStorageIdentifiers.messages.rawValue) - let receivedInviteStore = KeyedDatabase<[ReceivedInvite]>(storage: keyValueStorage, identifier: ChatStorageIdentifiers.receivedInvites.rawValue) - let threadStore: SyncStore = SyncStoreFactory.create(name: ChatStorageIdentifiers.thread.rawValue, syncClient: syncClient) + let messageStore = KeyedDatabase<[Message]>(storage: storage, identifier: ChatStorageIdentifiers.messages.rawValue) + let receivedInviteStore = KeyedDatabase<[ReceivedInvite]>(storage: storage, identifier: ChatStorageIdentifiers.receivedInvites.rawValue) + let threadStore: SyncStore = SyncStoreFactory.create(name: ChatStorageIdentifiers.thread.rawValue, syncClient: syncClient, storage: storage) let identityClient = IdentityClientFactory.create(keyserver: keyserverURL, keychain: keychain, logger: logger) let inviteKeyDelegate = InviteKeyDelegate(networkingInteractor: networkingInteractor, kms: kms, identityClient: identityClient) let sentInviteDelegate = SentInviteStoreDelegate(networkingInteractor: networkingInteractor, kms: kms) let threadDelegate = ThreadStoreDelegate(networkingInteractor: networkingInteractor, kms: kms) - let sentInviteStore: SyncStore = SyncStoreFactory.create(name: ChatStorageIdentifiers.sentInvite.rawValue, syncClient: syncClient) - let inviteKeyStore: SyncStore = SyncStoreFactory.create(name: ChatStorageIdentifiers.inviteKey.rawValue, syncClient: syncClient) + let sentInviteStore: SyncStore = SyncStoreFactory.create(name: ChatStorageIdentifiers.sentInvite.rawValue, syncClient: syncClient, storage: storage) + let inviteKeyStore: SyncStore = SyncStoreFactory.create(name: ChatStorageIdentifiers.inviteKey.rawValue, syncClient: syncClient, storage: storage) let chatStorage = ChatStorage(kms: kms, messageStore: messageStore, receivedInviteStore: receivedInviteStore, sentInviteStore: sentInviteStore, threadStore: threadStore, inviteKeyStore: inviteKeyStore, sentInviteStoreDelegate: sentInviteDelegate, threadStoreDelegate: threadDelegate, inviteKeyDelegate: inviteKeyDelegate) let resubscriptionService = ResubscriptionService(networkingInteractor: networkingInteractor, kms: kms, chatStorage: chatStorage, logger: logger) let invitationHandlingService = InvitationHandlingService(keyserverURL: keyserverURL, networkingInteractor: networkingInteractor, identityClient: identityClient, kms: kms, logger: logger, chatStorage: chatStorage) diff --git a/Sources/Chat/ChatStorageIdentifiers.swift b/Sources/Chat/ChatStorageIdentifiers.swift index 568e3718e..2b65abf84 100644 --- a/Sources/Chat/ChatStorageIdentifiers.swift +++ b/Sources/Chat/ChatStorageIdentifiers.swift @@ -4,7 +4,7 @@ enum ChatStorageIdentifiers: String { case topicToInvitationPubKey = "com.walletconnect.chat.topicToInvitationPubKey" case messages = "com.walletconnect.chat.messages" case receivedInvites = "com.walletconnect.chat.receivedInvites" - case thread = "com.walletconnect.web3inbox.chatThreads" - case sentInvite = "com.walletconnect.web3inbox.chatSentInvites" - case inviteKey = "com.walletconnect.web3inbox.chatInviteKeys" + case thread = "com.walletconnect.chat.chatThreads" + case sentInvite = "com.walletconnect.chat.chatSentInvites" + case inviteKey = "com.walletconnect.chat.chatInviteKeys" } diff --git a/Sources/Chat/ProtocolServices/Common/ResubscriptionService.swift b/Sources/Chat/ProtocolServices/Common/ResubscriptionService.swift index 1ebeb1e31..93d6c865e 100644 --- a/Sources/Chat/ProtocolServices/Common/ResubscriptionService.swift +++ b/Sources/Chat/ProtocolServices/Common/ResubscriptionService.swift @@ -19,4 +19,16 @@ class ResubscriptionService { self.logger = logger self.chatStorage = chatStorage } + + func subscribeForInvites(inviteKey: AgreementPublicKey) async throws { + let topic = inviteKey.rawRepresentation.sha256().toHexString() + try kms.setPublicKey(publicKey: inviteKey, for: topic) + try await networkingInteractor.subscribe(topic: topic) + } + + func unsubscribeFromInvites(inviteKey: AgreementPublicKey) { + let topic = inviteKey.rawRepresentation.sha256().toHexString() + kms.deletePublicKey(for: topic) + networkingInteractor.unsubscribe(topic: topic) + } } diff --git a/Sources/Chat/Storage/ChatStorage.swift b/Sources/Chat/Storage/ChatStorage.swift index 419a2a945..517259843 100644 --- a/Sources/Chat/Storage/ChatStorage.swift +++ b/Sources/Chat/Storage/ChatStorage.swift @@ -90,12 +90,20 @@ final class ChatStorage { setupSyncSubscriptions() } - func initialize(for account: Account) async throws { + // MARK: - Configuration + + func initializeStores(for account: Account) async throws { try await sentInviteStore.initialize(for: account) try await threadStore.initialize(for: account) try await inviteKeyStore.initialize(for: account) } + func initializeDelegates() async throws { + try await sentInviteStoreDelegate.onInitialization(sentInviteStore.getAll()) + try await threadStoreDelegate.onInitialization(threadStore.getAll()) + try await inviteKeyDelegate.onInitialization(inviteKeyStore.getAll()) + } + func setupSubscriptions(account: Account) throws { messageStore.onUpdate = { [unowned self] in messagesPublisherSubject.send(getMessages(account: account)) @@ -104,9 +112,9 @@ final class ChatStorage { receivedInvitesPublisherSubject.send(getReceivedInvites(account: account)) } - try threadStore.setupSubscriptions(account: account) - try sentInviteStore.setupSubscriptions(account: account) try sentInviteStore.setupSubscriptions(account: account) + try threadStore.setupSubscriptions(account: account) + try inviteKeyStore.setupSubscriptions(account: account) } // MARK: - Invites @@ -180,12 +188,11 @@ final class ChatStorage { // MARK: InviteKeys - func syncInviteKey(_ inviteKey: AgreementPublicKey, account: Account) async throws { + func setInviteKey(_ inviteKey: AgreementPublicKey, account: Account) async throws { if let privateKey = try kms.getPrivateKey(for: inviteKey) { - let key = InviteKey( - publicKey: inviteKey.hexRepresentation, - privateKey: privateKey.rawRepresentation.toHexString() - ) + let pubKeyHex = inviteKey.hexRepresentation + let privKeyHex = privateKey.rawRepresentation.toHexString() + let key = InviteKey(publicKey: pubKeyHex, privateKey: privKeyHex) try await inviteKeyStore.set(object: key, for: account) } } @@ -233,10 +240,6 @@ final class ChatStorage { private extension ChatStorage { func setupSyncSubscriptions() { - sentInviteStoreDelegate.onInitialization(sentInviteStore.getAll()) - threadStoreDelegate.onInitialization(threadStore.getAll()) - inviteKeyDelegate.onInitialization(inviteKeyStore.getAll()) - sentInviteStore.syncUpdatePublisher.sink { [unowned self] topic, account, update in switch update { case .set(let object): diff --git a/Sources/Chat/Storage/InviteKeyDelegate.swift b/Sources/Chat/Storage/InviteKeyDelegate.swift index 725bf459e..a2337df6f 100644 --- a/Sources/Chat/Storage/InviteKeyDelegate.swift +++ b/Sources/Chat/Storage/InviteKeyDelegate.swift @@ -12,15 +12,13 @@ final class InviteKeyDelegate { self.identityClient = identityClient } - func onInitialization(_ keys: [InviteKey]) { - Task(priority: .high) { - for key in keys { - try syncKms(key: key) - } - - let topics = keys.map { $0.topic } - try await networkingInteractor.batchSubscribe(topics: topics) + func onInitialization(_ keys: [InviteKey]) async throws { + for key in keys { + try syncKms(key: key) } + + let topics = keys.map { $0.topic } + try await networkingInteractor.batchSubscribe(topics: topics) } func onUpdate(_ key: InviteKey, account: Account) { diff --git a/Sources/Chat/Storage/SentInviteStoreDelegate.swift b/Sources/Chat/Storage/SentInviteStoreDelegate.swift index 081e75135..c48aadf76 100644 --- a/Sources/Chat/Storage/SentInviteStoreDelegate.swift +++ b/Sources/Chat/Storage/SentInviteStoreDelegate.swift @@ -10,15 +10,13 @@ final class SentInviteStoreDelegate { self.kms = kms } - func onInitialization(_ objects: [SentInvite]) { - Task(priority: .high) { - for invite in objects { - try syncKeychain(invite: invite) - } - - let topics = objects.map { $0.responseTopic } - try await networkingInteractor.batchSubscribe(topics: topics) + func onInitialization(_ objects: [SentInvite]) async throws { + for invite in objects { + try syncKeychain(invite: invite) } + + let topics = objects.map { $0.responseTopic } + try await networkingInteractor.batchSubscribe(topics: topics) } func onUpdate(_ object: SentInvite) { diff --git a/Sources/Chat/Storage/ThreadStoreDelegate.swift b/Sources/Chat/Storage/ThreadStoreDelegate.swift index 3ec14b0eb..1d10e00c9 100644 --- a/Sources/Chat/Storage/ThreadStoreDelegate.swift +++ b/Sources/Chat/Storage/ThreadStoreDelegate.swift @@ -10,11 +10,9 @@ final class ThreadStoreDelegate { self.kms = kms } - func onInitialization(_ threads: [Thread]) { - Task(priority: .high) { - let topics = threads.map { $0.topic } - try await networkingInteractor.batchSubscribe(topics: topics) - } + func onInitialization(_ threads: [Thread]) async throws { + let topics = threads.map { $0.topic } + try await networkingInteractor.batchSubscribe(topics: topics) } func onUpdate(_ thread: Thread) { diff --git a/Sources/WalletConnectSync/Services/SyncService.swift b/Sources/WalletConnectSync/Services/SyncService.swift index 3c7365f8a..1f1e71dca 100644 --- a/Sources/WalletConnectSync/Services/SyncService.swift +++ b/Sources/WalletConnectSync/Services/SyncService.swift @@ -51,8 +51,6 @@ final class SyncService { func create(account: Account, store: String) async throws { let topic = try getTopic(for: account, store: store) try await networkInteractor.subscribe(topic: topic) - - logger.debug("Store \(store) created. Subscribed on: \(topic)") } } diff --git a/Sources/WalletConnectSync/Stores/SyncStore.swift b/Sources/WalletConnectSync/Stores/SyncStore.swift index fb66471e9..378729c5a 100644 --- a/Sources/WalletConnectSync/Stores/SyncStore.swift +++ b/Sources/WalletConnectSync/Stores/SyncStore.swift @@ -47,6 +47,14 @@ public final class SyncStore { try await syncClient.create(account: account, store: name) } + public func setupSubscriptions(account: Account) throws { + let record = try indexStore.getRecord(account: account, name: name) + + objectStore.onUpdate = { [unowned self] in + dataUpdateSubject.send(objectStore.getAll(topic: record.topic)) + } + } + public func getAll(for account: Account) throws -> [Object] { let record = try indexStore.getRecord(account: account, name: name) return objectStore.getAll(topic: record.topic) @@ -68,14 +76,6 @@ public final class SyncStore { try await syncClient.delete(account: account, store: record.store, key: id) objectStore.delete(id: id, topic: record.topic) } - - public func setupSubscriptions(account: Account) throws { - let record = try indexStore.getRecord(account: account, name: name) - - objectStore.onUpdate = { [unowned self] in - dataUpdateSubject.send(objectStore.getAll(topic: record.topic)) - } - } } private extension SyncStore { diff --git a/Sources/WalletConnectSync/Stores/SyncStoreFactory.swift b/Sources/WalletConnectSync/Stores/SyncStoreFactory.swift index 17c680413..4e0ef9fe9 100644 --- a/Sources/WalletConnectSync/Stores/SyncStoreFactory.swift +++ b/Sources/WalletConnectSync/Stores/SyncStoreFactory.swift @@ -2,11 +2,11 @@ import Foundation public final class SyncStoreFactory { - public static func create(name: String, syncClient: SyncClient) -> SyncStore { + public static func create(name: String, syncClient: SyncClient, storage: KeyValueStorage) -> SyncStore { let indexDatabase = CodableStore(defaults: UserDefaults.standard, identifier: SyncStorageIdentifiers.index.identifier) let indexStore = SyncIndexStore(store: indexDatabase) let objectIdentifier = SyncStorageIdentifiers.object(store: name).identifier - let objectDatabase = NewKeyedDatabase<[String: Object]>(storage: UserDefaults.standard, identifier: objectIdentifier) + let objectDatabase = NewKeyedDatabase<[String: Object]>(storage: storage, identifier: objectIdentifier) let objectStore = SyncObjectStore(store: objectDatabase) return SyncStore(name: name, syncClient: syncClient, indexStore: indexStore, objectStore: objectStore) } diff --git a/Sources/WalletConnectSync/SyncClientFactory.swift b/Sources/WalletConnectSync/SyncClientFactory.swift index 6be21ccb1..e492da049 100644 --- a/Sources/WalletConnectSync/SyncClientFactory.swift +++ b/Sources/WalletConnectSync/SyncClientFactory.swift @@ -2,12 +2,12 @@ import Foundation final class SyncClientFactory { - static func create(networkInteractor: NetworkingInteractor, crypto: CryptoProvider) -> SyncClient { + static func create(networkInteractor: NetworkInteracting, crypto: CryptoProvider) -> SyncClient { let keychain = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk") return create(networkInteractor: networkInteractor, crypto: crypto, keychain: keychain) } - static func create(networkInteractor: NetworkingInteractor, crypto: CryptoProvider, keychain: KeychainStorageProtocol) -> SyncClient { + static func create(networkInteractor: NetworkInteracting, crypto: CryptoProvider, keychain: KeychainStorageProtocol) -> SyncClient { let signatureStore = SyncSignatureStore(keychain: keychain) let kms = KeyManagementService(keychain: keychain) let deriviationService = SyncDerivationService( From 2cb38b41fc65b4194ca6eeb630f61f1bdde0c93a Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Thu, 18 May 2023 00:49:58 +0300 Subject: [PATCH 22/85] Cacao formatter fixed --- Sources/WalletConnectIdentity/IdentityService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/WalletConnectIdentity/IdentityService.swift b/Sources/WalletConnectIdentity/IdentityService.swift index 7d9a7e2c7..8a1a984a8 100644 --- a/Sources/WalletConnectIdentity/IdentityService.swift +++ b/Sources/WalletConnectIdentity/IdentityService.swift @@ -99,7 +99,7 @@ private extension IdentityService { version: getVersion(), nonce: getNonce(), iat: iatProvader.iat, - nbf: nil, exp: nil, statement: nil, requestId: nil, + nbf: nil, exp: nil, statement: "statement", requestId: nil, resources: [DIDKey] ) From 873426f470355dfb66f52863ec4f359c95d00473 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Thu, 18 May 2023 01:41:43 +0300 Subject: [PATCH 23/85] Updating received invites --- Sources/Chat/ChatClient.swift | 1 + .../ProtocolServices/Common/MessagingService.swift | 2 ++ Sources/Chat/Storage/ChatStorage.swift | 11 ++++++++++- Sources/Chat/Storage/ThreadStoreDelegate.swift | 6 +++++- 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/Sources/Chat/ChatClient.swift b/Sources/Chat/ChatClient.swift index a194c6b66..a0862fcc2 100644 --- a/Sources/Chat/ChatClient.swift +++ b/Sources/Chat/ChatClient.swift @@ -133,6 +133,7 @@ public class ChatClient { public func goPrivate(account: Account) async throws { let inviteKey = try await identityClient.goPrivate(account: account) resubscriptionService.unsubscribeFromInvites(inviteKey: inviteKey) + try await chatStorage.removeInviteKey(inviteKey, account: account) } /// Registers an invite key if not yet registered on this client from keyserver diff --git a/Sources/Chat/ProtocolServices/Common/MessagingService.swift b/Sources/Chat/ProtocolServices/Common/MessagingService.swift index 2e330ec05..f032b3b97 100644 --- a/Sources/Chat/ProtocolServices/Common/MessagingService.swift +++ b/Sources/Chat/ProtocolServices/Common/MessagingService.swift @@ -82,6 +82,8 @@ private extension MessagingService { guard let (message, messageClaims) = try? MessagePayload.decodeAndVerify(from: payload.request) else { fatalError() /* TODO: Handle error */ } + // TODO: Compare message hash + Task(priority: .high) { let authorAccount = try await identityClient.resolveIdentity(iss: messageClaims.iss) diff --git a/Sources/Chat/Storage/ChatStorage.swift b/Sources/Chat/Storage/ChatStorage.swift index 517259843..5fc8e20fd 100644 --- a/Sources/Chat/Storage/ChatStorage.swift +++ b/Sources/Chat/Storage/ChatStorage.swift @@ -143,6 +143,11 @@ final class ChatStorage { return receivedInviteStore.getElements(for: account.absoluteString) ?? [] } + func getReceivedInvites(thread: Thread) -> [ReceivedInvite] { + return getReceivedInvites(account: thread.selfAccount) + .filter { $0.inviterAccount == thread.peerAccount } + } + func getSentInvites(account: Account) -> [SentInvite] { do { return try sentInviteStore.getAll(for: account) @@ -197,6 +202,10 @@ final class ChatStorage { } } + func removeInviteKey(_ inviteKey: AgreementPublicKey, account: Account) async throws { + try await inviteKeyStore.delete(id: inviteKey.hexRepresentation, for: account) + } + // MARK: - Threads func getAllThreads() -> [Thread] { @@ -252,7 +261,7 @@ private extension ChatStorage { threadStore.syncUpdatePublisher.sink { [unowned self] topic, account, update in switch update { case .set(let object): - self.threadStoreDelegate.onUpdate(object) + self.threadStoreDelegate.onUpdate(object, storage: self) case .delete(let id): self.threadStoreDelegate.onDelete(id) } diff --git a/Sources/Chat/Storage/ThreadStoreDelegate.swift b/Sources/Chat/Storage/ThreadStoreDelegate.swift index 1d10e00c9..d03a2b672 100644 --- a/Sources/Chat/Storage/ThreadStoreDelegate.swift +++ b/Sources/Chat/Storage/ThreadStoreDelegate.swift @@ -15,8 +15,12 @@ final class ThreadStoreDelegate { try await networkingInteractor.batchSubscribe(topics: topics) } - func onUpdate(_ thread: Thread) { + func onUpdate(_ thread: Thread, storage: ChatStorage) { Task(priority: .high) { + for receivedInvite in storage.getReceivedInvites(thread: thread) { + storage.accept(receivedInvite: receivedInvite, account: thread.selfAccount) + } + let symmetricKey = try SymmetricKey(hex: thread.symKey) try kms.setSymmetricKey(symmetricKey, for: thread.topic) try await networkingInteractor.subscribe(topic: thread.topic) From fa41ea62d557743e23a996239bca2b9e8c48f08e Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Thu, 18 May 2023 03:33:43 +0300 Subject: [PATCH 24/85] Invite resolving fix --- .../ProtocolServices/Invitee/InvitationHandlingService.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift b/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift index 2e3ea3aa1..4411eaee0 100644 --- a/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift +++ b/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift @@ -32,7 +32,8 @@ class InvitationHandlingService { guard let invite = chatStorage.getReceivedInvite(id: inviteId) else { throw Errors.inviteForIdNotFound } - let inviteePublicKey = try identityClient.getInviteKey(for: invite.inviteeAccount) + let inviteePublicKeyHex = try DIDKey(did: invite.inviteePublicKey).hexString + let inviteePublicKey = try AgreementPublicKey(hex: inviteePublicKeyHex) let inviterPublicKey = try DIDKey(did: invite.inviterPublicKey).hexString let symmetricKey = try kms.performKeyAgreement(selfPublicKey: inviteePublicKey, peerPublicKey: inviterPublicKey) @@ -116,7 +117,7 @@ private extension InvitationHandlingService { Task(priority: .high) { let inviterAccount = try await identityClient.resolveIdentity(iss: claims.iss) // TODO: Should we cache it? - let inviteePublicKey = try await identityClient.resolveInvite(account: inviterAccount) + let inviteePublicKey = try await identityClient.resolveInvite(account: invite.inviteeAccount) let inviterPublicKey = invite.inviterPublicKey.did(variant: .X25519) let invite = ReceivedInvite( From de87a490802455620adcd736a5a98577f99c8bae Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Fri, 19 May 2023 01:18:06 +0300 Subject: [PATCH 25/85] SyncDecoding fix --- Sources/Chat/ChatStorageIdentifiers.swift | 7 ++++--- Sources/Chat/Storage/ChatStorage.swift | 2 +- Sources/Chat/Types/Plain/InviteKey.swift | 3 ++- .../WalletConnectSync/Services/SyncService.swift | 6 +++--- Sources/WalletConnectSync/Stores/SyncStore.swift | 14 +++++++------- Sources/WalletConnectSync/Types/StoreUpdate.swift | 8 ++++---- 6 files changed, 21 insertions(+), 19 deletions(-) diff --git a/Sources/Chat/ChatStorageIdentifiers.swift b/Sources/Chat/ChatStorageIdentifiers.swift index 2b65abf84..3b58e2407 100644 --- a/Sources/Chat/ChatStorageIdentifiers.swift +++ b/Sources/Chat/ChatStorageIdentifiers.swift @@ -4,7 +4,8 @@ enum ChatStorageIdentifiers: String { case topicToInvitationPubKey = "com.walletconnect.chat.topicToInvitationPubKey" case messages = "com.walletconnect.chat.messages" case receivedInvites = "com.walletconnect.chat.receivedInvites" - case thread = "com.walletconnect.chat.chatThreads" - case sentInvite = "com.walletconnect.chat.chatSentInvites" - case inviteKey = "com.walletconnect.chat.chatInviteKeys" + + case thread = "com.walletconnect.chat.threads" + case sentInvite = "com.walletconnect.chat.sentInvites" + case inviteKey = "com.walletconnect.chat.inviteKeys" } diff --git a/Sources/Chat/Storage/ChatStorage.swift b/Sources/Chat/Storage/ChatStorage.swift index 5fc8e20fd..8c5030bc7 100644 --- a/Sources/Chat/Storage/ChatStorage.swift +++ b/Sources/Chat/Storage/ChatStorage.swift @@ -197,7 +197,7 @@ final class ChatStorage { if let privateKey = try kms.getPrivateKey(for: inviteKey) { let pubKeyHex = inviteKey.hexRepresentation let privKeyHex = privateKey.rawRepresentation.toHexString() - let key = InviteKey(publicKey: pubKeyHex, privateKey: privKeyHex) + let key = InviteKey(publicKey: pubKeyHex, privateKey: privKeyHex, account: account) try await inviteKeyStore.set(object: key, for: account) } } diff --git a/Sources/Chat/Types/Plain/InviteKey.swift b/Sources/Chat/Types/Plain/InviteKey.swift index f5dbffe01..1c5f3ddf8 100644 --- a/Sources/Chat/Types/Plain/InviteKey.swift +++ b/Sources/Chat/Types/Plain/InviteKey.swift @@ -3,12 +3,13 @@ import Foundation struct InviteKey: SyncObject { let publicKey: String let privateKey: String + let account: Account var topic: String { return Data(hex: publicKey).sha256().toHexString() } var syncId: String { - return publicKey + return account.absoluteString } } diff --git a/Sources/WalletConnectSync/Services/SyncService.swift b/Sources/WalletConnectSync/Services/SyncService.swift index 1f1e71dca..d77f1c977 100644 --- a/Sources/WalletConnectSync/Services/SyncService.swift +++ b/Sources/WalletConnectSync/Services/SyncService.swift @@ -31,7 +31,7 @@ final class SyncService { func set(account: Account, store: String, object: Object) async throws { let protocolMethod = SyncSetMethod() - let params = StoreSet(key: object.syncId, value: object) + let params = StoreSet(key: object.syncId, value: try object.json()) let request = RPCRequest(method: protocolMethod.method, params: params) let record = try indexStore.getRecord(account: account, name: store) try await networkInteractor.request(request, topic: record.topic, protocolMethod: protocolMethod) @@ -62,14 +62,14 @@ private extension SyncService { func setupSubscriptions() { networkInteractor.requestSubscription(on: SyncSetMethod()) - .sink { [unowned self] (payload: RequestSubscriptionPayload) in + .sink { [unowned self] (payload: RequestSubscriptionPayload) in self.updateSubject.send((payload.topic, .set(payload.request))) } .store(in: &publishers) networkInteractor.requestSubscription(on: SyncDeleteMethod()) .sink { [unowned self] (payload: RequestSubscriptionPayload) in - self.updateSubject.send((payload.topic, .delete(payload.request.key))) + self.updateSubject.send((payload.topic, .delete(payload.request))) } .store(in: &publishers) } diff --git a/Sources/WalletConnectSync/Stores/SyncStore.swift b/Sources/WalletConnectSync/Stores/SyncStore.swift index 378729c5a..413d90af7 100644 --- a/Sources/WalletConnectSync/Stores/SyncStore.swift +++ b/Sources/WalletConnectSync/Stores/SyncStore.swift @@ -88,14 +88,14 @@ private extension SyncStore { guard record.store == name else { return } switch update { - case .set(let value): - let decoded = try! value.get(StoreSet.self) - if try! setInStore(object: decoded.value, for: record.account) { - syncUpdateSubject.send((topic, record.account, .set(object: decoded.value))) + case .set(let set): + let object = try! JSONDecoder().decode(Object.self, from: Data(set.value.utf8)) + if try! setInStore(object: object, for: record.account) { + syncUpdateSubject.send((topic, record.account, .set(object: object))) } - case .delete(let key): - if try! deleteInStore(id: key, for: record.account) { - syncUpdateSubject.send((topic, record.account, .delete(id: key))) + case .delete(let delete): + if try! deleteInStore(id: delete.key, for: record.account) { + syncUpdateSubject.send((topic, record.account, .delete(id: delete.key))) } } }.store(in: &publishers) diff --git a/Sources/WalletConnectSync/Types/StoreUpdate.swift b/Sources/WalletConnectSync/Types/StoreUpdate.swift index 6c1f1b942..d0cf82ff0 100644 --- a/Sources/WalletConnectSync/Types/StoreUpdate.swift +++ b/Sources/WalletConnectSync/Types/StoreUpdate.swift @@ -1,13 +1,13 @@ import Foundation public enum StoreUpdate { - case set(AnyCodable) - case delete(String) + case set(StoreSet) + case delete(StoreDelete) } -public struct StoreSet: Codable, Equatable { +public struct StoreSet: Codable, Equatable { public let key: String - public let value: Object + public let value: String } public struct StoreDelete: Codable, Equatable { From cfcb57a690042cc0fda4c04bd88c1a988a0734d7 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Mon, 22 May 2023 14:53:00 +0300 Subject: [PATCH 26/85] ReceivedInviteStatus sync implemented --- Sources/Chat/ChatClientFactory.swift | 4 +++- Sources/Chat/ChatStorageIdentifiers.swift | 1 + .../Invitee/InvitationHandlingService.swift | 2 ++ Sources/Chat/Storage/ChatStorage.swift | 24 ++++++++++++++++++- .../ReceiviedInviteStatusDelegate.swift | 20 ++++++++++++++++ .../Types/Plain/ReceivedInviteStatus.swift | 10 ++++++++ 6 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 Sources/Chat/Storage/ReceiviedInviteStatusDelegate.swift create mode 100644 Sources/Chat/Types/Plain/ReceivedInviteStatus.swift diff --git a/Sources/Chat/ChatClientFactory.swift b/Sources/Chat/ChatClientFactory.swift index 12b9f83de..aab53a21c 100644 --- a/Sources/Chat/ChatClientFactory.swift +++ b/Sources/Chat/ChatClientFactory.swift @@ -35,7 +35,9 @@ public struct ChatClientFactory { let threadDelegate = ThreadStoreDelegate(networkingInteractor: networkingInteractor, kms: kms) let sentInviteStore: SyncStore = SyncStoreFactory.create(name: ChatStorageIdentifiers.sentInvite.rawValue, syncClient: syncClient, storage: storage) let inviteKeyStore: SyncStore = SyncStoreFactory.create(name: ChatStorageIdentifiers.inviteKey.rawValue, syncClient: syncClient, storage: storage) - let chatStorage = ChatStorage(kms: kms, messageStore: messageStore, receivedInviteStore: receivedInviteStore, sentInviteStore: sentInviteStore, threadStore: threadStore, inviteKeyStore: inviteKeyStore, sentInviteStoreDelegate: sentInviteDelegate, threadStoreDelegate: threadDelegate, inviteKeyDelegate: inviteKeyDelegate) + let receivedInviteStatusStore: SyncStore = SyncStoreFactory.create(name: ChatStorageIdentifiers.receivedInviteStatus.rawValue, syncClient: syncClient, storage: storage) + let receivedInviteStatusDelegate = ReceiviedInviteStatusDelegate() + let chatStorage = ChatStorage(kms: kms, messageStore: messageStore, receivedInviteStore: receivedInviteStore, sentInviteStore: sentInviteStore, threadStore: threadStore, inviteKeyStore: inviteKeyStore, receivedInviteStatusStore: receivedInviteStatusStore, sentInviteStoreDelegate: sentInviteDelegate, threadStoreDelegate: threadDelegate, inviteKeyDelegate: inviteKeyDelegate, receiviedInviteStatusDelegate: receivedInviteStatusDelegate) let resubscriptionService = ResubscriptionService(networkingInteractor: networkingInteractor, kms: kms, chatStorage: chatStorage, logger: logger) let invitationHandlingService = InvitationHandlingService(keyserverURL: keyserverURL, networkingInteractor: networkingInteractor, identityClient: identityClient, kms: kms, logger: logger, chatStorage: chatStorage) let inviteService = InviteService(keyserverURL: keyserverURL, networkingInteractor: networkingInteractor, identityClient: identityClient, kms: kms, chatStorage: chatStorage, logger: logger) diff --git a/Sources/Chat/ChatStorageIdentifiers.swift b/Sources/Chat/ChatStorageIdentifiers.swift index 3b58e2407..f20d9f2e1 100644 --- a/Sources/Chat/ChatStorageIdentifiers.swift +++ b/Sources/Chat/ChatStorageIdentifiers.swift @@ -8,4 +8,5 @@ enum ChatStorageIdentifiers: String { case thread = "com.walletconnect.chat.threads" case sentInvite = "com.walletconnect.chat.sentInvites" case inviteKey = "com.walletconnect.chat.inviteKeys" + case receivedInviteStatus = "com.walletconnect.chat.receivedInviteStatuses" } diff --git a/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift b/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift index 4411eaee0..6226f8704 100644 --- a/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift +++ b/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift @@ -97,6 +97,8 @@ class InvitationHandlingService { ) chatStorage.reject(receivedInvite: invite, account: invite.inviteeAccount) + + try await chatStorage.syncRejectedReceivedInviteStatus(id: inviteId, account: invite.inviteeAccount) } } diff --git a/Sources/Chat/Storage/ChatStorage.swift b/Sources/Chat/Storage/ChatStorage.swift index 8c5030bc7..b90a5f0be 100644 --- a/Sources/Chat/Storage/ChatStorage.swift +++ b/Sources/Chat/Storage/ChatStorage.swift @@ -11,10 +11,12 @@ final class ChatStorage { private let sentInviteStore: SyncStore private let threadStore: SyncStore private let inviteKeyStore: SyncStore + private let receivedInviteStatusStore: SyncStore private let sentInviteStoreDelegate: SentInviteStoreDelegate private let threadStoreDelegate: ThreadStoreDelegate private let inviteKeyDelegate: InviteKeyDelegate + private let receiviedInviteStatusDelegate: ReceiviedInviteStatusDelegate private var messagesPublisherSubject = PassthroughSubject<[Message], Never>() private var receivedInvitesPublisherSubject = PassthroughSubject<[ReceivedInvite], Never>() @@ -73,9 +75,11 @@ final class ChatStorage { sentInviteStore: SyncStore, threadStore: SyncStore, inviteKeyStore: SyncStore, + receivedInviteStatusStore: SyncStore, sentInviteStoreDelegate: SentInviteStoreDelegate, threadStoreDelegate: ThreadStoreDelegate, - inviteKeyDelegate: InviteKeyDelegate + inviteKeyDelegate: InviteKeyDelegate, + receiviedInviteStatusDelegate: ReceiviedInviteStatusDelegate ) { self.kms = kms self.messageStore = messageStore @@ -83,9 +87,11 @@ final class ChatStorage { self.sentInviteStore = sentInviteStore self.threadStore = threadStore self.inviteKeyStore = inviteKeyStore + self.receivedInviteStatusStore = receivedInviteStatusStore self.sentInviteStoreDelegate = sentInviteStoreDelegate self.threadStoreDelegate = threadStoreDelegate self.inviteKeyDelegate = inviteKeyDelegate + self.receiviedInviteStatusDelegate = receiviedInviteStatusDelegate setupSyncSubscriptions() } @@ -96,12 +102,14 @@ final class ChatStorage { try await sentInviteStore.initialize(for: account) try await threadStore.initialize(for: account) try await inviteKeyStore.initialize(for: account) + try await receivedInviteStatusStore.initialize(for: account) } func initializeDelegates() async throws { try await sentInviteStoreDelegate.onInitialization(sentInviteStore.getAll()) try await threadStoreDelegate.onInitialization(threadStore.getAll()) try await inviteKeyDelegate.onInitialization(inviteKeyStore.getAll()) + try await receiviedInviteStatusDelegate.onInitialization() } func setupSubscriptions(account: Account) throws { @@ -143,6 +151,11 @@ final class ChatStorage { return receivedInviteStore.getElements(for: account.absoluteString) ?? [] } + func syncRejectedReceivedInviteStatus(id: Int64, account: Account) async throws { + let status = ReceivedInviteStatus(id: id, status: .rejected) + try await receivedInviteStatusStore.set(object: status, for: account) + } + func getReceivedInvites(thread: Thread) -> [ReceivedInvite] { return getReceivedInvites(account: thread.selfAccount) .filter { $0.inviterAccount == thread.peerAccount } @@ -275,5 +288,14 @@ private extension ChatStorage { self.inviteKeyDelegate.onDelete(id) } }.store(in: &publishers) + + receivedInviteStatusStore.syncUpdatePublisher.sink { [unowned self] topic, account, update in + switch update { + case .set(let object): + self.receiviedInviteStatusDelegate.onUpdate(object, storage: self, account: account) + case .delete(let id): + self.receiviedInviteStatusDelegate.onDelete(id) + } + }.store(in: &publishers) } } diff --git a/Sources/Chat/Storage/ReceiviedInviteStatusDelegate.swift b/Sources/Chat/Storage/ReceiviedInviteStatusDelegate.swift new file mode 100644 index 000000000..1d5fe63b9 --- /dev/null +++ b/Sources/Chat/Storage/ReceiviedInviteStatusDelegate.swift @@ -0,0 +1,20 @@ +import Foundation + +final class ReceiviedInviteStatusDelegate { + + func onInitialization() async throws { + + } + + func onUpdate(_ status: ReceivedInviteStatus, storage: ChatStorage, account: Account) { + guard status.status == .rejected else { return } + + if let receivedInvite = storage.getReceivedInvite(id: status.id) { + storage.reject(receivedInvite: receivedInvite, account: account) + } + } + + func onDelete(_ id: String) { + + } +} diff --git a/Sources/Chat/Types/Plain/ReceivedInviteStatus.swift b/Sources/Chat/Types/Plain/ReceivedInviteStatus.swift new file mode 100644 index 000000000..ae144ccb0 --- /dev/null +++ b/Sources/Chat/Types/Plain/ReceivedInviteStatus.swift @@ -0,0 +1,10 @@ +import Foundation + +struct ReceivedInviteStatus: Codable, SyncObject { + let id: Int64 + let status: ReceivedInvite.Status + + var syncId: String { + return String(id) + } +} From 3c59258fc74768ba113766e69edfeb9dc4c7820d Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Tue, 23 May 2023 16:17:22 +0300 Subject: [PATCH 27/85] Web3Inbox URL params --- .../PresentationLayer/Chat/Welcome/WelcomePresenter.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/Welcome/WelcomePresenter.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Welcome/WelcomePresenter.swift index db4e676f4..2cf58ec21 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/Welcome/WelcomePresenter.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/Welcome/WelcomePresenter.swift @@ -33,6 +33,8 @@ final class WelcomePresenter: ObservableObject { private extension WelcomePresenter { func setupInitialState() { - + interactor.trackConnection().sink { status in + print("Socket connection status: \(status)") + }.store(in: &disposeBag) } } From 3180c822386c61ea1c6d76bb85c77c15c9857c10 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Tue, 23 May 2023 16:32:16 +0300 Subject: [PATCH 28/85] SentInvite responseTopic as syncId --- Sources/Chat/Types/Plain/SentInvite.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Chat/Types/Plain/SentInvite.swift b/Sources/Chat/Types/Plain/SentInvite.swift index 6109367fa..aa7c2cad3 100644 --- a/Sources/Chat/Types/Plain/SentInvite.swift +++ b/Sources/Chat/Types/Plain/SentInvite.swift @@ -55,7 +55,7 @@ public struct SentInvite: Codable, Equatable { extension SentInvite: SyncObject { public var syncId: String { - return String(id) + return responseTopic } } From 8bfe87951f39a9f3a11fde0a58b477513ac6478d Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Tue, 23 May 2023 16:50:33 +0300 Subject: [PATCH 29/85] Fix method typo --- Sources/Web3Inbox/Web3Inbox.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Web3Inbox/Web3Inbox.swift b/Sources/Web3Inbox/Web3Inbox.swift index 125971c51..759c0eedf 100644 --- a/Sources/Web3Inbox/Web3Inbox.swift +++ b/Sources/Web3Inbox/Web3Inbox.swift @@ -25,7 +25,7 @@ public final class Web3Inbox { crypto: CryptoProvider, config: [ConfigParam: Bool] = [:], environment: APNSEnvironment, - onSign: @escaping SigningCallback, + onSign: @escaping SigningCallback ) { Web3Inbox.account = account Web3Inbox.config = config From 3af40685cea531ca0606e3c54bb31a0caae73983 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Tue, 23 May 2023 23:37:56 +0300 Subject: [PATCH 30/85] Sync history store --- .../Services/SyncService.swift | 24 +++++++++++++++---- .../Stores/SyncHistoryStore.swift | 20 ++++++++++++++++ .../WalletConnectSync/SyncClientFactory.swift | 3 +++ .../SyncStorageIdentifiers.swift | 3 +++ 4 files changed, 45 insertions(+), 5 deletions(-) create mode 100644 Sources/WalletConnectSync/Stores/SyncHistoryStore.swift diff --git a/Sources/WalletConnectSync/Services/SyncService.swift b/Sources/WalletConnectSync/Services/SyncService.swift index d77f1c977..279661dd4 100644 --- a/Sources/WalletConnectSync/Services/SyncService.swift +++ b/Sources/WalletConnectSync/Services/SyncService.swift @@ -14,16 +14,18 @@ final class SyncService { private let networkInteractor: NetworkInteracting private let derivationService: SyncDerivationService private let signatureStore: SyncSignatureStore + private let historyStore: SyncHistoryStore private let logger: ConsoleLogging /// `account` to `Record` keyValue store private let indexStore: SyncIndexStore - init(networkInteractor: NetworkInteracting, derivationService: SyncDerivationService, signatureStore: SyncSignatureStore, indexStore: SyncIndexStore, logger: ConsoleLogging) { + init(networkInteractor: NetworkInteracting, derivationService: SyncDerivationService, signatureStore: SyncSignatureStore, indexStore: SyncIndexStore, historyStore: SyncHistoryStore, logger: ConsoleLogging) { self.networkInteractor = networkInteractor self.derivationService = derivationService self.signatureStore = signatureStore self.indexStore = indexStore + self.historyStore = historyStore self.logger = logger setupSubscriptions() @@ -32,19 +34,27 @@ final class SyncService { func set(account: Account, store: String, object: Object) async throws { let protocolMethod = SyncSetMethod() let params = StoreSet(key: object.syncId, value: try object.json()) - let request = RPCRequest(method: protocolMethod.method, params: params) + let rpcid = RPCID() + let request = RPCRequest(method: protocolMethod.method, params: params, rpcid: rpcid) let record = try indexStore.getRecord(account: account, name: store) + try await networkInteractor.request(request, topic: record.topic, protocolMethod: protocolMethod) + historyStore.set(rpcid: rpcid.integer, topic: record.topic) + logger.debug("Did set value for \(store). Sent on \(record.topic). Object: \n\(object)\n") } func delete(account: Account, store: String, key: String) async throws { let protocolMethod = SyncDeleteMethod() - let request = RPCRequest(method: protocolMethod.method, params: ["key": key]) + let rpcid = RPCID() + let request = RPCRequest(method: protocolMethod.method, params: ["key": key], rpcid: rpcid) let record = try indexStore.getRecord(account: account, name: store) + try await networkInteractor.request(request, topic: record.topic, protocolMethod: protocolMethod) + historyStore.set(rpcid: rpcid.integer, topic: record.topic) + logger.debug("Did delete value for \(store). Sent on: \(record.topic). Key: \n\(key)\n") } @@ -63,13 +73,17 @@ private extension SyncService { func setupSubscriptions() { networkInteractor.requestSubscription(on: SyncSetMethod()) .sink { [unowned self] (payload: RequestSubscriptionPayload) in - self.updateSubject.send((payload.topic, .set(payload.request))) + if historyStore.isNew(topic: payload.topic, rpcid: payload.id) { + self.updateSubject.send((payload.topic, .set(payload.request))) + } } .store(in: &publishers) networkInteractor.requestSubscription(on: SyncDeleteMethod()) .sink { [unowned self] (payload: RequestSubscriptionPayload) in - self.updateSubject.send((payload.topic, .delete(payload.request))) + if historyStore.isNew(topic: payload.topic, rpcid: payload.id) { + self.updateSubject.send((payload.topic, .delete(payload.request))) + } } .store(in: &publishers) } diff --git a/Sources/WalletConnectSync/Stores/SyncHistoryStore.swift b/Sources/WalletConnectSync/Stores/SyncHistoryStore.swift new file mode 100644 index 000000000..a8f2333a0 --- /dev/null +++ b/Sources/WalletConnectSync/Stores/SyncHistoryStore.swift @@ -0,0 +1,20 @@ +import Foundation + +final class SyncHistoryStore { + + /// `topic` to `rpcid` keyValue store + private let store: CodableStore + + init(store: CodableStore) { + self.store = store + } + + func set(rpcid: Int64, topic: String) { + store.set(rpcid, forKey: topic) + } + + func isNew(topic: String, rpcid: RPCID) -> Bool { + guard let old = try? store.get(key: topic) else { return true } + return old <= rpcid.integer + } +} diff --git a/Sources/WalletConnectSync/SyncClientFactory.swift b/Sources/WalletConnectSync/SyncClientFactory.swift index e492da049..0b3fb8918 100644 --- a/Sources/WalletConnectSync/SyncClientFactory.swift +++ b/Sources/WalletConnectSync/SyncClientFactory.swift @@ -17,11 +17,14 @@ final class SyncClientFactory { ) let indexStore = CodableStore(defaults: UserDefaults.standard, identifier: SyncStorageIdentifiers.index.identifier) let syncIndexStore = SyncIndexStore(store: indexStore) + let historyStore = CodableStore(defaults: UserDefaults.standard, identifier: SyncStorageIdentifiers.history.identifier) + let syncHistoryStore = SyncHistoryStore(store: historyStore) let syncService = SyncService( networkInteractor: networkInteractor, derivationService: deriviationService, signatureStore: signatureStore, indexStore: syncIndexStore, + historyStore: syncHistoryStore, logger: ConsoleLogger(loggingLevel: .debug) ) return SyncClient(syncService: syncService, syncSignatureStore: signatureStore) diff --git a/Sources/WalletConnectSync/SyncStorageIdentifiers.swift b/Sources/WalletConnectSync/SyncStorageIdentifiers.swift index a3c757c14..59cb06f23 100644 --- a/Sources/WalletConnectSync/SyncStorageIdentifiers.swift +++ b/Sources/WalletConnectSync/SyncStorageIdentifiers.swift @@ -2,12 +2,15 @@ import Foundation enum SyncStorageIdentifiers { case index + case history case object(store: String) var identifier: String { switch self { case .index: return "com.walletconnect.sync.index" + case .history: + return "com.walletconnect.sync.history" case .object(let store): return "com.walletconnect.sync.object.\(store)" } From db312504ecd2932189664355de4179ce1ea38976 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Tue, 23 May 2023 23:50:42 +0300 Subject: [PATCH 31/85] Update historyStore if newer --- Sources/WalletConnectSync/Services/SyncService.swift | 4 ++-- Sources/WalletConnectSync/Stores/SyncHistoryStore.swift | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Sources/WalletConnectSync/Services/SyncService.swift b/Sources/WalletConnectSync/Services/SyncService.swift index 279661dd4..3275cc7e5 100644 --- a/Sources/WalletConnectSync/Services/SyncService.swift +++ b/Sources/WalletConnectSync/Services/SyncService.swift @@ -73,7 +73,7 @@ private extension SyncService { func setupSubscriptions() { networkInteractor.requestSubscription(on: SyncSetMethod()) .sink { [unowned self] (payload: RequestSubscriptionPayload) in - if historyStore.isNew(topic: payload.topic, rpcid: payload.id) { + if historyStore.update(topic: payload.topic, rpcid: payload.id) { self.updateSubject.send((payload.topic, .set(payload.request))) } } @@ -81,7 +81,7 @@ private extension SyncService { networkInteractor.requestSubscription(on: SyncDeleteMethod()) .sink { [unowned self] (payload: RequestSubscriptionPayload) in - if historyStore.isNew(topic: payload.topic, rpcid: payload.id) { + if historyStore.update(topic: payload.topic, rpcid: payload.id) { self.updateSubject.send((payload.topic, .delete(payload.request))) } } diff --git a/Sources/WalletConnectSync/Stores/SyncHistoryStore.swift b/Sources/WalletConnectSync/Stores/SyncHistoryStore.swift index a8f2333a0..643f9260e 100644 --- a/Sources/WalletConnectSync/Stores/SyncHistoryStore.swift +++ b/Sources/WalletConnectSync/Stores/SyncHistoryStore.swift @@ -13,6 +13,15 @@ final class SyncHistoryStore { store.set(rpcid, forKey: topic) } + func update(topic: String, rpcid: RPCID) -> Bool { + guard isNew(topic: topic, rpcid: rpcid) else { return false } + store.set(rpcid.integer, forKey: topic) + return true + } +} + +private extension SyncHistoryStore { + func isNew(topic: String, rpcid: RPCID) -> Bool { guard let old = try? store.get(key: topic) else { return true } return old <= rpcid.integer From 67a74014df959e24bd03cc23513163905d623b31 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Wed, 24 May 2023 14:40:16 +0300 Subject: [PATCH 32/85] SyncHistoryStore isNew fix --- Sources/WalletConnectSync/Stores/SyncHistoryStore.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/WalletConnectSync/Stores/SyncHistoryStore.swift b/Sources/WalletConnectSync/Stores/SyncHistoryStore.swift index 643f9260e..a2f290074 100644 --- a/Sources/WalletConnectSync/Stores/SyncHistoryStore.swift +++ b/Sources/WalletConnectSync/Stores/SyncHistoryStore.swift @@ -24,6 +24,6 @@ private extension SyncHistoryStore { func isNew(topic: String, rpcid: RPCID) -> Bool { guard let old = try? store.get(key: topic) else { return true } - return old <= rpcid.integer + return old < rpcid.integer } } From 5ddd399f8c329b6d25a1a10e793a0750b5da3a7e Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Fri, 26 May 2023 14:41:17 +0300 Subject: [PATCH 33/85] Update SyncStore.swift --- Sources/WalletConnectSync/Stores/SyncStore.swift | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Sources/WalletConnectSync/Stores/SyncStore.swift b/Sources/WalletConnectSync/Stores/SyncStore.swift index 413d90af7..91be85ac7 100644 --- a/Sources/WalletConnectSync/Stores/SyncStore.swift +++ b/Sources/WalletConnectSync/Stores/SyncStore.swift @@ -66,15 +66,18 @@ public final class SyncStore { public func set(object: Object, for account: Account) async throws { let record = try indexStore.getRecord(account: account, name: name) - try await syncClient.set(account: account, store: record.store, object: object) - objectStore.set(object: object, topic: record.topic) + if objectStore.set(object: object, topic: record.topic) { + try await syncClient.set(account: account, store: record.store, object: object) + } } public func delete(id: String, for account: Account) async throws { let record = try indexStore.getRecord(account: account, name: name) - try await syncClient.delete(account: account, store: record.store, key: id) - objectStore.delete(id: id, topic: record.topic) + + if objectStore.delete(id: id, topic: record.topic) { + try await syncClient.delete(account: account, store: record.store, key: id) + } } } From 81307eae8240d2a796f60ac084a38c8f4eaa07f0 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Fri, 26 May 2023 15:26:43 +0300 Subject: [PATCH 34/85] Tests fixed --- Example/IntegrationTests/Sync/SyncTests.swift | 4 +++- Sources/Chat/ChatClientFactory.swift | 2 +- .../ProtocolServices/Common/ResubscriptionService.swift | 3 --- Tests/ChatTests/RegistryServiceTests.swift | 9 +-------- 4 files changed, 5 insertions(+), 13 deletions(-) diff --git a/Example/IntegrationTests/Sync/SyncTests.swift b/Example/IntegrationTests/Sync/SyncTests.swift index eda8af1d2..b79a9c383 100644 --- a/Example/IntegrationTests/Sync/SyncTests.swift +++ b/Example/IntegrationTests/Sync/SyncTests.swift @@ -54,7 +54,9 @@ final class SyncTests: XCTestCase { logger: logger, keychainStorage: keychain, keyValueStorage: RuntimeKeyValueStorage()) - let syncService = SyncService(networkInteractor: networkingInteractor, derivationService: derivationService, signatureStore: syncSignatureStore, indexStore: indexStore, logger: logger) + let historyStore = CodableStore(defaults: RuntimeKeyValueStorage(), identifier: "historyStore") + let syncHistoryStore = SyncHistoryStore(store: historyStore) + let syncService = SyncService(networkInteractor: networkingInteractor, derivationService: derivationService, signatureStore: syncSignatureStore, indexStore: indexStore, historyStore: syncHistoryStore, logger: logger) return SyncClient(syncService: syncService, syncSignatureStore: syncSignatureStore) } diff --git a/Sources/Chat/ChatClientFactory.swift b/Sources/Chat/ChatClientFactory.swift index aab53a21c..c341beec8 100644 --- a/Sources/Chat/ChatClientFactory.swift +++ b/Sources/Chat/ChatClientFactory.swift @@ -38,7 +38,7 @@ public struct ChatClientFactory { let receivedInviteStatusStore: SyncStore = SyncStoreFactory.create(name: ChatStorageIdentifiers.receivedInviteStatus.rawValue, syncClient: syncClient, storage: storage) let receivedInviteStatusDelegate = ReceiviedInviteStatusDelegate() let chatStorage = ChatStorage(kms: kms, messageStore: messageStore, receivedInviteStore: receivedInviteStore, sentInviteStore: sentInviteStore, threadStore: threadStore, inviteKeyStore: inviteKeyStore, receivedInviteStatusStore: receivedInviteStatusStore, sentInviteStoreDelegate: sentInviteDelegate, threadStoreDelegate: threadDelegate, inviteKeyDelegate: inviteKeyDelegate, receiviedInviteStatusDelegate: receivedInviteStatusDelegate) - let resubscriptionService = ResubscriptionService(networkingInteractor: networkingInteractor, kms: kms, chatStorage: chatStorage, logger: logger) + let resubscriptionService = ResubscriptionService(networkingInteractor: networkingInteractor, kms: kms, logger: logger) let invitationHandlingService = InvitationHandlingService(keyserverURL: keyserverURL, networkingInteractor: networkingInteractor, identityClient: identityClient, kms: kms, logger: logger, chatStorage: chatStorage) let inviteService = InviteService(keyserverURL: keyserverURL, networkingInteractor: networkingInteractor, identityClient: identityClient, kms: kms, chatStorage: chatStorage, logger: logger) let leaveService = LeaveService() diff --git a/Sources/Chat/ProtocolServices/Common/ResubscriptionService.swift b/Sources/Chat/ProtocolServices/Common/ResubscriptionService.swift index 93d6c865e..215a2c8bd 100644 --- a/Sources/Chat/ProtocolServices/Common/ResubscriptionService.swift +++ b/Sources/Chat/ProtocolServices/Common/ResubscriptionService.swift @@ -5,19 +5,16 @@ class ResubscriptionService { private let networkingInteractor: NetworkInteracting private let kms: KeyManagementServiceProtocol private let logger: ConsoleLogging - private var chatStorage: ChatStorage private var publishers = [AnyCancellable]() init( networkingInteractor: NetworkInteracting, kms: KeyManagementServiceProtocol, - chatStorage: ChatStorage, logger: ConsoleLogging ) { self.networkingInteractor = networkingInteractor self.kms = kms self.logger = logger - self.chatStorage = chatStorage } func subscribeForInvites(inviteKey: AgreementPublicKey) async throws { diff --git a/Tests/ChatTests/RegistryServiceTests.swift b/Tests/ChatTests/RegistryServiceTests.swift index 949350b7c..24d2c657e 100644 --- a/Tests/ChatTests/RegistryServiceTests.swift +++ b/Tests/ChatTests/RegistryServiceTests.swift @@ -40,14 +40,7 @@ final class RegistryServiceTests: XCTestCase { ) identityClient = IdentityClient(identityService: identitySevice, identityStorage: identityStorage, logger: ConsoleLoggerMock()) - let storage = RuntimeKeyValueStorage() - let chatStorage = ChatStorage( - messageStore: .init(storage: storage, identifier: ""), - receivedInviteStore: .init(storage: storage, identifier: ""), - sentInviteStore: .init(storage: storage, identifier: ""), - threadStore: .init(storage: storage, identifier: "") - ) - resubscriptionService = ResubscriptionService(networkingInteractor: networkingInteractor, kms: kms, chatStorage: chatStorage, logger: ConsoleLoggerMock()) + resubscriptionService = ResubscriptionService(networkingInteractor: networkingInteractor, kms: kms, logger: ConsoleLoggerMock()) } func testRegister() async throws { From c360f6b1bbd628f50a9dce2f0a8d75defcd0b52f Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Fri, 26 May 2023 16:27:07 +0300 Subject: [PATCH 35/85] Import networking to verify --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index af031fec1..c00641c97 100644 --- a/Package.swift +++ b/Package.swift @@ -123,7 +123,7 @@ let package = Package( dependencies: []), .target( name: "WalletConnectVerify", - dependencies: ["WalletConnectUtils"]), + dependencies: ["WalletConnectUtils", "WalletConnectNetworking"]), .target( name: "Web3Modal", dependencies: ["QRCode", "WalletConnectSign"]), From e4affce64357fb91300cc8565783b6613e436926 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Fri, 26 May 2023 16:33:41 +0300 Subject: [PATCH 36/85] WalletApp build error fixed --- .../ApplicationLayer/Configurator/ThirdPartyConfigurator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Example/WalletApp/ApplicationLayer/Configurator/ThirdPartyConfigurator.swift b/Example/WalletApp/ApplicationLayer/Configurator/ThirdPartyConfigurator.swift index 58064d376..3b39114d3 100644 --- a/Example/WalletApp/ApplicationLayer/Configurator/ThirdPartyConfigurator.swift +++ b/Example/WalletApp/ApplicationLayer/Configurator/ThirdPartyConfigurator.swift @@ -19,7 +19,7 @@ struct ThirdPartyConfigurator: Configurator { let account = Account(blockchain: Blockchain("eip155:1")!, address: EthKeyStore.shared.address)! - Web3Inbox.configure(account: account, config: [.chatEnabled: false, .settingsEnabled: false], onSign: Web3InboxSigner.onSing, environment: BuildConfiguration.shared.apnsEnvironment) + Web3Inbox.configure(account: account, crypto: DefaultCryptoProvider(), config: [.chatEnabled: false, .settingsEnabled: false], environment: BuildConfiguration.shared.apnsEnvironment, onSign: Web3InboxSigner.onSing) } } From 5d178713e94af79272dc2e0dd1d5f8a5a0e88537 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Fri, 26 May 2023 17:18:43 +0300 Subject: [PATCH 37/85] Random account for sync tests --- Example/IntegrationTests/Sync/SyncTests.swift | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/Example/IntegrationTests/Sync/SyncTests.swift b/Example/IntegrationTests/Sync/SyncTests.swift index b79a9c383..73ebea32a 100644 --- a/Example/IntegrationTests/Sync/SyncTests.swift +++ b/Example/IntegrationTests/Sync/SyncTests.swift @@ -1,6 +1,7 @@ import Foundation import Combine import XCTest +import Web3 @testable import WalletConnectSync @testable import WalletConnectSigner @@ -29,8 +30,16 @@ final class SyncTests: XCTestCase { var signer: MessageSigner! let storeName = "SyncTests_store" - let account = Account("eip155:1:0x1FF34C90a0850Fe7227fcFA642688b9712477482")! - let privateKey = Data(hex: "99c6f0a7ac44d40d3d7f31083e9f5b045d4bf932fdf9f4a3c241cdd3cbc98045") + + var account: Account { + return Account("eip155:1:" + pk.address.hex(eip55: true))! + } + + let pk = try! EthereumPrivateKey() + + var privateKey: Data { + return Data(pk.rawPrivateKey) + } override func setUp() async throws { indexStore1 = makeIndexStore() @@ -75,7 +84,7 @@ final class SyncTests: XCTestCase { let setExpectation = expectation(description: "syncSetTest") let delExpectation = expectation(description: "syncDelTest") - let object = TestObject(id: "id", value: "value") + let object = TestObject(id: "id-1", value: "value-1") syncStore1.syncUpdatePublisher.sink { (_, _, update) in switch update { From e9bde601bcaba06c5612511b43b3bd3db862333c Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Mon, 29 May 2023 16:08:35 +0300 Subject: [PATCH 38/85] testResolveAddress disabled --- .../IntegrationTests/Auth/ENS/ENSResolverTests.swift | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Example/IntegrationTests/Auth/ENS/ENSResolverTests.swift b/Example/IntegrationTests/Auth/ENS/ENSResolverTests.swift index df4a26f3e..e077b82e5 100644 --- a/Example/IntegrationTests/Auth/ENS/ENSResolverTests.swift +++ b/Example/IntegrationTests/Auth/ENS/ENSResolverTests.swift @@ -13,9 +13,10 @@ class ENSResolverTests: XCTestCase { XCTAssertEqual(resolved, ens) } - func testResolveAddress() async throws { - let resolver = ENSResolverFactory(crypto: DefaultCryptoProvider()).create(projectId: InputConfig.projectId) - let resolved = try await resolver.resolveAddress(ens: ens, blockchain: account.blockchain) - XCTAssertEqual(resolved, account) - } +// Note: - removed until RPC server fix +// func testResolveAddress() async throws { +// let resolver = ENSResolverFactory(crypto: DefaultCryptoProvider()).create(projectId: InputConfig.projectId) +// let resolved = try await resolver.resolveAddress(ens: ens, blockchain: account.blockchain) +// XCTAssertEqual(resolved, account) +// } } From 929d945337e472e639c691f215fa9053d9a7021e Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Tue, 30 May 2023 16:33:07 +0300 Subject: [PATCH 39/85] Sync message updated --- Sources/WalletConnectSync/SyncClient.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/WalletConnectSync/SyncClient.swift b/Sources/WalletConnectSync/SyncClient.swift index e82867e85..9385e7c3a 100644 --- a/Sources/WalletConnectSync/SyncClient.swift +++ b/Sources/WalletConnectSync/SyncClient.swift @@ -20,7 +20,7 @@ public final class SyncClient { return """ I authorize this app to sync my account: \(account.absoluteString) - Read more about Sync API: https://docs.walletconnect.com/2.0/specs/clients/core/sync + Read more about it here: https://walletconnect.com/faq """ } From 3a926dc3b027a424794d7f87624c87e92293b6b0 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Thu, 1 Jun 2023 15:09:18 +0300 Subject: [PATCH 40/85] testResolveEns disabled --- .../IntegrationTests/Auth/ENS/ENSResolverTests.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Example/IntegrationTests/Auth/ENS/ENSResolverTests.swift b/Example/IntegrationTests/Auth/ENS/ENSResolverTests.swift index e077b82e5..8ea3661e8 100644 --- a/Example/IntegrationTests/Auth/ENS/ENSResolverTests.swift +++ b/Example/IntegrationTests/Auth/ENS/ENSResolverTests.swift @@ -7,13 +7,13 @@ class ENSResolverTests: XCTestCase { private let account = Account("eip155:1:0xD02D090F8f99B61D65d8e8876Ea86c2720aB27BC")! private let ens = "web3.eth" - func testResolveEns() async throws { - let resolver = ENSResolverFactory(crypto: DefaultCryptoProvider()).create(projectId: InputConfig.projectId) - let resolved = try await resolver.resolveEns(account: account) - XCTAssertEqual(resolved, ens) - } - // Note: - removed until RPC server fix +// func testResolveEns() async throws { +// let resolver = ENSResolverFactory(crypto: DefaultCryptoProvider()).create(projectId: InputConfig.projectId) +// let resolved = try await resolver.resolveEns(account: account) +// XCTAssertEqual(resolved, ens) +// } +// // func testResolveAddress() async throws { // let resolver = ENSResolverFactory(crypto: DefaultCryptoProvider()).create(projectId: InputConfig.projectId) // let resolved = try await resolver.resolveAddress(ens: ens, blockchain: account.blockchain) From 75ea4a4ae21fef5d72ff8fc4932ace112f1eb83f Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Mon, 5 Jun 2023 18:18:42 +0300 Subject: [PATCH 41/85] Derivation provider --- Example/ExampleApp.xcodeproj/project.pbxproj | 12 ++++++++- Example/IntegrationTests/Chat/ChatTests.swift | 2 +- .../Sync/SyncDerivationServiceTests.swift | 2 +- Example/IntegrationTests/Sync/SyncTests.swift | 2 +- .../Shared/DefaultDerivationProvider.swift | 25 +++++++++++++++++++ .../DomainLayer/Chat/ChatService.swift | 2 +- .../Web3Inbox/Web3InboxViewController.swift | 2 +- Sources/Chat/Chat.swift | 4 +-- .../WalletConnectSigner/CryptoProvider.swift | 6 ----- .../DerivationProvider.swift | 10 ++++++++ .../Services/SyncDerivationService.swift | 8 +++--- Sources/WalletConnectSync/Sync.swift | 6 ++--- .../WalletConnectSync/SyncClientFactory.swift | 8 +++--- Sources/WalletConnectSync/SyncConfig.swift | 2 +- Sources/Web3Inbox/Web3Inbox.swift | 7 ++---- 15 files changed, 67 insertions(+), 31 deletions(-) create mode 100644 Example/Shared/DefaultDerivationProvider.swift create mode 100644 Sources/WalletConnectSigner/DerivationProvider.swift diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index a4a33494b..6a5cf61ed 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -76,6 +76,10 @@ A507BE1A29E8032E0038EF70 /* EIP55Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A507BE1929E8032E0038EF70 /* EIP55Tests.swift */; }; A50C036528AAD32200FE72D3 /* ClientDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50C036428AAD32200FE72D3 /* ClientDelegate.swift */; }; A50F3946288005B200064555 /* Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50F3945288005B200064555 /* Types.swift */; }; + A51606F32A2E2DE000CACB92 /* DefaultDerivationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51606F22A2E2DE000CACB92 /* DefaultDerivationProvider.swift */; }; + A51606F42A2E2DE000CACB92 /* DefaultDerivationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51606F22A2E2DE000CACB92 /* DefaultDerivationProvider.swift */; }; + A51606F52A2E2DE000CACB92 /* DefaultDerivationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51606F22A2E2DE000CACB92 /* DefaultDerivationProvider.swift */; }; + A51606F62A2E2DE000CACB92 /* DefaultDerivationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51606F22A2E2DE000CACB92 /* DefaultDerivationProvider.swift */; }; A518A98729683FB60035247E /* Web3InboxViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A518A98429683FB60035247E /* Web3InboxViewController.swift */; }; A518A98829683FB60035247E /* Web3InboxModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = A518A98529683FB60035247E /* Web3InboxModule.swift */; }; A518A98929683FB60035247E /* Web3InboxRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A518A98629683FB60035247E /* Web3InboxRouter.swift */; }; @@ -199,8 +203,8 @@ A5E22D222840C8D300E36487 /* WalletEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E22D212840C8D300E36487 /* WalletEngine.swift */; }; A5E22D242840C8DB00E36487 /* SafariEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E22D232840C8DB00E36487 /* SafariEngine.swift */; }; A5E22D2C2840EAC300E36487 /* XCUIElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E22D2B2840EAC300E36487 /* XCUIElement.swift */; }; - A74D32BA2A1E25AD00CB8536 /* QueryParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = A74D32B92A1E25AD00CB8536 /* QueryParameters.swift */; }; A5E776BA29F4362D00172091 /* AlertError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E776B929F4362D00172091 /* AlertError.swift */; }; + A74D32BA2A1E25AD00CB8536 /* QueryParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = A74D32B92A1E25AD00CB8536 /* QueryParameters.swift */; }; C5133A78294125CC00A8314C /* Web3 in Frameworks */ = {isa = PBXBuildFile; productRef = C5133A77294125CC00A8314C /* Web3 */; }; C53AA4362941251C008EA57C /* DefaultSignerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59CF4F5292F83D50031A42F /* DefaultSignerFactory.swift */; }; C55D347F295DD7140004314A /* AuthRequestModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = C55D347A295DD7140004314A /* AuthRequestModule.swift */; }; @@ -414,6 +418,7 @@ A507BE1929E8032E0038EF70 /* EIP55Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EIP55Tests.swift; sourceTree = ""; }; A50C036428AAD32200FE72D3 /* ClientDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClientDelegate.swift; sourceTree = ""; }; A50F3945288005B200064555 /* Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Types.swift; sourceTree = ""; }; + A51606F22A2E2DE000CACB92 /* DefaultDerivationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultDerivationProvider.swift; sourceTree = ""; }; A518A98429683FB60035247E /* Web3InboxViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Web3InboxViewController.swift; sourceTree = ""; }; A518A98529683FB60035247E /* Web3InboxModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Web3InboxModule.swift; sourceTree = ""; }; A518A98629683FB60035247E /* Web3InboxRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Web3InboxRouter.swift; sourceTree = ""; }; @@ -1286,6 +1291,7 @@ A5629AEF2877F73000094373 /* DefaultSocketFactory.swift */, A59CF4F5292F83D50031A42F /* DefaultSignerFactory.swift */, A5A0843B29D2F60A000B9B17 /* DefaultCryptoProvider.swift */, + A51606F22A2E2DE000CACB92 /* DefaultDerivationProvider.swift */, ); path = Shared; sourceTree = ""; @@ -2065,6 +2071,7 @@ A5BB7FA928B6A5FD00707FC6 /* AuthViewModel.swift in Sources */, 84CE6452279ED42B00142511 /* ConnectView.swift in Sources */, A5A8E47E293A1CFE00FEB97D /* DefaultSignerFactory.swift in Sources */, + A51606F32A2E2DE000CACB92 /* DefaultDerivationProvider.swift in Sources */, A5A0843D29D2F624000B9B17 /* DefaultCryptoProvider.swift in Sources */, 84CE6448279AE68600142511 /* AccountRequestViewController.swift in Sources */, A5BB7F9F28B69B7100707FC6 /* SignCoordinator.swift in Sources */, @@ -2120,6 +2127,7 @@ A5629ADE2876CC6E00094373 /* InviteListModule.swift in Sources */, A5E776BA29F4362D00172091 /* AlertError.swift in Sources */, A578FA322873036400AA7720 /* InputView.swift in Sources */, + A51606F52A2E2DE000CACB92 /* DefaultDerivationProvider.swift in Sources */, A5A0843F29D2F625000B9B17 /* DefaultCryptoProvider.swift in Sources */, A5C2021B287E1FD8007E3188 /* ImportRouter.swift in Sources */, A5629AE42876E6D200094373 /* ThreadViewModel.swift in Sources */, @@ -2209,6 +2217,7 @@ A5E03DFD286465D100888481 /* Stubs.swift in Sources */, A507BE1A29E8032E0038EF70 /* EIP55Tests.swift in Sources */, A5A8E47A293A1C9B00FEB97D /* DefaultSocketFactory.swift in Sources */, + A51606F42A2E2DE000CACB92 /* DefaultDerivationProvider.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2297,6 +2306,7 @@ C5B2F6F929705293000DBA0E /* SessionRequestPresenter.swift in Sources */, 84DB38F32983CDAE00BFEE37 /* PushRegisterer.swift in Sources */, C5B2F6FB297055B0000DBA0E /* ETHSigner.swift in Sources */, + A51606F62A2E2DE000CACB92 /* DefaultDerivationProvider.swift in Sources */, C56EE274293F56D7004840D1 /* SceneViewController.swift in Sources */, 847BD1E5298A806800076C90 /* NotificationsPresenter.swift in Sources */, C55D3496295DFA750004314A /* WelcomeInteractor.swift in Sources */, diff --git a/Example/IntegrationTests/Chat/ChatTests.swift b/Example/IntegrationTests/Chat/ChatTests.swift index 8d6f0f0c5..0566960f1 100644 --- a/Example/IntegrationTests/Chat/ChatTests.swift +++ b/Example/IntegrationTests/Chat/ChatTests.swift @@ -59,7 +59,7 @@ final class ChatTests: XCTestCase { let syncClient = SyncClientFactory.create( networkInteractor: networkingInteractor, - crypto: DefaultCryptoProvider(), + derivator: DefaultDerivationProvider(), keychain: keychain ) diff --git a/Example/IntegrationTests/Sync/SyncDerivationServiceTests.swift b/Example/IntegrationTests/Sync/SyncDerivationServiceTests.swift index e697748fe..01dc56c13 100644 --- a/Example/IntegrationTests/Sync/SyncDerivationServiceTests.swift +++ b/Example/IntegrationTests/Sync/SyncDerivationServiceTests.swift @@ -13,7 +13,7 @@ class SyncDerivationServiceTests: XCTestCase { let kms = KeyManagementService(keychain: keychain) let derivationService = SyncDerivationService( syncStorage: syncStorage, - crypto: DefaultCryptoProvider(), + derivator: DefaultDerivationProvider(), kms: kms ) diff --git a/Example/IntegrationTests/Sync/SyncTests.swift b/Example/IntegrationTests/Sync/SyncTests.swift index 73ebea32a..e44ace8c1 100644 --- a/Example/IntegrationTests/Sync/SyncTests.swift +++ b/Example/IntegrationTests/Sync/SyncTests.swift @@ -55,7 +55,7 @@ final class SyncTests: XCTestCase { let syncSignatureStore = SyncSignatureStore(keychain: KeychainStorageMock()) let keychain = KeychainStorageMock() let kms = KeyManagementService(keychain: keychain) - let derivationService = SyncDerivationService(syncStorage: syncSignatureStore, crypto: DefaultCryptoProvider(), kms: kms) + let derivationService = SyncDerivationService(syncStorage: syncSignatureStore, derivator: DefaultDerivationProvider(), kms: kms) let logger = ConsoleLogger(suffix: suffix, loggingLevel: .debug) let relayClient = RelayClient(relayHost: InputConfig.relayHost, projectId: InputConfig.projectId, keychainStorage: keychain, socketFactory: DefaultSocketFactory(), logger: logger) let networkingInteractor = NetworkingClientFactory.create( diff --git a/Example/Shared/DefaultDerivationProvider.swift b/Example/Shared/DefaultDerivationProvider.swift new file mode 100644 index 000000000..3152aba94 --- /dev/null +++ b/Example/Shared/DefaultDerivationProvider.swift @@ -0,0 +1,25 @@ +import Foundation +import Auth +import Web3 +import CryptoSwift +import HDWalletKit + +struct DefaultDerivationProvider: DerivationProvider { + + public func derive(entropy: Data, path: [WalletConnectSigner.DerivationPath]) -> Data { + let mnemonic = Mnemonic.create(entropy: entropy) + let seed = Mnemonic.createSeed(mnemonic: mnemonic) + let privateKey = PrivateKey(seed: seed, coin: .bitcoin) + + let derived = path.reduce(privateKey) { result, path in + switch path { + case .hardened(let index): + return result.derived(at: .hardened(index)) + case .notHardened(let index): + return result.derived(at: .notHardened(index)) + } + } + + return derived.raw + } +} diff --git a/Example/Showcase/Classes/DomainLayer/Chat/ChatService.swift b/Example/Showcase/Classes/DomainLayer/Chat/ChatService.swift index 10177170c..4a886eb84 100644 --- a/Example/Showcase/Classes/DomainLayer/Chat/ChatService.swift +++ b/Example/Showcase/Classes/DomainLayer/Chat/ChatService.swift @@ -8,7 +8,7 @@ typealias Stream = AnyPublisher final class ChatService { private var client: ChatClient = { - Chat.configure(crypto: DefaultCryptoProvider()) + Chat.configure(derivator: DefaultDerivationProvider()) return Chat.instance }() diff --git a/Example/Showcase/Classes/PresentationLayer/Web3Inbox/Web3InboxViewController.swift b/Example/Showcase/Classes/PresentationLayer/Web3Inbox/Web3InboxViewController.swift index 14b7aca43..30cce399c 100644 --- a/Example/Showcase/Classes/PresentationLayer/Web3Inbox/Web3InboxViewController.swift +++ b/Example/Showcase/Classes/PresentationLayer/Web3Inbox/Web3InboxViewController.swift @@ -18,7 +18,7 @@ final class Web3InboxViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - Web3Inbox.configure(account: importAccount.account, crypto: DefaultCryptoProvider(), config: [.pushEnabled: false], environment: .sandbox, onSign: onSing) + Web3Inbox.configure(account: importAccount.account, derivator: DefaultDerivationProvider(), config: [.pushEnabled: false], environment: .sandbox, onSign: onSing) edgesForExtendedLayout = [] navigationItem.title = "Web3Inbox SDK" diff --git a/Sources/Chat/Chat.swift b/Sources/Chat/Chat.swift index f1d2efcc8..83bcceea2 100644 --- a/Sources/Chat/Chat.swift +++ b/Sources/Chat/Chat.swift @@ -26,9 +26,9 @@ public class Chat { /// - crypto: Crypto utils implementation static public func configure( keyserverUrl: String = "https://keys.walletconnect.com", - crypto: CryptoProvider + derivator: DerivationProvider ) { Chat.keyserverUrl = keyserverUrl - Sync.configure(crypto: crypto) + Sync.configure(derivator: derivator) } } diff --git a/Sources/WalletConnectSigner/CryptoProvider.swift b/Sources/WalletConnectSigner/CryptoProvider.swift index 971572363..a04efbcff 100644 --- a/Sources/WalletConnectSigner/CryptoProvider.swift +++ b/Sources/WalletConnectSigner/CryptoProvider.swift @@ -1,12 +1,6 @@ import Foundation -public enum DerivationPath { - case hardened(UInt32) - case notHardened(UInt32) -} - public protocol CryptoProvider { func recoverPubKey(signature: EthereumSignature, message: Data) throws -> Data func keccak256(_ data: Data) -> Data - func derive(entropy: Data, path: [DerivationPath]) -> Data } diff --git a/Sources/WalletConnectSigner/DerivationProvider.swift b/Sources/WalletConnectSigner/DerivationProvider.swift new file mode 100644 index 000000000..89b94103d --- /dev/null +++ b/Sources/WalletConnectSigner/DerivationProvider.swift @@ -0,0 +1,10 @@ +import Foundation + +public enum DerivationPath { + case hardened(UInt32) + case notHardened(UInt32) +} + +public protocol DerivationProvider { + func derive(entropy: Data, path: [DerivationPath]) -> Data +} diff --git a/Sources/WalletConnectSync/Services/SyncDerivationService.swift b/Sources/WalletConnectSync/Services/SyncDerivationService.swift index 954d8e048..c3abf9b17 100644 --- a/Sources/WalletConnectSync/Services/SyncDerivationService.swift +++ b/Sources/WalletConnectSync/Services/SyncDerivationService.swift @@ -3,16 +3,16 @@ import Foundation final class SyncDerivationService { private let syncStorage: SyncSignatureStore - private let crypto: CryptoProvider + private let derivator: DerivationProvider private let kms: KeyManagementServiceProtocol init( syncStorage: SyncSignatureStore, - crypto: CryptoProvider, + derivator: DerivationProvider, kms: KeyManagementServiceProtocol ) { self.syncStorage = syncStorage - self.crypto = crypto + self.derivator = derivator self.kms = kms } @@ -34,7 +34,7 @@ final class SyncDerivationService { ] + slice.map { .notHardened($0) } let entropy = signatureData.sha256() - let storeKey = crypto.derive(entropy: entropy, path: path) + let storeKey = derivator.derive(entropy: entropy, path: path) let topic = storeKey.sha256().toHexString() let symmetricKey = try SymmetricKey(rawRepresentation: storeKey) diff --git a/Sources/WalletConnectSync/Sync.swift b/Sources/WalletConnectSync/Sync.swift index d524381c5..cf4261d72 100644 --- a/Sources/WalletConnectSync/Sync.swift +++ b/Sources/WalletConnectSync/Sync.swift @@ -10,7 +10,7 @@ public class Sync { } return SyncClientFactory.create( networkInteractor: Networking.interactor, - crypto: config.crypto + derivator: config.derivator ) }() @@ -21,7 +21,7 @@ public class Sync { /// Auth instance wallet config method. For DApp usage /// - Parameters: /// - crypto: Crypto utils implementation - static public func configure(crypto: CryptoProvider) { - Sync.config = Sync.Config(crypto: crypto) + static public func configure(derivator: DerivationProvider) { + Sync.config = Sync.Config(derivator: derivator) } } diff --git a/Sources/WalletConnectSync/SyncClientFactory.swift b/Sources/WalletConnectSync/SyncClientFactory.swift index 0b3fb8918..090756c92 100644 --- a/Sources/WalletConnectSync/SyncClientFactory.swift +++ b/Sources/WalletConnectSync/SyncClientFactory.swift @@ -2,17 +2,17 @@ import Foundation final class SyncClientFactory { - static func create(networkInteractor: NetworkInteracting, crypto: CryptoProvider) -> SyncClient { + static func create(networkInteractor: NetworkInteracting, derivator: DerivationProvider) -> SyncClient { let keychain = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk") - return create(networkInteractor: networkInteractor, crypto: crypto, keychain: keychain) + return create(networkInteractor: networkInteractor, derivator: derivator, keychain: keychain) } - static func create(networkInteractor: NetworkInteracting, crypto: CryptoProvider, keychain: KeychainStorageProtocol) -> SyncClient { + static func create(networkInteractor: NetworkInteracting, derivator: DerivationProvider, keychain: KeychainStorageProtocol) -> SyncClient { let signatureStore = SyncSignatureStore(keychain: keychain) let kms = KeyManagementService(keychain: keychain) let deriviationService = SyncDerivationService( syncStorage: signatureStore, - crypto: crypto, + derivator: derivator, kms: kms ) let indexStore = CodableStore(defaults: UserDefaults.standard, identifier: SyncStorageIdentifiers.index.identifier) diff --git a/Sources/WalletConnectSync/SyncConfig.swift b/Sources/WalletConnectSync/SyncConfig.swift index d0587eea7..53613361e 100644 --- a/Sources/WalletConnectSync/SyncConfig.swift +++ b/Sources/WalletConnectSync/SyncConfig.swift @@ -2,6 +2,6 @@ import Foundation extension Sync { struct Config { - let crypto: CryptoProvider + let derivator: DerivationProvider } } diff --git a/Sources/Web3Inbox/Web3Inbox.swift b/Sources/Web3Inbox/Web3Inbox.swift index 759c0eedf..aed0f5d86 100644 --- a/Sources/Web3Inbox/Web3Inbox.swift +++ b/Sources/Web3Inbox/Web3Inbox.swift @@ -17,12 +17,9 @@ public final class Web3Inbox { private init() { } /// Web3Inbox instance config method - /// - Parameters: - /// - account: Web3Inbox initial account - /// - crypto: Crypto utils implementation static public func configure( account: Account, - crypto: CryptoProvider, + derivator: DerivationProvider, config: [ConfigParam: Bool] = [:], environment: APNSEnvironment, onSign: @escaping SigningCallback @@ -30,7 +27,7 @@ public final class Web3Inbox { Web3Inbox.account = account Web3Inbox.config = config Web3Inbox.onSign = onSign - Chat.configure(crypto: crypto) + Chat.configure(derivator: derivator) Push.configure(environment: environment) } } From 2c4aa4e5fd2dfae0bf031492f0d7ea5eddce5be1 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Tue, 6 Jun 2023 13:56:08 +0300 Subject: [PATCH 42/85] BIP44Provider rename --- Example/ExampleApp.xcodeproj/project.pbxproj | 20 +++++++++---------- Example/IntegrationTests/Chat/ChatTests.swift | 2 +- .../Sync/SyncDerivationServiceTests.swift | 2 +- Example/IntegrationTests/Sync/SyncTests.swift | 2 +- ...vider.swift => DefaultBIP44Provider.swift} | 2 +- .../DomainLayer/Chat/ChatService.swift | 2 +- .../Web3Inbox/Web3InboxViewController.swift | 2 +- Sources/Chat/Chat.swift | 4 ++-- ...tionProvider.swift => BIP44Provider.swift} | 2 +- .../Services/SyncDerivationService.swift | 8 ++++---- Sources/WalletConnectSync/Sync.swift | 6 +++--- .../WalletConnectSync/SyncClientFactory.swift | 8 ++++---- Sources/WalletConnectSync/SyncConfig.swift | 2 +- Sources/Web3Inbox/Web3Inbox.swift | 4 ++-- 14 files changed, 33 insertions(+), 33 deletions(-) rename Example/Shared/{DefaultDerivationProvider.swift => DefaultBIP44Provider.swift} (92%) rename Sources/WalletConnectSigner/{DerivationProvider.swift => BIP44Provider.swift} (82%) diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index 6a5cf61ed..e8ec5c1be 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -76,10 +76,10 @@ A507BE1A29E8032E0038EF70 /* EIP55Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A507BE1929E8032E0038EF70 /* EIP55Tests.swift */; }; A50C036528AAD32200FE72D3 /* ClientDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50C036428AAD32200FE72D3 /* ClientDelegate.swift */; }; A50F3946288005B200064555 /* Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50F3945288005B200064555 /* Types.swift */; }; - A51606F32A2E2DE000CACB92 /* DefaultDerivationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51606F22A2E2DE000CACB92 /* DefaultDerivationProvider.swift */; }; - A51606F42A2E2DE000CACB92 /* DefaultDerivationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51606F22A2E2DE000CACB92 /* DefaultDerivationProvider.swift */; }; - A51606F52A2E2DE000CACB92 /* DefaultDerivationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51606F22A2E2DE000CACB92 /* DefaultDerivationProvider.swift */; }; - A51606F62A2E2DE000CACB92 /* DefaultDerivationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51606F22A2E2DE000CACB92 /* DefaultDerivationProvider.swift */; }; + A51606F82A2F47BD00CACB92 /* DefaultBIP44Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51606F72A2F47BD00CACB92 /* DefaultBIP44Provider.swift */; }; + A51606F92A2F47BD00CACB92 /* DefaultBIP44Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51606F72A2F47BD00CACB92 /* DefaultBIP44Provider.swift */; }; + A51606FA2A2F47BD00CACB92 /* DefaultBIP44Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51606F72A2F47BD00CACB92 /* DefaultBIP44Provider.swift */; }; + A51606FB2A2F47BD00CACB92 /* DefaultBIP44Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51606F72A2F47BD00CACB92 /* DefaultBIP44Provider.swift */; }; A518A98729683FB60035247E /* Web3InboxViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A518A98429683FB60035247E /* Web3InboxViewController.swift */; }; A518A98829683FB60035247E /* Web3InboxModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = A518A98529683FB60035247E /* Web3InboxModule.swift */; }; A518A98929683FB60035247E /* Web3InboxRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A518A98629683FB60035247E /* Web3InboxRouter.swift */; }; @@ -418,7 +418,7 @@ A507BE1929E8032E0038EF70 /* EIP55Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EIP55Tests.swift; sourceTree = ""; }; A50C036428AAD32200FE72D3 /* ClientDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClientDelegate.swift; sourceTree = ""; }; A50F3945288005B200064555 /* Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Types.swift; sourceTree = ""; }; - A51606F22A2E2DE000CACB92 /* DefaultDerivationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultDerivationProvider.swift; sourceTree = ""; }; + A51606F72A2F47BD00CACB92 /* DefaultBIP44Provider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultBIP44Provider.swift; sourceTree = ""; }; A518A98429683FB60035247E /* Web3InboxViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Web3InboxViewController.swift; sourceTree = ""; }; A518A98529683FB60035247E /* Web3InboxModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Web3InboxModule.swift; sourceTree = ""; }; A518A98629683FB60035247E /* Web3InboxRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Web3InboxRouter.swift; sourceTree = ""; }; @@ -1291,7 +1291,7 @@ A5629AEF2877F73000094373 /* DefaultSocketFactory.swift */, A59CF4F5292F83D50031A42F /* DefaultSignerFactory.swift */, A5A0843B29D2F60A000B9B17 /* DefaultCryptoProvider.swift */, - A51606F22A2E2DE000CACB92 /* DefaultDerivationProvider.swift */, + A51606F72A2F47BD00CACB92 /* DefaultBIP44Provider.swift */, ); path = Shared; sourceTree = ""; @@ -2071,7 +2071,6 @@ A5BB7FA928B6A5FD00707FC6 /* AuthViewModel.swift in Sources */, 84CE6452279ED42B00142511 /* ConnectView.swift in Sources */, A5A8E47E293A1CFE00FEB97D /* DefaultSignerFactory.swift in Sources */, - A51606F32A2E2DE000CACB92 /* DefaultDerivationProvider.swift in Sources */, A5A0843D29D2F624000B9B17 /* DefaultCryptoProvider.swift in Sources */, 84CE6448279AE68600142511 /* AccountRequestViewController.swift in Sources */, A5BB7F9F28B69B7100707FC6 /* SignCoordinator.swift in Sources */, @@ -2079,6 +2078,7 @@ 84CE644E279ED2FF00142511 /* SelectChainView.swift in Sources */, 84CE644B279EA1FA00142511 /* AccountRequestView.swift in Sources */, 84CE6431279820F600142511 /* AccountsView.swift in Sources */, + A51606F82A2F47BD00CACB92 /* DefaultBIP44Provider.swift in Sources */, 84CE643D2798322600142511 /* ConnectViewController.swift in Sources */, 84CE6444279AB5AD00142511 /* SelectChainViewController.swift in Sources */, ); @@ -2123,11 +2123,11 @@ A58E7D3D2872D55F0082D443 /* ChatView.swift in Sources */, A5629ABD2876CBC000094373 /* ChatListModule.swift in Sources */, A58E7CEB28729F550082D443 /* AppDelegate.swift in Sources */, + A51606FA2A2F47BD00CACB92 /* DefaultBIP44Provider.swift in Sources */, A578FA35287304A300AA7720 /* Color.swift in Sources */, A5629ADE2876CC6E00094373 /* InviteListModule.swift in Sources */, A5E776BA29F4362D00172091 /* AlertError.swift in Sources */, A578FA322873036400AA7720 /* InputView.swift in Sources */, - A51606F52A2E2DE000CACB92 /* DefaultDerivationProvider.swift in Sources */, A5A0843F29D2F625000B9B17 /* DefaultCryptoProvider.swift in Sources */, A5C2021B287E1FD8007E3188 /* ImportRouter.swift in Sources */, A5629AE42876E6D200094373 /* ThreadViewModel.swift in Sources */, @@ -2191,6 +2191,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + A51606F92A2F47BD00CACB92 /* DefaultBIP44Provider.swift in Sources */, A573C53729EC34A600E3CBFD /* SyncDerivationServiceTests.swift in Sources */, A5A0843E29D2F624000B9B17 /* DefaultCryptoProvider.swift in Sources */, 84CEC64628D89D6B00D081A8 /* PairingTests.swift in Sources */, @@ -2217,7 +2218,6 @@ A5E03DFD286465D100888481 /* Stubs.swift in Sources */, A507BE1A29E8032E0038EF70 /* EIP55Tests.swift in Sources */, A5A8E47A293A1C9B00FEB97D /* DefaultSocketFactory.swift in Sources */, - A51606F42A2E2DE000CACB92 /* DefaultDerivationProvider.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2243,6 +2243,7 @@ C56EE241293F566D004840D1 /* WalletModule.swift in Sources */, C56EE245293F566D004840D1 /* WalletPresenter.swift in Sources */, C56EE240293F566D004840D1 /* ScanQRView.swift in Sources */, + A51606FB2A2F47BD00CACB92 /* DefaultBIP44Provider.swift in Sources */, C56EE250293F566D004840D1 /* ScanTargetView.swift in Sources */, C56EE28F293F5757004840D1 /* MigrationConfigurator.swift in Sources */, 84B815552991217900FAD54E /* PushMessagesPresenter.swift in Sources */, @@ -2306,7 +2307,6 @@ C5B2F6F929705293000DBA0E /* SessionRequestPresenter.swift in Sources */, 84DB38F32983CDAE00BFEE37 /* PushRegisterer.swift in Sources */, C5B2F6FB297055B0000DBA0E /* ETHSigner.swift in Sources */, - A51606F62A2E2DE000CACB92 /* DefaultDerivationProvider.swift in Sources */, C56EE274293F56D7004840D1 /* SceneViewController.swift in Sources */, 847BD1E5298A806800076C90 /* NotificationsPresenter.swift in Sources */, C55D3496295DFA750004314A /* WelcomeInteractor.swift in Sources */, diff --git a/Example/IntegrationTests/Chat/ChatTests.swift b/Example/IntegrationTests/Chat/ChatTests.swift index 0566960f1..c2aaab6c5 100644 --- a/Example/IntegrationTests/Chat/ChatTests.swift +++ b/Example/IntegrationTests/Chat/ChatTests.swift @@ -59,7 +59,7 @@ final class ChatTests: XCTestCase { let syncClient = SyncClientFactory.create( networkInteractor: networkingInteractor, - derivator: DefaultDerivationProvider(), + bip44: DefaultBIP44Provider(), keychain: keychain ) diff --git a/Example/IntegrationTests/Sync/SyncDerivationServiceTests.swift b/Example/IntegrationTests/Sync/SyncDerivationServiceTests.swift index 01dc56c13..549ed6929 100644 --- a/Example/IntegrationTests/Sync/SyncDerivationServiceTests.swift +++ b/Example/IntegrationTests/Sync/SyncDerivationServiceTests.swift @@ -13,7 +13,7 @@ class SyncDerivationServiceTests: XCTestCase { let kms = KeyManagementService(keychain: keychain) let derivationService = SyncDerivationService( syncStorage: syncStorage, - derivator: DefaultDerivationProvider(), + bip44: DefaultBIP44Provider(), kms: kms ) diff --git a/Example/IntegrationTests/Sync/SyncTests.swift b/Example/IntegrationTests/Sync/SyncTests.swift index e44ace8c1..7eaf56828 100644 --- a/Example/IntegrationTests/Sync/SyncTests.swift +++ b/Example/IntegrationTests/Sync/SyncTests.swift @@ -55,7 +55,7 @@ final class SyncTests: XCTestCase { let syncSignatureStore = SyncSignatureStore(keychain: KeychainStorageMock()) let keychain = KeychainStorageMock() let kms = KeyManagementService(keychain: keychain) - let derivationService = SyncDerivationService(syncStorage: syncSignatureStore, derivator: DefaultDerivationProvider(), kms: kms) + let derivationService = SyncDerivationService(syncStorage: syncSignatureStore, bip44: DefaultBIP44Provider(), kms: kms) let logger = ConsoleLogger(suffix: suffix, loggingLevel: .debug) let relayClient = RelayClient(relayHost: InputConfig.relayHost, projectId: InputConfig.projectId, keychainStorage: keychain, socketFactory: DefaultSocketFactory(), logger: logger) let networkingInteractor = NetworkingClientFactory.create( diff --git a/Example/Shared/DefaultDerivationProvider.swift b/Example/Shared/DefaultBIP44Provider.swift similarity index 92% rename from Example/Shared/DefaultDerivationProvider.swift rename to Example/Shared/DefaultBIP44Provider.swift index 3152aba94..5aed02650 100644 --- a/Example/Shared/DefaultDerivationProvider.swift +++ b/Example/Shared/DefaultBIP44Provider.swift @@ -4,7 +4,7 @@ import Web3 import CryptoSwift import HDWalletKit -struct DefaultDerivationProvider: DerivationProvider { +struct DefaultBIP44Provider: BIP44Provider { public func derive(entropy: Data, path: [WalletConnectSigner.DerivationPath]) -> Data { let mnemonic = Mnemonic.create(entropy: entropy) diff --git a/Example/Showcase/Classes/DomainLayer/Chat/ChatService.swift b/Example/Showcase/Classes/DomainLayer/Chat/ChatService.swift index 4a886eb84..78d5a55cd 100644 --- a/Example/Showcase/Classes/DomainLayer/Chat/ChatService.swift +++ b/Example/Showcase/Classes/DomainLayer/Chat/ChatService.swift @@ -8,7 +8,7 @@ typealias Stream = AnyPublisher final class ChatService { private var client: ChatClient = { - Chat.configure(derivator: DefaultDerivationProvider()) + Chat.configure(bip44: DefaultBIP44Provider()) return Chat.instance }() diff --git a/Example/Showcase/Classes/PresentationLayer/Web3Inbox/Web3InboxViewController.swift b/Example/Showcase/Classes/PresentationLayer/Web3Inbox/Web3InboxViewController.swift index 30cce399c..4a7a87c8f 100644 --- a/Example/Showcase/Classes/PresentationLayer/Web3Inbox/Web3InboxViewController.swift +++ b/Example/Showcase/Classes/PresentationLayer/Web3Inbox/Web3InboxViewController.swift @@ -18,7 +18,7 @@ final class Web3InboxViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - Web3Inbox.configure(account: importAccount.account, derivator: DefaultDerivationProvider(), config: [.pushEnabled: false], environment: .sandbox, onSign: onSing) + Web3Inbox.configure(account: importAccount.account, bip44: DefaultBIP44Provider(), config: [.pushEnabled: false], environment: .sandbox, onSign: onSing) edgesForExtendedLayout = [] navigationItem.title = "Web3Inbox SDK" diff --git a/Sources/Chat/Chat.swift b/Sources/Chat/Chat.swift index 83bcceea2..003b64335 100644 --- a/Sources/Chat/Chat.swift +++ b/Sources/Chat/Chat.swift @@ -26,9 +26,9 @@ public class Chat { /// - crypto: Crypto utils implementation static public func configure( keyserverUrl: String = "https://keys.walletconnect.com", - derivator: DerivationProvider + bip44: BIP44Provider ) { Chat.keyserverUrl = keyserverUrl - Sync.configure(derivator: derivator) + Sync.configure(bip44: bip44) } } diff --git a/Sources/WalletConnectSigner/DerivationProvider.swift b/Sources/WalletConnectSigner/BIP44Provider.swift similarity index 82% rename from Sources/WalletConnectSigner/DerivationProvider.swift rename to Sources/WalletConnectSigner/BIP44Provider.swift index 89b94103d..806fe0c5e 100644 --- a/Sources/WalletConnectSigner/DerivationProvider.swift +++ b/Sources/WalletConnectSigner/BIP44Provider.swift @@ -5,6 +5,6 @@ public enum DerivationPath { case notHardened(UInt32) } -public protocol DerivationProvider { +public protocol BIP44Provider { func derive(entropy: Data, path: [DerivationPath]) -> Data } diff --git a/Sources/WalletConnectSync/Services/SyncDerivationService.swift b/Sources/WalletConnectSync/Services/SyncDerivationService.swift index c3abf9b17..d859f00fc 100644 --- a/Sources/WalletConnectSync/Services/SyncDerivationService.swift +++ b/Sources/WalletConnectSync/Services/SyncDerivationService.swift @@ -3,16 +3,16 @@ import Foundation final class SyncDerivationService { private let syncStorage: SyncSignatureStore - private let derivator: DerivationProvider + private let bip44: BIP44Provider private let kms: KeyManagementServiceProtocol init( syncStorage: SyncSignatureStore, - derivator: DerivationProvider, + bip44: BIP44Provider, kms: KeyManagementServiceProtocol ) { self.syncStorage = syncStorage - self.derivator = derivator + self.bip44 = bip44 self.kms = kms } @@ -34,7 +34,7 @@ final class SyncDerivationService { ] + slice.map { .notHardened($0) } let entropy = signatureData.sha256() - let storeKey = derivator.derive(entropy: entropy, path: path) + let storeKey = bip44.derive(entropy: entropy, path: path) let topic = storeKey.sha256().toHexString() let symmetricKey = try SymmetricKey(rawRepresentation: storeKey) diff --git a/Sources/WalletConnectSync/Sync.swift b/Sources/WalletConnectSync/Sync.swift index cf4261d72..6144b16e5 100644 --- a/Sources/WalletConnectSync/Sync.swift +++ b/Sources/WalletConnectSync/Sync.swift @@ -10,7 +10,7 @@ public class Sync { } return SyncClientFactory.create( networkInteractor: Networking.interactor, - derivator: config.derivator + bip44: config.bip44 ) }() @@ -21,7 +21,7 @@ public class Sync { /// Auth instance wallet config method. For DApp usage /// - Parameters: /// - crypto: Crypto utils implementation - static public func configure(derivator: DerivationProvider) { - Sync.config = Sync.Config(derivator: derivator) + static public func configure(bip44: BIP44Provider) { + Sync.config = Sync.Config(bip44: bip44) } } diff --git a/Sources/WalletConnectSync/SyncClientFactory.swift b/Sources/WalletConnectSync/SyncClientFactory.swift index 090756c92..da91d61fe 100644 --- a/Sources/WalletConnectSync/SyncClientFactory.swift +++ b/Sources/WalletConnectSync/SyncClientFactory.swift @@ -2,17 +2,17 @@ import Foundation final class SyncClientFactory { - static func create(networkInteractor: NetworkInteracting, derivator: DerivationProvider) -> SyncClient { + static func create(networkInteractor: NetworkInteracting, bip44: BIP44Provider) -> SyncClient { let keychain = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk") - return create(networkInteractor: networkInteractor, derivator: derivator, keychain: keychain) + return create(networkInteractor: networkInteractor, bip44: bip44, keychain: keychain) } - static func create(networkInteractor: NetworkInteracting, derivator: DerivationProvider, keychain: KeychainStorageProtocol) -> SyncClient { + static func create(networkInteractor: NetworkInteracting, bip44: BIP44Provider, keychain: KeychainStorageProtocol) -> SyncClient { let signatureStore = SyncSignatureStore(keychain: keychain) let kms = KeyManagementService(keychain: keychain) let deriviationService = SyncDerivationService( syncStorage: signatureStore, - derivator: derivator, + bip44: bip44, kms: kms ) let indexStore = CodableStore(defaults: UserDefaults.standard, identifier: SyncStorageIdentifiers.index.identifier) diff --git a/Sources/WalletConnectSync/SyncConfig.swift b/Sources/WalletConnectSync/SyncConfig.swift index 53613361e..a77e2d800 100644 --- a/Sources/WalletConnectSync/SyncConfig.swift +++ b/Sources/WalletConnectSync/SyncConfig.swift @@ -2,6 +2,6 @@ import Foundation extension Sync { struct Config { - let derivator: DerivationProvider + let bip44: BIP44Provider } } diff --git a/Sources/Web3Inbox/Web3Inbox.swift b/Sources/Web3Inbox/Web3Inbox.swift index aed0f5d86..e2555049e 100644 --- a/Sources/Web3Inbox/Web3Inbox.swift +++ b/Sources/Web3Inbox/Web3Inbox.swift @@ -19,7 +19,7 @@ public final class Web3Inbox { /// Web3Inbox instance config method static public func configure( account: Account, - derivator: DerivationProvider, + bip44: BIP44Provider, config: [ConfigParam: Bool] = [:], environment: APNSEnvironment, onSign: @escaping SigningCallback @@ -27,7 +27,7 @@ public final class Web3Inbox { Web3Inbox.account = account Web3Inbox.config = config Web3Inbox.onSign = onSign - Chat.configure(derivator: derivator) + Chat.configure(bip44: bip44) Push.configure(environment: environment) } } From 697babd6c239dca70e1a37b750a180ce29d233cd Mon Sep 17 00:00:00 2001 From: Radek Novak Date: Tue, 6 Jun 2023 14:28:30 +0200 Subject: [PATCH 43/85] Delete unused files --- .../Web3Modal/Extensions/View+Backport.swift | 38 +----- .../Networking/Common/Endpoint.swift | 113 ------------------ .../Networking/Common/HttpService.swift | 58 --------- 3 files changed, 2 insertions(+), 207 deletions(-) delete mode 100644 Sources/Web3Modal/Networking/Common/Endpoint.swift delete mode 100644 Sources/Web3Modal/Networking/Common/HttpService.swift diff --git a/Sources/Web3Modal/Extensions/View+Backport.swift b/Sources/Web3Modal/Extensions/View+Backport.swift index e178a1246..ccfd93ad4 100644 --- a/Sources/Web3Modal/Extensions/View+Backport.swift +++ b/Sources/Web3Modal/Extensions/View+Backport.swift @@ -1,37 +1,5 @@ -import SwiftUI import Combine - -struct Backport { - let content: Content -} - -extension View { - var backport: Backport { Backport(content: self) } -} - -extension Backport where Content: View { - - enum Visibility { - case automatic - case visible - case hidden - } - - @ViewBuilder func scrollContentBackground(_ visibility: Backport.Visibility) -> some View { - if #available(iOS 16, *) { - switch visibility { - case .automatic: - content.scrollContentBackground(.automatic) - case .hidden: - content.scrollContentBackground(.hidden) - case .visible: - content.scrollContentBackground(.visible) - } - } else { - content - } - } -} +import SwiftUI extension View { /// A backwards compatible wrapper for iOS 14 `onChange` @@ -39,11 +7,9 @@ extension View { if #available(iOS 14.0, *) { self.onChange(of: value, perform: perform) } else { - self.onReceive(Just(value)) { (value) in + self.onReceive(Just(value)) { value in perform(value) } } } } - - diff --git a/Sources/Web3Modal/Networking/Common/Endpoint.swift b/Sources/Web3Modal/Networking/Common/Endpoint.swift deleted file mode 100644 index 1962d080b..000000000 --- a/Sources/Web3Modal/Networking/Common/Endpoint.swift +++ /dev/null @@ -1,113 +0,0 @@ -import Foundation - -struct Endpoint { - let path: String - let queryItems: [URLQueryItem] - let headers: [Headers] - let method: Method - let host: String - let body: Data? - let validResponseCodes: Set - - public enum Method: String { - case GET - case POST - case PUT - case PATCH - case DELETE - } - - enum Headers { - /// Standard headers used for every network call - case standard - - public var makeHeader: [String: String] { - switch self { - case .standard: - return [ - "Content-Type": "application/json", - ] - } - } - } - - var urlRequest: URLRequest { - var urlRequest = URLRequest(url: urlForRequest) - urlRequest.httpMethod = method.rawValue - urlRequest.httpBody = body - urlRequest.allHTTPHeaderFields = makeHTTPHeaders(headers) - return urlRequest - } - - private var urlForRequest: URL { - var components = URLComponents() - components.scheme = "https" - components.host = host - components.path = path - - if !queryItems.isEmpty { - components.queryItems = queryItems - } - - guard - let url = components.url - else { - preconditionFailure( - """ - Failed to construct valid url, if setting up new endpoint - make sure you have prefix / in path such as /v1/users - """ - ) - } - - return url - } - - private func makeHTTPHeaders(_ headers: [Headers]) -> [String: String] { - headers.reduce(into: [String: String]()) { result, nextElement in - result = result.merging(nextElement.makeHeader) { _, new in new } - } - } -} - -extension Endpoint.Headers: Equatable { - static func == (lhs: Endpoint.Headers, rhs: Endpoint.Headers) -> Bool { - lhs.makeHeader == rhs.makeHeader - } -} - -extension Endpoint { - - /// Un-authenticated endpoint. - /// - Parameters: - /// - path: Path for your endpoint` - /// - headers: Specific headers - /// - method: .GET, .POST etc - /// - host: Host url - /// - shouldEncodePath: This setting affects how your url is constructed. - /// - body: If you need to pass parameters, provide them here. - /// - validResponseCodes: This is set to default value `Set(200 ..< 300)` - /// and can be overridden if needed - /// - Returns: Endpoint with URLRequest that gets passed directly to HttpService. - static func bare( - path: String, - queryItems: [URLQueryItem] = [], - headers: [Endpoint.Headers] = [], - method: Endpoint.Method, - host: String, - body: Data? = nil, - validResponseCodes: Set = Set(200 ..< 300) - ) -> Self { - var headers = headers - headers.append(.standard) - return Self( - path: path, - queryItems: queryItems, - headers: headers, - method: method, - host: host, - body: body, - validResponseCodes: validResponseCodes - ) - } -} diff --git a/Sources/Web3Modal/Networking/Common/HttpService.swift b/Sources/Web3Modal/Networking/Common/HttpService.swift deleted file mode 100644 index 3b4fe6971..000000000 --- a/Sources/Web3Modal/Networking/Common/HttpService.swift +++ /dev/null @@ -1,58 +0,0 @@ -import Foundation - -struct HttpService { - var performRequest: (_ endpoint: Endpoint) async throws -> Result -} - -extension HttpService { - - static var live: Self = .init(performRequest: { endpoint in - - let (data, response) = try await URLSession.shared.data(for: endpoint.urlRequest) - - let error = errorForResponse(response, data, validResponseCodes: endpoint.validResponseCodes) - if let error = error { - return .failure(error) - } else { - return .success(data) - } - }) - - private static func errorForResponse( - _ response: URLResponse?, - _ data: Data?, validResponseCodes: Set - ) -> Error? { - guard let httpResponse = response as? HTTPURLResponse else { - return nil - } - - if !validResponseCodes.contains(httpResponse.statusCode) { - return Errors.badResponseCode( - code: httpResponse.statusCode, - payload: data - ) - } - - return nil - } - - enum Errors: Error, Equatable { - case emptyResponse - case badResponseCode(code: Int, payload: Data?) - - public var properties: [String: String] { - switch self { - case let .badResponseCode(code, _): - return [ - "category": "http_error", - "http_code": "\(String(code))" - ] - case .emptyResponse: - return [ - "category": "payload", - "message": "Failed for empty response" - ] - } - } - } -} From ead6e10c1943d49f2d0327267f3323fe7a87a3be Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Tue, 6 Jun 2023 15:49:35 +0300 Subject: [PATCH 44/85] ENSResolverTests disabled for testplan --- Example/ExampleApp.xcodeproj/IntegrationTests.xctestplan | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Example/ExampleApp.xcodeproj/IntegrationTests.xctestplan b/Example/ExampleApp.xcodeproj/IntegrationTests.xctestplan index 597f8cc1f..47c6a2314 100644 --- a/Example/ExampleApp.xcodeproj/IntegrationTests.xctestplan +++ b/Example/ExampleApp.xcodeproj/IntegrationTests.xctestplan @@ -30,7 +30,8 @@ { "skippedTests" : [ "AuthTests\/testEIP1271RespondSuccess()", - "ChatTests" + "ChatTests", + "ENSResolverTests" ], "target" : { "containerPath" : "container:ExampleApp.xcodeproj", From 374161cf26c47435cba3ed2f5d2fc3e6a1cc6814 Mon Sep 17 00:00:00 2001 From: Radek Novak Date: Wed, 7 Jun 2023 11:19:23 +0200 Subject: [PATCH 45/85] Fix previews & add toast message for feedback --- .../Web3Modal/Modal/ModalContainerView.swift | 2 +- .../Web3Modal/Modal/ModalSheet+Previews.swift | 45 ++++-- Sources/Web3Modal/Modal/ModalSheet.swift | 1 + Sources/Web3Modal/Modal/ModalViewModel.swift | 21 ++- .../Web3Modal/Modal/Screens/WalletList.swift | 33 ++++- Sources/Web3Modal/UI/Common/Toast.swift | 130 ++++++++++++++++++ Sources/Web3Modal/Web3Modal.swift | 19 ++- 7 files changed, 214 insertions(+), 37 deletions(-) create mode 100644 Sources/Web3Modal/UI/Common/Toast.swift diff --git a/Sources/Web3Modal/Modal/ModalContainerView.swift b/Sources/Web3Modal/Modal/ModalContainerView.swift index 799fe13e5..c7583f14e 100644 --- a/Sources/Web3Modal/Modal/ModalContainerView.swift +++ b/Sources/Web3Modal/Modal/ModalContainerView.swift @@ -9,7 +9,7 @@ public struct ModalContainerView: View { public var body: some View { - VStack(spacing: 0) { + VStack(spacing: -10) { Color.thickOverlay .colorScheme(.light) diff --git a/Sources/Web3Modal/Modal/ModalSheet+Previews.swift b/Sources/Web3Modal/Modal/ModalSheet+Previews.swift index 0ac74e2f4..81b8a6f80 100644 --- a/Sources/Web3Modal/Modal/ModalSheet+Previews.swift +++ b/Sources/Web3Modal/Modal/ModalSheet+Previews.swift @@ -24,22 +24,39 @@ class WebSocketFactoryMock: WebSocketFactory { @available(iOS 14.0, *) struct ModalSheet_Previews: PreviewProvider { - static let projectId = "9bfe94c9cbf74aaa0597094ef561f703" - static let metadata = AppMetadata( - name: "Showcase App", - description: "Showcase description", - url: "example.wallet", - icons: ["https://avatars.githubusercontent.com/u/37784886"] - ) - + static var previews: some View { - ModalSheet( - viewModel: .init( - isShown: .constant(true), - interactor: DefaultModalSheetInteractor() + Content() + .previewLayout(.sizeThatFits) + } + + struct Content: View { + + init() { + + let projectId = "9bfe94c9cbf74aaa0597094ef561f703" + let metadata = AppMetadata( + name: "Showcase App", + description: "Showcase description", + url: "example.wallet", + icons: ["https://avatars.githubusercontent.com/u/37784886"] ) - ) - .previewLayout(.sizeThatFits) + + Networking.configure(projectId: projectId, socketFactory: WebSocketFactoryMock()) + Web3Modal.configure(projectId: projectId, metadata: metadata) + } + + var body: some View { + +// ModalSheet( +// viewModel: .init( +// isShown: .constant(true), +// interactor: DefaultModalSheetInteractor() +// ) +// ) + + ModalContainerView() + } } } diff --git a/Sources/Web3Modal/Modal/ModalSheet.swift b/Sources/Web3Modal/Modal/ModalSheet.swift index aa2d7bdad..fe7f8d63f 100644 --- a/Sources/Web3Modal/Modal/ModalSheet.swift +++ b/Sources/Web3Modal/Modal/ModalSheet.swift @@ -31,6 +31,7 @@ public struct ModalSheet: View { Color.background1 } ) + .toastView(toast: $viewModel.toast) } private func modalHeader() -> some View { diff --git a/Sources/Web3Modal/Modal/ModalViewModel.swift b/Sources/Web3Modal/Modal/ModalViewModel.swift index e58658085..637a3735c 100644 --- a/Sources/Web3Modal/Modal/ModalViewModel.swift +++ b/Sources/Web3Modal/Modal/ModalViewModel.swift @@ -30,18 +30,16 @@ enum Destination: Equatable { } final class ModalViewModel: ObservableObject { - - var isShown: Binding let interactor: ModalSheetInteractor let uiApplicationWrapper: UIApplicationWrapper - @Published private(set) var destinationStack: [Destination] = [.welcome] @Published private(set) var uri: String? - @Published private(set) var errorMessage: String? @Published private(set) var wallets: [Listing] = [] + @Published var toast: Toast? + var destination: Destination { destinationStack.last! } @@ -62,7 +60,8 @@ final class ModalViewModel: ObservableObject { .receive(on: DispatchQueue.main) .sink { sessions in print(sessions) - isShown.wrappedValue = false +// isShown.wrappedValue = false + self.toast = Toast(style: .success, message: "Session estabilished", duration: 15) } .store(in: &disposeBag) @@ -71,7 +70,7 @@ final class ModalViewModel: ObservableObject { .sink { (proposal, reason) in print(reason) - self.errorMessage = reason.message + self.toast = Toast(style: .error, message: reason.message) Task { await self.createURI() @@ -88,7 +87,7 @@ final class ModalViewModel: ObservableObject { deeplinkUri = wcUri.deeplinkUri } catch { print(error) - errorMessage = error.localizedDescription + toast = Toast(style: .error, message: error.localizedDescription) } } @@ -120,8 +119,8 @@ final class ModalViewModel: ObservableObject { func onCopyButton() { UIPasteboard.general.string = uri + toast = Toast(style: .info, message: "URI copied into clipboard") } - @MainActor func fetchWallets() async { @@ -144,7 +143,7 @@ final class ModalViewModel: ObservableObject { } } } catch { - print(error) + toast = Toast(style: .error, message: error.localizedDescription) } } } @@ -167,9 +166,7 @@ private extension ModalViewModel { throw Errors.noWalletLinkFound } } catch { - let alertController = UIAlertController(title: "Unable to open the app", message: nil, preferredStyle: .alert) - alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) - UIApplication.shared.windows.first?.rootViewController?.present(alertController, animated: true, completion: nil) + toast = Toast(style: .error, message: error.localizedDescription) } } diff --git a/Sources/Web3Modal/Modal/Screens/WalletList.swift b/Sources/Web3Modal/Modal/Screens/WalletList.swift index 2a6b4f6d7..e918e474a 100644 --- a/Sources/Web3Modal/Modal/Screens/WalletList.swift +++ b/Sources/Web3Modal/Modal/Screens/WalletList.swift @@ -6,6 +6,7 @@ struct WalletList: View { @Binding var wallets: [Listing] @Binding var destination: Destination + @State var retryButtonShown: Bool = false var navigateTo: (Destination) -> Void var onListingTap: (Listing) -> Void @@ -138,15 +139,11 @@ struct WalletList: View { guard let wallet else { return } navigateTo(.walletDetail(wallet)) - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - onListingTap(wallet) - } } } private func walletDetail(_ wallet: Listing) -> some View { - VStack { + VStack(spacing: 8) { WalletImage(wallet: wallet, size: .large) .frame(maxWidth: 96, maxHeight: 96) @@ -157,7 +154,33 @@ struct WalletList: View { Text("Accept connection request in the app") .font(.system(size: 14)) .foregroundColor(.foreground3) + + if retryButtonShown { + Button { + onListingTap(wallet) + } label: { + HStack { + Text("Try Again") + Image("external_link", bundle: .module) + } + } + .buttonStyle(W3MButtonStyle()) + .padding() + } } .padding() + .onAppear { + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + onListingTap(wallet) + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + retryButtonShown = true + } + } + .onDisappear { + retryButtonShown = false + } } } diff --git a/Sources/Web3Modal/UI/Common/Toast.swift b/Sources/Web3Modal/UI/Common/Toast.swift new file mode 100644 index 000000000..5093db23d --- /dev/null +++ b/Sources/Web3Modal/UI/Common/Toast.swift @@ -0,0 +1,130 @@ +import SwiftUI + +struct Toast: Equatable { + var style: ToastStyle + var message: String + var duration: Double = 3 + var width: Double = .infinity +} + +enum ToastStyle { + case error + case warning + case success + case info + + var themeColor: Color { + switch self { + case .error: return Color.red + case .warning: return Color.orange + case .info: return Color.blue + case .success: return Color.green + } + } + + var iconFileName: String { + switch self { + case .info: return "info.circle.fill" + case .warning: return "exclamationmark.triangle.fill" + case .success: return "checkmark.circle.fill" + case .error: return "xmark.circle.fill" + } + } +} + +struct ToastView: View { + var style: ToastStyle + var message: String + var width = CGFloat.infinity + var onCancelTapped: () -> Void + + var body: some View { + HStack(alignment: .center, spacing: 12) { + Image(systemName: style.iconFileName) + .foregroundColor(style.themeColor) + Text(message) + .font(Font.caption) + .foregroundColor(.foreground1) + + Spacer(minLength: 10) + + Button { + onCancelTapped() + } label: { + Image(systemName: "xmark") + .foregroundColor(style.themeColor) + } + } + .padding() + .frame(minWidth: 0, maxWidth: width) + .background(Color.background2) + .cornerRadius(8) + .padding(.horizontal, 16) + } +} + +struct ToastModifier: ViewModifier { + @Binding var toast: Toast? + @State private var workItem: DispatchWorkItem? + + func body(content: Content) -> some View { + content + .overlay( + ZStack { + mainToastView() + .offset(y: -64) + }.animation(.spring(), value: toast) + ) + .onChangeBackported(of: toast) { _ in + showToast() + } + } + + @ViewBuilder func mainToastView() -> some View { + if let toast = toast { + VStack { + ToastView( + style: toast.style, + message: toast.message, + width: toast.width + ) { + dismissToast() + } + Spacer() + } + } + } + + private func showToast() { + guard let toast = toast else { return } + + UIImpactFeedbackGenerator(style: .light) + .impactOccurred() + + if toast.duration > 0 { + workItem?.cancel() + + let task = DispatchWorkItem { + dismissToast() + } + + workItem = task + DispatchQueue.main.asyncAfter(deadline: .now() + toast.duration, execute: task) + } + } + + private func dismissToast() { + withAnimation { + toast = nil + } + + workItem?.cancel() + workItem = nil + } +} + +extension View { + func toastView(toast: Binding) -> some View { + modifier(ToastModifier(toast: toast)) + } +} diff --git a/Sources/Web3Modal/Web3Modal.swift b/Sources/Web3Modal/Web3Modal.swift index 286e2473c..ecffa540d 100644 --- a/Sources/Web3Modal/Web3Modal.swift +++ b/Sources/Web3Modal/Web3Modal.swift @@ -56,7 +56,10 @@ public class Web3Modal { } public static func present(from presentingViewController: UIViewController? = nil) { - let vc = presentingViewController ?? topViewController() + guard let vc = presentingViewController ?? topViewController() else { + assertionFailure("No controller found for presenting modal") + return + } if #available(iOS 14.0, *) { let modal = Web3ModalSheetController() @@ -64,9 +67,15 @@ public class Web3Modal { } } - static func topViewController( - _ base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController - ) -> UIViewController { + private static func topViewController(_ base: UIViewController? = nil) -> UIViewController? { + + let base = base ?? UIApplication + .shared + .connectedScenes + .flatMap { ($0 as? UIWindowScene)?.windows ?? [] } + .last { $0.isKeyWindow }? + .rootViewController + if let nav = base as? UINavigationController { return topViewController(nav.visibleViewController) } @@ -81,7 +90,7 @@ public class Web3Modal { return topViewController(presented) } - return base! + return base } } From 0eed6c7319e1799bb153a7ee75993a016cc37494 Mon Sep 17 00:00:00 2001 From: Radek Novak Date: Wed, 7 Jun 2023 11:39:29 +0200 Subject: [PATCH 46/85] Fix tests --- ...et+Previews.swift => Modal+Previews.swift} | 10 +----- .../Mocks/ModalSheetInteractorMock.swift | 19 ++++++++++-- .../Web3ModalTests/ModalViewModelTests.swift | 7 ++--- .../Mocks/SignClientMock.swift | 31 +++++++++++++++++++ 4 files changed, 52 insertions(+), 15 deletions(-) rename Sources/Web3Modal/Modal/{ModalSheet+Previews.swift => Modal+Previews.swift} (82%) diff --git a/Sources/Web3Modal/Modal/ModalSheet+Previews.swift b/Sources/Web3Modal/Modal/Modal+Previews.swift similarity index 82% rename from Sources/Web3Modal/Modal/ModalSheet+Previews.swift rename to Sources/Web3Modal/Modal/Modal+Previews.swift index 81b8a6f80..748a4a8f1 100644 --- a/Sources/Web3Modal/Modal/ModalSheet+Previews.swift +++ b/Sources/Web3Modal/Modal/Modal+Previews.swift @@ -23,7 +23,7 @@ class WebSocketFactoryMock: WebSocketFactory { } @available(iOS 14.0, *) -struct ModalSheet_Previews: PreviewProvider { +struct ModalContainerView_Previews: PreviewProvider { static var previews: some View { Content() @@ -47,14 +47,6 @@ struct ModalSheet_Previews: PreviewProvider { } var body: some View { - -// ModalSheet( -// viewModel: .init( -// isShown: .constant(true), -// interactor: DefaultModalSheetInteractor() -// ) -// ) - ModalContainerView() } } diff --git a/Tests/Web3ModalTests/Mocks/ModalSheetInteractorMock.swift b/Tests/Web3ModalTests/Mocks/ModalSheetInteractorMock.swift index c2f9e7e54..bcd2e3f01 100644 --- a/Tests/Web3ModalTests/Mocks/ModalSheetInteractorMock.swift +++ b/Tests/Web3ModalTests/Mocks/ModalSheetInteractorMock.swift @@ -19,11 +19,11 @@ final class ModalSheetInteractorMock: ModalSheetInteractor { self.listings = listings } - func getListings() async throws -> [Web3Modal.Listing] { + func getListings() async throws -> [Listing] { listings } - func connect() async throws -> WalletConnectURI { + func createPairingAndConnect() async throws -> WalletConnectURI { .init(topic: "foo", symKey: "bar", relay: .init(protocol: "irn", data: nil)) } @@ -31,4 +31,19 @@ final class ModalSheetInteractorMock: ModalSheetInteractor { Result.Publisher(Session(topic: "", pairingTopic: "", peer: .stub(), namespaces: [:], expiryDate: Date())) .eraseToAnyPublisher() } + + var sessionRejectionPublisher: AnyPublisher<(Session.Proposal, Reason), Never> { + let sessionProposal = Session.Proposal( + id: "", + pairingTopic: "", + proposer: AppMetadata(name: "", description: "", url: "", icons: []), + requiredNamespaces: [:], + optionalNamespaces: nil, + sessionProperties: nil, + proposal: SessionProposal(relays: [], proposer: Participant(publicKey: "", metadata: AppMetadata(name: "", description: "", url: "", icons: [])), requiredNamespaces: [:], optionalNamespaces: [:], sessionProperties: [:]) + ) + + return Result.Publisher((sessionProposal, SignReasonCode.userRejectedChains)) + .eraseToAnyPublisher() + } } diff --git a/Tests/Web3ModalTests/ModalViewModelTests.swift b/Tests/Web3ModalTests/ModalViewModelTests.swift index 7965a201d..fb01ebc23 100644 --- a/Tests/Web3ModalTests/ModalViewModelTests.swift +++ b/Tests/Web3ModalTests/ModalViewModelTests.swift @@ -3,7 +3,7 @@ import TestingUtils import XCTest final class ModalViewModelTests: XCTestCase { - private var sut: ModalSheet.ModalViewModel! + private var sut: ModalViewModel! private var openURLFuncTest: FuncTest! private var canOpenURLFuncTest: FuncTest! @@ -17,7 +17,6 @@ final class ModalViewModelTests: XCTestCase { sut = .init( isShown: .constant(true), - projectId: "", interactor: ModalSheetInteractorMock(listings: [ Listing(id: "1", name: "Sample App", homepage: "https://example.com", order: 1, imageId: "1", app: Listing.App(ios: "https://example.com/download-ios", mac: "https://example.com/download-mac", safari: "https://example.com/download-safari"), mobile: Listing.Mobile(native: nil, universal: "https://example.com/universal")), Listing(id: "2", name: "Awesome App", homepage: "https://example.com/awesome", order: 2, imageId: "2", app: Listing.App(ios: "https://example.com/download-ios", mac: "https://example.com/download-mac", safari: "https://example.com/download-safari"), mobile: Listing.Mobile(native: "awesomeapp://deeplink", universal: "https://awesome.com/awesome/universal")), @@ -57,7 +56,7 @@ final class ModalViewModelTests: XCTestCase { expectation = XCTestExpectation(description: "Wait for openUrl to be called") - sut.onWalletTapped(index: 0) + sut.onListingTap(sut.wallets[0]) XCTWaiter.wait(for: [expectation], timeout: 3) @@ -68,7 +67,7 @@ final class ModalViewModelTests: XCTestCase { expectation = XCTestExpectation(description: "Wait for openUrl to be called 2nd time") - sut.onWalletTapped(index: 1) + sut.onListingTap(sut.wallets[1]) XCTWaiter.wait(for: [expectation], timeout: 3) diff --git a/Tests/Web3WalletTests/Mocks/SignClientMock.swift b/Tests/Web3WalletTests/Mocks/SignClientMock.swift index 3a9b6336d..fec7b51c4 100644 --- a/Tests/Web3WalletTests/Mocks/SignClientMock.swift +++ b/Tests/Web3WalletTests/Mocks/SignClientMock.swift @@ -4,6 +4,7 @@ import Combine @testable import WalletConnectSign final class SignClientMock: SignClientProtocol { + var approveCalled = false var rejectCalled = false var updateCalled = false @@ -13,6 +14,8 @@ final class SignClientMock: SignClientProtocol { var pairCalled = false var disconnectCalled = false var cleanupCalled = false + var connectCalled = false + var requestCalled = false private let metadata = AppMetadata(name: "", description: "", url: "", icons: []) private let request = WalletConnectSign.Request(id: .left(""), topic: "", method: "", params: "", chainId: Blockchain("eip155:1")!, expiry: nil) @@ -57,6 +60,21 @@ final class SignClientMock: SignClientProtocol { .eraseToAnyPublisher() } + var sessionRejectionPublisher: AnyPublisher<(Session.Proposal, Reason), Never> { + let sessionProposal = Session.Proposal( + id: "", + pairingTopic: "", + proposer: AppMetadata(name: "", description: "", url: "", icons: []), + requiredNamespaces: [:], + optionalNamespaces: nil, + sessionProperties: nil, + proposal: SessionProposal(relays: [], proposer: Participant(publicKey: "", metadata: AppMetadata(name: "", description: "", url: "", icons: [])), requiredNamespaces: [:], optionalNamespaces: [:], sessionProperties: [:]) + ) + + return Result.Publisher((sessionProposal, SignReasonCode.userRejectedChains)) + .eraseToAnyPublisher() + } + var sessionResponsePublisher: AnyPublisher { return Result.Publisher(.success(response)) .eraseToAnyPublisher() @@ -109,4 +127,17 @@ final class SignClientMock: SignClientProtocol { func cleanup() async throws { cleanupCalled = true } + + func connect( + requiredNamespaces: [String : WalletConnectSign.ProposalNamespace], + optionalNamespaces: [String : WalletConnectSign.ProposalNamespace]?, + sessionProperties: [String : String]?, + topic: String + ) async throws { + connectCalled = true + } + + func request(params: WalletConnectSign.Request) async throws { + requestCalled = true + } } From 084f58687ff4b25642524aecf0eb893cb1f7c6ff Mon Sep 17 00:00:00 2001 From: Radek Novak Date: Wed, 7 Jun 2023 12:03:49 +0200 Subject: [PATCH 47/85] Cleanup --- Example/DApp/Sign/SignCoordinator.swift | 3 +- .../Configurator/AppearanceConfigurator.swift | 2 +- .../WalletConnectPairing/PairingClient.swift | 2 +- .../PairingInteracting.swift | 2 +- .../Web3Modal/Extensions/View+Backport.swift | 34 ------ .../Networking/Common/Endpoint.swift | 113 ------------------ .../Networking/Common/HttpService.swift | 58 --------- 7 files changed, 4 insertions(+), 210 deletions(-) delete mode 100644 Sources/Web3Modal/Networking/Common/Endpoint.swift delete mode 100644 Sources/Web3Modal/Networking/Common/HttpService.swift diff --git a/Example/DApp/Sign/SignCoordinator.swift b/Example/DApp/Sign/SignCoordinator.swift index fa527a6e6..55344a902 100644 --- a/Example/DApp/Sign/SignCoordinator.swift +++ b/Example/DApp/Sign/SignCoordinator.swift @@ -24,8 +24,7 @@ final class SignCoordinator { name: "Swift Dapp", description: "WalletConnect DApp sample", url: "wallet.connect", - icons: ["https://avatars.githubusercontent.com/u/37784886"] - ) + icons: ["https://avatars.githubusercontent.com/u/37784886"]) Pair.configure(metadata: metadata) #if DEBUG diff --git a/Example/Showcase/Classes/ApplicationLayer/Configurator/AppearanceConfigurator.swift b/Example/Showcase/Classes/ApplicationLayer/Configurator/AppearanceConfigurator.swift index d6bb58b30..5ad69dfb5 100644 --- a/Example/Showcase/Classes/ApplicationLayer/Configurator/AppearanceConfigurator.swift +++ b/Example/Showcase/Classes/ApplicationLayer/Configurator/AppearanceConfigurator.swift @@ -14,6 +14,6 @@ struct AppearanceConfigurator: Configurator { UINavigationBar.appearance().scrollEdgeAppearance = appearance UINavigationBar.appearance().compactAppearance = appearance -// UIApplication.currentWindow.overrideUserInterfaceStyle = .dark + UIApplication.currentWindow.overrideUserInterfaceStyle = .dark } } diff --git a/Sources/WalletConnectPairing/PairingClient.swift b/Sources/WalletConnectPairing/PairingClient.swift index 257b90ae7..389c11354 100644 --- a/Sources/WalletConnectPairing/PairingClient.swift +++ b/Sources/WalletConnectPairing/PairingClient.swift @@ -74,7 +74,7 @@ public class PairingClient: PairingRegisterer, PairingInteracting, PairingClient try await walletPairService.pair(uri) } - public func create() async throws -> WalletConnectURI { + public func create() async throws -> WalletConnectURI { return try await appPairService.create() } diff --git a/Sources/WalletConnectPairing/PairingInteracting.swift b/Sources/WalletConnectPairing/PairingInteracting.swift index c71d8c9bc..b0eb2cd38 100644 --- a/Sources/WalletConnectPairing/PairingInteracting.swift +++ b/Sources/WalletConnectPairing/PairingInteracting.swift @@ -3,7 +3,7 @@ import Foundation public protocol PairingInteracting { func pair(uri: WalletConnectURI) async throws - func create() async throws -> WalletConnectURI + func create() async throws -> WalletConnectURI func getPairings() -> [Pairing] diff --git a/Sources/Web3Modal/Extensions/View+Backport.swift b/Sources/Web3Modal/Extensions/View+Backport.swift index 917f57955..ccfd93ad4 100644 --- a/Sources/Web3Modal/Extensions/View+Backport.swift +++ b/Sources/Web3Modal/Extensions/View+Backport.swift @@ -1,38 +1,6 @@ import Combine import SwiftUI -struct Backport { - let content: Content -} - -extension View { - var backport: Backport { Backport(content: self) } -} - -extension Backport where Content: View { - - enum Visibility { - case automatic - case visible - case hidden - } - - @ViewBuilder func scrollContentBackground(_ visibility: Backport.Visibility) -> some View { - if #available(iOS 16, *) { - switch visibility { - case .automatic: - content.scrollContentBackground(.automatic) - case .hidden: - content.scrollContentBackground(.hidden) - case .visible: - content.scrollContentBackground(.visible) - } - } else { - content - } - } -} - extension View { /// A backwards compatible wrapper for iOS 14 `onChange` @ViewBuilder func onChangeBackported(of value: T, perform: @escaping (T) -> Void) -> some View { @@ -45,5 +13,3 @@ extension View { } } } - - diff --git a/Sources/Web3Modal/Networking/Common/Endpoint.swift b/Sources/Web3Modal/Networking/Common/Endpoint.swift deleted file mode 100644 index 1962d080b..000000000 --- a/Sources/Web3Modal/Networking/Common/Endpoint.swift +++ /dev/null @@ -1,113 +0,0 @@ -import Foundation - -struct Endpoint { - let path: String - let queryItems: [URLQueryItem] - let headers: [Headers] - let method: Method - let host: String - let body: Data? - let validResponseCodes: Set - - public enum Method: String { - case GET - case POST - case PUT - case PATCH - case DELETE - } - - enum Headers { - /// Standard headers used for every network call - case standard - - public var makeHeader: [String: String] { - switch self { - case .standard: - return [ - "Content-Type": "application/json", - ] - } - } - } - - var urlRequest: URLRequest { - var urlRequest = URLRequest(url: urlForRequest) - urlRequest.httpMethod = method.rawValue - urlRequest.httpBody = body - urlRequest.allHTTPHeaderFields = makeHTTPHeaders(headers) - return urlRequest - } - - private var urlForRequest: URL { - var components = URLComponents() - components.scheme = "https" - components.host = host - components.path = path - - if !queryItems.isEmpty { - components.queryItems = queryItems - } - - guard - let url = components.url - else { - preconditionFailure( - """ - Failed to construct valid url, if setting up new endpoint - make sure you have prefix / in path such as /v1/users - """ - ) - } - - return url - } - - private func makeHTTPHeaders(_ headers: [Headers]) -> [String: String] { - headers.reduce(into: [String: String]()) { result, nextElement in - result = result.merging(nextElement.makeHeader) { _, new in new } - } - } -} - -extension Endpoint.Headers: Equatable { - static func == (lhs: Endpoint.Headers, rhs: Endpoint.Headers) -> Bool { - lhs.makeHeader == rhs.makeHeader - } -} - -extension Endpoint { - - /// Un-authenticated endpoint. - /// - Parameters: - /// - path: Path for your endpoint` - /// - headers: Specific headers - /// - method: .GET, .POST etc - /// - host: Host url - /// - shouldEncodePath: This setting affects how your url is constructed. - /// - body: If you need to pass parameters, provide them here. - /// - validResponseCodes: This is set to default value `Set(200 ..< 300)` - /// and can be overridden if needed - /// - Returns: Endpoint with URLRequest that gets passed directly to HttpService. - static func bare( - path: String, - queryItems: [URLQueryItem] = [], - headers: [Endpoint.Headers] = [], - method: Endpoint.Method, - host: String, - body: Data? = nil, - validResponseCodes: Set = Set(200 ..< 300) - ) -> Self { - var headers = headers - headers.append(.standard) - return Self( - path: path, - queryItems: queryItems, - headers: headers, - method: method, - host: host, - body: body, - validResponseCodes: validResponseCodes - ) - } -} diff --git a/Sources/Web3Modal/Networking/Common/HttpService.swift b/Sources/Web3Modal/Networking/Common/HttpService.swift deleted file mode 100644 index 3b4fe6971..000000000 --- a/Sources/Web3Modal/Networking/Common/HttpService.swift +++ /dev/null @@ -1,58 +0,0 @@ -import Foundation - -struct HttpService { - var performRequest: (_ endpoint: Endpoint) async throws -> Result -} - -extension HttpService { - - static var live: Self = .init(performRequest: { endpoint in - - let (data, response) = try await URLSession.shared.data(for: endpoint.urlRequest) - - let error = errorForResponse(response, data, validResponseCodes: endpoint.validResponseCodes) - if let error = error { - return .failure(error) - } else { - return .success(data) - } - }) - - private static func errorForResponse( - _ response: URLResponse?, - _ data: Data?, validResponseCodes: Set - ) -> Error? { - guard let httpResponse = response as? HTTPURLResponse else { - return nil - } - - if !validResponseCodes.contains(httpResponse.statusCode) { - return Errors.badResponseCode( - code: httpResponse.statusCode, - payload: data - ) - } - - return nil - } - - enum Errors: Error, Equatable { - case emptyResponse - case badResponseCode(code: Int, payload: Data?) - - public var properties: [String: String] { - switch self { - case let .badResponseCode(code, _): - return [ - "category": "http_error", - "http_code": "\(String(code))" - ] - case .emptyResponse: - return [ - "category": "payload", - "message": "Failed for empty response" - ] - } - } - } -} From cb39450b9ac45939df50ddf2beb0515f85823c0a Mon Sep 17 00:00:00 2001 From: Radek Novak Date: Wed, 7 Jun 2023 12:54:12 +0200 Subject: [PATCH 48/85] Remove restore key for spm cache --- .github/actions/build/action.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml index 0e3715428..3abeab9c8 100644 --- a/.github/actions/build/action.yml +++ b/.github/actions/build/action.yml @@ -20,8 +20,6 @@ runs: **/SourcePackagesCache DerivedDataCache key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }} - restore-keys: | - ${{ runner.os }}-spm- - name: Build for testing shell: bash From 5b8fc9088d44fea268f5f512efaaf0ddf5396983 Mon Sep 17 00:00:00 2001 From: Radek Novak Date: Wed, 7 Jun 2023 12:54:12 +0200 Subject: [PATCH 49/85] Remove restore key for spm cache --- .github/actions/build/action.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml index 0e3715428..3abeab9c8 100644 --- a/.github/actions/build/action.yml +++ b/.github/actions/build/action.yml @@ -20,8 +20,6 @@ runs: **/SourcePackagesCache DerivedDataCache key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }} - restore-keys: | - ${{ runner.os }}-spm- - name: Build for testing shell: bash From a41e3499c2a174a166b9d1b3a39ab8dfd4022ba7 Mon Sep 17 00:00:00 2001 From: Radek Novak Date: Wed, 7 Jun 2023 14:24:04 +0200 Subject: [PATCH 50/85] Cache only SPM & Track Package.resolved --- .github/actions/build/action.yml | 1 - .github/workflows/build_artifacts.yml | 3 - .gitignore | 1 - .../xcshareddata/swiftpm/Package.resolved | 133 ++++++++++++++++++ 4 files changed, 133 insertions(+), 5 deletions(-) create mode 100644 Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml index 3abeab9c8..eaa7f077a 100644 --- a/.github/actions/build/action.yml +++ b/.github/actions/build/action.yml @@ -18,7 +18,6 @@ runs: with: path: | **/SourcePackagesCache - DerivedDataCache key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }} - name: Build for testing diff --git a/.github/workflows/build_artifacts.yml b/.github/workflows/build_artifacts.yml index b51a09e9f..cc8adb7a5 100644 --- a/.github/workflows/build_artifacts.yml +++ b/.github/workflows/build_artifacts.yml @@ -25,10 +25,7 @@ jobs: with: path: | **/SourcePackagesCache - DerivedDataCache key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }} - restore-keys: | - ${{ runner.os }}-spm- - name: Build for testing on workflow_dispatch if: ${{ github.event_name == 'workflow_dispatch' }} diff --git a/.gitignore b/.gitignore index 172334855..9ad6aa3e8 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,6 @@ xcuserdata/ # Swift Package Manager Packages/ Package.pins -Package.resolved /*.xcodeproj # Fastlane diff --git a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 000000000..223fbcb65 --- /dev/null +++ b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,133 @@ +{ + "object": { + "pins": [ + { + "package": "BigInt", + "repositoryURL": "https://github.com/attaswift/BigInt.git", + "state": { + "branch": null, + "revision": "0ed110f7555c34ff468e72e1686e59721f2b0da6", + "version": "5.3.0" + } + }, + { + "package": "CryptoSwift", + "repositoryURL": "https://github.com/krzyzanowskim/CryptoSwift.git", + "state": { + "branch": null, + "revision": "eee9ad754926c40a0f7e73f152357d37b119b7fa", + "version": "1.7.1" + } + }, + { + "package": "HDWalletKit", + "repositoryURL": "https://github.com/WalletConnect/HDWallet", + "state": { + "branch": "develop", + "revision": "748a85b1dfe9a2fa592bd9266c5a926e4e1d3f44", + "version": null + } + }, + { + "package": "PromiseKit", + "repositoryURL": "https://github.com/mxcl/PromiseKit.git", + "state": { + "branch": null, + "revision": "8a98e31a47854d3180882c8068cc4d9381bf382d", + "version": "6.22.1" + } + }, + { + "package": "QRCode", + "repositoryURL": "https://github.com/WalletConnect/QRCode", + "state": { + "branch": null, + "revision": "263f280d2c8144adfb0b6676109846cfc8dd552b", + "version": "14.3.1" + } + }, + { + "package": "secp256k1", + "repositoryURL": "https://github.com/Boilertalk/secp256k1.swift.git", + "state": { + "branch": null, + "revision": "cd187c632fb812fd93711a9f7e644adb7e5f97f0", + "version": "0.1.7" + } + }, + { + "package": "SolanaSwift", + "repositoryURL": "https://github.com/flypaper0/solana-swift", + "state": { + "branch": "feature/available-13", + "revision": "a98811518e0a90c2dfc60c30cfd3ec85c33b6790", + "version": null + } + }, + { + "package": "Starscream", + "repositoryURL": "https://github.com/daltoniam/Starscream", + "state": { + "branch": null, + "revision": "a063fda2b8145a231953c20e7a646be254365396", + "version": "3.1.2" + } + }, + { + "package": "swift-qrcode-generator", + "repositoryURL": "https://github.com/dagronf/swift-qrcode-generator", + "state": { + "branch": null, + "revision": "5ca09b6a2ad190f94aa3d6ddef45b187f8c0343b", + "version": "1.0.3" + } + }, + { + "package": "SwiftImageReadWrite", + "repositoryURL": "https://github.com/dagronf/SwiftImageReadWrite", + "state": { + "branch": null, + "revision": "5596407d1cf61b953b8e658fa8636a471df3c509", + "version": "1.1.6" + } + }, + { + "package": "swiftui-async-button", + "repositoryURL": "https://github.com/lorenzofiamingo/swiftui-async-button", + "state": { + "branch": null, + "revision": "9fe9ccddf59c7e4185aa978547fbb9d95236455e", + "version": "1.1.0" + } + }, + { + "package": "Task_retrying", + "repositoryURL": "https://github.com/bigearsenal/task-retrying-swift.git", + "state": { + "branch": null, + "revision": "1249b3524378423c848cef39fb220041e00a08ec", + "version": "1.0.4" + } + }, + { + "package": "TweetNacl", + "repositoryURL": "https://github.com/bitmark-inc/tweetnacl-swiftwrap.git", + "state": { + "branch": null, + "revision": "f8fd111642bf2336b11ef9ea828510693106e954", + "version": "1.1.0" + } + }, + { + "package": "Web3", + "repositoryURL": "https://github.com/WalletConnect/Web3.swift", + "state": { + "branch": null, + "revision": "569255adcfff0b37e4cb8004aea29d0e2d6266df", + "version": "1.0.2" + } + } + ] + }, + "version": 1 +} From b3750ef0cadcfc10b52749fc92c0d2f20319056c Mon Sep 17 00:00:00 2001 From: Radek Novak Date: Wed, 7 Jun 2023 16:25:57 +0200 Subject: [PATCH 51/85] Fix tests and Web3Inbox.configure call --- .../Configurator/ThirdPartyConfigurator.swift | 8 +++++++- Tests/Web3ModalTests/ModalViewModelTests.swift | 6 +++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Example/WalletApp/ApplicationLayer/Configurator/ThirdPartyConfigurator.swift b/Example/WalletApp/ApplicationLayer/Configurator/ThirdPartyConfigurator.swift index 3b39114d3..d90554a5d 100644 --- a/Example/WalletApp/ApplicationLayer/Configurator/ThirdPartyConfigurator.swift +++ b/Example/WalletApp/ApplicationLayer/Configurator/ThirdPartyConfigurator.swift @@ -19,7 +19,13 @@ struct ThirdPartyConfigurator: Configurator { let account = Account(blockchain: Blockchain("eip155:1")!, address: EthKeyStore.shared.address)! - Web3Inbox.configure(account: account, crypto: DefaultCryptoProvider(), config: [.chatEnabled: false, .settingsEnabled: false], environment: BuildConfiguration.shared.apnsEnvironment, onSign: Web3InboxSigner.onSing) + Web3Inbox.configure( + account: account, + bip44: DefaultBIP44Provider(), + config: [.chatEnabled: false, .settingsEnabled: false], + environment: BuildConfiguration.shared.apnsEnvironment, + onSign: Web3InboxSigner.onSing + ) } } diff --git a/Tests/Web3ModalTests/ModalViewModelTests.swift b/Tests/Web3ModalTests/ModalViewModelTests.swift index 7965a201d..1c9cc3996 100644 --- a/Tests/Web3ModalTests/ModalViewModelTests.swift +++ b/Tests/Web3ModalTests/ModalViewModelTests.swift @@ -3,7 +3,7 @@ import TestingUtils import XCTest final class ModalViewModelTests: XCTestCase { - private var sut: ModalSheet.ModalViewModel! + private var sut: ModalViewModel! private var openURLFuncTest: FuncTest! private var canOpenURLFuncTest: FuncTest! @@ -57,7 +57,7 @@ final class ModalViewModelTests: XCTestCase { expectation = XCTestExpectation(description: "Wait for openUrl to be called") - sut.onWalletTapped(index: 0) + sut.onListingTap(sut.wallets[0]) XCTWaiter.wait(for: [expectation], timeout: 3) @@ -68,7 +68,7 @@ final class ModalViewModelTests: XCTestCase { expectation = XCTestExpectation(description: "Wait for openUrl to be called 2nd time") - sut.onWalletTapped(index: 1) + sut.onListingTap(sut.wallets[1]) XCTWaiter.wait(for: [expectation], timeout: 3) From ad6c35759dd48cfa48d3cab205ee1a657a9e9244 Mon Sep 17 00:00:00 2001 From: Radek Novak Date: Wed, 7 Jun 2023 16:57:27 +0200 Subject: [PATCH 52/85] Fix checkout --- .github/actions/build/action.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml index eaa7f077a..caba3649b 100644 --- a/.github/actions/build/action.yml +++ b/.github/actions/build/action.yml @@ -13,6 +13,8 @@ runs: using: "composite" steps: - uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.sha }} - uses: actions/cache@v3 with: From 90f25819ab2918436ab79813ecb8c18dd395d045 Mon Sep 17 00:00:00 2001 From: Radek Novak Date: Wed, 7 Jun 2023 17:14:22 +0200 Subject: [PATCH 53/85] Test different ref --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c6ced26fd..7acd8274b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,8 @@ jobs: runs-on: macos-12 steps: - uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.sha }} - uses: ./.github/actions/build with: @@ -41,6 +43,8 @@ jobs: steps: - uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.sha }} - uses: actions/cache/restore@v3 with: From 942e901f6974029c7be7185259349c4dd6eb3642 Mon Sep 17 00:00:00 2001 From: Radek Novak Date: Thu, 8 Jun 2023 14:42:21 +0200 Subject: [PATCH 54/85] Sign using web3modal account --- .../xcshareddata/swiftpm/Package.resolved | 12 +-- .../AccountStorage/AccountStorage.swift | 2 + .../DomainLayer/Chat/ChatService.swift | 75 +++++++++++++++++-- .../DomainLayer/Chat/ImportAccount.swift | 7 +- .../Chat/ChatList/ChatListInteractor.swift | 2 +- .../Chat/Import/ImportInteractor.swift | 2 +- .../Chat/Import/ImportPresenter.swift | 10 +++ .../Chat/Welcome/WelcomeInteractor.swift | 2 +- 8 files changed, 95 insertions(+), 17 deletions(-) diff --git a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 223fbcb65..25ef09b34 100644 --- a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -15,8 +15,8 @@ "repositoryURL": "https://github.com/krzyzanowskim/CryptoSwift.git", "state": { "branch": null, - "revision": "eee9ad754926c40a0f7e73f152357d37b119b7fa", - "version": "1.7.1" + "revision": "19b3c3ceed117c5cc883517c4e658548315ba70b", + "version": "1.6.0" } }, { @@ -33,8 +33,8 @@ "repositoryURL": "https://github.com/mxcl/PromiseKit.git", "state": { "branch": null, - "revision": "8a98e31a47854d3180882c8068cc4d9381bf382d", - "version": "6.22.1" + "revision": "43772616c46a44a9977e41924ae01d0e55f2f9ca", + "version": "6.18.1" } }, { @@ -105,8 +105,8 @@ "repositoryURL": "https://github.com/bigearsenal/task-retrying-swift.git", "state": { "branch": null, - "revision": "1249b3524378423c848cef39fb220041e00a08ec", - "version": "1.0.4" + "revision": "645eaaf207a6f39ab4b469558d916ae23df199b5", + "version": "1.0.3" } }, { diff --git a/Example/Showcase/Classes/DomainLayer/AccountStorage/AccountStorage.swift b/Example/Showcase/Classes/DomainLayer/AccountStorage/AccountStorage.swift index c8bfc44db..0f8c805ab 100644 --- a/Example/Showcase/Classes/DomainLayer/AccountStorage/AccountStorage.swift +++ b/Example/Showcase/Classes/DomainLayer/AccountStorage/AccountStorage.swift @@ -33,6 +33,8 @@ private extension ImportAccount { return name case .custom(let privateKey): return privateKey + case .web3Modal(let account): + return account.absoluteString } } } diff --git a/Example/Showcase/Classes/DomainLayer/Chat/ChatService.swift b/Example/Showcase/Classes/DomainLayer/Chat/ChatService.swift index 78d5a55cd..9e21f81c3 100644 --- a/Example/Showcase/Classes/DomainLayer/Chat/ChatService.swift +++ b/Example/Showcase/Classes/DomainLayer/Chat/ChatService.swift @@ -2,6 +2,7 @@ import Foundation import Combine import WalletConnectChat import WalletConnectRelay +import WalletConnectSign typealias Stream = AnyPublisher @@ -81,7 +82,7 @@ final class ChatService { try await client.reject(inviteId: invite.id) } - func goPublic(account: Account, privateKey: String) async throws { + func goPublic(account: Account) async throws { try await client.goPublic(account: account) } @@ -91,17 +92,15 @@ final class ChatService { try await client.invite(invite: invite) } - func register(account: Account, privateKey: String) async throws { + func register(account: Account, importAccount: ImportAccount) async throws { _ = try await client.register(account: account) { message in - let signature = self.onSign(message: message, privateKey: privateKey) - return SigningResult.signed(signature) + return await self.onSign(message: message, importAccount: importAccount) } } - func unregister(account: Account, privateKey: String) async throws { + func unregister(account: Account, importAccount: ImportAccount) async throws { try await client.unregister(account: account) { message in - let signature = self.onSign(message: message, privateKey: privateKey) - return SigningResult.signed(signature) + return await self.onSign(message: message, importAccount: importAccount) } } @@ -116,9 +115,71 @@ final class ChatService { private extension ChatService { + func onSign(message: String, importAccount: ImportAccount) async -> SigningResult { + switch importAccount { + case .swift, .kotlin, .js, .custom: + return .signed(onSign(message: message, privateKey: importAccount.privateKey)) + case .web3Modal(let account): + return await onWeb3ModalSign(message: message, account: account) + } + } + func onSign(message: String, privateKey: String) -> CacaoSignature { let privateKey = Data(hex: privateKey) let signer = MessageSignerFactory(signerFactory: DefaultSignerFactory()).create() return try! signer.sign(message: message, privateKey: privateKey, type: .eip191) } + + func onWeb3ModalSign(message: String, account: Account) async -> SigningResult { + guard let session = Sign.instance.getSessions().last else { return .rejected } + + do { + let request = makeRequest(session: session, message: message, account: account) + try await Sign.instance.request(params: request) + + let signature: CacaoSignature = try await withCheckedThrowingContinuation { continuation in + var cancellable: AnyCancellable? + cancellable = Sign.instance.sessionResponsePublisher + .sink { response in + defer { cancellable?.cancel() } + switch response.result { + case .response(let value): + do { + let string = try value.get(String.self) + let signature = CacaoSignature(t: .eip191, s: string.deleting0x()) + continuation.resume(returning: signature) + } catch { + continuation.resume(throwing: error) + } + case .error(let error): + continuation.resume(throwing: error) + } + } + } + + return .signed(signature) + } catch { + return .rejected + } + } + + func makeRequest(session: WalletConnectSign.Session, message: String, account: Account) -> Request { + return Request( + topic: session.topic, + method: "personal_sign", + params: AnyCodable(["0x" + message.data(using: .utf8)!.toHexString(), account.address]), + chainId: Blockchain("eip155:1")! + ) + } +} + +fileprivate extension String { + + func deleting0x() -> String { + var string = self + if starts(with: "0x") { + string.removeFirst(2) + } + return string + } } diff --git a/Example/Showcase/Classes/DomainLayer/Chat/ImportAccount.swift b/Example/Showcase/Classes/DomainLayer/Chat/ImportAccount.swift index 648271f77..9f0800edb 100644 --- a/Example/Showcase/Classes/DomainLayer/Chat/ImportAccount.swift +++ b/Example/Showcase/Classes/DomainLayer/Chat/ImportAccount.swift @@ -6,6 +6,7 @@ enum ImportAccount { case kotlin case js case custom(privateKey: String) + case web3Modal(account: Account) init?(input: String) { switch input.lowercased() { @@ -34,7 +35,7 @@ enum ImportAccount { return "kotlin.eth" case .js: return "js.eth" - case .custom: + case .custom, .web3Modal: return account.address } } @@ -50,6 +51,8 @@ enum ImportAccount { case .custom(let privateKey): let address = try! EthereumPrivateKey(hexPrivateKey: "0x" + privateKey, ctx: nil).address.hex(eip55: true) return Account("eip155:1:\(address)")! + case .web3Modal(let account): + return account } } @@ -63,6 +66,8 @@ enum ImportAccount { return "de15cb11963e9bde0a5cce06a5ee2bda1cf3a67be6fbcd7a4fc8c0e4c4db0298" case .custom(let privateKey): return privateKey + case .web3Modal: + fatalError("Private key not available") } } diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListInteractor.swift b/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListInteractor.swift index 27993fa6f..0ddb01a9c 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListInteractor.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListInteractor.swift @@ -45,7 +45,7 @@ final class ChatListInteractor { func logout() async throws { guard let importAccount = accountStorage.importAccount else { return } try await chatService.goPrivate(account: importAccount.account) - try await chatService.unregister(account: importAccount.account, privateKey: importAccount.privateKey) + try await chatService.unregister(account: importAccount.account, importAccount: importAccount) accountStorage.importAccount = nil } } diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportInteractor.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportInteractor.swift index 95c1cf133..c412afe30 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportInteractor.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportInteractor.swift @@ -13,6 +13,6 @@ final class ImportInteractor { } func register(importAccount: ImportAccount) async throws { - try await chatService.register(account: importAccount.account, privateKey: importAccount.privateKey) + try await chatService.register(account: importAccount.account, importAccount: importAccount) } } diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportPresenter.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportPresenter.swift index a3fb26567..70478b49f 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportPresenter.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportPresenter.swift @@ -1,5 +1,6 @@ import UIKit import Combine +import WalletConnectSign final class ImportPresenter: ObservableObject { @@ -52,7 +53,16 @@ extension ImportPresenter: SceneViewModel { private extension ImportPresenter { func setupInitialState() { + Sign.instance.sessionSettlePublisher.sink { session in + let accounts = session.namespaces.values.reduce(into: []) { result, namespace in + result = result + Array(namespace.accounts) + } + Task(priority: .userInitiated) { + try await self.importAccount(.web3Modal(account: accounts.first!)) + } + + }.store(in: &disposeBag) } @MainActor diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/Welcome/WelcomeInteractor.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Welcome/WelcomeInteractor.swift index 318dce292..cb76537c5 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/Welcome/WelcomeInteractor.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/Welcome/WelcomeInteractor.swift @@ -34,7 +34,7 @@ final class WelcomeInteractor { func goPublic() async throws { guard let importAccount = importAccount else { return } - try await chatService.goPublic(account: importAccount.account, privateKey: importAccount.privateKey) + try await chatService.goPublic(account: importAccount.account) } } From bc5d1a44352fc00208b59c39c69bffe339f520ee Mon Sep 17 00:00:00 2001 From: Radek Novak Date: Fri, 9 Jun 2023 11:57:20 +0200 Subject: [PATCH 55/85] Fixed web3modal import --- .../AccountStorage/AccountStorage.swift | 14 ----- .../DomainLayer/Chat/ChatService.swift | 8 +-- .../DomainLayer/Chat/ImportAccount.swift | 54 +++++++++++++------ .../Chat/Import/ImportPresenter.swift | 25 +++++---- Sources/WalletConnectSign/Session.swift | 6 +++ 5 files changed, 62 insertions(+), 45 deletions(-) diff --git a/Example/Showcase/Classes/DomainLayer/AccountStorage/AccountStorage.swift b/Example/Showcase/Classes/DomainLayer/AccountStorage/AccountStorage.swift index 0f8c805ab..fafa8bc8b 100644 --- a/Example/Showcase/Classes/DomainLayer/AccountStorage/AccountStorage.swift +++ b/Example/Showcase/Classes/DomainLayer/AccountStorage/AccountStorage.swift @@ -24,17 +24,3 @@ final class AccountStorage { } } } - -private extension ImportAccount { - - var storageId: String { - switch self { - case .swift, .kotlin, .js: - return name - case .custom(let privateKey): - return privateKey - case .web3Modal(let account): - return account.absoluteString - } - } -} diff --git a/Example/Showcase/Classes/DomainLayer/Chat/ChatService.swift b/Example/Showcase/Classes/DomainLayer/Chat/ChatService.swift index 9e21f81c3..799726841 100644 --- a/Example/Showcase/Classes/DomainLayer/Chat/ChatService.swift +++ b/Example/Showcase/Classes/DomainLayer/Chat/ChatService.swift @@ -119,8 +119,8 @@ private extension ChatService { switch importAccount { case .swift, .kotlin, .js, .custom: return .signed(onSign(message: message, privateKey: importAccount.privateKey)) - case .web3Modal(let account): - return await onWeb3ModalSign(message: message, account: account) + case .web3Modal(let account, let topic): + return await onWeb3ModalSign(message: message, account: account, topic: topic) } } @@ -130,8 +130,8 @@ private extension ChatService { return try! signer.sign(message: message, privateKey: privateKey, type: .eip191) } - func onWeb3ModalSign(message: String, account: Account) async -> SigningResult { - guard let session = Sign.instance.getSessions().last else { return .rejected } + func onWeb3ModalSign(message: String, account: Account, topic: String) async -> SigningResult { + guard let session = Sign.instance.getSessions().first(where: { $0.topic == topic }) else { return .rejected } do { let request = makeRequest(session: session, message: message, account: account) diff --git a/Example/Showcase/Classes/DomainLayer/Chat/ImportAccount.swift b/Example/Showcase/Classes/DomainLayer/Chat/ImportAccount.swift index 9f0800edb..80b9c3665 100644 --- a/Example/Showcase/Classes/DomainLayer/Chat/ImportAccount.swift +++ b/Example/Showcase/Classes/DomainLayer/Chat/ImportAccount.swift @@ -1,42 +1,62 @@ import Foundation import Web3 +import WalletConnectSign -enum ImportAccount { +enum ImportAccount: Codable { case swift case kotlin case js case custom(privateKey: String) - case web3Modal(account: Account) + case web3Modal(account: Account, topic: String) + + static let swiftId = "swift.eth" + static let kotlinId = "kotlin.eth" + static let jsId = "js.eth" + static let privateKeyId = "privateKey" + static let web3ModalId = "web3Modal" init?(input: String) { switch input.lowercased() { - case ImportAccount.swift.name: + case ImportAccount.swiftId: self = .swift - case ImportAccount.kotlin.name: + case ImportAccount.kotlinId: self = .kotlin - case ImportAccount.js.name: + case ImportAccount.jsId: self = .js default: - if let _ = try? EthereumPrivateKey(hexPrivateKey: "0x" + input, ctx: nil) { - self = .custom(privateKey: input) - } else if let _ = try? EthereumPrivateKey(hexPrivateKey: input, ctx: nil) { - self = .custom(privateKey: input.replacingOccurrences(of: "0x", with: "")) - } else { + switch true { + case input.starts(with: ImportAccount.privateKeyId): + if let _ = try? EthereumPrivateKey(hexPrivateKey: "0x" + input, ctx: nil) { + self = .custom(privateKey: input) + } else if let _ = try? EthereumPrivateKey(hexPrivateKey: input, ctx: nil) { + self = .custom(privateKey: input.replacingOccurrences(of: "0x", with: "")) + } else { + return nil + } + case input.starts(with: ImportAccount.web3ModalId): + let components = input.components(separatedBy: "-") + guard components.count == 3, let account = Account(components[1]) else { + return nil + } + self = .web3Modal(account: account, topic: components[2]) + default: return nil } } } - var name: String { + var storageId: String { switch self { case .swift: - return "swift.eth" + return ImportAccount.swiftId case .kotlin: - return "kotlin.eth" + return ImportAccount.kotlinId case .js: - return "js.eth" - case .custom, .web3Modal: - return account.address + return ImportAccount.jsId + case .custom(let privateKey): + return "\(ImportAccount.privateKeyId)-\(privateKey)" + case .web3Modal(let account, let topic): + return "\(ImportAccount.web3ModalId)-\(account.absoluteString)-\(topic)" } } @@ -51,7 +71,7 @@ enum ImportAccount { case .custom(let privateKey): let address = try! EthereumPrivateKey(hexPrivateKey: "0x" + privateKey, ctx: nil).address.hex(eip55: true) return Account("eip155:1:\(address)")! - case .web3Modal(let account): + case .web3Modal(let account, _): return account } } diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportPresenter.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportPresenter.swift index 70478b49f..a6a418aa4 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportPresenter.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportPresenter.swift @@ -19,6 +19,20 @@ final class ImportPresenter: ObservableObject { @MainActor func didPressWeb3Modal() async throws { router.presentWeb3Modal() + + let session: Session = try await withCheckedThrowingContinuation { continuation in + var cancellable: AnyCancellable? + cancellable = Sign.instance.sessionSettlePublisher.sink { session in + defer { cancellable?.cancel() } + return continuation.resume(returning: session) + } + } + + guard let account = session.accounts.first(where: { $0.blockchain.absoluteString == "eip155:1" }) else { + throw AlertError(message: "Тo matching accounts found in namespaces") + } + + try await importAccount(.web3Modal(account: account, topic: session.topic)) } @MainActor @@ -53,22 +67,13 @@ extension ImportPresenter: SceneViewModel { private extension ImportPresenter { func setupInitialState() { - Sign.instance.sessionSettlePublisher.sink { session in - let accounts = session.namespaces.values.reduce(into: []) { result, namespace in - result = result + Array(namespace.accounts) - } - - Task(priority: .userInitiated) { - try await self.importAccount(.web3Modal(account: accounts.first!)) - } - }.store(in: &disposeBag) } @MainActor func importAccount(_ importAccount: ImportAccount) async throws { + try! await interactor.register(importAccount: importAccount) interactor.save(importAccount: importAccount) - try await interactor.register(importAccount: importAccount) router.presentChat(importAccount: importAccount) } } diff --git a/Sources/WalletConnectSign/Session.swift b/Sources/WalletConnectSign/Session.swift index c880f6cd0..ef3a2205f 100644 --- a/Sources/WalletConnectSign/Session.swift +++ b/Sources/WalletConnectSign/Session.swift @@ -41,4 +41,10 @@ extension Session { SessionType.EventParams.Event(name: name, data: data) } } + + public var accounts: [Account] { + return namespaces.values.reduce(into: []) { result, namespace in + result = result + Array(namespace.accounts) + } + } } From a22edbbf4a2ac9bf0172e78a3d2e1877077d7e5e Mon Sep 17 00:00:00 2001 From: Radek Novak Date: Mon, 12 Jun 2023 14:24:36 +0200 Subject: [PATCH 56/85] PR feedback --- .../SelectChainViewController.swift | 6 +- Sources/Web3Modal/Modal/ModalInteractor.swift | 6 +- Sources/Web3Modal/Modal/ModalViewModel.swift | 5 +- Sources/Web3Modal/Web3Modal.swift | 2 +- Sources/Web3Modal/Web3ModalClient.swift | 55 +++++++++++-------- .../Mocks/ModalSheetInteractorMock.swift | 2 +- 6 files changed, 41 insertions(+), 35 deletions(-) diff --git a/Example/DApp/Sign/SelectChain/SelectChainViewController.swift b/Example/DApp/Sign/SelectChain/SelectChainViewController.swift index ea95acab9..4bbf7a67e 100644 --- a/Example/DApp/Sign/SelectChain/SelectChainViewController.swift +++ b/Example/DApp/Sign/SelectChain/SelectChainViewController.swift @@ -71,19 +71,15 @@ class SelectChainViewController: UIViewController, UITableViewDataSource { ] Task { - Web3Modal.set(sessionParams: .init( requiredNamespaces: namespaces, optionalNamespaces: optionalNamespaces, sessionProperties: sessionProperties )) - let uri = try await Web3Modal.instance.createPairingAndConnect( - - ) + let uri = try await Web3Modal.instance.connect(topic: nil) } - Web3Modal.present(from: self) } diff --git a/Sources/Web3Modal/Modal/ModalInteractor.swift b/Sources/Web3Modal/Modal/ModalInteractor.swift index 69f8671a0..30cdf30e6 100644 --- a/Sources/Web3Modal/Modal/ModalInteractor.swift +++ b/Sources/Web3Modal/Modal/ModalInteractor.swift @@ -4,7 +4,7 @@ import Foundation protocol ModalSheetInteractor { func getListings() async throws -> [Listing] - func createPairingAndConnect() async throws -> WalletConnectURI + func createPairingAndConnect() async throws -> WalletConnectURI? var sessionSettlePublisher: AnyPublisher { get } var sessionRejectionPublisher: AnyPublisher<(Session.Proposal, Reason), Never> { get } @@ -26,7 +26,7 @@ final class DefaultModalSheetInteractor: ModalSheetInteractor { return response.listings.values.compactMap { $0 } } - func createPairingAndConnect() async throws -> WalletConnectURI { - try await Web3Modal.instance.createPairingAndConnect() + func createPairingAndConnect() async throws -> WalletConnectURI? { + try await Web3Modal.instance.connect(topic: nil) } } diff --git a/Sources/Web3Modal/Modal/ModalViewModel.swift b/Sources/Web3Modal/Modal/ModalViewModel.swift index 637a3735c..7f48aa873 100644 --- a/Sources/Web3Modal/Modal/ModalViewModel.swift +++ b/Sources/Web3Modal/Modal/ModalViewModel.swift @@ -82,7 +82,10 @@ final class ModalViewModel: ObservableObject { @MainActor func createURI() async { do { - let wcUri = try await interactor.createPairingAndConnect() + guard let wcUri = try await interactor.createPairingAndConnect() else { + toast = Toast(style: .error, message: "Failed to create pairing") + return + } uri = wcUri.absoluteString deeplinkUri = wcUri.deeplinkUri } catch { diff --git a/Sources/Web3Modal/Web3Modal.swift b/Sources/Web3Modal/Web3Modal.swift index ecffa540d..d9a150a79 100644 --- a/Sources/Web3Modal/Web3Modal.swift +++ b/Sources/Web3Modal/Web3Modal.swift @@ -26,7 +26,7 @@ public class Web3Modal { } return Web3ModalClient( signClient: Sign.instance, - pairingClient: Pair.instance as! (PairingClientProtocol & PairingInteracting) + pairingClient: Pair.instance as! (PairingClientProtocol & PairingInteracting & PairingRegisterer) ) }() diff --git a/Sources/Web3Modal/Web3ModalClient.swift b/Sources/Web3Modal/Web3ModalClient.swift index f2f41506a..db87e9253 100644 --- a/Sources/Web3Modal/Web3ModalClient.swift +++ b/Sources/Web3Modal/Web3ModalClient.swift @@ -49,12 +49,13 @@ public class Web3ModalClient { } // MARK: - Private Properties + private let signClient: SignClientProtocol - private let pairingClient: (PairingClientProtocol & PairingInteracting) + private let pairingClient: PairingClientProtocol & PairingInteracting & PairingRegisterer init( signClient: SignClientProtocol, - pairingClient: (PairingClientProtocol & PairingInteracting) + pairingClient: PairingClientProtocol & PairingInteracting & PairingRegisterer ) { self.signClient = signClient self.pairingClient = pairingClient @@ -67,29 +68,35 @@ public class Web3ModalClient { /// For proposing a session to a wallet. /// Function will propose a session on existing pairing. - /// - Parameter topic: topic from existing pairing. - public func connect(on topic: String) async throws { - try await signClient.connect( - requiredNamespaces: Web3Modal.config.sessionParams.requiredNamespaces, - optionalNamespaces: Web3Modal.config.sessionParams.optionalNamespaces, - sessionProperties: Web3Modal.config.sessionParams.sessionProperties, - topic: topic - ) - } - - /// For proposing a session to a wallet. - /// Function will propose a session on newly created pairing. - public func createPairingAndConnect() async throws -> WalletConnectURI { - let uri = try await createPairing() - - try await signClient.connect( - requiredNamespaces: Web3Modal.config.sessionParams.requiredNamespaces, - optionalNamespaces: Web3Modal.config.sessionParams.optionalNamespaces, - sessionProperties: Web3Modal.config.sessionParams.sessionProperties, - topic: uri.topic - ) + /// - Parameters: + /// - topic: pairing topic + public func connect( + topic: String? + ) async throws -> WalletConnectURI? { + var topic = topic + if topic == nil { + topic = try await createPairing().topic + } - return uri + if let topic = topic { + try pairingClient.validatePairingExistance(topic) + try await signClient.connect( + requiredNamespaces: Web3Modal.config.sessionParams.requiredNamespaces, + optionalNamespaces: Web3Modal.config.sessionParams.optionalNamespaces, + sessionProperties: Web3Modal.config.sessionParams.sessionProperties, + topic: topic + ) + return nil + } else { + let pairingURI = try await pairingClient.create() + try await signClient.connect( + requiredNamespaces: Web3Modal.config.sessionParams.requiredNamespaces, + optionalNamespaces: Web3Modal.config.sessionParams.optionalNamespaces, + sessionProperties: Web3Modal.config.sessionParams.sessionProperties, + topic: pairingURI.topic + ) + return pairingURI + } } /// For proposing a session to a wallet. diff --git a/Tests/Web3ModalTests/Mocks/ModalSheetInteractorMock.swift b/Tests/Web3ModalTests/Mocks/ModalSheetInteractorMock.swift index bcd2e3f01..2197afbaa 100644 --- a/Tests/Web3ModalTests/Mocks/ModalSheetInteractorMock.swift +++ b/Tests/Web3ModalTests/Mocks/ModalSheetInteractorMock.swift @@ -23,7 +23,7 @@ final class ModalSheetInteractorMock: ModalSheetInteractor { listings } - func createPairingAndConnect() async throws -> WalletConnectURI { + func createPairingAndConnect() async throws -> WalletConnectURI? { .init(topic: "foo", symKey: "bar", relay: .init(protocol: "irn", data: nil)) } From 3cc418fdf7f8ac4f5f49c9e265e9dd24ea68feb6 Mon Sep 17 00:00:00 2001 From: radeknovis Date: Mon, 12 Jun 2023 13:09:28 +0000 Subject: [PATCH 57/85] Set User Agent --- Sources/WalletConnectRelay/PackageConfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/WalletConnectRelay/PackageConfig.json b/Sources/WalletConnectRelay/PackageConfig.json index 18a6f779c..0a1b0e189 100644 --- a/Sources/WalletConnectRelay/PackageConfig.json +++ b/Sources/WalletConnectRelay/PackageConfig.json @@ -1 +1 @@ -{"version": "1.6.7"} +{"version": "1.6.8"} From 97a6c3b82d6233ac1de08b455c9fa8689412ccef Mon Sep 17 00:00:00 2001 From: Radek Novak Date: Mon, 12 Jun 2023 17:33:49 +0200 Subject: [PATCH 58/85] Fix connect logic --- Sources/Web3Modal/Web3ModalClient.swift | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Sources/Web3Modal/Web3ModalClient.swift b/Sources/Web3Modal/Web3ModalClient.swift index db87e9253..dad05c45e 100644 --- a/Sources/Web3Modal/Web3ModalClient.swift +++ b/Sources/Web3Modal/Web3ModalClient.swift @@ -73,11 +73,6 @@ public class Web3ModalClient { public func connect( topic: String? ) async throws -> WalletConnectURI? { - var topic = topic - if topic == nil { - topic = try await createPairing().topic - } - if let topic = topic { try pairingClient.validatePairingExistance(topic) try await signClient.connect( From 7870443e8c54215d4d88c8f8f227d8e7c42ce83f Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Tue, 13 Jun 2023 11:58:47 +0200 Subject: [PATCH 59/85] add slack hook url --- .../actions/run_tests_without_building/action.yml | 5 ++++- .../xcshareddata/swiftpm/Package.resolved | 12 ++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/actions/run_tests_without_building/action.yml b/.github/actions/run_tests_without_building/action.yml index 4a9b2a44c..3dbf22ebe 100644 --- a/.github/actions/run_tests_without_building/action.yml +++ b/.github/actions/run_tests_without_building/action.yml @@ -11,6 +11,9 @@ inputs: project-id: description: 'WalletConnect project id' required: true + slack-webhook-url: + description: 'Smoke tests slack webhoook url' + required: true runs: using: "composite" @@ -59,7 +62,7 @@ runs: status: ${{ job.status }} text: The smoke tests failed in the CI pipeline. Check the logs for more details. env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + SLACK_WEBHOOK_URL: ${{ inputs.slack-webhook-url }} - name: Publish Test Report uses: mikepenz/action-junit-report@v3 diff --git a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 25ef09b34..223fbcb65 100644 --- a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -15,8 +15,8 @@ "repositoryURL": "https://github.com/krzyzanowskim/CryptoSwift.git", "state": { "branch": null, - "revision": "19b3c3ceed117c5cc883517c4e658548315ba70b", - "version": "1.6.0" + "revision": "eee9ad754926c40a0f7e73f152357d37b119b7fa", + "version": "1.7.1" } }, { @@ -33,8 +33,8 @@ "repositoryURL": "https://github.com/mxcl/PromiseKit.git", "state": { "branch": null, - "revision": "43772616c46a44a9977e41924ae01d0e55f2f9ca", - "version": "6.18.1" + "revision": "8a98e31a47854d3180882c8068cc4d9381bf382d", + "version": "6.22.1" } }, { @@ -105,8 +105,8 @@ "repositoryURL": "https://github.com/bigearsenal/task-retrying-swift.git", "state": { "branch": null, - "revision": "645eaaf207a6f39ab4b469558d916ae23df199b5", - "version": "1.0.3" + "revision": "1249b3524378423c848cef39fb220041e00a08ec", + "version": "1.0.4" } }, { From 1e67a56bfbdb35da3996799455e338e49dee8650 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Tue, 13 Jun 2023 11:59:32 +0200 Subject: [PATCH 60/85] exclude sync tests --- Example/SmokeTests.xctestplan | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Example/SmokeTests.xctestplan b/Example/SmokeTests.xctestplan index 894a5d133..142fa8062 100644 --- a/Example/SmokeTests.xctestplan +++ b/Example/SmokeTests.xctestplan @@ -70,7 +70,9 @@ "SignClientTests\/testSessionRequestFailureResponse()", "SignClientTests\/testSuccessfulSessionExtend()", "SignClientTests\/testSuccessfulSessionUpdateNamespaces()", - "SignerTest" + "SignerTest", + "SyncDerivationServiceTests", + "SyncTests" ], "target" : { "containerPath" : "container:ExampleApp.xcodeproj", From 733dd36836fba354b1e849be92e5f1e1f6151988 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Tue, 13 Jun 2023 12:27:02 +0200 Subject: [PATCH 61/85] savepoint --- .github/actions/run_tests_without_building/action.yml | 2 +- Example/RelayIntegrationTests/RelayClientEndToEndTests.swift | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/actions/run_tests_without_building/action.yml b/.github/actions/run_tests_without_building/action.yml index 3dbf22ebe..f9567cf73 100644 --- a/.github/actions/run_tests_without_building/action.yml +++ b/.github/actions/run_tests_without_building/action.yml @@ -56,7 +56,7 @@ runs: run: make smoke_tests RELAY_HOST=${{ inputs.relay-endpoint }} PROJECT_ID=${{ inputs.project-id }} - name: Slack Notification for Failure - if: failure() && matrix.type == 'smoke-tests' + if: failure() && (inputs.type == 'smoke-tests' || inputs.type == 'relay-tests') uses: 8398a7/action-slack@v3 with: status: ${{ job.status }} diff --git a/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift b/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift index 129ebb8a0..4412a45ad 100644 --- a/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift +++ b/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift @@ -58,6 +58,8 @@ final class RelayClientEndToEndTests: XCTestCase { } func testSubscribe() { + XCTFail() + return let relayClient = makeRelayClient(prefix: "") try! relayClient.connect() @@ -74,6 +76,8 @@ final class RelayClientEndToEndTests: XCTestCase { } func testEndToEndPayload() { + XCTFail() + return let relayA = makeRelayClient(prefix: "⚽️ A ") let relayB = makeRelayClient(prefix: "🏀 B ") From b16ffd51e9f71812660646f786a5d098fe496c23 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Tue, 13 Jun 2023 12:34:29 +0200 Subject: [PATCH 62/85] fix relay tests --- Example/RelayIntegrationTests/RelayClientEndToEndTests.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift b/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift index 4412a45ad..129ebb8a0 100644 --- a/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift +++ b/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift @@ -58,8 +58,6 @@ final class RelayClientEndToEndTests: XCTestCase { } func testSubscribe() { - XCTFail() - return let relayClient = makeRelayClient(prefix: "") try! relayClient.connect() @@ -76,8 +74,6 @@ final class RelayClientEndToEndTests: XCTestCase { } func testEndToEndPayload() { - XCTFail() - return let relayA = makeRelayClient(prefix: "⚽️ A ") let relayB = makeRelayClient(prefix: "🏀 B ") From 676725a909334f80d7b0630dc3ba7f20f55bf4c8 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Mon, 29 May 2023 17:51:23 +0300 Subject: [PATCH 63/85] HistoryNetworkService --- .../xcschemes/WalletConnectHistory.xcscheme | 66 +++++++++++++++++++ Package.swift | 6 ++ Sources/WalletConnectHistory/HistoryAPI.swift | 37 +++++++++++ .../WalletConnectHistory/HistoryClient.swift | 9 +++ .../WalletConnectHistory/HistoryImports.swift | 4 ++ .../HistoryNetworkService.swift | 25 +++++++ .../Types/RegisterPayload.swift | 11 ++++ 7 files changed, 158 insertions(+) create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/WalletConnectHistory.xcscheme create mode 100644 Sources/WalletConnectHistory/HistoryAPI.swift create mode 100644 Sources/WalletConnectHistory/HistoryClient.swift create mode 100644 Sources/WalletConnectHistory/HistoryImports.swift create mode 100644 Sources/WalletConnectHistory/HistoryNetworkService.swift create mode 100644 Sources/WalletConnectHistory/Types/RegisterPayload.swift diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnectHistory.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnectHistory.xcscheme new file mode 100644 index 000000000..4311c49f7 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnectHistory.xcscheme @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Package.swift b/Package.swift index c00641c97..6c0bc8e03 100644 --- a/Package.swift +++ b/Package.swift @@ -43,6 +43,9 @@ let package = Package( .library( name: "WalletConnectVerify", targets: ["WalletConnectVerify"]), + .library( + name: "WalletConnectHistory", + targets: ["WalletConnectHistory"]), .library( name: "Web3Inbox", targets: ["Web3Inbox"]), @@ -91,6 +94,9 @@ let package = Package( .target( name: "WalletConnectPairing", dependencies: ["WalletConnectNetworking"]), + .target( + name: "WalletConnectHistory", + dependencies: ["HTTPClient", "WalletConnectRelay"]), .target( name: "Web3Inbox", dependencies: ["WalletConnectChat", "WalletConnectPush"]), diff --git a/Sources/WalletConnectHistory/HistoryAPI.swift b/Sources/WalletConnectHistory/HistoryAPI.swift new file mode 100644 index 000000000..0bd5e7f61 --- /dev/null +++ b/Sources/WalletConnectHistory/HistoryAPI.swift @@ -0,0 +1,37 @@ +import Foundation + +enum HistoryAPI: HTTPService { + case register(payload: RegisterPayload, jwt: String) + + var path: String { + switch self { + case .register: + return "/register" + } + } + + var method: HTTPMethod { + switch self { + case .register: + return .post + } + } + + var body: Data? { + switch self { + case .register(let payload, _): + return try? JSONEncoder().encode(payload) + } + } + + var additionalHeaderFields: [String : String]? { + switch self { + case .register(_, let jwt): + return ["Authorization": jwt] + } + } + + var queryParameters: [String : String]? { + return nil + } +} diff --git a/Sources/WalletConnectHistory/HistoryClient.swift b/Sources/WalletConnectHistory/HistoryClient.swift new file mode 100644 index 000000000..d465751b7 --- /dev/null +++ b/Sources/WalletConnectHistory/HistoryClient.swift @@ -0,0 +1,9 @@ +import Foundation + +public final class HistoryClient { + + + public func registerTags(payload: RegisterPayload, historyUrl: String) async throws { + + } +} diff --git a/Sources/WalletConnectHistory/HistoryImports.swift b/Sources/WalletConnectHistory/HistoryImports.swift new file mode 100644 index 000000000..e6d4b859c --- /dev/null +++ b/Sources/WalletConnectHistory/HistoryImports.swift @@ -0,0 +1,4 @@ +#if !CocoaPods +@_exported import HTTPClient +@_exported import WalletConnectRelay +#endif diff --git a/Sources/WalletConnectHistory/HistoryNetworkService.swift b/Sources/WalletConnectHistory/HistoryNetworkService.swift new file mode 100644 index 000000000..4af5f29b9 --- /dev/null +++ b/Sources/WalletConnectHistory/HistoryNetworkService.swift @@ -0,0 +1,25 @@ +import Foundation + +final class HistoryNetworkService { + + private let clientIdStorage: ClientIdStorage + + init(clientIdStorage: ClientIdStorage) { + self.clientIdStorage = clientIdStorage + } + + func registerTags(payload: RegisterPayload, historyUrl: String) async throws { + let service = HTTPNetworkClient(host: historyUrl) + let api = HistoryAPI.register(payload: payload, jwt: try await getJwt()) + try await service.request(service: api) + } +} + +private extension HistoryNetworkService { + + func getJwt() async throws -> String { + let keyPair = try clientIdStorage.getOrCreateKeyPair() + let payload = RelayAuthPayload(subject: getSubject(), audience: getAudience()) + return try payload.signAndCreateWrapper(keyPair: keyPair).jwtString + } +} diff --git a/Sources/WalletConnectHistory/Types/RegisterPayload.swift b/Sources/WalletConnectHistory/Types/RegisterPayload.swift new file mode 100644 index 000000000..b759c5ce5 --- /dev/null +++ b/Sources/WalletConnectHistory/Types/RegisterPayload.swift @@ -0,0 +1,11 @@ +import Foundation + +public struct RegisterPayload: Codable { + public let tags: [String] + public let relayUrl: String + + public init(tags: [String], relayUrl: String) { + self.tags = tags + self.relayUrl = relayUrl + } +} From 355fb02b61b19fc20b3e75e60075ec9eb9b0ce7a Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Mon, 29 May 2023 17:56:35 +0300 Subject: [PATCH 64/85] Generic ClientIdAuthenticator --- .../RelayClientEndToEndTests.swift | 2 +- ...cator.swift => ClientIdAuthenticator.swift} | 18 +++++++----------- Sources/WalletConnectRelay/RelayClient.swift | 4 ++-- .../WalletConnectRelay/RelayURLFactory.swift | 4 ++-- .../AuthTests/SocketAuthenticatorTests.swift | 6 +++--- Tests/RelayerTests/DispatcherTests.swift | 4 ++-- 6 files changed, 17 insertions(+), 21 deletions(-) rename Sources/WalletConnectRelay/ClientAuth/{SocketAuthenticator.swift => ClientIdAuthenticator.swift} (56%) diff --git a/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift b/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift index 129ebb8a0..ef72e942e 100644 --- a/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift +++ b/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift @@ -32,7 +32,7 @@ final class RelayClientEndToEndTests: XCTestCase { func makeRelayClient(prefix: String) -> RelayClient { let clientIdStorage = ClientIdStorage(keychain: KeychainStorageMock()) - let socketAuthenticator = SocketAuthenticator( + let socketAuthenticator = ClientIdAuthenticator( clientIdStorage: clientIdStorage, relayHost: InputConfig.relayHost ) diff --git a/Sources/WalletConnectRelay/ClientAuth/SocketAuthenticator.swift b/Sources/WalletConnectRelay/ClientAuth/ClientIdAuthenticator.swift similarity index 56% rename from Sources/WalletConnectRelay/ClientAuth/SocketAuthenticator.swift rename to Sources/WalletConnectRelay/ClientAuth/ClientIdAuthenticator.swift index bb7a8b540..2055e1813 100644 --- a/Sources/WalletConnectRelay/ClientAuth/SocketAuthenticator.swift +++ b/Sources/WalletConnectRelay/ClientAuth/ClientIdAuthenticator.swift @@ -1,28 +1,24 @@ import Foundation -protocol SocketAuthenticating { +public protocol ClientIdAuthenticating { func createAuthToken() throws -> String } -struct SocketAuthenticator: SocketAuthenticating { +public struct ClientIdAuthenticator: ClientIdAuthenticating { private let clientIdStorage: ClientIdStoring - private let relayHost: String + private let url: String - init(clientIdStorage: ClientIdStoring, relayHost: String) { + public init(clientIdStorage: ClientIdStoring, url: String) { self.clientIdStorage = clientIdStorage - self.relayHost = relayHost + self.url = url } - func createAuthToken() throws -> String { + public func createAuthToken() throws -> String { let keyPair = try clientIdStorage.getOrCreateKeyPair() - let payload = RelayAuthPayload(subject: getSubject(), audience: getAudience()) + let payload = RelayAuthPayload(subject: getSubject(), audience: url) return try payload.signAndCreateWrapper(keyPair: keyPair).jwtString } - private func getAudience() -> String { - return "wss://\(relayHost)" - } - private func getSubject() -> String { return Data.randomBytes(count: 32).toHexString() } diff --git a/Sources/WalletConnectRelay/RelayClient.swift b/Sources/WalletConnectRelay/RelayClient.swift index 880e4b79c..a84c4c264 100644 --- a/Sources/WalletConnectRelay/RelayClient.swift +++ b/Sources/WalletConnectRelay/RelayClient.swift @@ -80,9 +80,9 @@ public final class RelayClient { logger: ConsoleLogging = ConsoleLogger(loggingLevel: .debug) ) { let clientIdStorage = ClientIdStorage(keychain: keychainStorage) - let socketAuthenticator = SocketAuthenticator( + let socketAuthenticator = ClientIdAuthenticator( clientIdStorage: clientIdStorage, - relayHost: relayHost + url: "wss://\(relayHost)" ) let relayUrlFactory = RelayUrlFactory( relayHost: relayHost, diff --git a/Sources/WalletConnectRelay/RelayURLFactory.swift b/Sources/WalletConnectRelay/RelayURLFactory.swift index d3b9d155c..ec35ce87c 100644 --- a/Sources/WalletConnectRelay/RelayURLFactory.swift +++ b/Sources/WalletConnectRelay/RelayURLFactory.swift @@ -3,12 +3,12 @@ import Foundation struct RelayUrlFactory { private let relayHost: String private let projectId: String - private let socketAuthenticator: SocketAuthenticating + private let socketAuthenticator: ClientIdAuthenticating init( relayHost: String, projectId: String, - socketAuthenticator: SocketAuthenticating + socketAuthenticator: ClientIdAuthenticating ) { self.relayHost = relayHost self.projectId = projectId diff --git a/Tests/RelayerTests/AuthTests/SocketAuthenticatorTests.swift b/Tests/RelayerTests/AuthTests/SocketAuthenticatorTests.swift index 60ac1390a..72dd37bc5 100644 --- a/Tests/RelayerTests/AuthTests/SocketAuthenticatorTests.swift +++ b/Tests/RelayerTests/AuthTests/SocketAuthenticatorTests.swift @@ -5,14 +5,14 @@ import WalletConnectKMS final class SocketAuthenticatorTests: XCTestCase { var clientIdStorage: ClientIdStorageMock! - var sut: SocketAuthenticator! + var sut: ClientIdAuthenticator! let expectedToken = "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtvZEhad25lVlJTaHRhTGY4SktZa3hwREdwMXZHWm5wR21kQnBYOE0yZXh4SCIsInN1YiI6ImM0NzlmZTVkYzQ2NGU3NzFlNzhiMTkzZDIzOWE2NWI1OGQyNzhjYWQxYzM0YmZiMGI1NzE2ZTViYjUxNDkyOGUifQ.0JkxOM-FV21U7Hk-xycargj_qNRaYV2H5HYtE4GzAeVQYiKWj7YySY5AdSqtCgGzX4Gt98XWXn2kSr9rE1qvCA" override func setUp() { clientIdStorage = ClientIdStorageMock() - sut = SocketAuthenticator( + sut = ClientIdAuthenticator( clientIdStorage: clientIdStorage, - relayHost: "relay.walletconnect.com" + url: "wss://relay.walletconnect.com" ) } diff --git a/Tests/RelayerTests/DispatcherTests.swift b/Tests/RelayerTests/DispatcherTests.swift index 170bbcb37..35660c6f1 100644 --- a/Tests/RelayerTests/DispatcherTests.swift +++ b/Tests/RelayerTests/DispatcherTests.swift @@ -62,9 +62,9 @@ final class DispatcherTests: XCTestCase { networkMonitor = NetworkMonitoringMock() let keychainStorageMock = DispatcherKeychainStorageMock() let clientIdStorage = ClientIdStorage(keychain: keychainStorageMock) - let socketAuthenticator = SocketAuthenticator( + let socketAuthenticator = ClientIdAuthenticator( clientIdStorage: clientIdStorage, - relayHost: "relay.walletconnect.com" + url: "wss://relay.walletconnect.com" ) let relayUrlFactory = RelayUrlFactory( relayHost: "relay.walletconnect.com", From a249c68ba00adb63a7a77adf3206e2df3d290e06 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Mon, 29 May 2023 18:57:59 +0300 Subject: [PATCH 65/85] HistoryClient --- Sources/WalletConnectHistory/HistoryAPI.swift | 19 +++++++++++-- .../WalletConnectHistory/HistoryClient.swift | 27 ++++++++++++++++--- .../HistoryNetworkService.swift | 25 ----------------- .../Types/GetMessagesPayload.swift | 19 +++++++++++++ .../Types/GetMessagesResponse.swift | 8 ++++++ 5 files changed, 68 insertions(+), 30 deletions(-) delete mode 100644 Sources/WalletConnectHistory/HistoryNetworkService.swift create mode 100644 Sources/WalletConnectHistory/Types/GetMessagesPayload.swift create mode 100644 Sources/WalletConnectHistory/Types/GetMessagesResponse.swift diff --git a/Sources/WalletConnectHistory/HistoryAPI.swift b/Sources/WalletConnectHistory/HistoryAPI.swift index 0bd5e7f61..b9b63e8b6 100644 --- a/Sources/WalletConnectHistory/HistoryAPI.swift +++ b/Sources/WalletConnectHistory/HistoryAPI.swift @@ -2,11 +2,14 @@ import Foundation enum HistoryAPI: HTTPService { case register(payload: RegisterPayload, jwt: String) + case messages(payload: GetMessagesPayload) var path: String { switch self { case .register: return "/register" + case .messages: + return "/messages" } } @@ -14,6 +17,8 @@ enum HistoryAPI: HTTPService { switch self { case .register: return .post + case .messages: + return .get } } @@ -21,17 +26,27 @@ enum HistoryAPI: HTTPService { switch self { case .register(let payload, _): return try? JSONEncoder().encode(payload) + case .messages: + return nil } } var additionalHeaderFields: [String : String]? { switch self { case .register(_, let jwt): - return ["Authorization": jwt] + return ["Authorization": "Bearer \(jwt)"] + case .messages: + return nil } } var queryParameters: [String : String]? { - return nil + switch self { + case .messages(let payload): + let data = try! JSONEncoder().encode(payload) + return try? JSONDecoder().decode([String : String].self, from: data) + case .register: + return nil + } } } diff --git a/Sources/WalletConnectHistory/HistoryClient.swift b/Sources/WalletConnectHistory/HistoryClient.swift index d465751b7..d4ea09918 100644 --- a/Sources/WalletConnectHistory/HistoryClient.swift +++ b/Sources/WalletConnectHistory/HistoryClient.swift @@ -1,9 +1,30 @@ import Foundation -public final class HistoryClient { +final class HistoryClient { + private let clientIdStorage: ClientIdStorage - public func registerTags(payload: RegisterPayload, historyUrl: String) async throws { - + init(clientIdStorage: ClientIdStorage) { + self.clientIdStorage = clientIdStorage + } + + func registerTags(payload: RegisterPayload, historyUrl: String) async throws { + let service = HTTPNetworkClient(host: historyUrl) + let api = HistoryAPI.register(payload: payload, jwt: try getJwt(historyUrl: historyUrl)) + try await service.request(service: api) + } + + func getMessages(payload: GetMessagesPayload, historyUrl: String) async throws -> GetMessagesResponse { + let service = HTTPNetworkClient(host: historyUrl) + let api = HistoryAPI.messages(payload: payload) + return try await service.request(GetMessagesResponse.self, at: api) + } +} + +private extension HistoryClient { + + func getJwt(historyUrl: String) throws -> String { + let authenticator = ClientIdAuthenticator(clientIdStorage: clientIdStorage, url: historyUrl) + return try authenticator.createAuthToken() } } diff --git a/Sources/WalletConnectHistory/HistoryNetworkService.swift b/Sources/WalletConnectHistory/HistoryNetworkService.swift deleted file mode 100644 index 4af5f29b9..000000000 --- a/Sources/WalletConnectHistory/HistoryNetworkService.swift +++ /dev/null @@ -1,25 +0,0 @@ -import Foundation - -final class HistoryNetworkService { - - private let clientIdStorage: ClientIdStorage - - init(clientIdStorage: ClientIdStorage) { - self.clientIdStorage = clientIdStorage - } - - func registerTags(payload: RegisterPayload, historyUrl: String) async throws { - let service = HTTPNetworkClient(host: historyUrl) - let api = HistoryAPI.register(payload: payload, jwt: try await getJwt()) - try await service.request(service: api) - } -} - -private extension HistoryNetworkService { - - func getJwt() async throws -> String { - let keyPair = try clientIdStorage.getOrCreateKeyPair() - let payload = RelayAuthPayload(subject: getSubject(), audience: getAudience()) - return try payload.signAndCreateWrapper(keyPair: keyPair).jwtString - } -} diff --git a/Sources/WalletConnectHistory/Types/GetMessagesPayload.swift b/Sources/WalletConnectHistory/Types/GetMessagesPayload.swift new file mode 100644 index 000000000..7dcc9a08d --- /dev/null +++ b/Sources/WalletConnectHistory/Types/GetMessagesPayload.swift @@ -0,0 +1,19 @@ +import Foundation + +public struct GetMessagesPayload: Codable { + public enum Direction: String, Codable { + case forward + case backward + } + public let topic: String + public let originId: Int64? + public let messageCount: Int? + public let direction: Direction + + public init(topic: String, originId: Int64?, messageCount: Int?, direction: GetMessagesPayload.Direction) { + self.topic = topic + self.originId = originId + self.messageCount = messageCount + self.direction = direction + } +} diff --git a/Sources/WalletConnectHistory/Types/GetMessagesResponse.swift b/Sources/WalletConnectHistory/Types/GetMessagesResponse.swift new file mode 100644 index 000000000..8ea18add3 --- /dev/null +++ b/Sources/WalletConnectHistory/Types/GetMessagesResponse.swift @@ -0,0 +1,8 @@ +import Foundation + +public struct GetMessagesResponse: Codable { + public let topic: String + public let direction: GetMessagesPayload.Direction + public let nextId: Int64? + public let messages: [String] +} From 2269e548072227210bea8687535f57e2d64f7140 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Mon, 29 May 2023 20:18:08 +0300 Subject: [PATCH 66/85] testRegister test case --- Example/ExampleApp.xcodeproj/project.pbxproj | 19 ++++++++++ .../History/HistoryTests.swift | 37 +++++++++++++++++++ .../RelayClientEndToEndTests.swift | 2 +- Example/Shared/Tests/InputConfig.swift | 4 ++ .../WalletConnectHistory/HistoryClient.swift | 17 ++++++--- 5 files changed, 73 insertions(+), 6 deletions(-) create mode 100644 Example/IntegrationTests/History/HistoryTests.swift diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index bb13cfbc0..7fcb4ca7e 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -75,6 +75,7 @@ 84FE684628ACDB4700C893FF /* RequestParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FE684528ACDB4700C893FF /* RequestParams.swift */; }; A507BE1A29E8032E0038EF70 /* EIP55Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A507BE1929E8032E0038EF70 /* EIP55Tests.swift */; }; A50C036528AAD32200FE72D3 /* ClientDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50C036428AAD32200FE72D3 /* ClientDelegate.swift */; }; + A50DF19D2A25084A0036EA6C /* WalletConnectHistory in Frameworks */ = {isa = PBXBuildFile; productRef = A50DF19C2A25084A0036EA6C /* WalletConnectHistory */; }; A50F3946288005B200064555 /* Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50F3945288005B200064555 /* Types.swift */; }; A51606F82A2F47BD00CACB92 /* DefaultBIP44Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51606F72A2F47BD00CACB92 /* DefaultBIP44Provider.swift */; }; A51606F92A2F47BD00CACB92 /* DefaultBIP44Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51606F72A2F47BD00CACB92 /* DefaultBIP44Provider.swift */; }; @@ -86,6 +87,7 @@ A518B31428E33A6500A2CE93 /* InputConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A518B31328E33A6500A2CE93 /* InputConfig.swift */; }; A51AC0D928E436A3001BACF9 /* InputConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51AC0D828E436A3001BACF9 /* InputConfig.swift */; }; A51AC0DF28E4379F001BACF9 /* InputConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51AC0DE28E4379F001BACF9 /* InputConfig.swift */; }; + A5321C2B2A250367006CADC3 /* HistoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5321C2A2A250367006CADC3 /* HistoryTests.swift */; }; A5417BBE299BFC3E00B469F3 /* ImportAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5417BBD299BFC3E00B469F3 /* ImportAccount.swift */; }; A541959E2934BFEF0035AD19 /* CacaoSignerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A541959A2934BFEF0035AD19 /* CacaoSignerTests.swift */; }; A541959F2934BFEF0035AD19 /* SignerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A541959B2934BFEF0035AD19 /* SignerTests.swift */; }; @@ -426,6 +428,7 @@ A518B31328E33A6500A2CE93 /* InputConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputConfig.swift; sourceTree = ""; }; A51AC0D828E436A3001BACF9 /* InputConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputConfig.swift; sourceTree = ""; }; A51AC0DE28E4379F001BACF9 /* InputConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputConfig.swift; sourceTree = ""; }; + A5321C2A2A250367006CADC3 /* HistoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryTests.swift; sourceTree = ""; }; A5417BBD299BFC3E00B469F3 /* ImportAccount.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportAccount.swift; sourceTree = ""; }; A541959A2934BFEF0035AD19 /* CacaoSignerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CacaoSignerTests.swift; sourceTree = ""; }; A541959B2934BFEF0035AD19 /* SignerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignerTests.swift; sourceTree = ""; }; @@ -665,6 +668,7 @@ A5E03DFF2864662500888481 /* WalletConnect in Frameworks */, A561C80529DFCD4500DF540D /* WalletConnectSync in Frameworks */, A5E03DF52864651200888481 /* Starscream in Frameworks */, + A50DF19D2A25084A0036EA6C /* WalletConnectHistory in Frameworks */, 847CF3AF28E3141700F1D760 /* WalletConnectPush in Frameworks */, A5C8BE85292FE20B006CC85C /* Web3 in Frameworks */, 84DDB4ED28ABB663003D66ED /* WalletConnectAuth in Frameworks */, @@ -955,6 +959,14 @@ path = Types; sourceTree = ""; }; + A5321C292A25035A006CADC3 /* History */ = { + isa = PBXGroup; + children = ( + A5321C2A2A250367006CADC3 /* HistoryTests.swift */, + ); + path = History; + sourceTree = ""; + }; A54195992934BFDD0035AD19 /* Signer */ = { isa = PBXGroup; children = ( @@ -1376,6 +1388,7 @@ A5E03DEE286464DB00888481 /* IntegrationTests */ = { isa = PBXGroup; children = ( + A5321C292A25035A006CADC3 /* History */, A561C80129DFCCD300DF540D /* Sync */, 849D7A91292E2115006A2BD4 /* Push */, 84CEC64728D8A98900D081A8 /* Pairing */, @@ -1860,6 +1873,7 @@ C5DD5BE0294E09E3008FD3A4 /* Web3Wallet */, A561C80429DFCD4500DF540D /* WalletConnectSync */, A573C53A29EC365800E3CBFD /* HDWalletKit */, + A50DF19C2A25084A0036EA6C /* WalletConnectHistory */, ); productName = IntegrationTests; productReference = A5E03DED286464DB00888481 /* IntegrationTests.xctest */; @@ -2212,6 +2226,7 @@ 7694A5262874296A0001257E /* RegistryTests.swift in Sources */, A541959F2934BFEF0035AD19 /* SignerTests.swift in Sources */, A50C036528AAD32200FE72D3 /* ClientDelegate.swift in Sources */, + A5321C2B2A250367006CADC3 /* HistoryTests.swift in Sources */, A58A1ECC29BF458600A82A20 /* ENSResolverTests.swift in Sources */, A5E03DFA286465C700888481 /* SignClientTests.swift in Sources */, A54195A02934BFEF0035AD19 /* EIP1271VerifierTests.swift in Sources */, @@ -3110,6 +3125,10 @@ isa = XCSwiftPackageProductDependency; productName = WalletConnectPush; }; + A50DF19C2A25084A0036EA6C /* WalletConnectHistory */ = { + isa = XCSwiftPackageProductDependency; + productName = WalletConnectHistory; + }; A54195A42934E83F0035AD19 /* Web3 */ = { isa = XCSwiftPackageProductDependency; package = A5AE354528A1A2AC0059AE8A /* XCRemoteSwiftPackageReference "Web3" */; diff --git a/Example/IntegrationTests/History/HistoryTests.swift b/Example/IntegrationTests/History/HistoryTests.swift new file mode 100644 index 000000000..cd9ebf227 --- /dev/null +++ b/Example/IntegrationTests/History/HistoryTests.swift @@ -0,0 +1,37 @@ +import Foundation +import Combine +import XCTest +@testable import WalletConnectHistory + +final class HistoryTests: XCTestCase { + + var relayClient1: RelayClient! + var historyClient: HistoryClient! + + override func setUp() { + let keychain = KeychainStorageMock() + relayClient1 = makeRelayClient(prefix: "🐄", keychain: keychain) + historyClient = makeHistoryClient(keychain: keychain) + } + + private func makeRelayClient(prefix: String, keychain: KeychainStorageProtocol) -> RelayClient { + return RelayClient( + relayHost: InputConfig.relayHost, + projectId: InputConfig.projectId, + keyValueStorage: RuntimeKeyValueStorage(), + keychainStorage: keychain, + socketFactory: DefaultSocketFactory(), + logger: ConsoleLogger(suffix: prefix + " [Relay]", loggingLevel: .debug)) + } + + private func makeHistoryClient(keychain: KeychainStorageProtocol) -> HistoryClient { + let clientIdStorage = ClientIdStorage(keychain: keychain) + return HistoryClient(clientIdStorage: clientIdStorage) + } + + func testRegister() async throws { + let payload = RegisterPayload(tags: ["7000"], relayUrl: "wss://relay.walletconnect.com") + + try await historyClient.registerTags(payload: payload, historyUrl: "https://history.walletconnect.com") + } +} diff --git a/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift b/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift index ef72e942e..8c95e6b2b 100644 --- a/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift +++ b/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift @@ -34,7 +34,7 @@ final class RelayClientEndToEndTests: XCTestCase { let clientIdStorage = ClientIdStorage(keychain: KeychainStorageMock()) let socketAuthenticator = ClientIdAuthenticator( clientIdStorage: clientIdStorage, - relayHost: InputConfig.relayHost + url: InputConfig.relayUrl ) let urlFactory = RelayUrlFactory( relayHost: InputConfig.relayHost, diff --git a/Example/Shared/Tests/InputConfig.swift b/Example/Shared/Tests/InputConfig.swift index 0a71a0bed..8a19855af 100644 --- a/Example/Shared/Tests/InputConfig.swift +++ b/Example/Shared/Tests/InputConfig.swift @@ -6,6 +6,10 @@ struct InputConfig { return config(for: "RELAY_HOST")! } + static var relayUrl: String { + return "wss://\(relayHost)" + } + static var projectId: String { return config(for: "PROJECT_ID")! } diff --git a/Sources/WalletConnectHistory/HistoryClient.swift b/Sources/WalletConnectHistory/HistoryClient.swift index d4ea09918..1dcbf9196 100644 --- a/Sources/WalletConnectHistory/HistoryClient.swift +++ b/Sources/WalletConnectHistory/HistoryClient.swift @@ -1,20 +1,23 @@ import Foundation -final class HistoryClient { +public final class HistoryClient { private let clientIdStorage: ClientIdStorage - init(clientIdStorage: ClientIdStorage) { + public init(clientIdStorage: ClientIdStorage) { self.clientIdStorage = clientIdStorage } - func registerTags(payload: RegisterPayload, historyUrl: String) async throws { - let service = HTTPNetworkClient(host: historyUrl) + public func registerTags(payload: RegisterPayload, historyUrl: String) async throws { + guard let host = URL(string: historyUrl)?.host else { + throw Errors.couldNotResolveHost + } + let service = HTTPNetworkClient(host: host) let api = HistoryAPI.register(payload: payload, jwt: try getJwt(historyUrl: historyUrl)) try await service.request(service: api) } - func getMessages(payload: GetMessagesPayload, historyUrl: String) async throws -> GetMessagesResponse { + public func getMessages(payload: GetMessagesPayload, historyUrl: String) async throws -> GetMessagesResponse { let service = HTTPNetworkClient(host: historyUrl) let api = HistoryAPI.messages(payload: payload) return try await service.request(GetMessagesResponse.self, at: api) @@ -23,6 +26,10 @@ final class HistoryClient { private extension HistoryClient { + enum Errors: Error { + case couldNotResolveHost + } + func getJwt(historyUrl: String) throws -> String { let authenticator = ClientIdAuthenticator(clientIdStorage: clientIdStorage, url: historyUrl) return try authenticator.createAuthToken() From e0f961811db80f5cc94fa6084cc87907264d9c9a Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Tue, 30 May 2023 13:47:34 +0300 Subject: [PATCH 67/85] History integration tests --- .../History/HistoryTests.swift | 50 +++++++++++++++++-- Sources/WalletConnectHistory/HistoryAPI.swift | 10 ++-- .../WalletConnectHistory/HistoryClient.swift | 14 ++++-- 3 files changed, 61 insertions(+), 13 deletions(-) diff --git a/Example/IntegrationTests/History/HistoryTests.swift b/Example/IntegrationTests/History/HistoryTests.swift index cd9ebf227..54384f8d3 100644 --- a/Example/IntegrationTests/History/HistoryTests.swift +++ b/Example/IntegrationTests/History/HistoryTests.swift @@ -5,13 +5,22 @@ import XCTest final class HistoryTests: XCTestCase { + var publishers = Set() + + let relayUrl = "wss://relay.walletconnect.com" + let historyUrl = "https://history.walletconnect.com" + var relayClient1: RelayClient! + var relayClient2: RelayClient! + var historyClient: HistoryClient! override func setUp() { - let keychain = KeychainStorageMock() - relayClient1 = makeRelayClient(prefix: "🐄", keychain: keychain) - historyClient = makeHistoryClient(keychain: keychain) + let keychain1 = KeychainStorageMock() + let keychain2 = KeychainStorageMock() + relayClient1 = makeRelayClient(prefix: "🐄", keychain: keychain1) + relayClient2 = makeRelayClient(prefix: "🐫", keychain: keychain2) + historyClient = makeHistoryClient(keychain: keychain1) } private func makeRelayClient(prefix: String, keychain: KeychainStorageProtocol) -> RelayClient { @@ -30,8 +39,39 @@ final class HistoryTests: XCTestCase { } func testRegister() async throws { - let payload = RegisterPayload(tags: ["7000"], relayUrl: "wss://relay.walletconnect.com") + let payload = RegisterPayload(tags: ["7000"], relayUrl: relayUrl) + + try await historyClient.registerTags(payload: payload, historyUrl: historyUrl) + } + + func testGetMessages() async throws { + let exp = expectation(description: "Test Get Messages") + let tag = 7000 + let payload = "{}" + let agreement = AgreementPrivateKey() + let topic = agreement.publicKey.rawRepresentation.sha256().hex + + relayClient2.messagePublisher.sink { (topic, message, publishedAt) in + exp.fulfill() + }.store(in: &publishers) + + try await historyClient.registerTags( + payload: RegisterPayload(tags: [String(tag)], relayUrl: relayUrl), + historyUrl: historyUrl) + + try await relayClient2.subscribe(topic: topic) + try await relayClient1.publish(topic: topic, payload: payload, tag: tag, prompt: false, ttl: 3000) + + await fulfillment(of: [exp], timeout: InputConfig.defaultTimeout) + + let messages = try await historyClient.getMessages( + payload: GetMessagesPayload( + topic: topic, + originId: nil, + messageCount: 200, + direction: .forward), + historyUrl: historyUrl) - try await historyClient.registerTags(payload: payload, historyUrl: "https://history.walletconnect.com") + XCTAssertEqual(messages.messages, [payload]) } } diff --git a/Sources/WalletConnectHistory/HistoryAPI.swift b/Sources/WalletConnectHistory/HistoryAPI.swift index b9b63e8b6..58f6b0cbd 100644 --- a/Sources/WalletConnectHistory/HistoryAPI.swift +++ b/Sources/WalletConnectHistory/HistoryAPI.swift @@ -40,11 +40,15 @@ enum HistoryAPI: HTTPService { } } - var queryParameters: [String : String]? { + var queryParameters: [String: String]? { switch self { case .messages(let payload): - let data = try! JSONEncoder().encode(payload) - return try? JSONDecoder().decode([String : String].self, from: data) + return [ + "topic": payload.topic, + "originId": payload.originId.map { String($0) }, + "messageCount": payload.messageCount.map { String($0) }, + "direction": payload.direction.rawValue + ].compactMapValues { $0 } case .register: return nil } diff --git a/Sources/WalletConnectHistory/HistoryClient.swift b/Sources/WalletConnectHistory/HistoryClient.swift index 1dcbf9196..46c1ae3aa 100644 --- a/Sources/WalletConnectHistory/HistoryClient.swift +++ b/Sources/WalletConnectHistory/HistoryClient.swift @@ -9,16 +9,13 @@ public final class HistoryClient { } public func registerTags(payload: RegisterPayload, historyUrl: String) async throws { - guard let host = URL(string: historyUrl)?.host else { - throw Errors.couldNotResolveHost - } - let service = HTTPNetworkClient(host: host) + let service = HTTPNetworkClient(host: try host(from: historyUrl)) let api = HistoryAPI.register(payload: payload, jwt: try getJwt(historyUrl: historyUrl)) try await service.request(service: api) } public func getMessages(payload: GetMessagesPayload, historyUrl: String) async throws -> GetMessagesResponse { - let service = HTTPNetworkClient(host: historyUrl) + let service = HTTPNetworkClient(host: try host(from: historyUrl)) let api = HistoryAPI.messages(payload: payload) return try await service.request(GetMessagesResponse.self, at: api) } @@ -34,4 +31,11 @@ private extension HistoryClient { let authenticator = ClientIdAuthenticator(clientIdStorage: clientIdStorage, url: historyUrl) return try authenticator.createAuthToken() } + + func host(from url: String) throws -> String { + guard let host = URL(string: url)?.host else { + throw Errors.couldNotResolveHost + } + return host + } } From 554661a9d9e3895322762688fdb47a80362fe86b Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Thu, 1 Jun 2023 16:03:20 +0300 Subject: [PATCH 68/85] History message mapping --- .../History/HistoryTests.swift | 2 ++ .../DomainLayer/Chat/ImportAccount.swift | 12 +++++----- .../Types/GetMessagesResponse.swift | 22 ++++++++++++++++++- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/Example/IntegrationTests/History/HistoryTests.swift b/Example/IntegrationTests/History/HistoryTests.swift index 54384f8d3..341ebb949 100644 --- a/Example/IntegrationTests/History/HistoryTests.swift +++ b/Example/IntegrationTests/History/HistoryTests.swift @@ -64,6 +64,8 @@ final class HistoryTests: XCTestCase { await fulfillment(of: [exp], timeout: InputConfig.defaultTimeout) + sleep(5) // History server has a queue + let messages = try await historyClient.getMessages( payload: GetMessagesPayload( topic: topic, diff --git a/Example/Showcase/Classes/DomainLayer/Chat/ImportAccount.swift b/Example/Showcase/Classes/DomainLayer/Chat/ImportAccount.swift index 80b9c3665..508b70a69 100644 --- a/Example/Showcase/Classes/DomainLayer/Chat/ImportAccount.swift +++ b/Example/Showcase/Classes/DomainLayer/Chat/ImportAccount.swift @@ -63,11 +63,11 @@ enum ImportAccount: Codable { var account: Account { switch self { case .swift: - return Account("eip155:1:0x1AAe9864337E821f2F86b5D27468C59AA333C877")! + return Account("eip155:1:0x5F847B18b4a2Dd0F428796E89CaEe71480a2a98e")! case .kotlin: - return Account("eip155:1:0x4c0fb06CD854ab7D5909E830a5f49D184EB41BF5")! + return Account("eip155:1:0xC313B6F74FcB89147e751220184F0C56D37a210e")! case .js: - return Account("eip155:1:0x7ABa5B1F436e42f6d4A579FB3Ad6D204F6A91863")! + return Account("eip155:1:0x265F4Eb49ab95ED142C4995EF8B5FC9e57538836")! case .custom(let privateKey): let address = try! EthereumPrivateKey(hexPrivateKey: "0x" + privateKey, ctx: nil).address.hex(eip55: true) return Account("eip155:1:\(address)")! @@ -79,11 +79,11 @@ enum ImportAccount: Codable { var privateKey: String { switch self { case .swift: - return "4dc0055d1831f7df8d855fc8cd9118f4a85ddc05395104c4cb0831a6752621a8" + return "85f52ec43821c1e2e24a248ee464e8d3f883e460acb0506e1eb6b520eb67ae15" case .kotlin: - return "ebe738a76b9a3b7457c3d5eca8d3d9ea6909bc563e05b6e0c5c35448f93100a0" + return "646a0ebac6bd34ba5f498b809148b2aca3793374cafe9dc417cf63bea80450bf" case .js: - return "de15cb11963e9bde0a5cce06a5ee2bda1cf3a67be6fbcd7a4fc8c0e4c4db0298" + return "8df6b8206eebcd3da89b750f1cf9bba887630c3c5eade83f44c06fa4f7cc5f65" case .custom(let privateKey): return privateKey case .web3Modal: diff --git a/Sources/WalletConnectHistory/Types/GetMessagesResponse.swift b/Sources/WalletConnectHistory/Types/GetMessagesResponse.swift index 8ea18add3..032bad07e 100644 --- a/Sources/WalletConnectHistory/Types/GetMessagesResponse.swift +++ b/Sources/WalletConnectHistory/Types/GetMessagesResponse.swift @@ -1,8 +1,28 @@ import Foundation -public struct GetMessagesResponse: Codable { +public struct GetMessagesResponse: Decodable { + public struct Message: Codable { + public let message: String + } public let topic: String public let direction: GetMessagesPayload.Direction public let nextId: Int64? public let messages: [String] + + enum CodingKeys: String, CodingKey { + case topic + case direction + case nextId + case messages + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.topic = try container.decode(String.self, forKey: .topic) + self.direction = try container.decode(GetMessagesPayload.Direction.self, forKey: .direction) + self.nextId = try container.decodeIfPresent(Int64.self, forKey: .nextId) + + let messages = try container.decode([Message].self, forKey: .messages) + self.messages = messages.map { $0.message } + } } From 91d93ba5095d7c2d82874946da9a9d06f7ed445d Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Thu, 1 Jun 2023 23:57:54 +0300 Subject: [PATCH 69/85] fetchMessageHistory --- Package.swift | 2 +- Sources/Chat/ChatClient.swift | 7 +++ Sources/Chat/ChatClientFactory.swift | 6 ++- Sources/Chat/ChatImports.swift | 1 + Sources/Chat/Storage/ChatStorage.swift | 2 +- .../Chat/Storage/ThreadStoreDelegate.swift | 49 +++++++++++++++++-- WalletConnectSwiftV2.podspec | 7 +++ 7 files changed, 67 insertions(+), 7 deletions(-) diff --git a/Package.swift b/Package.swift index 6c0bc8e03..9a61e0040 100644 --- a/Package.swift +++ b/Package.swift @@ -64,7 +64,7 @@ let package = Package( path: "Sources/WalletConnectSign"), .target( name: "WalletConnectChat", - dependencies: ["WalletConnectIdentity", "WalletConnectSync"], + dependencies: ["WalletConnectIdentity", "WalletConnectSync", "WalletConnectHistory"], path: "Sources/Chat"), .target( name: "Auth", diff --git a/Sources/Chat/ChatClient.swift b/Sources/Chat/ChatClient.swift index a0862fcc2..eb85954c8 100644 --- a/Sources/Chat/ChatClient.swift +++ b/Sources/Chat/ChatClient.swift @@ -4,6 +4,7 @@ import Combine public class ChatClient { private var publishers = [AnyCancellable]() private let identityClient: IdentityClient + private let historyClient: HistoryClient private let messagingService: MessagingService private let resubscriptionService: ResubscriptionService private let invitationHandlingService: InvitationHandlingService @@ -58,6 +59,7 @@ public class ChatClient { // MARK: - Initialization init(identityClient: IdentityClient, + historyClient: HistoryClient, messagingService: MessagingService, resubscriptionService: ResubscriptionService, invitationHandlingService: InvitationHandlingService, @@ -69,6 +71,7 @@ public class ChatClient { socketConnectionStatusPublisher: AnyPublisher ) { self.identityClient = identityClient + self.historyClient = historyClient self.messagingService = messagingService self.resubscriptionService = resubscriptionService self.invitationHandlingService = invitationHandlingService @@ -93,6 +96,10 @@ public class ChatClient { ) async throws -> String { let publicKey = try await identityClient.register(account: account, onSign: onSign) + + let payload = RegisterPayload(tags: ["2002"], relayUrl: "wss://relay.walletconnect.com") + try await historyClient.registerTags(payload: payload, historyUrl: "https://history.walletconnect.com") + try await syncRegisterService.register(account: account, onSign: onSign) guard !isPrivate else { diff --git a/Sources/Chat/ChatClientFactory.swift b/Sources/Chat/ChatClientFactory.swift index c341beec8..1fb8d073c 100644 --- a/Sources/Chat/ChatClientFactory.swift +++ b/Sources/Chat/ChatClientFactory.swift @@ -25,14 +25,17 @@ public struct ChatClientFactory { storage: KeyValueStorage, syncClient: SyncClient ) -> ChatClient { + let clientIdStorage = ClientIdStorage(keychain: keychain) + let historyClient = HistoryClient(clientIdStorage: clientIdStorage) let kms = KeyManagementService(keychain: keychain) + let serializer = Serializer(kms: kms) let messageStore = KeyedDatabase<[Message]>(storage: storage, identifier: ChatStorageIdentifiers.messages.rawValue) let receivedInviteStore = KeyedDatabase<[ReceivedInvite]>(storage: storage, identifier: ChatStorageIdentifiers.receivedInvites.rawValue) let threadStore: SyncStore = SyncStoreFactory.create(name: ChatStorageIdentifiers.thread.rawValue, syncClient: syncClient, storage: storage) let identityClient = IdentityClientFactory.create(keyserver: keyserverURL, keychain: keychain, logger: logger) let inviteKeyDelegate = InviteKeyDelegate(networkingInteractor: networkingInteractor, kms: kms, identityClient: identityClient) let sentInviteDelegate = SentInviteStoreDelegate(networkingInteractor: networkingInteractor, kms: kms) - let threadDelegate = ThreadStoreDelegate(networkingInteractor: networkingInteractor, kms: kms) + let threadDelegate = ThreadStoreDelegate(networkingInteractor: networkingInteractor, kms: kms, historyClient: historyClient, serializer: serializer) let sentInviteStore: SyncStore = SyncStoreFactory.create(name: ChatStorageIdentifiers.sentInvite.rawValue, syncClient: syncClient, storage: storage) let inviteKeyStore: SyncStore = SyncStoreFactory.create(name: ChatStorageIdentifiers.inviteKey.rawValue, syncClient: syncClient, storage: storage) let receivedInviteStatusStore: SyncStore = SyncStoreFactory.create(name: ChatStorageIdentifiers.receivedInviteStatus.rawValue, syncClient: syncClient, storage: storage) @@ -47,6 +50,7 @@ public struct ChatClientFactory { let client = ChatClient( identityClient: identityClient, + historyClient: historyClient, messagingService: messagingService, resubscriptionService: resubscriptionService, invitationHandlingService: invitationHandlingService, diff --git a/Sources/Chat/ChatImports.swift b/Sources/Chat/ChatImports.swift index 24dfe29a5..447ee4a25 100644 --- a/Sources/Chat/ChatImports.swift +++ b/Sources/Chat/ChatImports.swift @@ -2,4 +2,5 @@ @_exported import WalletConnectSigner @_exported import WalletConnectIdentity @_exported import WalletConnectSync +@_exported import WalletConnectHistory #endif diff --git a/Sources/Chat/Storage/ChatStorage.swift b/Sources/Chat/Storage/ChatStorage.swift index b90a5f0be..2787f84b1 100644 --- a/Sources/Chat/Storage/ChatStorage.swift +++ b/Sources/Chat/Storage/ChatStorage.swift @@ -107,7 +107,7 @@ final class ChatStorage { func initializeDelegates() async throws { try await sentInviteStoreDelegate.onInitialization(sentInviteStore.getAll()) - try await threadStoreDelegate.onInitialization(threadStore.getAll()) + try await threadStoreDelegate.onInitialization(storage: self) try await inviteKeyDelegate.onInitialization(inviteKeyStore.getAll()) try await receiviedInviteStatusDelegate.onInitialization() } diff --git a/Sources/Chat/Storage/ThreadStoreDelegate.swift b/Sources/Chat/Storage/ThreadStoreDelegate.swift index d03a2b672..2715fc02b 100644 --- a/Sources/Chat/Storage/ThreadStoreDelegate.swift +++ b/Sources/Chat/Storage/ThreadStoreDelegate.swift @@ -4,15 +4,23 @@ final class ThreadStoreDelegate { private let networkingInteractor: NetworkInteracting private let kms: KeyManagementServiceProtocol + private let historyClient: HistoryClient + private let serializer: Serializing - init(networkingInteractor: NetworkInteracting, kms: KeyManagementServiceProtocol) { + init(networkingInteractor: NetworkInteracting, kms: KeyManagementServiceProtocol, historyClient: HistoryClient, serializer: Serializing) { self.networkingInteractor = networkingInteractor self.kms = kms + self.serializer = serializer + self.historyClient = historyClient } - func onInitialization(_ threads: [Thread]) async throws { - let topics = threads.map { $0.topic } - try await networkingInteractor.batchSubscribe(topics: topics) + func onInitialization(storage: ChatStorage) async throws { + let threads = storage.getAllThreads() + try await networkingInteractor.batchSubscribe(topics: threads.map { $0.topic }) + + for thread in threads { + try await fetchMessageHistory(thread: thread, storage: storage) + } } func onUpdate(_ thread: Thread, storage: ChatStorage) { @@ -24,6 +32,8 @@ final class ThreadStoreDelegate { let symmetricKey = try SymmetricKey(hex: thread.symKey) try kms.setSymmetricKey(symmetricKey, for: thread.topic) try await networkingInteractor.subscribe(topic: thread.topic) + + // Relay Client injection! } } @@ -31,3 +41,34 @@ final class ThreadStoreDelegate { } } + +private extension ThreadStoreDelegate { + + func fetchMessageHistory(thread: Thread, storage: ChatStorage) async throws { + let historyPayload = GetMessagesPayload(topic: thread.topic, originId: nil, messageCount: 200, direction: .forward) + let response = try await historyClient.getMessages(payload: historyPayload, historyUrl: "https://history.walletconnect.com") + + for messagePayload in response.messages { + let (request, _, _): (RPCRequest, _, _) = try serializer.deserialize( + topic: thread.topic, + encodedEnvelope: messagePayload + ) + + let wrapper = try request.params!.get(MessagePayload.Wrapper.self) + + let (messagePayload, messageClaims) = try MessagePayload.decodeAndVerify(from: wrapper) + + let authorAccount = messagePayload.recipientAccount == thread.selfAccount + ? thread.peerAccount + : thread.selfAccount + + let message = Message( + topic: thread.topic, + message: messagePayload.message, + authorAccount: authorAccount, + timestamp: messageClaims.iat) + + storage.set(message: message, account: thread.selfAccount) + } + } +} diff --git a/WalletConnectSwiftV2.podspec b/WalletConnectSwiftV2.podspec index ed2856704..6b3b759a6 100644 --- a/WalletConnectSwiftV2.podspec +++ b/WalletConnectSwiftV2.podspec @@ -101,10 +101,17 @@ Pod::Spec.new do |spec| ss.dependency 'WalletConnectSwiftV2/WalletConnectNetworking' end + spec.subspec 'WalletConnectHistory' do |ss| + ss.source_files = 'Sources/WalletConnectHistory/**/*.{h,m,swift}' + ss.dependency 'WalletConnectSwiftV2/WalletConnectRelay' + ss.dependency 'WalletConnectSwiftV2/HTTPClient' + end + spec.subspec 'WalletConnectChat' do |ss| ss.source_files = 'Sources/Chat/**/*.{h,m,swift}' ss.dependency 'WalletConnectSwiftV2/WalletConnectSync' ss.dependency 'WalletConnectSwiftV2/WalletConnectIdentity' + ss.dependency 'WalletConnectSwiftV2/WalletConnectHistory' end spec.subspec 'WalletConnectSync' do |ss| From 9225265b44880d6d6c46a3593e6d4849ee90755b Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Fri, 2 Jun 2023 14:12:59 +0300 Subject: [PATCH 70/85] Savepoint --- .../History/HistoryTests.swift | 6 +- Sources/Chat/ChatClient.swift | 9 ++- Sources/Chat/ChatClientFactory.swift | 3 +- .../Sync/SyncRegisterService.swift | 4 ++ .../Chat/Storage/ThreadStoreDelegate.swift | 23 ++++---- .../WalletConnectHistory/HistoryClient.swift | 59 +++++++++---------- .../HistoryClientFactory.swift | 25 ++++++++ .../HistoryNetworkService.swift | 41 +++++++++++++ .../Stores/SyncSignatureStore.swift | 4 ++ .../WalletConnectSync/Stores/SyncStore.swift | 6 +- Sources/WalletConnectSync/SyncClient.swift | 5 ++ .../WalletConnectUtils/NewKeyedDatabase.swift | 2 +- 12 files changed, 132 insertions(+), 55 deletions(-) create mode 100644 Sources/WalletConnectHistory/HistoryClientFactory.swift create mode 100644 Sources/WalletConnectHistory/HistoryNetworkService.swift diff --git a/Example/IntegrationTests/History/HistoryTests.swift b/Example/IntegrationTests/History/HistoryTests.swift index 341ebb949..c5c1913a7 100644 --- a/Example/IntegrationTests/History/HistoryTests.swift +++ b/Example/IntegrationTests/History/HistoryTests.swift @@ -13,7 +13,7 @@ final class HistoryTests: XCTestCase { var relayClient1: RelayClient! var relayClient2: RelayClient! - var historyClient: HistoryClient! + var historyClient: HistoryNetworkService! override func setUp() { let keychain1 = KeychainStorageMock() @@ -33,9 +33,9 @@ final class HistoryTests: XCTestCase { logger: ConsoleLogger(suffix: prefix + " [Relay]", loggingLevel: .debug)) } - private func makeHistoryClient(keychain: KeychainStorageProtocol) -> HistoryClient { + private func makeHistoryClient(keychain: KeychainStorageProtocol) -> HistoryNetworkService { let clientIdStorage = ClientIdStorage(keychain: keychain) - return HistoryClient(clientIdStorage: clientIdStorage) + return HistoryNetworkService(clientIdStorage: clientIdStorage) } func testRegister() async throws { diff --git a/Sources/Chat/ChatClient.swift b/Sources/Chat/ChatClient.swift index eb85954c8..b7dc59f8e 100644 --- a/Sources/Chat/ChatClient.swift +++ b/Sources/Chat/ChatClient.swift @@ -96,11 +96,10 @@ public class ChatClient { ) async throws -> String { let publicKey = try await identityClient.register(account: account, onSign: onSign) - - let payload = RegisterPayload(tags: ["2002"], relayUrl: "wss://relay.walletconnect.com") - try await historyClient.registerTags(payload: payload, historyUrl: "https://history.walletconnect.com") - - try await syncRegisterService.register(account: account, onSign: onSign) + if syncRegisterService.isRegistered(account: account) { + try await historyClient.register(tags: ["2002"]) + try await syncRegisterService.register(account: account, onSign: onSign) + } guard !isPrivate else { return publicKey diff --git a/Sources/Chat/ChatClientFactory.swift b/Sources/Chat/ChatClientFactory.swift index 1fb8d073c..fe2be3f3a 100644 --- a/Sources/Chat/ChatClientFactory.swift +++ b/Sources/Chat/ChatClientFactory.swift @@ -25,8 +25,7 @@ public struct ChatClientFactory { storage: KeyValueStorage, syncClient: SyncClient ) -> ChatClient { - let clientIdStorage = ClientIdStorage(keychain: keychain) - let historyClient = HistoryClient(clientIdStorage: clientIdStorage) + let historyClient = HistoryClientFactory.create(keychain: keychain) let kms = KeyManagementService(keychain: keychain) let serializer = Serializer(kms: kms) let messageStore = KeyedDatabase<[Message]>(storage: storage, identifier: ChatStorageIdentifiers.messages.rawValue) diff --git a/Sources/Chat/ProtocolServices/Sync/SyncRegisterService.swift b/Sources/Chat/ProtocolServices/Sync/SyncRegisterService.swift index bcfe25fc5..8bffdfbbe 100644 --- a/Sources/Chat/ProtocolServices/Sync/SyncRegisterService.swift +++ b/Sources/Chat/ProtocolServices/Sync/SyncRegisterService.swift @@ -18,6 +18,10 @@ final class SyncRegisterService { throw Errors.signatureRejected } } + + func isRegistered(account: Account) -> Bool { + return syncClient.isRegistered(account: account) + } } private extension SyncRegisterService { diff --git a/Sources/Chat/Storage/ThreadStoreDelegate.swift b/Sources/Chat/Storage/ThreadStoreDelegate.swift index 2715fc02b..56070d99d 100644 --- a/Sources/Chat/Storage/ThreadStoreDelegate.swift +++ b/Sources/Chat/Storage/ThreadStoreDelegate.swift @@ -45,30 +45,27 @@ final class ThreadStoreDelegate { private extension ThreadStoreDelegate { func fetchMessageHistory(thread: Thread, storage: ChatStorage) async throws { - let historyPayload = GetMessagesPayload(topic: thread.topic, originId: nil, messageCount: 200, direction: .forward) - let response = try await historyClient.getMessages(payload: historyPayload, historyUrl: "https://history.walletconnect.com") - for messagePayload in response.messages { - let (request, _, _): (RPCRequest, _, _) = try serializer.deserialize( - topic: thread.topic, - encodedEnvelope: messagePayload - ) - - let wrapper = try request.params!.get(MessagePayload.Wrapper.self) + let wrappers: [MessagePayload.Wrapper] = try await historyClient.getMessages( + topic: thread.topic, + count: 200, direction: .backward + ) - let (messagePayload, messageClaims) = try MessagePayload.decodeAndVerify(from: wrapper) + let messages = wrappers.map { wrapper in + let (messagePayload, messageClaims) = try! MessagePayload.decodeAndVerify(from: wrapper) let authorAccount = messagePayload.recipientAccount == thread.selfAccount ? thread.peerAccount : thread.selfAccount - let message = Message( + return Message( topic: thread.topic, message: messagePayload.message, authorAccount: authorAccount, timestamp: messageClaims.iat) - - storage.set(message: message, account: thread.selfAccount) } + +// TODO: Set in store +// storage.set(message: message, account: thread.selfAccount) } } diff --git a/Sources/WalletConnectHistory/HistoryClient.swift b/Sources/WalletConnectHistory/HistoryClient.swift index 46c1ae3aa..be8673b4e 100644 --- a/Sources/WalletConnectHistory/HistoryClient.swift +++ b/Sources/WalletConnectHistory/HistoryClient.swift @@ -2,40 +2,39 @@ import Foundation public final class HistoryClient { - private let clientIdStorage: ClientIdStorage - - public init(clientIdStorage: ClientIdStorage) { - self.clientIdStorage = clientIdStorage - } - - public func registerTags(payload: RegisterPayload, historyUrl: String) async throws { - let service = HTTPNetworkClient(host: try host(from: historyUrl)) - let api = HistoryAPI.register(payload: payload, jwt: try getJwt(historyUrl: historyUrl)) - try await service.request(service: api) - } - - public func getMessages(payload: GetMessagesPayload, historyUrl: String) async throws -> GetMessagesResponse { - let service = HTTPNetworkClient(host: try host(from: historyUrl)) - let api = HistoryAPI.messages(payload: payload) - return try await service.request(GetMessagesResponse.self, at: api) + private let historyUrl: String + private let relayUrl: String + private let serializer: Serializer + private let historyNetworkService: HistoryNetworkService + + init(historyUrl: String, relayUrl: String, serializer: Serializer, historyNetworkService: HistoryNetworkService) { + self.historyUrl = historyUrl + self.relayUrl = relayUrl + self.serializer = serializer + self.historyNetworkService = historyNetworkService } -} - -private extension HistoryClient { - enum Errors: Error { - case couldNotResolveHost + public func register(tags: [String]) async throws { + let payload = RegisterPayload(tags: tags, relayUrl: relayUrl) + try await historyNetworkService.registerTags(payload: payload, historyUrl: historyUrl) } - func getJwt(historyUrl: String) throws -> String { - let authenticator = ClientIdAuthenticator(clientIdStorage: clientIdStorage, url: historyUrl) - return try authenticator.createAuthToken() - } - - func host(from url: String) throws -> String { - guard let host = URL(string: url)?.host else { - throw Errors.couldNotResolveHost + public func getMessages(topic: String, count: Int, direction: GetMessagesPayload.Direction) async throws -> [T] { + let payload = GetMessagesPayload(topic: topic, originId: nil, messageCount: count, direction: direction) + let response = try await historyNetworkService.getMessages(payload: payload, historyUrl: historyUrl) + + let objects = response.messages.compactMap { payload in + do { + let (request, _, _): (RPCRequest, _, _) = try serializer.deserialize( + topic: topic, + encodedEnvelope: payload + ) + return try request.params?.get(T.self) + } catch { + fatalError(error.localizedDescription) + } } - return host + + return objects } } diff --git a/Sources/WalletConnectHistory/HistoryClientFactory.swift b/Sources/WalletConnectHistory/HistoryClientFactory.swift new file mode 100644 index 000000000..d8c69b49a --- /dev/null +++ b/Sources/WalletConnectHistory/HistoryClientFactory.swift @@ -0,0 +1,25 @@ +import Foundation + +public class HistoryClientFactory { + + public static func create(keychain: KeychainStorageProtocol) -> HistoryClient { + return HistoryClientFactory.create( + historyUrl: "https://history.walletconnect.com", + relayUrl: "wss://relay.walletconnect.com", + keychain: keychain + ) + } + + static func create(historyUrl: String, relayUrl: String, keychain: KeychainStorageProtocol) -> HistoryClient { + let clientIdStorage = ClientIdStorage(keychain: keychain) + let kms = KeyManagementService(keychain: keychain) + let serializer = Serializer(kms: kms) + let historyNetworkService = HistoryNetworkService(clientIdStorage: clientIdStorage) + return HistoryClient( + historyUrl: historyUrl, + relayUrl: relayUrl, + serializer: serializer, + historyNetworkService: historyNetworkService + ) + } +} diff --git a/Sources/WalletConnectHistory/HistoryNetworkService.swift b/Sources/WalletConnectHistory/HistoryNetworkService.swift new file mode 100644 index 000000000..f4d01ddb8 --- /dev/null +++ b/Sources/WalletConnectHistory/HistoryNetworkService.swift @@ -0,0 +1,41 @@ +import Foundation + +final class HistoryNetworkService { + + private let clientIdStorage: ClientIdStorage + + init(clientIdStorage: ClientIdStorage) { + self.clientIdStorage = clientIdStorage + } + + func registerTags(payload: RegisterPayload, historyUrl: String) async throws { + let service = HTTPNetworkClient(host: try host(from: historyUrl)) + let api = HistoryAPI.register(payload: payload, jwt: try getJwt(historyUrl: historyUrl)) + try await service.request(service: api) + } + + func getMessages(payload: GetMessagesPayload, historyUrl: String) async throws -> GetMessagesResponse { + let service = HTTPNetworkClient(host: try host(from: historyUrl)) + let api = HistoryAPI.messages(payload: payload) + return try await service.request(GetMessagesResponse.self, at: api) + } +} + +private extension HistoryNetworkService { + + enum Errors: Error { + case couldNotResolveHost + } + + func getJwt(historyUrl: String) throws -> String { + let authenticator = ClientIdAuthenticator(clientIdStorage: clientIdStorage, url: historyUrl) + return try authenticator.createAuthToken() + } + + func host(from url: String) throws -> String { + guard let host = URL(string: url)?.host else { + throw Errors.couldNotResolveHost + } + return host + } +} diff --git a/Sources/WalletConnectSync/Stores/SyncSignatureStore.swift b/Sources/WalletConnectSync/Stores/SyncSignatureStore.swift index 81e9158f1..e8c5b69c0 100644 --- a/Sources/WalletConnectSync/Stores/SyncSignatureStore.swift +++ b/Sources/WalletConnectSync/Stores/SyncSignatureStore.swift @@ -21,6 +21,10 @@ final class SyncSignatureStore { return key } + + func isSignatureExists(account: Account) -> Bool { + return (try? getSignature(for: account)) != nil + } } private extension SyncSignatureStore { diff --git a/Sources/WalletConnectSync/Stores/SyncStore.swift b/Sources/WalletConnectSync/Stores/SyncStore.swift index 91be85ac7..a345812cd 100644 --- a/Sources/WalletConnectSync/Stores/SyncStore.swift +++ b/Sources/WalletConnectSync/Stores/SyncStore.swift @@ -1,10 +1,14 @@ import Foundation import Combine -public protocol SyncObject: Codable & Equatable { +public protocol SyncObject: Codable & Equatable & Identifiable { var syncId: String { get } } +extension SyncObject { + var id: String { return syncId } +} + public enum SyncUpdate { case set(object: Object) case delete(id: String) diff --git a/Sources/WalletConnectSync/SyncClient.swift b/Sources/WalletConnectSync/SyncClient.swift index 9385e7c3a..3687bf97c 100644 --- a/Sources/WalletConnectSync/SyncClient.swift +++ b/Sources/WalletConnectSync/SyncClient.swift @@ -24,6 +24,11 @@ public final class SyncClient { """ } + /// Checks if account is already registered in sync + public func isRegistered(account: Account) -> Bool { + return syncSignatureStore.isSignatureExists(account: account) + } + /// Register an account to sync public func register(account: Account, signature: CacaoSignature) async throws { // TODO: Signature verify diff --git a/Sources/WalletConnectUtils/NewKeyedDatabase.swift b/Sources/WalletConnectUtils/NewKeyedDatabase.swift index 844b92ef3..96981ddec 100644 --- a/Sources/WalletConnectUtils/NewKeyedDatabase.swift +++ b/Sources/WalletConnectUtils/NewKeyedDatabase.swift @@ -1,6 +1,6 @@ import Foundation -public class NewKeyedDatabase where Element: Codable & Equatable { +public class NewKeyedDatabase where Element: Codable & Equatable & Identifiable { public var index: [String: Element] = [:] { didSet { From 0d8cf96442ccbdf0f5bc22dbc6e429390afd7fee Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Fri, 2 Jun 2023 14:57:37 +0300 Subject: [PATCH 71/85] Database refactor --- Sources/Chat/Types/Plain/InviteKey.swift | 4 +- .../Types/Plain/ReceivedInviteStatus.swift | 4 +- Sources/Chat/Types/Plain/SentInvite.swift | 4 +- Sources/Chat/Types/Plain/Thread.swift | 4 +- .../Services/SyncService.swift | 4 +- .../Stores/SyncObjectStore.swift | 67 ------------------- .../WalletConnectSync/Stores/SyncStore.swift | 28 +++----- .../Stores/SyncStoreFactory.swift | 5 +- Sources/WalletConnectSync/SyncClient.swift | 2 +- .../WalletConnectUtils/NewKeyedDatabase.swift | 50 +++++++++++--- 10 files changed, 64 insertions(+), 108 deletions(-) delete mode 100644 Sources/WalletConnectSync/Stores/SyncObjectStore.swift diff --git a/Sources/Chat/Types/Plain/InviteKey.swift b/Sources/Chat/Types/Plain/InviteKey.swift index 1c5f3ddf8..bf5cb4796 100644 --- a/Sources/Chat/Types/Plain/InviteKey.swift +++ b/Sources/Chat/Types/Plain/InviteKey.swift @@ -1,6 +1,6 @@ import Foundation -struct InviteKey: SyncObject { +struct InviteKey: DatabaseObject { let publicKey: String let privateKey: String let account: Account @@ -9,7 +9,7 @@ struct InviteKey: SyncObject { return Data(hex: publicKey).sha256().toHexString() } - var syncId: String { + var databaseId: String { return account.absoluteString } } diff --git a/Sources/Chat/Types/Plain/ReceivedInviteStatus.swift b/Sources/Chat/Types/Plain/ReceivedInviteStatus.swift index ae144ccb0..c046392d7 100644 --- a/Sources/Chat/Types/Plain/ReceivedInviteStatus.swift +++ b/Sources/Chat/Types/Plain/ReceivedInviteStatus.swift @@ -1,10 +1,10 @@ import Foundation -struct ReceivedInviteStatus: Codable, SyncObject { +struct ReceivedInviteStatus: Codable, DatabaseObject { let id: Int64 let status: ReceivedInvite.Status - var syncId: String { + var databaseId: String { return String(id) } } diff --git a/Sources/Chat/Types/Plain/SentInvite.swift b/Sources/Chat/Types/Plain/SentInvite.swift index aa7c2cad3..011cf96dc 100644 --- a/Sources/Chat/Types/Plain/SentInvite.swift +++ b/Sources/Chat/Types/Plain/SentInvite.swift @@ -52,9 +52,9 @@ public struct SentInvite: Codable, Equatable { } } -extension SentInvite: SyncObject { +extension SentInvite: DatabaseObject { - public var syncId: String { + public var databaseId: String { return responseTopic } } diff --git a/Sources/Chat/Types/Plain/Thread.swift b/Sources/Chat/Types/Plain/Thread.swift index 1ddd6e7b1..1c1015377 100644 --- a/Sources/Chat/Types/Plain/Thread.swift +++ b/Sources/Chat/Types/Plain/Thread.swift @@ -7,9 +7,9 @@ public struct Thread: Codable, Equatable { public let symKey: String } -extension Thread: SyncObject { +extension Thread: DatabaseObject { - public var syncId: String { + public var databaseId: String { return topic } } diff --git a/Sources/WalletConnectSync/Services/SyncService.swift b/Sources/WalletConnectSync/Services/SyncService.swift index 3275cc7e5..d18a5580a 100644 --- a/Sources/WalletConnectSync/Services/SyncService.swift +++ b/Sources/WalletConnectSync/Services/SyncService.swift @@ -31,9 +31,9 @@ final class SyncService { setupSubscriptions() } - func set(account: Account, store: String, object: Object) async throws { + func set(account: Account, store: String, object: Object) async throws { let protocolMethod = SyncSetMethod() - let params = StoreSet(key: object.syncId, value: try object.json()) + let params = StoreSet(key: object.databaseId, value: try object.json()) let rpcid = RPCID() let request = RPCRequest(method: protocolMethod.method, params: params, rpcid: rpcid) let record = try indexStore.getRecord(account: account, name: store) diff --git a/Sources/WalletConnectSync/Stores/SyncObjectStore.swift b/Sources/WalletConnectSync/Stores/SyncObjectStore.swift deleted file mode 100644 index 2e7253906..000000000 --- a/Sources/WalletConnectSync/Stores/SyncObjectStore.swift +++ /dev/null @@ -1,67 +0,0 @@ -import Foundation - -final class SyncObjectStore { - - /// `storeTopic` to [`id`: `Object`] map keyValue store - private let store: NewKeyedDatabase<[String: Object]> - - var onUpdate: (() -> Void)? { - get { - return store.onUpdate - } - set { - store.onUpdate = newValue - } - } - - init(store: NewKeyedDatabase<[String : Object]>) { - self.store = store - } - - func getMap(topic: String) -> [String: Object] { - return store.getElement(for: topic) ?? [:] - } - - func getAll(topic: String) -> [Object] { - let map = getMap(topic: topic) - return Array(map.values) - } - - func getAll() -> [Object] { - return store.index.values.reduce([]) { result, values in - return result + values.values - } - } - - @discardableResult func set(object: Object, topic: String) -> Bool { - guard isChanged(object, topic: topic) else { return false } - var map = getMap(topic: topic) - map[object.syncId] = object - store.set(element: map, for: topic) - return true - } - - - @discardableResult func delete(id: String, topic: String) -> Bool { - guard isExists(id: id, topic: topic) else { return false } - var map = getMap(topic: topic) - map[id] = nil - store.set(element: map, for: topic) - return true - } -} - -private extension SyncObjectStore { - - func isExists(id: String, topic: String) -> Bool { - return getElement(id: id, topic: topic) != nil - } - - func getElement(id: String, topic: String) -> Object? { - return store.getElement(for: topic)?[id] - } - - func isChanged(_ object: Object, topic: String) -> Bool { - return object != getElement(id: object.syncId, topic: topic) - } -} diff --git a/Sources/WalletConnectSync/Stores/SyncStore.swift b/Sources/WalletConnectSync/Stores/SyncStore.swift index a345812cd..68eacc8c4 100644 --- a/Sources/WalletConnectSync/Stores/SyncStore.swift +++ b/Sources/WalletConnectSync/Stores/SyncStore.swift @@ -1,20 +1,12 @@ import Foundation import Combine -public protocol SyncObject: Codable & Equatable & Identifiable { - var syncId: String { get } -} - -extension SyncObject { - var id: String { return syncId } -} - -public enum SyncUpdate { +public enum SyncUpdate { case set(object: Object) case delete(id: String) } -public final class SyncStore { +public final class SyncStore { private var publishers = Set() @@ -25,7 +17,7 @@ public final class SyncStore { private let indexStore: SyncIndexStore /// `storeTopic` to [`id`: `Object`] map keyValue store - private let objectStore: SyncObjectStore + private let objectStore: NewKeyedDatabase private let dataUpdateSubject = PassthroughSubject<[Object], Never>() private let syncUpdateSubject = PassthroughSubject<(String, Account, SyncUpdate), Never>() @@ -38,7 +30,7 @@ public final class SyncStore { return syncUpdateSubject.eraseToAnyPublisher() } - init(name: String, syncClient: SyncClient, indexStore: SyncIndexStore, objectStore: SyncObjectStore) { + init(name: String, syncClient: SyncClient, indexStore: SyncIndexStore, objectStore: NewKeyedDatabase) { self.name = name self.syncClient = syncClient self.indexStore = indexStore @@ -55,13 +47,13 @@ public final class SyncStore { let record = try indexStore.getRecord(account: account, name: name) objectStore.onUpdate = { [unowned self] in - dataUpdateSubject.send(objectStore.getAll(topic: record.topic)) + dataUpdateSubject.send(objectStore.getAll(for: record.topic)) } } public func getAll(for account: Account) throws -> [Object] { let record = try indexStore.getRecord(account: account, name: name) - return objectStore.getAll(topic: record.topic) + return objectStore.getAll(for: record.topic) } public func getAll() -> [Object] { @@ -71,7 +63,7 @@ public final class SyncStore { public func set(object: Object, for account: Account) async throws { let record = try indexStore.getRecord(account: account, name: name) - if objectStore.set(object: object, topic: record.topic) { + if objectStore.set(element: object, for: record.topic) { try await syncClient.set(account: account, store: record.store, object: object) } } @@ -79,7 +71,7 @@ public final class SyncStore { public func delete(id: String, for account: Account) async throws { let record = try indexStore.getRecord(account: account, name: name) - if objectStore.delete(id: id, topic: record.topic) { + if objectStore.delete(id: id, for: record.topic) { try await syncClient.delete(account: account, store: record.store, key: id) } } @@ -110,11 +102,11 @@ private extension SyncStore { func setInStore(object: Object, for account: Account) throws -> Bool { let record = try indexStore.getRecord(account: account, name: name) - return objectStore.set(object: object, topic: record.topic) + return objectStore.set(element: object, for: record.topic) } func deleteInStore(id: String, for account: Account) throws -> Bool { let record = try indexStore.getRecord(account: account, name: name) - return objectStore.delete(id: id, topic: record.topic) + return objectStore.delete(id: id, for: record.topic) } } diff --git a/Sources/WalletConnectSync/Stores/SyncStoreFactory.swift b/Sources/WalletConnectSync/Stores/SyncStoreFactory.swift index 4e0ef9fe9..17b787e02 100644 --- a/Sources/WalletConnectSync/Stores/SyncStoreFactory.swift +++ b/Sources/WalletConnectSync/Stores/SyncStoreFactory.swift @@ -2,12 +2,11 @@ import Foundation public final class SyncStoreFactory { - public static func create(name: String, syncClient: SyncClient, storage: KeyValueStorage) -> SyncStore { + public static func create(name: String, syncClient: SyncClient, storage: KeyValueStorage) -> SyncStore { let indexDatabase = CodableStore(defaults: UserDefaults.standard, identifier: SyncStorageIdentifiers.index.identifier) let indexStore = SyncIndexStore(store: indexDatabase) let objectIdentifier = SyncStorageIdentifiers.object(store: name).identifier - let objectDatabase = NewKeyedDatabase<[String: Object]>(storage: storage, identifier: objectIdentifier) - let objectStore = SyncObjectStore(store: objectDatabase) + let objectStore = NewKeyedDatabase(storage: storage, identifier: objectIdentifier) return SyncStore(name: name, syncClient: syncClient, indexStore: indexStore, objectStore: objectStore) } } diff --git a/Sources/WalletConnectSync/SyncClient.swift b/Sources/WalletConnectSync/SyncClient.swift index 3687bf97c..fbe09bf6f 100644 --- a/Sources/WalletConnectSync/SyncClient.swift +++ b/Sources/WalletConnectSync/SyncClient.swift @@ -41,7 +41,7 @@ public final class SyncClient { } // Set value to store - public func set( + public func set( account: Account, store: String, object: Object diff --git a/Sources/WalletConnectUtils/NewKeyedDatabase.swift b/Sources/WalletConnectUtils/NewKeyedDatabase.swift index 96981ddec..b15336b4b 100644 --- a/Sources/WalletConnectUtils/NewKeyedDatabase.swift +++ b/Sources/WalletConnectUtils/NewKeyedDatabase.swift @@ -1,8 +1,14 @@ import Foundation -public class NewKeyedDatabase where Element: Codable & Equatable & Identifiable { +public protocol DatabaseObject: Codable & Equatable { + var databaseId: String { get } +} + +public class NewKeyedDatabase where Element: DatabaseObject { - public var index: [String: Element] = [:] { + public typealias Index = [String: [String: Element]] + + public var index: Index = [:] { didSet { guard oldValue != index else { return } set(index, for: identifier) @@ -23,15 +29,41 @@ public class NewKeyedDatabase where Element: Codable & Equatable & Iden } public func getAll() -> [Element] { - return Array(index.values) + return index.values.reduce([]) { result, map in + return result + map.values + } + } + + public func getAll(for key: String) -> [Element] { + return index[key].map { Array($0.values) } ?? [] + } + + public func getElement(for key: String, id: String) -> Element? { + return index[key]?[id] } - public func getElement(for key: String) -> Element? { - return index[key] + public func set(element: Element, for key: String) -> Bool { + var map = index[key] ?? [:] + + guard + map[element.databaseId] == nil else { return false } + map[element.databaseId] = element + + index[key] = map + + return true } - public func set(element: Element, for key: String) { - index[key] = element + public func delete(id: String, for key: String) -> Bool { + var map = index[key] + + guard + map?[id] != nil else { return false } + map?[id] = nil + + index[key] = map + + return true } } @@ -40,13 +72,13 @@ private extension NewKeyedDatabase { func initializeIndex() { guard let data = storage.object(forKey: identifier) as? Data, - let decoded = try? JSONDecoder().decode([String: Element].self, from: data) + let decoded = try? JSONDecoder().decode(Index.self, from: data) else { return } index = decoded } - func set(_ value: [String: Element], for key: String) { + func set(_ value: Index, for key: String) { let data = try! JSONEncoder().encode(value) storage.set(data, forKey: key) } From 24023c9bf4f839a84a79f45005a574b79013d0cb Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Fri, 2 Jun 2023 16:13:21 +0300 Subject: [PATCH 72/85] Integration tests repaired --- Example/IntegrationTests/Sync/SyncTests.swift | 7 +-- Sources/Chat/ChatClient.swift | 2 +- Sources/Chat/ChatClientFactory.swift | 4 +- Sources/Chat/Storage/ChatStorage.swift | 24 ++++---- Sources/Chat/Types/Plain/Message.swift | 6 +- Sources/Chat/Types/Plain/ReceivedInvite.swift | 6 +- .../Types/Plain/ReceivedInviteStatus.swift | 2 +- .../WalletConnectUtils/KeyedDatabase.swift | 60 ------------------- 8 files changed, 29 insertions(+), 82 deletions(-) delete mode 100644 Sources/WalletConnectUtils/KeyedDatabase.swift diff --git a/Example/IntegrationTests/Sync/SyncTests.swift b/Example/IntegrationTests/Sync/SyncTests.swift index 7eaf56828..96f550d2c 100644 --- a/Example/IntegrationTests/Sync/SyncTests.swift +++ b/Example/IntegrationTests/Sync/SyncTests.swift @@ -7,11 +7,11 @@ import Web3 final class SyncTests: XCTestCase { - struct TestObject: SyncObject { + struct TestObject: DatabaseObject { let id: String let value: String - var syncId: String { + var databaseId: String { return id } } @@ -75,8 +75,7 @@ final class SyncTests: XCTestCase { } func makeSyncStore(client: SyncClient, indexStore: SyncIndexStore) -> SyncStore { - let store = NewKeyedDatabase<[String: TestObject]>(storage: RuntimeKeyValueStorage(), identifier: "objectStore") - let objectStore = SyncObjectStore(store: store) + let objectStore = NewKeyedDatabase(storage: RuntimeKeyValueStorage(), identifier: "objectStore") return SyncStore(name: storeName, syncClient: client, indexStore: indexStore, objectStore: objectStore) } diff --git a/Sources/Chat/ChatClient.swift b/Sources/Chat/ChatClient.swift index b7dc59f8e..850cb86e7 100644 --- a/Sources/Chat/ChatClient.swift +++ b/Sources/Chat/ChatClient.swift @@ -96,7 +96,7 @@ public class ChatClient { ) async throws -> String { let publicKey = try await identityClient.register(account: account, onSign: onSign) - if syncRegisterService.isRegistered(account: account) { + if !syncRegisterService.isRegistered(account: account) { try await historyClient.register(tags: ["2002"]) try await syncRegisterService.register(account: account, onSign: onSign) } diff --git a/Sources/Chat/ChatClientFactory.swift b/Sources/Chat/ChatClientFactory.swift index fe2be3f3a..f571c4b0d 100644 --- a/Sources/Chat/ChatClientFactory.swift +++ b/Sources/Chat/ChatClientFactory.swift @@ -28,8 +28,8 @@ public struct ChatClientFactory { let historyClient = HistoryClientFactory.create(keychain: keychain) let kms = KeyManagementService(keychain: keychain) let serializer = Serializer(kms: kms) - let messageStore = KeyedDatabase<[Message]>(storage: storage, identifier: ChatStorageIdentifiers.messages.rawValue) - let receivedInviteStore = KeyedDatabase<[ReceivedInvite]>(storage: storage, identifier: ChatStorageIdentifiers.receivedInvites.rawValue) + let messageStore = NewKeyedDatabase(storage: storage, identifier: ChatStorageIdentifiers.messages.rawValue) + let receivedInviteStore = NewKeyedDatabase(storage: storage, identifier: ChatStorageIdentifiers.receivedInvites.rawValue) let threadStore: SyncStore = SyncStoreFactory.create(name: ChatStorageIdentifiers.thread.rawValue, syncClient: syncClient, storage: storage) let identityClient = IdentityClientFactory.create(keyserver: keyserverURL, keychain: keychain, logger: logger) let inviteKeyDelegate = InviteKeyDelegate(networkingInteractor: networkingInteractor, kms: kms, identityClient: identityClient) diff --git a/Sources/Chat/Storage/ChatStorage.swift b/Sources/Chat/Storage/ChatStorage.swift index 2787f84b1..1966d1e20 100644 --- a/Sources/Chat/Storage/ChatStorage.swift +++ b/Sources/Chat/Storage/ChatStorage.swift @@ -6,8 +6,8 @@ final class ChatStorage { private var publishers = Set() private let kms: KeyManagementServiceProtocol - private let messageStore: KeyedDatabase<[Message]> - private let receivedInviteStore: KeyedDatabase<[ReceivedInvite]> + private let messageStore: NewKeyedDatabase + private let receivedInviteStore: NewKeyedDatabase private let sentInviteStore: SyncStore private let threadStore: SyncStore private let inviteKeyStore: SyncStore @@ -70,8 +70,8 @@ final class ChatStorage { init( kms: KeyManagementServiceProtocol, - messageStore: KeyedDatabase<[Message]>, - receivedInviteStore: KeyedDatabase<[ReceivedInvite]>, + messageStore: NewKeyedDatabase, + receivedInviteStore: NewKeyedDatabase, sentInviteStore: SyncStore, threadStore: SyncStore, inviteKeyStore: SyncStore, @@ -138,7 +138,7 @@ final class ChatStorage { } func set(receivedInvite: ReceivedInvite, account: Account) { - receivedInviteStore.set(receivedInvite, for: account.absoluteString) + receivedInviteStore.set(element: receivedInvite, for: account.absoluteString) newReceivedInvitePublisherSubject.send(receivedInvite) } @@ -148,7 +148,7 @@ final class ChatStorage { } func getReceivedInvites(account: Account) -> [ReceivedInvite] { - return receivedInviteStore.getElements(for: account.absoluteString) ?? [] + return receivedInviteStore.getAll(for: account.absoluteString) ?? [] } func syncRejectedReceivedInviteStatus(id: Int64, account: Account) async throws { @@ -171,17 +171,17 @@ final class ChatStorage { } func accept(receivedInvite: ReceivedInvite, account: Account) { - receivedInviteStore.delete(receivedInvite, for: account.absoluteString) + receivedInviteStore.delete(id: receivedInvite.databaseId, for: account.absoluteString) let accepted = ReceivedInvite(invite: receivedInvite, status: .approved) - receivedInviteStore.set(accepted, for: account.absoluteString) + receivedInviteStore.set(element: accepted, for: account.absoluteString) } func reject(receivedInvite: ReceivedInvite, account: Account) { - receivedInviteStore.delete(receivedInvite, for: account.absoluteString) + receivedInviteStore.delete(id: receivedInvite.databaseId, for: account.absoluteString) let rejected = ReceivedInvite(invite: receivedInvite, status: .rejected) - receivedInviteStore.set(rejected, for: account.absoluteString) + receivedInviteStore.set(element: rejected, for: account.absoluteString) } func accept(sentInviteId: Int64, topic: String) async throws { @@ -246,7 +246,7 @@ final class ChatStorage { // MARK: - Messages func set(message: Message, account: Account) { - messageStore.set(message, for: account.absoluteString) + messageStore.set(element: message, for: account.absoluteString) newMessagePublisherSubject.send(message) } @@ -255,7 +255,7 @@ final class ChatStorage { } func getMessages(account: Account) -> [Message] { - return messageStore.getElements(for: account.absoluteString) ?? [] + return messageStore.getAll(for: account.absoluteString) ?? [] } } diff --git a/Sources/Chat/Types/Plain/Message.swift b/Sources/Chat/Types/Plain/Message.swift index fe6d07c2c..f6bd8e70c 100644 --- a/Sources/Chat/Types/Plain/Message.swift +++ b/Sources/Chat/Types/Plain/Message.swift @@ -1,12 +1,16 @@ import Foundation -public struct Message: Codable, Equatable { +public struct Message: DatabaseObject { public let topic: String public let message: String public let authorAccount: Account public let timestamp: UInt64 public let media: Media? + public var databaseId: String { + return String(timestamp) + } + init( topic: String, message: String, diff --git a/Sources/Chat/Types/Plain/ReceivedInvite.swift b/Sources/Chat/Types/Plain/ReceivedInvite.swift index dde40c77b..64d20f357 100644 --- a/Sources/Chat/Types/Plain/ReceivedInvite.swift +++ b/Sources/Chat/Types/Plain/ReceivedInvite.swift @@ -1,6 +1,6 @@ import Foundation -public struct ReceivedInvite: Codable, Equatable { +public struct ReceivedInvite: DatabaseObject { public let id: Int64 public let message: String public let inviterAccount: Account @@ -10,6 +10,10 @@ public struct ReceivedInvite: Codable, Equatable { public let timestamp: UInt64 public var status: Status + public var databaseId: String { + return String(id) + } + public init( id: Int64, message: String, diff --git a/Sources/Chat/Types/Plain/ReceivedInviteStatus.swift b/Sources/Chat/Types/Plain/ReceivedInviteStatus.swift index c046392d7..3263669b7 100644 --- a/Sources/Chat/Types/Plain/ReceivedInviteStatus.swift +++ b/Sources/Chat/Types/Plain/ReceivedInviteStatus.swift @@ -1,6 +1,6 @@ import Foundation -struct ReceivedInviteStatus: Codable, DatabaseObject { +struct ReceivedInviteStatus: DatabaseObject { let id: Int64 let status: ReceivedInvite.Status diff --git a/Sources/WalletConnectUtils/KeyedDatabase.swift b/Sources/WalletConnectUtils/KeyedDatabase.swift deleted file mode 100644 index b15a87a5b..000000000 --- a/Sources/WalletConnectUtils/KeyedDatabase.swift +++ /dev/null @@ -1,60 +0,0 @@ -import Foundation - -public class KeyedDatabase where Element: Codable & Equatable { - - public var index: [String: Element] = [:] { - didSet { - guard oldValue != index else { return } - set(index, for: identifier) - onUpdate?() - } - } - - private let storage: KeyValueStorage - private let identifier: String - - public var onUpdate: (() -> Void)? - - public init(storage: KeyValueStorage, identifier: String) { - self.storage = storage - self.identifier = identifier - - initializeIndex() - } -} - -extension KeyedDatabase where Element: RangeReplaceableCollection, Element.Element: Equatable { - - public func getAll() -> [Element.Element] { - return index.values.reduce([], +) - } - - public func set(_ element: Element.Element, for key: String) { - index.append(element, for: key) - } - - public func delete(_ element: Element.Element, for key: String) { - index.delete(element, for: key) - } - - public func getElements(for key: String) -> Element? { - return index[key] - } -} - -private extension KeyedDatabase { - - func initializeIndex() { - guard - let data = storage.object(forKey: identifier) as? Data, - let decoded = try? JSONDecoder().decode([String: Element].self, from: data) - else { return } - - index = decoded - } - - func set(_ value: [String: Element], for key: String) { - let data = try! JSONEncoder().encode(value) - storage.set(data, forKey: key) - } -} From 4be7e30aaf8c9d6f4625695bcf958ff5fa8c6aa1 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Fri, 2 Jun 2023 16:13:54 +0300 Subject: [PATCH 73/85] KeyedDatabase rename --- Example/IntegrationTests/Sync/SyncTests.swift | 2 +- Sources/Chat/ChatClientFactory.swift | 4 ++-- Sources/Chat/Storage/ChatStorage.swift | 8 ++++---- Sources/WalletConnectSync/Stores/SyncStore.swift | 4 ++-- Sources/WalletConnectSync/Stores/SyncStoreFactory.swift | 2 +- .../{NewKeyedDatabase.swift => KeyedDatabase.swift} | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) rename Sources/WalletConnectUtils/{NewKeyedDatabase.swift => KeyedDatabase.swift} (94%) diff --git a/Example/IntegrationTests/Sync/SyncTests.swift b/Example/IntegrationTests/Sync/SyncTests.swift index 96f550d2c..595052dd0 100644 --- a/Example/IntegrationTests/Sync/SyncTests.swift +++ b/Example/IntegrationTests/Sync/SyncTests.swift @@ -75,7 +75,7 @@ final class SyncTests: XCTestCase { } func makeSyncStore(client: SyncClient, indexStore: SyncIndexStore) -> SyncStore { - let objectStore = NewKeyedDatabase(storage: RuntimeKeyValueStorage(), identifier: "objectStore") + let objectStore = KeyedDatabase(storage: RuntimeKeyValueStorage(), identifier: "objectStore") return SyncStore(name: storeName, syncClient: client, indexStore: indexStore, objectStore: objectStore) } diff --git a/Sources/Chat/ChatClientFactory.swift b/Sources/Chat/ChatClientFactory.swift index f571c4b0d..cbf7d972d 100644 --- a/Sources/Chat/ChatClientFactory.swift +++ b/Sources/Chat/ChatClientFactory.swift @@ -28,8 +28,8 @@ public struct ChatClientFactory { let historyClient = HistoryClientFactory.create(keychain: keychain) let kms = KeyManagementService(keychain: keychain) let serializer = Serializer(kms: kms) - let messageStore = NewKeyedDatabase(storage: storage, identifier: ChatStorageIdentifiers.messages.rawValue) - let receivedInviteStore = NewKeyedDatabase(storage: storage, identifier: ChatStorageIdentifiers.receivedInvites.rawValue) + let messageStore = KeyedDatabase(storage: storage, identifier: ChatStorageIdentifiers.messages.rawValue) + let receivedInviteStore = KeyedDatabase(storage: storage, identifier: ChatStorageIdentifiers.receivedInvites.rawValue) let threadStore: SyncStore = SyncStoreFactory.create(name: ChatStorageIdentifiers.thread.rawValue, syncClient: syncClient, storage: storage) let identityClient = IdentityClientFactory.create(keyserver: keyserverURL, keychain: keychain, logger: logger) let inviteKeyDelegate = InviteKeyDelegate(networkingInteractor: networkingInteractor, kms: kms, identityClient: identityClient) diff --git a/Sources/Chat/Storage/ChatStorage.swift b/Sources/Chat/Storage/ChatStorage.swift index 1966d1e20..a688ffc13 100644 --- a/Sources/Chat/Storage/ChatStorage.swift +++ b/Sources/Chat/Storage/ChatStorage.swift @@ -6,8 +6,8 @@ final class ChatStorage { private var publishers = Set() private let kms: KeyManagementServiceProtocol - private let messageStore: NewKeyedDatabase - private let receivedInviteStore: NewKeyedDatabase + private let messageStore: KeyedDatabase + private let receivedInviteStore: KeyedDatabase private let sentInviteStore: SyncStore private let threadStore: SyncStore private let inviteKeyStore: SyncStore @@ -70,8 +70,8 @@ final class ChatStorage { init( kms: KeyManagementServiceProtocol, - messageStore: NewKeyedDatabase, - receivedInviteStore: NewKeyedDatabase, + messageStore: KeyedDatabase, + receivedInviteStore: KeyedDatabase, sentInviteStore: SyncStore, threadStore: SyncStore, inviteKeyStore: SyncStore, diff --git a/Sources/WalletConnectSync/Stores/SyncStore.swift b/Sources/WalletConnectSync/Stores/SyncStore.swift index 68eacc8c4..57b5320b4 100644 --- a/Sources/WalletConnectSync/Stores/SyncStore.swift +++ b/Sources/WalletConnectSync/Stores/SyncStore.swift @@ -17,7 +17,7 @@ public final class SyncStore { private let indexStore: SyncIndexStore /// `storeTopic` to [`id`: `Object`] map keyValue store - private let objectStore: NewKeyedDatabase + private let objectStore: KeyedDatabase private let dataUpdateSubject = PassthroughSubject<[Object], Never>() private let syncUpdateSubject = PassthroughSubject<(String, Account, SyncUpdate), Never>() @@ -30,7 +30,7 @@ public final class SyncStore { return syncUpdateSubject.eraseToAnyPublisher() } - init(name: String, syncClient: SyncClient, indexStore: SyncIndexStore, objectStore: NewKeyedDatabase) { + init(name: String, syncClient: SyncClient, indexStore: SyncIndexStore, objectStore: KeyedDatabase) { self.name = name self.syncClient = syncClient self.indexStore = indexStore diff --git a/Sources/WalletConnectSync/Stores/SyncStoreFactory.swift b/Sources/WalletConnectSync/Stores/SyncStoreFactory.swift index 17b787e02..cfd55b22d 100644 --- a/Sources/WalletConnectSync/Stores/SyncStoreFactory.swift +++ b/Sources/WalletConnectSync/Stores/SyncStoreFactory.swift @@ -6,7 +6,7 @@ public final class SyncStoreFactory { let indexDatabase = CodableStore(defaults: UserDefaults.standard, identifier: SyncStorageIdentifiers.index.identifier) let indexStore = SyncIndexStore(store: indexDatabase) let objectIdentifier = SyncStorageIdentifiers.object(store: name).identifier - let objectStore = NewKeyedDatabase(storage: storage, identifier: objectIdentifier) + let objectStore = KeyedDatabase(storage: storage, identifier: objectIdentifier) return SyncStore(name: name, syncClient: syncClient, indexStore: indexStore, objectStore: objectStore) } } diff --git a/Sources/WalletConnectUtils/NewKeyedDatabase.swift b/Sources/WalletConnectUtils/KeyedDatabase.swift similarity index 94% rename from Sources/WalletConnectUtils/NewKeyedDatabase.swift rename to Sources/WalletConnectUtils/KeyedDatabase.swift index b15336b4b..8ff5213a3 100644 --- a/Sources/WalletConnectUtils/NewKeyedDatabase.swift +++ b/Sources/WalletConnectUtils/KeyedDatabase.swift @@ -4,7 +4,7 @@ public protocol DatabaseObject: Codable & Equatable { var databaseId: String { get } } -public class NewKeyedDatabase where Element: DatabaseObject { +public class KeyedDatabase where Element: DatabaseObject { public typealias Index = [String: [String: Element]] @@ -67,7 +67,7 @@ public class NewKeyedDatabase where Element: DatabaseObject { } } -private extension NewKeyedDatabase { +private extension KeyedDatabase { func initializeIndex() { guard From b303033de2ab4d00c385dba1737d243a1e99f092 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Fri, 2 Jun 2023 16:14:47 +0300 Subject: [PATCH 74/85] KeyedDatabase discardableResult --- Sources/WalletConnectUtils/KeyedDatabase.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/WalletConnectUtils/KeyedDatabase.swift b/Sources/WalletConnectUtils/KeyedDatabase.swift index 8ff5213a3..8d938f271 100644 --- a/Sources/WalletConnectUtils/KeyedDatabase.swift +++ b/Sources/WalletConnectUtils/KeyedDatabase.swift @@ -42,6 +42,7 @@ public class KeyedDatabase where Element: DatabaseObject { return index[key]?[id] } + @discardableResult public func set(element: Element, for key: String) -> Bool { var map = index[key] ?? [:] @@ -54,6 +55,7 @@ public class KeyedDatabase where Element: DatabaseObject { return true } + @discardableResult public func delete(id: String, for key: String) -> Bool { var map = index[key] From 69cbacdbcd34c5b670df996a9b39ad1d9c77eba6 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Fri, 2 Jun 2023 18:33:32 +0300 Subject: [PATCH 75/85] Message store batch update --- Sources/Chat/ChatClient.swift | 5 +-- Sources/Chat/ChatClientFactory.swift | 6 +-- .../History/HistoryService.swift | 37 ++++++++++++++++ Sources/Chat/Storage/ChatStorage.swift | 18 +++++++- .../Chat/Storage/ThreadStoreDelegate.swift | 43 +++---------------- .../WalletConnectUtils/KeyedDatabase.swift | 15 +++++++ 6 files changed, 78 insertions(+), 46 deletions(-) create mode 100644 Sources/Chat/ProtocolServices/History/HistoryService.swift diff --git a/Sources/Chat/ChatClient.swift b/Sources/Chat/ChatClient.swift index 850cb86e7..7d0f8ed5a 100644 --- a/Sources/Chat/ChatClient.swift +++ b/Sources/Chat/ChatClient.swift @@ -4,7 +4,6 @@ import Combine public class ChatClient { private var publishers = [AnyCancellable]() private let identityClient: IdentityClient - private let historyClient: HistoryClient private let messagingService: MessagingService private let resubscriptionService: ResubscriptionService private let invitationHandlingService: InvitationHandlingService @@ -59,7 +58,6 @@ public class ChatClient { // MARK: - Initialization init(identityClient: IdentityClient, - historyClient: HistoryClient, messagingService: MessagingService, resubscriptionService: ResubscriptionService, invitationHandlingService: InvitationHandlingService, @@ -71,7 +69,6 @@ public class ChatClient { socketConnectionStatusPublisher: AnyPublisher ) { self.identityClient = identityClient - self.historyClient = historyClient self.messagingService = messagingService self.resubscriptionService = resubscriptionService self.invitationHandlingService = invitationHandlingService @@ -97,7 +94,7 @@ public class ChatClient { let publicKey = try await identityClient.register(account: account, onSign: onSign) if !syncRegisterService.isRegistered(account: account) { - try await historyClient.register(tags: ["2002"]) + try await chatStorage.initializeHistory(account: account) try await syncRegisterService.register(account: account, onSign: onSign) } diff --git a/Sources/Chat/ChatClientFactory.swift b/Sources/Chat/ChatClientFactory.swift index cbf7d972d..c304954c0 100644 --- a/Sources/Chat/ChatClientFactory.swift +++ b/Sources/Chat/ChatClientFactory.swift @@ -28,18 +28,19 @@ public struct ChatClientFactory { let historyClient = HistoryClientFactory.create(keychain: keychain) let kms = KeyManagementService(keychain: keychain) let serializer = Serializer(kms: kms) + let historyService = HistoryService(historyClient: historyClient, seiralizer: serializer) let messageStore = KeyedDatabase(storage: storage, identifier: ChatStorageIdentifiers.messages.rawValue) let receivedInviteStore = KeyedDatabase(storage: storage, identifier: ChatStorageIdentifiers.receivedInvites.rawValue) let threadStore: SyncStore = SyncStoreFactory.create(name: ChatStorageIdentifiers.thread.rawValue, syncClient: syncClient, storage: storage) let identityClient = IdentityClientFactory.create(keyserver: keyserverURL, keychain: keychain, logger: logger) let inviteKeyDelegate = InviteKeyDelegate(networkingInteractor: networkingInteractor, kms: kms, identityClient: identityClient) let sentInviteDelegate = SentInviteStoreDelegate(networkingInteractor: networkingInteractor, kms: kms) - let threadDelegate = ThreadStoreDelegate(networkingInteractor: networkingInteractor, kms: kms, historyClient: historyClient, serializer: serializer) + let threadDelegate = ThreadStoreDelegate(networkingInteractor: networkingInteractor, kms: kms, historyService: historyService) let sentInviteStore: SyncStore = SyncStoreFactory.create(name: ChatStorageIdentifiers.sentInvite.rawValue, syncClient: syncClient, storage: storage) let inviteKeyStore: SyncStore = SyncStoreFactory.create(name: ChatStorageIdentifiers.inviteKey.rawValue, syncClient: syncClient, storage: storage) let receivedInviteStatusStore: SyncStore = SyncStoreFactory.create(name: ChatStorageIdentifiers.receivedInviteStatus.rawValue, syncClient: syncClient, storage: storage) let receivedInviteStatusDelegate = ReceiviedInviteStatusDelegate() - let chatStorage = ChatStorage(kms: kms, messageStore: messageStore, receivedInviteStore: receivedInviteStore, sentInviteStore: sentInviteStore, threadStore: threadStore, inviteKeyStore: inviteKeyStore, receivedInviteStatusStore: receivedInviteStatusStore, sentInviteStoreDelegate: sentInviteDelegate, threadStoreDelegate: threadDelegate, inviteKeyDelegate: inviteKeyDelegate, receiviedInviteStatusDelegate: receivedInviteStatusDelegate) + let chatStorage = ChatStorage(kms: kms, messageStore: messageStore, receivedInviteStore: receivedInviteStore, sentInviteStore: sentInviteStore, threadStore: threadStore, inviteKeyStore: inviteKeyStore, receivedInviteStatusStore: receivedInviteStatusStore, historyService: historyService, sentInviteStoreDelegate: sentInviteDelegate, threadStoreDelegate: threadDelegate, inviteKeyDelegate: inviteKeyDelegate, receiviedInviteStatusDelegate: receivedInviteStatusDelegate) let resubscriptionService = ResubscriptionService(networkingInteractor: networkingInteractor, kms: kms, logger: logger) let invitationHandlingService = InvitationHandlingService(keyserverURL: keyserverURL, networkingInteractor: networkingInteractor, identityClient: identityClient, kms: kms, logger: logger, chatStorage: chatStorage) let inviteService = InviteService(keyserverURL: keyserverURL, networkingInteractor: networkingInteractor, identityClient: identityClient, kms: kms, chatStorage: chatStorage, logger: logger) @@ -49,7 +50,6 @@ public struct ChatClientFactory { let client = ChatClient( identityClient: identityClient, - historyClient: historyClient, messagingService: messagingService, resubscriptionService: resubscriptionService, invitationHandlingService: invitationHandlingService, diff --git a/Sources/Chat/ProtocolServices/History/HistoryService.swift b/Sources/Chat/ProtocolServices/History/HistoryService.swift new file mode 100644 index 000000000..ebad3a50d --- /dev/null +++ b/Sources/Chat/ProtocolServices/History/HistoryService.swift @@ -0,0 +1,37 @@ +import Foundation + +final class HistoryService { + + private let historyClient: HistoryClient + private let seiralizer: Serializing + + init(historyClient: HistoryClient, seiralizer: Serializing) { + self.historyClient = historyClient + self.seiralizer = seiralizer + } + + func register() async throws { + try await historyClient.register(tags: ["2002"]) + } + + func fetchMessageHistory(thread: Thread) async throws -> [Message] { + let wrappers: [MessagePayload.Wrapper] = try await historyClient.getMessages( + topic: thread.topic, + count: 200, direction: .backward + ) + + return wrappers.map { wrapper in + let (messagePayload, messageClaims) = try! MessagePayload.decodeAndVerify(from: wrapper) + + let authorAccount = messagePayload.recipientAccount == thread.selfAccount + ? thread.peerAccount + : thread.selfAccount + + return Message( + topic: thread.topic, + message: messagePayload.message, + authorAccount: authorAccount, + timestamp: messageClaims.iat) + } + } +} diff --git a/Sources/Chat/Storage/ChatStorage.swift b/Sources/Chat/Storage/ChatStorage.swift index a688ffc13..fb88530c3 100644 --- a/Sources/Chat/Storage/ChatStorage.swift +++ b/Sources/Chat/Storage/ChatStorage.swift @@ -12,6 +12,7 @@ final class ChatStorage { private let threadStore: SyncStore private let inviteKeyStore: SyncStore private let receivedInviteStatusStore: SyncStore + private let historyService: HistoryService private let sentInviteStoreDelegate: SentInviteStoreDelegate private let threadStoreDelegate: ThreadStoreDelegate @@ -76,6 +77,7 @@ final class ChatStorage { threadStore: SyncStore, inviteKeyStore: SyncStore, receivedInviteStatusStore: SyncStore, + historyService: HistoryService, sentInviteStoreDelegate: SentInviteStoreDelegate, threadStoreDelegate: ThreadStoreDelegate, inviteKeyDelegate: InviteKeyDelegate, @@ -88,6 +90,7 @@ final class ChatStorage { self.threadStore = threadStore self.inviteKeyStore = inviteKeyStore self.receivedInviteStatusStore = receivedInviteStatusStore + self.historyService = historyService self.sentInviteStoreDelegate = sentInviteStoreDelegate self.threadStoreDelegate = threadStoreDelegate self.inviteKeyDelegate = inviteKeyDelegate @@ -112,6 +115,15 @@ final class ChatStorage { try await receiviedInviteStatusDelegate.onInitialization() } + func initializeHistory(account: Account) async throws { + try await historyService.register() + + for thread in getAllThreads() { + let messages = try await historyService.fetchMessageHistory(thread: thread) + set(messages: messages, account: account) + } + } + func setupSubscriptions(account: Account) throws { messageStore.onUpdate = { [unowned self] in messagesPublisherSubject.send(getMessages(account: account)) @@ -250,12 +262,16 @@ final class ChatStorage { newMessagePublisherSubject.send(message) } + func set(messages: [Message], account: Account) { + messageStore.set(elements: messages, for: account.absoluteString) + } + func getMessages(topic: String) -> [Message] { return messageStore.getAll().filter { $0.topic == topic } } func getMessages(account: Account) -> [Message] { - return messageStore.getAll(for: account.absoluteString) ?? [] + return messageStore.getAll(for: account.absoluteString) } } diff --git a/Sources/Chat/Storage/ThreadStoreDelegate.swift b/Sources/Chat/Storage/ThreadStoreDelegate.swift index 56070d99d..91e8a4afc 100644 --- a/Sources/Chat/Storage/ThreadStoreDelegate.swift +++ b/Sources/Chat/Storage/ThreadStoreDelegate.swift @@ -4,23 +4,17 @@ final class ThreadStoreDelegate { private let networkingInteractor: NetworkInteracting private let kms: KeyManagementServiceProtocol - private let historyClient: HistoryClient - private let serializer: Serializing + private let historyService: HistoryService - init(networkingInteractor: NetworkInteracting, kms: KeyManagementServiceProtocol, historyClient: HistoryClient, serializer: Serializing) { + init(networkingInteractor: NetworkInteracting, kms: KeyManagementServiceProtocol, historyService: HistoryService) { self.networkingInteractor = networkingInteractor self.kms = kms - self.serializer = serializer - self.historyClient = historyClient + self.historyService = historyService } func onInitialization(storage: ChatStorage) async throws { let threads = storage.getAllThreads() try await networkingInteractor.batchSubscribe(topics: threads.map { $0.topic }) - - for thread in threads { - try await fetchMessageHistory(thread: thread, storage: storage) - } } func onUpdate(_ thread: Thread, storage: ChatStorage) { @@ -33,7 +27,8 @@ final class ThreadStoreDelegate { try kms.setSymmetricKey(symmetricKey, for: thread.topic) try await networkingInteractor.subscribe(topic: thread.topic) - // Relay Client injection! + let messages = try await historyService.fetchMessageHistory(thread: thread) + storage.set(messages: messages, account: thread.selfAccount) } } @@ -41,31 +36,3 @@ final class ThreadStoreDelegate { } } - -private extension ThreadStoreDelegate { - - func fetchMessageHistory(thread: Thread, storage: ChatStorage) async throws { - - let wrappers: [MessagePayload.Wrapper] = try await historyClient.getMessages( - topic: thread.topic, - count: 200, direction: .backward - ) - - let messages = wrappers.map { wrapper in - let (messagePayload, messageClaims) = try! MessagePayload.decodeAndVerify(from: wrapper) - - let authorAccount = messagePayload.recipientAccount == thread.selfAccount - ? thread.peerAccount - : thread.selfAccount - - return Message( - topic: thread.topic, - message: messagePayload.message, - authorAccount: authorAccount, - timestamp: messageClaims.iat) - } - -// TODO: Set in store -// storage.set(message: message, account: thread.selfAccount) - } -} diff --git a/Sources/WalletConnectUtils/KeyedDatabase.swift b/Sources/WalletConnectUtils/KeyedDatabase.swift index 8d938f271..0deffcc8d 100644 --- a/Sources/WalletConnectUtils/KeyedDatabase.swift +++ b/Sources/WalletConnectUtils/KeyedDatabase.swift @@ -42,6 +42,21 @@ public class KeyedDatabase where Element: DatabaseObject { return index[key]?[id] } + @discardableResult + public func set(elements: [Element], for key: String) -> Bool { + var map = index[key] ?? [:] + + for element in elements { + guard + map[element.databaseId] == nil else { continue } + map[element.databaseId] = element + } + + index[key] = map + + return true + } + @discardableResult public func set(element: Element, for key: String) -> Bool { var map = index[key] ?? [:] From d16fb5471654bce283d4f6af66a2eb17b5e093d0 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Mon, 5 Jun 2023 15:53:31 +0300 Subject: [PATCH 76/85] Todo --- Sources/WalletConnectUtils/KeyedDatabase.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/WalletConnectUtils/KeyedDatabase.swift b/Sources/WalletConnectUtils/KeyedDatabase.swift index 0deffcc8d..bc2dd325f 100644 --- a/Sources/WalletConnectUtils/KeyedDatabase.swift +++ b/Sources/WalletConnectUtils/KeyedDatabase.swift @@ -19,6 +19,7 @@ public class KeyedDatabase where Element: DatabaseObject { private let storage: KeyValueStorage private let identifier: String + // TODO: Replace with publisher public var onUpdate: (() -> Void)? public init(storage: KeyValueStorage, identifier: String) { From fb78f76b59923fb33dd907d24f66cbc3c281058c Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Wed, 7 Jun 2023 15:46:45 +0300 Subject: [PATCH 77/85] Revert "Todo" This reverts commit cc25c0a36fa5c9373dd80bedab007975fc257a3a. --- Sources/WalletConnectUtils/KeyedDatabase.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/WalletConnectUtils/KeyedDatabase.swift b/Sources/WalletConnectUtils/KeyedDatabase.swift index bc2dd325f..0deffcc8d 100644 --- a/Sources/WalletConnectUtils/KeyedDatabase.swift +++ b/Sources/WalletConnectUtils/KeyedDatabase.swift @@ -19,7 +19,6 @@ public class KeyedDatabase where Element: DatabaseObject { private let storage: KeyValueStorage private let identifier: String - // TODO: Replace with publisher public var onUpdate: (() -> Void)? public init(storage: KeyValueStorage, identifier: String) { From c6cb6ad6dfdb601ad0f0eec8b97c1fe2604cd1aa Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Thu, 8 Jun 2023 14:40:15 +0300 Subject: [PATCH 78/85] Web3Modal import --- Example/IntegrationTests/History/HistoryTests.swift | 4 ++-- .../PresentationLayer/Chat/Import/ImportPresenter.swift | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Example/IntegrationTests/History/HistoryTests.swift b/Example/IntegrationTests/History/HistoryTests.swift index c5c1913a7..ebd8f202e 100644 --- a/Example/IntegrationTests/History/HistoryTests.swift +++ b/Example/IntegrationTests/History/HistoryTests.swift @@ -61,8 +61,8 @@ final class HistoryTests: XCTestCase { try await relayClient2.subscribe(topic: topic) try await relayClient1.publish(topic: topic, payload: payload, tag: tag, prompt: false, ttl: 3000) - - await fulfillment(of: [exp], timeout: InputConfig.defaultTimeout) + + wait(for: [exp], timeout: InputConfig.defaultTimeout) sleep(5) // History server has a queue diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportPresenter.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportPresenter.swift index a6a418aa4..870bb1501 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportPresenter.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportPresenter.swift @@ -67,7 +67,16 @@ extension ImportPresenter: SceneViewModel { private extension ImportPresenter { func setupInitialState() { + Sign.instance.sessionSettlePublisher.sink { session in + let accounts = session.namespaces.values.reduce(into: []) { result, namespace in + result = result + Array(namespace.accounts) + } + + Task(priority: .userInitiated) { + try await self.importAccount(.web3Modal(account: accounts.first!)) + } + }.store(in: &disposeBag) } @MainActor From 3df4725a080c0e7a8eef6427deb7cf441a3b4b54 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Tue, 13 Jun 2023 13:41:04 +0300 Subject: [PATCH 79/85] Rebase fix --- .../PresentationLayer/Chat/Import/ImportPresenter.swift | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportPresenter.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportPresenter.swift index 870bb1501..6146979f3 100644 --- a/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportPresenter.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportPresenter.swift @@ -68,12 +68,8 @@ private extension ImportPresenter { func setupInitialState() { Sign.instance.sessionSettlePublisher.sink { session in - let accounts = session.namespaces.values.reduce(into: []) { result, namespace in - result = result + Array(namespace.accounts) - } - Task(priority: .userInitiated) { - try await self.importAccount(.web3Modal(account: accounts.first!)) + try await self.importAccount(.web3Modal(account: session.accounts.first!, topic: session.topic)) } }.store(in: &disposeBag) From db811e6ca06e36358374c2828f1e17a279430a3a Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Tue, 13 Jun 2023 16:30:36 +0300 Subject: [PATCH 80/85] GenericPasswordConvertible for string --- .../Keychain/GenericPasswordConvertible.swift | 19 ++++++++++++++++++ .../WalletConnectSync/Extensions/String.swift | 20 ------------------- 2 files changed, 19 insertions(+), 20 deletions(-) delete mode 100644 Sources/WalletConnectSync/Extensions/String.swift diff --git a/Sources/WalletConnectKMS/Keychain/GenericPasswordConvertible.swift b/Sources/WalletConnectKMS/Keychain/GenericPasswordConvertible.swift index 4db1e32fa..4ad483315 100644 --- a/Sources/WalletConnectKMS/Keychain/GenericPasswordConvertible.swift +++ b/Sources/WalletConnectKMS/Keychain/GenericPasswordConvertible.swift @@ -4,3 +4,22 @@ public protocol GenericPasswordConvertible { init(rawRepresentation data: D) throws where D: ContiguousBytes var rawRepresentation: Data { get } } + +extension String: GenericPasswordConvertible { + + enum Errors: Error { + case notUTF8 + } + + public init(rawRepresentation data: D) throws where D: ContiguousBytes { + let buffer = data.withUnsafeBytes { Data($0) } + guard let string = String(data: buffer, encoding: .utf8) else { + throw Errors.notUTF8 + } + self = string + } + + public var rawRepresentation: Data { + return data(using: .utf8) ?? Data() + } +} diff --git a/Sources/WalletConnectSync/Extensions/String.swift b/Sources/WalletConnectSync/Extensions/String.swift deleted file mode 100644 index a32883da5..000000000 --- a/Sources/WalletConnectSync/Extensions/String.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Foundation - -extension String: GenericPasswordConvertible { - - enum Errors: Error { - case notUTF8 - } - - public init(rawRepresentation data: D) throws where D: ContiguousBytes { - let buffer = data.withUnsafeBytes { Data($0) } - guard let string = String(data: buffer, encoding: .utf8) else { - throw Errors.notUTF8 - } - self = string - } - - public var rawRepresentation: Data { - return data(using: .utf8) ?? Data() - } -} From f7c1996c4d32bdde3be126566b106cf5115902c1 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Tue, 13 Jun 2023 17:45:14 +0300 Subject: [PATCH 81/85] String extension cleanup --- .../xcshareddata/swiftpm/Package.resolved | 4 ++-- Sources/WalletConnectUtils/Extensions/String.swift | 12 ------------ 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 223fbcb65..b9a5ff3f7 100644 --- a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -15,8 +15,8 @@ "repositoryURL": "https://github.com/krzyzanowskim/CryptoSwift.git", "state": { "branch": null, - "revision": "eee9ad754926c40a0f7e73f152357d37b119b7fa", - "version": "1.7.1" + "revision": "32f641cf24fc7abc1c591a2025e9f2f572648b0f", + "version": "1.7.2" } }, { diff --git a/Sources/WalletConnectUtils/Extensions/String.swift b/Sources/WalletConnectUtils/Extensions/String.swift index 2133ff6cc..117ee7bfe 100644 --- a/Sources/WalletConnectUtils/Extensions/String.swift +++ b/Sources/WalletConnectUtils/Extensions/String.swift @@ -15,18 +15,6 @@ public extension String { return keyData.toHexString() } - init(rawRepresentation data: D) throws where D: ContiguousBytes { - let bytes = data.withUnsafeBytes { Data(Array($0)) } - guard let string = String(data: bytes, encoding: .utf8) else { - fatalError() // FIXME: Throw error - } - self = string - } - - var rawRepresentation: Data { - self.data(using: .utf8) ?? Data() - } - func asURL() throws -> URL { guard let url = URL(string: self) else { throw Errors.notAnURL } return url From c1187013459039ad8e8cb5bf5b03c5a0181ed426 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Wed, 14 Jun 2023 09:47:11 +0200 Subject: [PATCH 82/85] fix testEndToEndPayload --- .../RelayClientEndToEndTests.swift | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift b/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift index 129ebb8a0..84cd88693 100644 --- a/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift +++ b/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift @@ -101,18 +101,23 @@ final class RelayClientEndToEndTests: XCTestCase { relayB.messagePublisher.sink { topic, payload, _ in (subscriptionBTopic, subscriptionBPayload) = (topic, payload) + Task(priority: .high) { + sleep(1) + try await relayB.publish(topic: randomTopic, payload: payloadB, tag: 0, prompt: false, ttl: 60) + } expectationB.fulfill() }.store(in: &publishers) - relayA.socketConnectionStatusPublisher.sink { _ in + relayA.socketConnectionStatusPublisher.sink { status in + guard status == .connected else {return} Task(priority: .high) { - try await relayA.publish(topic: randomTopic, payload: payloadA, tag: 0, prompt: false, ttl: 60) try await relayA.subscribe(topic: randomTopic) + try await relayA.publish(topic: randomTopic, payload: payloadA, tag: 0, prompt: false, ttl: 60) } }.store(in: &publishers) - relayB.socketConnectionStatusPublisher.sink { _ in + relayB.socketConnectionStatusPublisher.sink { status in + guard status == .connected else {return} Task(priority: .high) { - try await relayB.publish(topic: randomTopic, payload: payloadB, tag: 0, prompt: false, ttl: 60) try await relayB.subscribe(topic: randomTopic) } }.store(in: &publishers) From 21c4f417a119398244a182814d524a9e98556156 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Wed, 14 Jun 2023 13:10:57 +0300 Subject: [PATCH 83/85] HistoryTests removed from SmokeTests --- Example/SmokeTests.xctestplan | 1 + 1 file changed, 1 insertion(+) diff --git a/Example/SmokeTests.xctestplan b/Example/SmokeTests.xctestplan index 142fa8062..06e6e3467 100644 --- a/Example/SmokeTests.xctestplan +++ b/Example/SmokeTests.xctestplan @@ -44,6 +44,7 @@ "EIP191VerifierTests", "EIP55Tests", "ENSResolverTests", + "HistoryTests", "PairingTests", "PushTests", "PushTests\/testDappDeletePushSubscription()", From 5cd58d8b6df15b200fc239c4e62be48cf9e20466 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Wed, 14 Jun 2023 15:23:36 +0300 Subject: [PATCH 84/85] Sync: Cocoapods support --- Sources/Chat/Storage/ChatStorage.swift | 2 +- .../Keychain/GenericPasswordConvertible.swift | 19 ------------------ .../String+GenericPasswordConvertible.swift | 20 +++++++++++++++++++ .../Extensions/String.swift | 8 ++++---- 4 files changed, 25 insertions(+), 24 deletions(-) create mode 100644 Sources/WalletConnectKMS/Keychain/String+GenericPasswordConvertible.swift diff --git a/Sources/Chat/Storage/ChatStorage.swift b/Sources/Chat/Storage/ChatStorage.swift index fb88530c3..da0afe8a6 100644 --- a/Sources/Chat/Storage/ChatStorage.swift +++ b/Sources/Chat/Storage/ChatStorage.swift @@ -160,7 +160,7 @@ final class ChatStorage { } func getReceivedInvites(account: Account) -> [ReceivedInvite] { - return receivedInviteStore.getAll(for: account.absoluteString) ?? [] + return receivedInviteStore.getAll(for: account.absoluteString) } func syncRejectedReceivedInviteStatus(id: Int64, account: Account) async throws { diff --git a/Sources/WalletConnectKMS/Keychain/GenericPasswordConvertible.swift b/Sources/WalletConnectKMS/Keychain/GenericPasswordConvertible.swift index 4ad483315..4db1e32fa 100644 --- a/Sources/WalletConnectKMS/Keychain/GenericPasswordConvertible.swift +++ b/Sources/WalletConnectKMS/Keychain/GenericPasswordConvertible.swift @@ -4,22 +4,3 @@ public protocol GenericPasswordConvertible { init(rawRepresentation data: D) throws where D: ContiguousBytes var rawRepresentation: Data { get } } - -extension String: GenericPasswordConvertible { - - enum Errors: Error { - case notUTF8 - } - - public init(rawRepresentation data: D) throws where D: ContiguousBytes { - let buffer = data.withUnsafeBytes { Data($0) } - guard let string = String(data: buffer, encoding: .utf8) else { - throw Errors.notUTF8 - } - self = string - } - - public var rawRepresentation: Data { - return data(using: .utf8) ?? Data() - } -} diff --git a/Sources/WalletConnectKMS/Keychain/String+GenericPasswordConvertible.swift b/Sources/WalletConnectKMS/Keychain/String+GenericPasswordConvertible.swift new file mode 100644 index 000000000..4c8717a4e --- /dev/null +++ b/Sources/WalletConnectKMS/Keychain/String+GenericPasswordConvertible.swift @@ -0,0 +1,20 @@ +import Foundation + +extension String: GenericPasswordConvertible { + + public init(rawRepresentation data: D) throws where D: ContiguousBytes { + let buffer = data.withUnsafeBytes { Data($0) } + guard let string = String(data: buffer, encoding: .utf8) else { + throw Errors.notUTF8 + } + self = string + } + + public var rawRepresentation: Data { + return data(using: .utf8) ?? Data() + } +} + +fileprivate enum Errors: Error { + case notUTF8 +} diff --git a/Sources/WalletConnectUtils/Extensions/String.swift b/Sources/WalletConnectUtils/Extensions/String.swift index 117ee7bfe..9587a1687 100644 --- a/Sources/WalletConnectUtils/Extensions/String.swift +++ b/Sources/WalletConnectUtils/Extensions/String.swift @@ -2,10 +2,6 @@ import Foundation public extension String { - enum Errors: Error { - case notAnURL - } - func toHexEncodedString(uppercase: Bool = true, prefix: String = "", separator: String = "") -> String { return unicodeScalars.map { prefix + .init($0.value, radix: 16, uppercase: uppercase) } .joined(separator: separator) } @@ -20,3 +16,7 @@ public extension String { return url } } + +fileprivate enum Errors: Error { + case notAnURL +} From 88bc9af304a89b5bf02d9d2df3c41d75c040d9ae Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Wed, 14 Jun 2023 15:28:47 +0300 Subject: [PATCH 85/85] lib link script updated --- .github/workflows/cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index bb5ce36ae..500646255 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -28,4 +28,4 @@ jobs: - name: Lint CocoaPods run: | - pod lib lint WalletConnectSwiftV2.podspec --verbose --allow-warnings \ No newline at end of file + pod lib lint --verbose --no-clean --quick --allow-warnings --platforms=ios WalletConnectSwiftV2.podspec \ No newline at end of file