diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index df62cc8a9..06c696525 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,10 +1,6 @@ name: release on: - schedule: - # Runs "Every Monday 10am CET" - - cron: '0 10 * * 1' - workflow_dispatch: jobs: diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnectSignTests.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnectSignTests.xcscheme new file mode 100644 index 000000000..bb6f830b9 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnectSignTests.xcscheme @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnectUtilsTests.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnectUtilsTests.xcscheme new file mode 100644 index 000000000..c78f495d4 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnectUtilsTests.xcscheme @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/DApp/ApplicationLayer/Application.swift b/Example/DApp/ApplicationLayer/Application.swift index feba5e373..e4c9d7c63 100644 --- a/Example/DApp/ApplicationLayer/Application.swift +++ b/Example/DApp/ApplicationLayer/Application.swift @@ -4,5 +4,4 @@ import WalletConnectUtils final class Application { var uri: WalletConnectURI? - var requestSent = false } diff --git a/Example/DApp/Common/ActivityIndicatorManager.swift b/Example/DApp/Common/ActivityIndicatorManager.swift new file mode 100644 index 000000000..2405ea052 --- /dev/null +++ b/Example/DApp/Common/ActivityIndicatorManager.swift @@ -0,0 +1,42 @@ +import UIKit + +class ActivityIndicatorManager { + static let shared = ActivityIndicatorManager() + private var activityIndicator: UIActivityIndicatorView? + private let serialQueue = DispatchQueue(label: "com.yourapp.activityIndicatorManager") + + private init() {} + + func start() { + serialQueue.async { + self.stopInternal() + + DispatchQueue.main.async { + guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let window = windowScene.windows.first(where: { $0.isKeyWindow }) else { return } + + let activityIndicator = UIActivityIndicatorView(style: .large) + activityIndicator.center = window.center + activityIndicator.color = .white + activityIndicator.startAnimating() + window.addSubview(activityIndicator) + + self.activityIndicator = activityIndicator + } + } + } + + func stop() { + serialQueue.async { + self.stopInternal() + } + } + + private func stopInternal() { + DispatchQueue.main.sync { + self.activityIndicator?.stopAnimating() + self.activityIndicator?.removeFromSuperview() + self.activityIndicator = nil + } + } +} diff --git a/Example/DApp/Common/AlertPresenter.swift b/Example/DApp/Common/AlertPresenter.swift new file mode 100644 index 000000000..5da5d4668 --- /dev/null +++ b/Example/DApp/Common/AlertPresenter.swift @@ -0,0 +1,35 @@ +import Foundation +import SwiftMessages +import UIKit + +struct AlertPresenter { + enum MessageType { + case warning + case error + case info + case success + } + + static func present(message: String, type: AlertPresenter.MessageType) { + DispatchQueue.main.async { + let view = MessageView.viewFromNib(layout: .cardView) + switch type { + case .warning: + view.configureTheme(.warning, iconStyle: .subtle) + case .error: + view.configureTheme(.error, iconStyle: .subtle) + case .info: + view.configureTheme(.info, iconStyle: .subtle) + case .success: + view.configureTheme(.success, iconStyle: .subtle) + } + view.button?.isHidden = true + view.layoutMarginAdditions = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) + view.configureContent(title: "", body: message) + var config = SwiftMessages.Config() + config.presentationStyle = .top + config.duration = .seconds(seconds: 1.5) + SwiftMessages.show(config: config, view: view) + } + } +} diff --git a/Example/DApp/Modules/Auth/AuthInteractor.swift b/Example/DApp/Modules/Auth/AuthInteractor.swift deleted file mode 100644 index ffddc61dd..000000000 --- a/Example/DApp/Modules/Auth/AuthInteractor.swift +++ /dev/null @@ -1,3 +0,0 @@ -import Foundation - -final class AuthInteractor {} diff --git a/Example/DApp/Modules/Auth/AuthModule.swift b/Example/DApp/Modules/Auth/AuthModule.swift deleted file mode 100644 index 9252f89e3..000000000 --- a/Example/DApp/Modules/Auth/AuthModule.swift +++ /dev/null @@ -1,16 +0,0 @@ -import SwiftUI - -final class AuthModule { - @discardableResult - static func create(app: Application) -> UIViewController { - let router = AuthRouter(app: app) - let interactor = AuthInteractor() - let presenter = AuthPresenter(interactor: interactor, router: router) - let view = AuthView().environmentObject(presenter) - let viewController = SceneViewController(viewModel: presenter, content: view) - - router.viewController = viewController - - return viewController - } -} diff --git a/Example/DApp/Modules/Auth/AuthPresenter.swift b/Example/DApp/Modules/Auth/AuthPresenter.swift deleted file mode 100644 index 8e01e32e6..000000000 --- a/Example/DApp/Modules/Auth/AuthPresenter.swift +++ /dev/null @@ -1,116 +0,0 @@ -import UIKit -import Combine - -import Auth - -final class AuthPresenter: ObservableObject { - enum SigningState { - case none - case signed(Cacao) - case error(Error) - } - - private let interactor: AuthInteractor - private let router: AuthRouter - - @Published var qrCodeImageData: Data? - @Published var signingState = SigningState.none - @Published var showSigningState = false - - private var walletConnectUri: WalletConnectURI? - - private var subscriptions = Set() - - init( - interactor: AuthInteractor, - router: AuthRouter - ) { - defer { - Task { - await setupInitialState() - } - } - self.interactor = interactor - self.router = router - } - - func onAppear() { - generateQR() - } - - func copyUri() { - UIPasteboard.general.string = walletConnectUri?.absoluteString - } - - func connectWallet() { - if let walletConnectUri { - let walletUri = URL(string: "walletapp://wc?uri=\(walletConnectUri.deeplinkUri.removingPercentEncoding!)")! - DispatchQueue.main.async { - UIApplication.shared.open(walletUri) - } - } - } -} - -// MARK: - Private functions -extension AuthPresenter { - @MainActor - private func setupInitialState() { - Auth.instance.authResponsePublisher.sink { [weak self] (_, result) in - switch result { - case .success(let cacao): - self?.signingState = .signed(cacao) - self?.generateQR() - self?.showSigningState.toggle() - - case .failure(let error): - self?.signingState = .error(error) - self?.showSigningState.toggle() - } - } - .store(in: &subscriptions) - } - - private func generateQR() { - Task { @MainActor in - let uri = try! await Pair.instance.create() - walletConnectUri = uri - try await Auth.instance.request(.stub(), topic: uri.topic) - let qrCodeImage = QRCodeGenerator.generateQRCode(from: uri.absoluteString) - DispatchQueue.main.async { - self.qrCodeImageData = qrCodeImage.pngData() - } - } - } -} - -// MARK: - SceneViewModel -extension AuthPresenter: SceneViewModel {} - -// MARK: - Auth request stub -private extension RequestParams { - static func stub( - domain: String = "service.invalid", - chainId: String = "eip155:1", - nonce: String = "32891756", - aud: String = "https://service.invalid/login", - nbf: String? = nil, - exp: String? = nil, - statement: String? = "I accept the ServiceOrg Terms of Service: https://service.invalid/tos", - requestId: String? = nil, - resources: [String]? = ["ipfs://bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq/", "https://example.com/my-web2-claim.json"] - ) -> RequestParams { - return RequestParams( - domain: domain, - chainId: chainId, - nonce: nonce, - aud: aud, - nbf: nbf, - exp: exp, - statement: statement, - requestId: requestId, - resources: resources - ) - } -} - diff --git a/Example/DApp/Modules/Auth/AuthRouter.swift b/Example/DApp/Modules/Auth/AuthRouter.swift deleted file mode 100644 index 3caacfd38..000000000 --- a/Example/DApp/Modules/Auth/AuthRouter.swift +++ /dev/null @@ -1,16 +0,0 @@ -import UIKit - -final class AuthRouter { - weak var viewController: UIViewController! - - private let app: Application - - init(app: Application) { - self.app = app - } - - func dismiss() { - viewController.dismiss(animated: true) - UIApplication.shared.open(URL(string: "showcase://")!) - } -} diff --git a/Example/DApp/Modules/Auth/AuthView.swift b/Example/DApp/Modules/Auth/AuthView.swift deleted file mode 100644 index 8e15bacc0..000000000 --- a/Example/DApp/Modules/Auth/AuthView.swift +++ /dev/null @@ -1,140 +0,0 @@ -import SwiftUI - -struct AuthView: View { - @EnvironmentObject var presenter: AuthPresenter - - var body: some View { - NavigationStack { - ZStack { - Color(red: 25/255, green: 26/255, blue: 26/255) - .ignoresSafeArea() - - VStack { - ZStack { - RoundedRectangle(cornerRadius: 25) - .fill(.white) - .aspectRatio(1, contentMode: .fit) - .padding(20) - - if let data = presenter.qrCodeImageData { - let qrCodeImage = UIImage(data: data) ?? UIImage() - Image(uiImage: qrCodeImage) - .resizable() - .aspectRatio(1, contentMode: .fit) - .padding(40) - } - } - - Button { - presenter.connectWallet() - } label: { - Text("Connect Sample Wallet") - .font(.system(size: 16, weight: .semibold)) - .foregroundColor(.white) - .padding(.horizontal, 16) - .padding(.vertical, 10) - .background(Color(red: 95/255, green: 159/255, blue: 248/255)) - .cornerRadius(16) - } - - Button { - presenter.copyUri() - } label: { - HStack { - Image("copy") - Text("Copy link") - .font(.system(size: 14, weight: .semibold)) - .foregroundColor(Color(red: 0.58, green: 0.62, blue: 0.62)) - } - } - .padding(.top, 16) - - Spacer() - } - } - .navigationTitle("Auth") - .navigationBarTitleDisplayMode(.inline) - .toolbarColorScheme(.dark, for: .navigationBar) - .toolbarBackground(.visible, for: .navigationBar) - .toolbarBackground( - Color(red: 25/255, green: 26/255, blue: 26/255), - for: .navigationBar - ) - .onAppear { - presenter.onAppear() - } - .sheet(isPresented: $presenter.showSigningState) { - ZStack { - Color(red: 25/255, green: 26/255, blue: 26/255) - .ignoresSafeArea() - - VStack { - HStack { - RoundedRectangle(cornerRadius: 2) - .fill(.gray.opacity(0.5)) - .frame(width: 30, height: 4) - - } - .padding(20) - - Image("profile") - .resizable() - .frame(width: 64, height: 64) - - switch presenter.signingState { - case .error(let error): - Text(error.localizedDescription) - .font(.system(size: 16, weight: .semibold)) - .foregroundColor(.white) - .padding(.horizontal, 16) - .padding(.vertical, 10) - .background(.green) - .cornerRadius(16) - .padding(.top, 16) - - case .signed(let cacao): - HStack { - Text(cacao.p.iss.split(separator: ":").last ?? "") - .lineLimit(1) - .truncationMode(.middle) - .frame(width: 135) - .font(.system(size: 24, weight: .semibold)) - .foregroundColor(Color(red: 0.89, green: 0.91, blue: 0.91)) - - Button { - UIPasteboard.general.string = String(cacao.p.iss.split(separator: ":").last ?? "") - } label: { - Image("copy") - .resizable() - .frame(width: 14, height: 14) - } - } - - Text("Authenticated") - .font(.system(size: 16, weight: .semibold)) - .foregroundColor(.white) - .padding(.horizontal, 16) - .padding(.vertical, 10) - .background(.green) - .cornerRadius(16) - .padding(.top, 16) - - case .none: - EmptyView() - } - - Spacer() - } - } - .presentationDetents([.medium]) - } - } - } -} - -struct AuthView_Previews: PreviewProvider { - static var previews: some View { - AuthView() - } -} - diff --git a/Example/DApp/Modules/Main/MainInteractor.swift b/Example/DApp/Modules/Main/MainInteractor.swift deleted file mode 100644 index a3954796d..000000000 --- a/Example/DApp/Modules/Main/MainInteractor.swift +++ /dev/null @@ -1,3 +0,0 @@ -import Foundation - -final class MainInteractor {} diff --git a/Example/DApp/Modules/Main/MainModule.swift b/Example/DApp/Modules/Main/MainModule.swift deleted file mode 100644 index 67a9d6060..000000000 --- a/Example/DApp/Modules/Main/MainModule.swift +++ /dev/null @@ -1,15 +0,0 @@ -import SwiftUI - -final class MainModule { - @discardableResult - static func create(app: Application) -> UIViewController { - let router = MainRouter(app: app) - let interactor = MainInteractor() - let presenter = MainPresenter(router: router, interactor: interactor) - let viewController = MainViewController(presenter: presenter) - - router.viewController = viewController - - return viewController - } -} diff --git a/Example/DApp/Modules/Main/MainPresenter.swift b/Example/DApp/Modules/Main/MainPresenter.swift deleted file mode 100644 index c3c7d47ee..000000000 --- a/Example/DApp/Modules/Main/MainPresenter.swift +++ /dev/null @@ -1,32 +0,0 @@ -import UIKit -import Combine - -final class MainPresenter { - private let interactor: MainInteractor - private let router: MainRouter - private var disposeBag = Set() - - var tabs: [TabPage] { - return TabPage.allCases - } - - var viewControllers: [UIViewController] { - return [ - router.signViewController(), - router.authViewController() - ] - } - - init(router: MainRouter, interactor: MainInteractor) { - defer { - setupInitialState() - } - self.router = router - self.interactor = interactor - } -} - -// MARK: - Private functions -extension MainPresenter { - private func setupInitialState() {} -} diff --git a/Example/DApp/Modules/Main/MainRouter.swift b/Example/DApp/Modules/Main/MainRouter.swift deleted file mode 100644 index 4b3fef3de..000000000 --- a/Example/DApp/Modules/Main/MainRouter.swift +++ /dev/null @@ -1,19 +0,0 @@ -import UIKit - -final class MainRouter { - weak var viewController: UIViewController! - - private let app: Application - - init(app: Application) { - self.app = app - } - - func signViewController() -> UIViewController { - return SignModule.create(app: app) - } - - func authViewController() -> UIViewController { - return AuthModule.create(app: app) - } -} diff --git a/Example/DApp/Modules/Main/MainViewController.swift b/Example/DApp/Modules/Main/MainViewController.swift deleted file mode 100644 index 539b2a789..000000000 --- a/Example/DApp/Modules/Main/MainViewController.swift +++ /dev/null @@ -1,43 +0,0 @@ -import UIKit -import Sentry - -enum LoginError: Error { - case wrongUser(id: String) - case wrongPassword -} - -final class MainViewController: UITabBarController { - - private let presenter: MainPresenter - - init(presenter: MainPresenter) { - self.presenter = presenter - super.init(nibName: nil, bundle: nil) - } - - override func viewDidLoad() { - super.viewDidLoad() - setupTabs() - } - - private func setupTabs() { - let viewControllers = presenter.viewControllers - - for (index, viewController) in viewControllers.enumerated() { - let model = presenter.tabs[index] - let item = UITabBarItem() - item.title = model.title - item.image = model.icon - item.isEnabled = TabPage.enabledTabs.contains(model) - viewController.tabBarItem = item - viewController.view.backgroundColor = .w_background - } - - self.viewControllers = viewControllers - self.selectedIndex = TabPage.selectedIndex - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} diff --git a/Example/DApp/Modules/Main/Model/TabPage.swift b/Example/DApp/Modules/Main/Model/TabPage.swift deleted file mode 100644 index faec2ba34..000000000 --- a/Example/DApp/Modules/Main/Model/TabPage.swift +++ /dev/null @@ -1,32 +0,0 @@ -import UIKit - -enum TabPage: CaseIterable { - case sign - case auth - - var title: String { - switch self { - case .sign: - return "Sign" - case .auth: - return "Auth" - } - } - - var icon: UIImage { - switch self { - case .sign: - return UIImage(named: "pen")! - case .auth: - return UIImage(named: "auth")! - } - } - - static var selectedIndex: Int { - return 0 - } - - static var enabledTabs: [TabPage] { - return [.sign, .auth] - } -} diff --git a/Example/DApp/Modules/Sign/NewPairing/NewPairingRouter.swift b/Example/DApp/Modules/Sign/NewPairing/NewPairingRouter.swift index 51fc5b2f2..89c0ea8a7 100644 --- a/Example/DApp/Modules/Sign/NewPairing/NewPairingRouter.swift +++ b/Example/DApp/Modules/Sign/NewPairing/NewPairingRouter.swift @@ -11,6 +11,5 @@ final class NewPairingRouter { func dismiss() { viewController.dismiss(animated: true) - UIApplication.shared.open(URL(string: "showcase://")!) } } diff --git a/Example/DApp/Modules/Sign/SessionAccount/SessionAccountPresenter.swift b/Example/DApp/Modules/Sign/SessionAccount/SessionAccountPresenter.swift index 15b0b7e74..31920ce46 100644 --- a/Example/DApp/Modules/Sign/SessionAccount/SessionAccountPresenter.swift +++ b/Example/DApp/Modules/Sign/SessionAccount/SessionAccountPresenter.swift @@ -12,7 +12,10 @@ final class SessionAccountPresenter: ObservableObject { @Published var showError = false @Published var errorMessage = String.empty @Published var showRequestSent = false - + @Published var requesting = false + var lastRequest: Request? + + private let interactor: SessionAccountInteractor private let router: SessionAccountRouter private let session: Session @@ -41,14 +44,21 @@ final class SessionAccountPresenter: ObservableObject { do { let requestParams = try getRequest(for: method) - let request = Request(topic: session.topic, method: method, params: requestParams, chainId: Blockchain(sessionAccount.chain)!) + let ttl: TimeInterval = 300 + let request = try Request(topic: session.topic, method: method, params: requestParams, chainId: Blockchain(sessionAccount.chain)!, ttl: ttl) Task { do { + ActivityIndicatorManager.shared.start() try await Sign.instance.request(params: request) + lastRequest = request + ActivityIndicatorManager.shared.stop() + requesting = true DispatchQueue.main.async { [weak self] in self?.openWallet() } } catch { + ActivityIndicatorManager.shared.stop() + requesting = false showError.toggle() errorMessage = error.localizedDescription } @@ -70,6 +80,7 @@ extension SessionAccountPresenter { Sign.instance.sessionResponsePublisher .receive(on: DispatchQueue.main) .sink { [unowned self] response in + requesting = false presentResponse(response: response) } .store(in: &subscriptions) diff --git a/Example/DApp/Modules/Sign/SessionAccount/SessionAccountView.swift b/Example/DApp/Modules/Sign/SessionAccount/SessionAccountView.swift index 1b8a3ebb8..939a9edb6 100644 --- a/Example/DApp/Modules/Sign/SessionAccount/SessionAccountView.swift +++ b/Example/DApp/Modules/Sign/SessionAccount/SessionAccountView.swift @@ -8,9 +8,11 @@ struct SessionAccountView: View { var body: some View { NavigationStack { ZStack { + Color(red: 25/255, green: 26/255, blue: 26/255) .ignoresSafeArea() - + + ScrollView { VStack(spacing: 12) { networkView(title: String(presenter.sessionAccount.chain.split(separator: ":").first ?? "")) @@ -21,6 +23,14 @@ struct SessionAccountView: View { } .padding(12) } + + if presenter.requesting { + loadingView + .frame(width: 200, height: 200) + .background(Color.gray.opacity(0.95)) + .cornerRadius(20) + .shadow(radius: 10) + } } .navigationTitle(presenter.sessionAccount.chain) .navigationBarTitleDisplayMode(.inline) @@ -179,7 +189,18 @@ struct SessionAccountView: View { } } } - + + private var loadingView: some View { + VStack { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .black)) + .scaleEffect(1.5) + Text("Request sent, waiting for response") + .foregroundColor(.white) + .padding(.top, 20) + } + } + private func responseView(response: Response) -> some View { ZStack { RoundedRectangle(cornerRadius: 16) @@ -209,14 +230,14 @@ struct SessionAccountView: View { .padding(12) Spacer() - - let record = Sign.instance.getSessionRequestRecord(id: response.id)! - Text(record.request.method) - .font( - Font.system(size: 14, weight: .medium) - ) - .foregroundColor(Color(red: 0.58, green: 0.62, blue: 0.62)) - .padding(12) + if let lastRequest = presenter.lastRequest { + Text(lastRequest.method) + .font( + Font.system(size: 14, weight: .medium) + ) + .foregroundColor(Color(red: 0.58, green: 0.62, blue: 0.62)) + .padding(12) + } } ZStack { diff --git a/Example/DApp/Modules/Sign/SignPresenter.swift b/Example/DApp/Modules/Sign/SignPresenter.swift index b42d2663c..e96791894 100644 --- a/Example/DApp/Modules/Sign/SignPresenter.swift +++ b/Example/DApp/Modules/Sign/SignPresenter.swift @@ -2,6 +2,7 @@ import UIKit import Combine import Web3Modal +import WalletConnectModal import WalletConnectSign final class SignPresenter: ObservableObject { @@ -52,17 +53,31 @@ final class SignPresenter: ObservableObject { Web3Modal.present(from: nil) } + func connectWalletWithWCM() { + WalletConnectModal.set(sessionParams: .init( + requiredNamespaces: Proposal.requiredNamespaces, + optionalNamespaces: Proposal.optionalNamespaces + )) + WalletConnectModal.present(from: nil) + } + @MainActor func connectWalletWithSign() { Task { let uri = try await Pair.instance.create() walletConnectUri = uri - try await Sign.instance.connect( - requiredNamespaces: Proposal.requiredNamespaces, - optionalNamespaces: Proposal.optionalNamespaces, - topic: uri.topic - ) - router.presentNewPairing(walletConnectUri: uri) + do { + ActivityIndicatorManager.shared.start() + try await Sign.instance.connect( + requiredNamespaces: Proposal.requiredNamespaces, + optionalNamespaces: Proposal.optionalNamespaces, + topic: uri.topic + ) + ActivityIndicatorManager.shared.stop() + router.presentNewPairing(walletConnectUri: uri) + } catch { + ActivityIndicatorManager.shared.stop() + } } } @@ -70,9 +85,12 @@ final class SignPresenter: ObservableObject { if let session { Task { @MainActor in do { + ActivityIndicatorManager.shared.start() try await Sign.instance.disconnect(topic: session.topic) + ActivityIndicatorManager.shared.stop() accountsDetails.removeAll() } catch { + ActivityIndicatorManager.shared.stop() showError.toggle() errorMessage = error.localizedDescription } @@ -104,8 +122,26 @@ extension SignPresenter { .receive(on: DispatchQueue.main) .sink { [unowned self] _ in self.accountsDetails.removeAll() + router.popToRoot() + Task(priority: .high) { ActivityIndicatorManager.shared.stop() } } .store(in: &subscriptions) + + Sign.instance.sessionResponsePublisher + .receive(on: DispatchQueue.main) + .sink { response in + Task(priority: .high) { ActivityIndicatorManager.shared.stop() } + } + .store(in: &subscriptions) + + Sign.instance.requestExpirationPublisher + .receive(on: DispatchQueue.main) + .sink { _ in + Task(priority: .high) { ActivityIndicatorManager.shared.stop() } + AlertPresenter.present(message: "Session Request has expired", type: .warning) + } + .store(in: &subscriptions) + } private func getSession() { diff --git a/Example/DApp/Modules/Sign/SignRouter.swift b/Example/DApp/Modules/Sign/SignRouter.swift index 60da1928c..a68ad5f9a 100644 --- a/Example/DApp/Modules/Sign/SignRouter.swift +++ b/Example/DApp/Modules/Sign/SignRouter.swift @@ -20,14 +20,14 @@ final class SignRouter { func presentSessionAccount(sessionAccount: AccountDetails, session: Session) { SessionAccountModule.create(app: app, sessionAccount: sessionAccount, session: session) - .present(from: viewController) + .push(from: viewController) } - - func dismissNewPairing() { - newPairingViewController?.dismiss() - } - + func dismiss() { viewController.dismiss(animated: true) } + + func popToRoot() { + viewController.popToRoot() + } } diff --git a/Example/DApp/Modules/Sign/SignView.swift b/Example/DApp/Modules/Sign/SignView.swift index fc60a1754..51d12a806 100644 --- a/Example/DApp/Modules/Sign/SignView.swift +++ b/Example/DApp/Modules/Sign/SignView.swift @@ -18,29 +18,42 @@ struct SignView: View { Spacer() - Button { - presenter.connectWalletWithW3M() - } label: { - Text("Connect with Web3Modal") - .font(.system(size: 16, weight: .semibold)) - .foregroundColor(.white) - .padding(.horizontal, 16) - .padding(.vertical, 10) - .background(Color(red: 95/255, green: 159/255, blue: 248/255)) - .cornerRadius(16) - } - .padding(.top, 20) - - Button { - presenter.connectWalletWithSign() - } label: { - Text("Connect with Sign API") - .font(.system(size: 16, weight: .semibold)) - .foregroundColor(.white) - .padding(.horizontal, 16) - .padding(.vertical, 10) - .background(Color(red: 95/255, green: 159/255, blue: 248/255)) - .cornerRadius(16) + VStack(spacing: 10) { + Button { + presenter.connectWalletWithW3M() + } label: { + Text("Connect with Web3Modal") + .font(.system(size: 16, weight: .semibold)) + .foregroundColor(.white) + .padding(.horizontal, 16) + .padding(.vertical, 10) + .background(Color(red: 95/255, green: 159/255, blue: 248/255)) + .cornerRadius(16) + } + + Button { + presenter.connectWalletWithSign() + } label: { + Text("Connect with Sign API") + .font(.system(size: 16, weight: .semibold)) + .foregroundColor(.white) + .padding(.horizontal, 16) + .padding(.vertical, 10) + .background(Color(red: 95/255, green: 159/255, blue: 248/255)) + .cornerRadius(16) + } + + Button { + presenter.connectWalletWithWCM() + } label: { + Text("Connect with WalletConnectModal") + .font(.system(size: 16, weight: .semibold)) + .foregroundColor(.white) + .padding(.horizontal, 16) + .padding(.vertical, 10) + .background(Color(red: 95/255, green: 159/255, blue: 248/255)) + .cornerRadius(16) + } } .padding(.top, 10) } @@ -59,6 +72,7 @@ struct SignView: View { .padding(12) } } + .padding(.bottom, presenter.accountsDetails.isEmpty ? 0 : 76) .onAppear { presenter.onAppear() } diff --git a/Example/DApp/SceneDelegate.swift b/Example/DApp/SceneDelegate.swift index b52f47555..38e007ed9 100644 --- a/Example/DApp/SceneDelegate.swift +++ b/Example/DApp/SceneDelegate.swift @@ -1,12 +1,15 @@ import UIKit import Web3Modal +import WalletConnectModal import Auth import WalletConnectRelay import WalletConnectNetworking +import Combine class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? + private var publishers = Set() private let app = Application() @@ -27,10 +30,43 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { ) Web3Modal.configure( + projectId: InputConfig.projectId, + metadata: metadata, + customWallets: [ + .init( + id: "swift-sample", + name: "Swift Sample Wallet", + homepage: "https://walletconnect.com/", + imageUrl: "https://avatars.githubusercontent.com/u/37784886?s=200&v=4", + order: 1, + mobileLink: "walletapp://" + ) + ] + ) + + WalletConnectModal.configure( projectId: InputConfig.projectId, metadata: metadata ) + + Sign.instance.logsPublisher.sink { log in + switch log { + case .error(let logMessage): + AlertPresenter.present(message: logMessage.message, type: .error) + default: return + } + }.store(in: &publishers) + + Sign.instance.socketConnectionStatusPublisher.sink { status in + switch status { + case .connected: + AlertPresenter.present(message: "Your web socket has connected", type: .success) + case .disconnected: + AlertPresenter.present(message: "Your web socket is disconnected", type: .warning) + } + }.store(in: &publishers) + setupWindow(scene: scene) } @@ -38,7 +74,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { guard let windowScene = (scene as? UIWindowScene) else { return } window = UIWindow(windowScene: windowScene) - let viewController = MainModule.create(app: app) + let viewController = SignModule.create(app: app) + .wrapToNavigationController() window?.rootViewController = viewController window?.makeKeyAndVisible() diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index f90232086..900432d34 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -32,6 +32,8 @@ 847BD1E8298A806800076C90 /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847BD1E3298A806800076C90 /* NotificationsView.swift */; }; 847BD1EB298A87AB00076C90 /* SubscriptionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847BD1EA298A87AB00076C90 /* SubscriptionsViewModel.swift */; }; 847F08012A25DBFF00B2A5A4 /* XPlatformW3WTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847F08002A25DBFF00B2A5A4 /* XPlatformW3WTests.swift */; }; + 8486EDD12B4F2DC1008E53C3 /* AlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8486EDD02B4F2DC1008E53C3 /* AlertPresenter.swift */; }; + 8486EDD32B4F2EA6008E53C3 /* SwiftMessages in Frameworks */ = {isa = PBXBuildFile; productRef = 8486EDD22B4F2EA6008E53C3 /* SwiftMessages */; }; 8487A9442A836C2A0003D5AF /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 8487A9432A836C2A0003D5AF /* Sentry */; }; 8487A9462A836C3F0003D5AF /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 8487A9452A836C3F0003D5AF /* Sentry */; }; 8487A9482A83AD680003D5AF /* LoggingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8487A9472A83AD680003D5AF /* LoggingService.swift */; }; @@ -40,6 +42,9 @@ 849D7A93292E2169006A2BD4 /* NotifyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849D7A92292E2169006A2BD4 /* NotifyTests.swift */; }; 84A6E3C32A386BBC008A0571 /* Publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A6E3C22A386BBC008A0571 /* Publisher.swift */; }; 84AA01DB28CF0CD7005D48D8 /* XCTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AA01DA28CF0CD7005D48D8 /* XCTest.swift */; }; + 84AEC24F2B4D1EE400E27A5B /* ActivityIndicatorManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AEC24E2B4D1EE400E27A5B /* ActivityIndicatorManager.swift */; }; + 84AEC2512B4D42C100E27A5B /* AlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AEC2502B4D42C100E27A5B /* AlertPresenter.swift */; }; + 84AEC2542B4D43CD00E27A5B /* SwiftMessages in Frameworks */ = {isa = PBXBuildFile; productRef = 84AEC2532B4D43CD00E27A5B /* SwiftMessages */; }; 84B8154E2991099000FAD54E /* BuildConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B8154D2991099000FAD54E /* BuildConfiguration.swift */; }; 84B8155B2992A18D00FAD54E /* NotifyMessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B8155A2992A18D00FAD54E /* NotifyMessageViewModel.swift */; }; 84CE641F27981DED00142511 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CE641E27981DED00142511 /* AppDelegate.swift */; }; @@ -47,6 +52,7 @@ 84CE642827981DF000142511 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 84CE642727981DF000142511 /* Assets.xcassets */; }; 84CE642B27981DF000142511 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 84CE642927981DF000142511 /* LaunchScreen.storyboard */; }; 84CEC64628D89D6B00D081A8 /* PairingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CEC64528D89D6B00D081A8 /* PairingTests.swift */; }; + 84D093EB2B4EA6CB005B1925 /* ActivityIndicatorManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D093EA2B4EA6CB005B1925 /* ActivityIndicatorManager.swift */; }; 84D2A66628A4F51E0088AE09 /* AuthTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D2A66528A4F51E0088AE09 /* AuthTests.swift */; }; 84DB38F32983CDAE00BFEE37 /* PushRegisterer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DB38F22983CDAE00BFEE37 /* PushRegisterer.swift */; }; 84DDB4ED28ABB663003D66ED /* WalletConnectAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 84DDB4EC28ABB663003D66ED /* WalletConnectAuth */; }; @@ -61,7 +67,6 @@ A50D53C32ABA055700A4FD8B /* NotifyPreferencesRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50D53BE2ABA055700A4FD8B /* NotifyPreferencesRouter.swift */; }; A50D53C42ABA055700A4FD8B /* NotifyPreferencesInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50D53BF2ABA055700A4FD8B /* NotifyPreferencesInteractor.swift */; }; A50D53C52ABA055700A4FD8B /* NotifyPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50D53C02ABA055700A4FD8B /* NotifyPreferencesView.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 */; }; @@ -79,7 +84,6 @@ 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 */; }; @@ -177,6 +181,7 @@ A5B6C0F32A6EAB1700927332 /* WalletConnectNotify in Frameworks */ = {isa = PBXBuildFile; productRef = A5B6C0F22A6EAB1700927332 /* WalletConnectNotify */; }; A5B6C0F52A6EAB2800927332 /* WalletConnectNotify in Frameworks */ = {isa = PBXBuildFile; productRef = A5B6C0F42A6EAB2800927332 /* WalletConnectNotify */; }; A5B6C0F72A6EAB3200927332 /* WalletConnectNotify in Frameworks */ = {isa = PBXBuildFile; productRef = A5B6C0F62A6EAB3200927332 /* WalletConnectNotify */; }; + A5B814FA2B5AAA2F00AECCFD /* WalletConnectIdentity in Frameworks */ = {isa = PBXBuildFile; productRef = A5B814F92B5AAA2F00AECCFD /* WalletConnectIdentity */; }; A5BB7FA328B6A50400707FC6 /* WalletConnectAuth in Frameworks */ = {isa = PBXBuildFile; productRef = A5BB7FA228B6A50400707FC6 /* WalletConnectAuth */; }; A5BB7FAD28B6AA7D00707FC6 /* QRCodeGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BB7FAC28B6AA7D00707FC6 /* QRCodeGenerator.swift */; }; A5C2020B287D9DEE007E3188 /* WelcomeModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5C20206287D9DEE007E3188 /* WelcomeModule.swift */; }; @@ -289,12 +294,7 @@ C5B2F7052970573D000DBA0E /* SolanaSwift in Frameworks */ = {isa = PBXBuildFile; productRef = C5B2F7042970573D000DBA0E /* SolanaSwift */; }; C5B2F71029705827000DBA0E /* EthereumTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F568C32795832A00D0A289 /* EthereumTransaction.swift */; }; C5B4C4C42AF11C8B00B4274A /* SignView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5B4C4C32AF11C8B00B4274A /* SignView.swift */; }; - C5B4C4CF2AF12F1600B4274A /* AuthView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5B4C4CE2AF12F1600B4274A /* AuthView.swift */; }; C5BE01D12AF661D70064FC88 /* NewPairingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5BE01D02AF661D70064FC88 /* NewPairingView.swift */; }; - C5BE01D72AF691CD0064FC88 /* AuthModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5BE01D62AF691CD0064FC88 /* AuthModule.swift */; }; - C5BE01D92AF691FE0064FC88 /* AuthPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5BE01D82AF691FE0064FC88 /* AuthPresenter.swift */; }; - C5BE01DB2AF692060064FC88 /* AuthRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5BE01DA2AF692060064FC88 /* AuthRouter.swift */; }; - C5BE01DD2AF692100064FC88 /* AuthInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5BE01DC2AF692100064FC88 /* AuthInteractor.swift */; }; C5BE01DF2AF692D80064FC88 /* WalletConnectRouter in Frameworks */ = {isa = PBXBuildFile; productRef = C5BE01DE2AF692D80064FC88 /* WalletConnectRouter */; }; C5BE01E22AF693080064FC88 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5BE01E12AF693080064FC88 /* Application.swift */; }; C5BE01E32AF696540064FC88 /* SceneViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C56EE264293F56D6004840D1 /* SceneViewController.swift */; }; @@ -310,12 +310,6 @@ C5BE02022AF774CB0064FC88 /* NewPairingInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5BE01F62AF6CA2B0064FC88 /* NewPairingInteractor.swift */; }; C5BE02032AF774CB0064FC88 /* NewPairingPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5BE01F42AF6CA2B0064FC88 /* NewPairingPresenter.swift */; }; C5BE02042AF7764F0064FC88 /* UIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C56EE26C293F56D6004840D1 /* UIViewController.swift */; }; - C5BE020E2AF777AD0064FC88 /* MainRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5BE02082AF777AD0064FC88 /* MainRouter.swift */; }; - C5BE020F2AF777AD0064FC88 /* TabPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5BE020D2AF777AD0064FC88 /* TabPage.swift */; }; - C5BE02102AF777AD0064FC88 /* MainModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5BE02072AF777AD0064FC88 /* MainModule.swift */; }; - C5BE02112AF777AD0064FC88 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5BE020A2AF777AD0064FC88 /* MainViewController.swift */; }; - C5BE02122AF777AD0064FC88 /* MainPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5BE020B2AF777AD0064FC88 /* MainPresenter.swift */; }; - C5BE02132AF777AD0064FC88 /* MainInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5BE02092AF777AD0064FC88 /* MainInteractor.swift */; }; C5BE02142AF77A940064FC88 /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C5F32A352954FE3C00A6476E /* Colors.xcassets */; }; C5BE021B2AF79B9A0064FC88 /* SessionAccountPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5BE02172AF79B950064FC88 /* SessionAccountPresenter.swift */; }; C5BE021C2AF79B9A0064FC88 /* SessionAccountRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5BE02162AF79B950064FC88 /* SessionAccountRouter.swift */; }; @@ -347,6 +341,7 @@ CF25F2892A432476009C7E49 /* WalletConnectModal in Frameworks */ = {isa = PBXBuildFile; productRef = CF25F2882A432476009C7E49 /* WalletConnectModal */; }; CF6704DF29E59DDC003326A4 /* XCUIElementQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF6704DE29E59DDC003326A4 /* XCUIElementQuery.swift */; }; CF6704E129E5A014003326A4 /* XCTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF6704E029E5A014003326A4 /* XCTestCase.swift */; }; + CFDB50722B2869AA00A0CBC2 /* WalletConnectModal in Frameworks */ = {isa = PBXBuildFile; productRef = CFDB50712B2869AA00A0CBC2 /* WalletConnectModal */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -426,6 +421,7 @@ 847BD1E3298A806800076C90 /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = ""; }; 847BD1EA298A87AB00076C90 /* SubscriptionsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionsViewModel.swift; sourceTree = ""; }; 847F08002A25DBFF00B2A5A4 /* XPlatformW3WTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XPlatformW3WTests.swift; sourceTree = ""; }; + 8486EDD02B4F2DC1008E53C3 /* AlertPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertPresenter.swift; sourceTree = ""; }; 8487A92E2A7BD2F30003D5AF /* XPlatformProtocolTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = XPlatformProtocolTests.xctestplan; path = ../XPlatformProtocolTests.xctestplan; sourceTree = ""; }; 8487A9472A83AD680003D5AF /* LoggingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggingService.swift; sourceTree = ""; }; 849A4F18298281E300E61ACE /* WalletAppRelease.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WalletAppRelease.entitlements; sourceTree = ""; }; @@ -433,6 +429,8 @@ 849D7A92292E2169006A2BD4 /* NotifyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotifyTests.swift; sourceTree = ""; }; 84A6E3C22A386BBC008A0571 /* Publisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Publisher.swift; sourceTree = ""; }; 84AA01DA28CF0CD7005D48D8 /* XCTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCTest.swift; sourceTree = ""; }; + 84AEC24E2B4D1EE400E27A5B /* ActivityIndicatorManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorManager.swift; sourceTree = ""; }; + 84AEC2502B4D42C100E27A5B /* AlertPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertPresenter.swift; sourceTree = ""; }; 84B8154D2991099000FAD54E /* BuildConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildConfiguration.swift; sourceTree = ""; }; 84B8155A2992A18D00FAD54E /* NotifyMessageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotifyMessageViewModel.swift; sourceTree = ""; }; 84CE641C27981DED00142511 /* DApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -443,6 +441,7 @@ 84CE642C27981DF000142511 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 84CE6453279FFE1100142511 /* Wallet.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Wallet.entitlements; sourceTree = ""; }; 84CEC64528D89D6B00D081A8 /* PairingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PairingTests.swift; sourceTree = ""; }; + 84D093EA2B4EA6CB005B1925 /* ActivityIndicatorManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorManager.swift; sourceTree = ""; }; 84D2A66528A4F51E0088AE09 /* AuthTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthTests.swift; sourceTree = ""; }; 84D72FC62B4692770057EAF3 /* DApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DApp.entitlements; sourceTree = ""; }; 84DB38F029828A7C00BFEE37 /* WalletApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WalletApp.entitlements; sourceTree = ""; }; @@ -475,7 +474,6 @@ 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 = ""; }; @@ -648,12 +646,7 @@ C5B2F6F42970511B000DBA0E /* SessionRequestInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionRequestInteractor.swift; sourceTree = ""; }; C5B2F6F52970511B000DBA0E /* SessionRequestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionRequestView.swift; sourceTree = ""; }; C5B4C4C32AF11C8B00B4274A /* SignView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignView.swift; sourceTree = ""; }; - C5B4C4CE2AF12F1600B4274A /* AuthView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthView.swift; sourceTree = ""; }; C5BE01D02AF661D70064FC88 /* NewPairingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewPairingView.swift; sourceTree = ""; }; - C5BE01D62AF691CD0064FC88 /* AuthModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthModule.swift; sourceTree = ""; }; - C5BE01D82AF691FE0064FC88 /* AuthPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthPresenter.swift; sourceTree = ""; }; - C5BE01DA2AF692060064FC88 /* AuthRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthRouter.swift; sourceTree = ""; }; - C5BE01DC2AF692100064FC88 /* AuthInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthInteractor.swift; sourceTree = ""; }; C5BE01E12AF693080064FC88 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; C5BE01ED2AF6C9DF0064FC88 /* SignPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignPresenter.swift; sourceTree = ""; }; C5BE01EE2AF6C9DF0064FC88 /* SignModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignModule.swift; sourceTree = ""; }; @@ -663,12 +656,6 @@ C5BE01F42AF6CA2B0064FC88 /* NewPairingPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewPairingPresenter.swift; sourceTree = ""; }; C5BE01F52AF6CA2B0064FC88 /* NewPairingModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewPairingModule.swift; sourceTree = ""; }; C5BE01F62AF6CA2B0064FC88 /* NewPairingInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewPairingInteractor.swift; sourceTree = ""; }; - C5BE02072AF777AD0064FC88 /* MainModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainModule.swift; sourceTree = ""; }; - C5BE02082AF777AD0064FC88 /* MainRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainRouter.swift; sourceTree = ""; }; - C5BE02092AF777AD0064FC88 /* MainInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainInteractor.swift; sourceTree = ""; }; - C5BE020A2AF777AD0064FC88 /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; - C5BE020B2AF777AD0064FC88 /* MainPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainPresenter.swift; sourceTree = ""; }; - C5BE020D2AF777AD0064FC88 /* TabPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabPage.swift; sourceTree = ""; }; C5BE02162AF79B950064FC88 /* SessionAccountRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionAccountRouter.swift; sourceTree = ""; }; C5BE02172AF79B950064FC88 /* SessionAccountPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionAccountPresenter.swift; sourceTree = ""; }; C5BE02182AF79B950064FC88 /* SessionAccountInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionAccountInteractor.swift; sourceTree = ""; }; @@ -722,9 +709,11 @@ A54195A52934E83F0035AD19 /* Web3 in Frameworks */, 8487A9442A836C2A0003D5AF /* Sentry in Frameworks */, A5D85228286333E300DAF5C3 /* Starscream in Frameworks */, + 8486EDD32B4F2EA6008E53C3 /* SwiftMessages in Frameworks */, 84943C7B2A9BA206007EBAC2 /* Mixpanel in Frameworks */, A573C53929EC365000E3CBFD /* HDWalletKit in Frameworks */, A5BB7FA328B6A50400707FC6 /* WalletConnectAuth in Frameworks */, + CFDB50722B2869AA00A0CBC2 /* WalletConnectModal in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -766,8 +755,8 @@ files = ( A5E03DFF2864662500888481 /* WalletConnect in Frameworks */, A561C80529DFCD4500DF540D /* WalletConnectSync in Frameworks */, + A5B814FA2B5AAA2F00AECCFD /* WalletConnectIdentity in Frameworks */, A5E03DF52864651200888481 /* Starscream in Frameworks */, - A50DF19D2A25084A0036EA6C /* WalletConnectHistory in Frameworks */, A5C8BE85292FE20B006CC85C /* Web3 in Frameworks */, 84DDB4ED28ABB663003D66ED /* WalletConnectAuth in Frameworks */, C5DD5BE1294E09E3008FD3A4 /* Web3Wallet in Frameworks */, @@ -789,6 +778,7 @@ C55D349929630D440004314A /* Web3Wallet in Frameworks */, A5F1526F2ACDC46B00D745A6 /* Web3ModalUI in Frameworks */, C56EE255293F569A004840D1 /* Starscream in Frameworks */, + 84AEC2542B4D43CD00E27A5B /* SwiftMessages in Frameworks */, A5B6C0F52A6EAB2800927332 /* WalletConnectNotify in Frameworks */, C56EE27B293F56F8004840D1 /* WalletConnectAuth in Frameworks */, C54C24902AEB1B5600DA4BF6 /* WalletConnectRouter in Frameworks */, @@ -1047,14 +1037,6 @@ path = Settings; sourceTree = ""; }; - A5321C292A25035A006CADC3 /* History */ = { - isa = PBXGroup; - children = ( - A5321C2A2A250367006CADC3 /* HistoryTests.swift */, - ); - path = History; - sourceTree = ""; - }; A54195992934BFDD0035AD19 /* Signer */ = { isa = PBXGroup; children = ( @@ -1420,6 +1402,8 @@ children = ( A51AC0D828E436A3001BACF9 /* InputConfig.swift */, A5BB7FAC28B6AA7D00707FC6 /* QRCodeGenerator.swift */, + 84D093EA2B4EA6CB005B1925 /* ActivityIndicatorManager.swift */, + 8486EDD02B4F2DC1008E53C3 /* AlertPresenter.swift */, ); path = Common; sourceTree = ""; @@ -1481,7 +1465,6 @@ isa = PBXGroup; children = ( 847F07FE2A25DBC700B2A5A4 /* XPlatform */, - A5321C292A25035A006CADC3 /* History */, A561C80129DFCCD300DF540D /* Sync */, 849D7A91292E2115006A2BD4 /* Push */, 84CEC64728D8A98900D081A8 /* Pairing */, @@ -1666,6 +1649,8 @@ C56EE2A1293F6B9E004840D1 /* Helpers */, C56EE262293F56D6004840D1 /* Extensions */, C56EE263293F56D6004840D1 /* VIPER */, + 84AEC24E2B4D1EE400E27A5B /* ActivityIndicatorManager.swift */, + 84AEC2502B4D42C100E27A5B /* AlertPresenter.swift */, ); path = Common; sourceTree = ""; @@ -1808,18 +1793,6 @@ path = Sign; sourceTree = ""; }; - C5B4C4CD2AF12F0B00B4274A /* Auth */ = { - isa = PBXGroup; - children = ( - C5BE01D62AF691CD0064FC88 /* AuthModule.swift */, - C5BE01D82AF691FE0064FC88 /* AuthPresenter.swift */, - C5BE01DA2AF692060064FC88 /* AuthRouter.swift */, - C5BE01DC2AF692100064FC88 /* AuthInteractor.swift */, - C5B4C4CE2AF12F1600B4274A /* AuthView.swift */, - ); - path = Auth; - sourceTree = ""; - }; C5BE01E02AF692F80064FC88 /* ApplicationLayer */ = { isa = PBXGroup; children = ( @@ -1840,27 +1813,6 @@ path = NewPairing; sourceTree = ""; }; - C5BE02062AF777AD0064FC88 /* Main */ = { - isa = PBXGroup; - children = ( - C5BE02072AF777AD0064FC88 /* MainModule.swift */, - C5BE020B2AF777AD0064FC88 /* MainPresenter.swift */, - C5BE02092AF777AD0064FC88 /* MainInteractor.swift */, - C5BE02082AF777AD0064FC88 /* MainRouter.swift */, - C5BE020A2AF777AD0064FC88 /* MainViewController.swift */, - C5BE020C2AF777AD0064FC88 /* Model */, - ); - path = Main; - sourceTree = ""; - }; - C5BE020C2AF777AD0064FC88 /* Model */ = { - isa = PBXGroup; - children = ( - C5BE020D2AF777AD0064FC88 /* TabPage.swift */, - ); - path = Model; - sourceTree = ""; - }; C5BE02152AF79B860064FC88 /* SessionAccount */ = { isa = PBXGroup; children = ( @@ -1876,9 +1828,7 @@ C5BE02202AF7DDE70064FC88 /* Modules */ = { isa = PBXGroup; children = ( - C5BE02062AF777AD0064FC88 /* Main */, C5B4C4C52AF12C2900B4274A /* Sign */, - C5B4C4CD2AF12F0B00B4274A /* Auth */, ); path = Modules; sourceTree = ""; @@ -2013,6 +1963,8 @@ 84943C7A2A9BA206007EBAC2 /* Mixpanel */, C5BE01DE2AF692D80064FC88 /* WalletConnectRouter */, C579FEB52AFA86CD008855EB /* Web3Modal */, + CFDB50712B2869AA00A0CBC2 /* WalletConnectModal */, + 8486EDD22B4F2EA6008E53C3 /* SwiftMessages */, ); productName = DApp; productReference = 84CE641C27981DED00142511 /* DApp.app */; @@ -2107,8 +2059,8 @@ C5DD5BE0294E09E3008FD3A4 /* Web3Wallet */, A561C80429DFCD4500DF540D /* WalletConnectSync */, A573C53A29EC365800E3CBFD /* HDWalletKit */, - A50DF19C2A25084A0036EA6C /* WalletConnectHistory */, A5B6C0F22A6EAB1700927332 /* WalletConnectNotify */, + A5B814F92B5AAA2F00AECCFD /* WalletConnectIdentity */, ); productName = IntegrationTests; productReference = A5E03DED286464DB00888481 /* IntegrationTests.xctest */; @@ -2142,6 +2094,7 @@ A59D25ED2AB3672700D7EA3A /* AsyncButton */, A5F1526E2ACDC46B00D745A6 /* Web3ModalUI */, C54C248F2AEB1B5600DA4BF6 /* WalletConnectRouter */, + 84AEC2532B4D43CD00E27A5B /* SwiftMessages */, ); productName = ChatWallet; productReference = C56EE21B293F55ED004840D1 /* WalletApp.app */; @@ -2220,6 +2173,7 @@ 8487A9422A836C2A0003D5AF /* XCRemoteSwiftPackageReference "sentry-cocoa" */, 84943C792A9BA206007EBAC2 /* XCRemoteSwiftPackageReference "mixpanel-swift" */, A5F1526D2ACDC46B00D745A6 /* XCRemoteSwiftPackageReference "web3modal-swift" */, + 84AEC2522B4D43CD00E27A5B /* XCRemoteSwiftPackageReference "SwiftMessages" */, ); productRefGroup = 764E1D3D26F8D3FC00A1FB15 /* Products */; projectDirPath = ""; @@ -2320,45 +2274,36 @@ buildActionMask = 2147483647; files = ( C5BE021C2AF79B9A0064FC88 /* SessionAccountRouter.swift in Sources */, - C5BE02102AF777AD0064FC88 /* MainModule.swift in Sources */, - C5BE01DD2AF692100064FC88 /* AuthInteractor.swift in Sources */, C5B4C4C42AF11C8B00B4274A /* SignView.swift in Sources */, C5BE02042AF7764F0064FC88 /* UIViewController.swift in Sources */, C5BE021E2AF79B9A0064FC88 /* SessionAccountView.swift in Sources */, + 84D093EB2B4EA6CB005B1925 /* ActivityIndicatorManager.swift in Sources */, C5BE02032AF774CB0064FC88 /* NewPairingPresenter.swift in Sources */, 84CE641F27981DED00142511 /* AppDelegate.swift in Sources */, C5BE01E32AF696540064FC88 /* SceneViewController.swift in Sources */, - C5BE020F2AF777AD0064FC88 /* TabPage.swift in Sources */, A5A8E47D293A1CFE00FEB97D /* DefaultSocketFactory.swift in Sources */, C5BE01E52AF697470064FC88 /* Color.swift in Sources */, - C5B4C4CF2AF12F1600B4274A /* AuthView.swift in Sources */, A5BB7FAD28B6AA7D00707FC6 /* QRCodeGenerator.swift in Sources */, A51AC0D928E436A3001BACF9 /* InputConfig.swift in Sources */, - C5BE02122AF777AD0064FC88 /* MainPresenter.swift in Sources */, C5BE01E22AF693080064FC88 /* Application.swift in Sources */, C5BE021B2AF79B9A0064FC88 /* SessionAccountPresenter.swift in Sources */, A5A8E47E293A1CFE00FEB97D /* DefaultSignerFactory.swift in Sources */, - C5BE020E2AF777AD0064FC88 /* MainRouter.swift in Sources */, A5A0843D29D2F624000B9B17 /* DefaultCryptoProvider.swift in Sources */, C5BE021F2AF79B9A0064FC88 /* SessionAccountModule.swift in Sources */, C5BE01E42AF697100064FC88 /* UIColor.swift in Sources */, - C5BE01DB2AF692060064FC88 /* AuthRouter.swift in Sources */, C5BE01E62AF697FA0064FC88 /* String.swift in Sources */, C5BE02012AF774CB0064FC88 /* NewPairingModule.swift in Sources */, C5BE02002AF774CB0064FC88 /* NewPairingRouter.swift in Sources */, C5BE01F82AF6CB270064FC88 /* SignInteractor.swift in Sources */, + 8486EDD12B4F2DC1008E53C3 /* AlertPresenter.swift in Sources */, C5BE01D12AF661D70064FC88 /* NewPairingView.swift in Sources */, 84CE642127981DED00142511 /* SceneDelegate.swift in Sources */, C5BE02022AF774CB0064FC88 /* NewPairingInteractor.swift in Sources */, - C5BE01D92AF691FE0064FC88 /* AuthPresenter.swift in Sources */, - C5BE02112AF777AD0064FC88 /* MainViewController.swift in Sources */, C5BE021D2AF79B9A0064FC88 /* SessionAccountInteractor.swift in Sources */, A51606F82A2F47BD00CACB92 /* DefaultBIP44Provider.swift in Sources */, C5BE01FB2AF6CB270064FC88 /* SignRouter.swift in Sources */, - C5BE01D72AF691CD0064FC88 /* AuthModule.swift in Sources */, C5BE01F72AF6CB250064FC88 /* SignModule.swift in Sources */, C5BE01FA2AF6CB270064FC88 /* SignPresenter.swift in Sources */, - C5BE02132AF777AD0064FC88 /* MainInteractor.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2488,7 +2433,6 @@ 84FE684628ACDB4700C893FF /* RequestParams.swift in Sources */, 7694A5262874296A0001257E /* RegistryTests.swift in Sources */, A541959F2934BFEF0035AD19 /* SignerTests.swift in Sources */, - A5321C2B2A250367006CADC3 /* HistoryTests.swift in Sources */, A58A1ECC29BF458600A82A20 /* ENSResolverTests.swift in Sources */, A5E03DFA286465C700888481 /* SignClientTests.swift in Sources */, 84A6E3C32A386BBC008A0571 /* Publisher.swift in Sources */, @@ -2512,10 +2456,12 @@ A51811A02A52E83100A52B15 /* SettingsPresenter.swift in Sources */, 847BD1DA2989492500076C90 /* MainRouter.swift in Sources */, C5F32A2E2954814A00A6476E /* ConnectionDetailsRouter.swift in Sources */, + 84AEC24F2B4D1EE400E27A5B /* ActivityIndicatorManager.swift in Sources */, C55D3482295DD7140004314A /* AuthRequestInteractor.swift in Sources */, C55D34B12965FB750004314A /* SessionProposalInteractor.swift in Sources */, C56EE247293F566D004840D1 /* ScanModule.swift in Sources */, C56EE28D293F5757004840D1 /* AppearanceConfigurator.swift in Sources */, + 84AEC2512B4D42C100E27A5B /* AlertPresenter.swift in Sources */, 847BD1D82989492500076C90 /* MainModule.swift in Sources */, 847BD1E7298A806800076C90 /* NotificationsInteractor.swift in Sources */, C56EE241293F566D004840D1 /* WalletModule.swift in Sources */, @@ -3352,6 +3298,14 @@ kind = branch; }; }; + 84AEC2522B4D43CD00E27A5B /* XCRemoteSwiftPackageReference "SwiftMessages" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/SwiftKickMobile/SwiftMessages"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 9.0.9; + }; + }; A5434021291E6A270068F706 /* XCRemoteSwiftPackageReference "solana-swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/flypaper0/solana-swift"; @@ -3397,7 +3351,7 @@ repositoryURL = "https://github.com/WalletConnect/web3modal-swift"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.0.9; + minimumVersion = 1.0.15; }; }; /* End XCRemoteSwiftPackageReference section */ @@ -3420,6 +3374,11 @@ isa = XCSwiftPackageProductDependency; productName = Web3Wallet; }; + 8486EDD22B4F2EA6008E53C3 /* SwiftMessages */ = { + isa = XCSwiftPackageProductDependency; + package = 84AEC2522B4D43CD00E27A5B /* XCRemoteSwiftPackageReference "SwiftMessages" */; + productName = SwiftMessages; + }; 8487A9432A836C2A0003D5AF /* Sentry */ = { isa = XCSwiftPackageProductDependency; package = 8487A9422A836C2A0003D5AF /* XCRemoteSwiftPackageReference "sentry-cocoa" */; @@ -3440,13 +3399,14 @@ package = 84943C792A9BA206007EBAC2 /* XCRemoteSwiftPackageReference "mixpanel-swift" */; productName = Mixpanel; }; - 84DDB4EC28ABB663003D66ED /* WalletConnectAuth */ = { + 84AEC2532B4D43CD00E27A5B /* SwiftMessages */ = { isa = XCSwiftPackageProductDependency; - productName = WalletConnectAuth; + package = 84AEC2522B4D43CD00E27A5B /* XCRemoteSwiftPackageReference "SwiftMessages" */; + productName = SwiftMessages; }; - A50DF19C2A25084A0036EA6C /* WalletConnectHistory */ = { + 84DDB4EC28ABB663003D66ED /* WalletConnectAuth */ = { isa = XCSwiftPackageProductDependency; - productName = WalletConnectHistory; + productName = WalletConnectAuth; }; A54195A42934E83F0035AD19 /* Web3 */ = { isa = XCSwiftPackageProductDependency; @@ -3530,6 +3490,10 @@ isa = XCSwiftPackageProductDependency; productName = WalletConnectNotify; }; + A5B814F92B5AAA2F00AECCFD /* WalletConnectIdentity */ = { + isa = XCSwiftPackageProductDependency; + productName = WalletConnectIdentity; + }; A5BB7FA228B6A50400707FC6 /* WalletConnectAuth */ = { isa = XCSwiftPackageProductDependency; productName = WalletConnectAuth; @@ -3606,6 +3570,10 @@ isa = XCSwiftPackageProductDependency; productName = WalletConnectModal; }; + CFDB50712B2869AA00A0CBC2 /* WalletConnectModal */ = { + isa = XCSwiftPackageProductDependency; + productName = WalletConnectModal; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 764E1D3426F8D3FC00A1FB15 /* Project object */; diff --git a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index f11eb5be8..07a8aa9dd 100644 --- a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -127,6 +127,15 @@ "version": "1.1.6" } }, + { + "package": "SwiftMessages", + "repositoryURL": "https://github.com/SwiftKickMobile/SwiftMessages", + "state": { + "branch": null, + "revision": "62e12e138fc3eedf88c7553dd5d98712aa119f40", + "version": "9.0.9" + } + }, { "package": "swiftui-async-button", "repositoryURL": "https://github.com/lorenzofiamingo/swiftui-async-button", @@ -168,8 +177,8 @@ "repositoryURL": "https://github.com/WalletConnect/web3modal-swift", "state": { "branch": null, - "revision": "3295d69d1b12df29a5040578d107f56986b1b399", - "version": "1.0.13" + "revision": "296b2b72c116807a862e4c08ecf0a78ff044f87a", + "version": "1.0.16" } } ] diff --git a/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/Wallet.xcscheme b/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/Wallet.xcscheme index d786c3306..9619bb4be 100644 --- a/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/Wallet.xcscheme +++ b/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/Wallet.xcscheme @@ -73,6 +73,15 @@ allowLocationSimulation = "YES"> + + + + - + + + + + diff --git a/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/WalletConnectPairing.xcscheme b/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/WalletConnectPairing.xcscheme index dc1c7488b..f4bd38aa8 100644 --- a/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/WalletConnectPairing.xcscheme +++ b/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/WalletConnectPairing.xcscheme @@ -28,6 +28,16 @@ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> + + + + () - - let relayUrl = "wss://relay.walletconnect.com" - let historyUrl = "https://history.walletconnect.com" - - var relayClient1: RelayClient! - var relayClient2: RelayClient! - - var historyClient: HistoryNetworkService! - - override func setUp() { - let keychain1 = KeychainStorageMock() - let keychain2 = KeychainStorageMock() - let logger1 = ConsoleLoggerMock() - let defaults1 = RuntimeKeyValueStorage() - relayClient1 = makeRelayClient(prefix: "🐄", keychain: keychain1) - relayClient2 = makeRelayClient(prefix: "🐫", keychain: keychain2) - historyClient = makeHistoryClient(defaults: defaults1, keychain: keychain1, logger: logger1) - } - - private func makeRelayClient(prefix: String, keychain: KeychainStorageProtocol) -> RelayClient { - return RelayClientFactory.create( - relayHost: InputConfig.relayHost, - projectId: InputConfig.projectId, - keyValueStorage: RuntimeKeyValueStorage(), - keychainStorage: keychain, - socketFactory: DefaultSocketFactory(), - logger: ConsoleLogger(prefix: prefix + " [Relay]", loggingLevel: .debug)) - } - - private func makeHistoryClient(defaults: KeyValueStorage, keychain: KeychainStorageProtocol, logger: ConsoleLogging) -> HistoryNetworkService { - let clientIdStorage = ClientIdStorage(defaults: defaults, keychain: keychain, logger: logger) - return HistoryNetworkService(clientIdStorage: clientIdStorage) - } - - func testRegister() async throws { - 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) - - wait(for: [exp], timeout: InputConfig.defaultTimeout) - - sleep(5) // History server has a queue - - let messages = try await historyClient.getMessages( - payload: GetMessagesPayload( - topic: topic, - originId: nil, - messageCount: 200, - direction: .forward), - historyUrl: historyUrl) - - XCTAssertEqual(messages.messages, [payload]) - } -} diff --git a/Example/IntegrationTests/Pairing/PairingTests.swift b/Example/IntegrationTests/Pairing/PairingTests.swift index 657530902..d9e83de6e 100644 --- a/Example/IntegrationTests/Pairing/PairingTests.swift +++ b/Example/IntegrationTests/Pairing/PairingTests.swift @@ -9,7 +9,6 @@ import WalletConnectPush @testable import Auth @testable import WalletConnectPairing @testable import WalletConnectSync -@testable import WalletConnectHistory final class PairingTests: XCTestCase { @@ -35,6 +34,7 @@ final class PairingTests: XCTestCase { keyValueStorage: RuntimeKeyValueStorage(), keychainStorage: keychain, socketFactory: DefaultSocketFactory(), + networkMonitor: NetworkMonitor(), logger: logger) let networkingClient = NetworkingClientFactory.create( diff --git a/Example/IntegrationTests/Push/NotifyTests.swift b/Example/IntegrationTests/Push/NotifyTests.swift index 8d26941ae..1618881e6 100644 --- a/Example/IntegrationTests/Push/NotifyTests.swift +++ b/Example/IntegrationTests/Push/NotifyTests.swift @@ -8,7 +8,6 @@ import Combine import WalletConnectNetworking import WalletConnectPush @testable import WalletConnectNotify -@testable import WalletConnectPairing import WalletConnectIdentity import WalletConnectSigner @@ -30,12 +29,11 @@ final class NotifyTests: XCTestCase { private var publishers = Set() - func makeClientDependencies(prefix: String) -> (PairingClient, NetworkInteracting, KeychainStorageProtocol, KeyValueStorage) { + func makeClientDependencies(prefix: String) -> (NetworkInteracting, KeychainStorageProtocol, KeyValueStorage) { let keychain = KeychainStorageMock() let keyValueStorage = RuntimeKeyValueStorage() let relayLogger = ConsoleLogger(prefix: prefix + " [Relay]", loggingLevel: .debug) - let pairingLogger = ConsoleLogger(prefix: prefix + " [Pairing]", loggingLevel: .debug) let networkingLogger = ConsoleLogger(prefix: prefix + " [Networking]", loggingLevel: .debug) let kmsLogger = ConsoleLogger(prefix: prefix + " [KMS]", loggingLevel: .debug) @@ -45,6 +43,7 @@ final class NotifyTests: XCTestCase { keyValueStorage: keyValueStorage, keychainStorage: keychain, socketFactory: DefaultSocketFactory(), + networkMonitor: NetworkMonitor(), logger: relayLogger) let networkingClient = NetworkingClientFactory.create( @@ -54,19 +53,13 @@ final class NotifyTests: XCTestCase { keyValueStorage: keyValueStorage, kmsLogger: kmsLogger) - let pairingClient = PairingClientFactory.create( - logger: pairingLogger, - keyValueStorage: keyValueStorage, - keychainStorage: keychain, - networkingClient: networkingClient) - let clientId = try! networkingClient.getClientId() networkingLogger.debug("My client id is: \(clientId)") - return (pairingClient, networkingClient, keychain, keyValueStorage) + return (networkingClient, keychain, keyValueStorage) } func makeWalletClient(prefix: String = "🦋 Wallet: ") -> NotifyClient { - let (pairingClient, networkingInteractor, keychain, keyValueStorage) = makeClientDependencies(prefix: prefix) + let (networkingInteractor, keychain, keyValueStorage) = makeClientDependencies(prefix: prefix) let notifyLogger = ConsoleLogger(prefix: prefix + " [Notify]", loggingLevel: .debug) let pushClient = PushClientFactory.create(projectId: "", pushHost: "echo.walletconnect.com", @@ -83,7 +76,6 @@ final class NotifyTests: XCTestCase { keychainStorage: keychain, groupKeychainStorage: KeychainStorageMock(), networkInteractor: networkingInteractor, - pairingRegisterer: pairingClient, pushClient: pushClient, crypto: DefaultCryptoProvider(), notifyHost: InputConfig.notifyHost, @@ -165,7 +157,6 @@ final class NotifyTests: XCTestCase { } } - /* func testWalletCreatesAndUpdatesSubscription() async throws { let created = expectation(description: "Subscription created") @@ -202,7 +193,6 @@ final class NotifyTests: XCTestCase { try await walletNotifyClientA.deleteSubscription(topic: subscription.topic) } - */ func testNotifyServerSubscribeAndNotifies() async throws { let subscribeExpectation = expectation(description: "creates notify subscription") @@ -255,6 +245,29 @@ final class NotifyTests: XCTestCase { } } + func testFetchHistory() async throws { + let subscribeExpectation = expectation(description: "fetch notify subscription") + let account = Account("eip155:1:0x622b17376F76d72C43527a917f59273247A917b4")! + + var subscription: NotifySubscription! + walletNotifyClientA.subscriptionsPublisher + .sink { subscriptions in + subscription = subscriptions.first + subscribeExpectation.fulfill() + }.store(in: &publishers) + + try await walletNotifyClientA.register(account: account, domain: gmDappDomain) { message in + let privateKey = Data(hex: "c3ff8a0ae33ac5d58e515055c5870fa2f220d070997bd6fd77a5f2c148528ff0") + let signer = MessageSignerFactory(signerFactory: DefaultSignerFactory()).create(projectId: InputConfig.projectId) + return try! signer.sign(message: message, privateKey: privateKey, type: .eip191) + } + + await fulfillment(of: [subscribeExpectation], timeout: InputConfig.defaultTimeout) + + let hasMore = try await walletNotifyClientA.fetchHistory(subscription: subscription, after: nil, limit: 20) + XCTAssertTrue(hasMore) + XCTAssertTrue(walletNotifyClientA.getMessageHistory(topic: subscription.topic).count == 20) + } } diff --git a/Example/IntegrationTests/Sign/SignClientTests.swift b/Example/IntegrationTests/Sign/SignClientTests.swift index 4cecd7bb0..b1cfb2978 100644 --- a/Example/IntegrationTests/Sign/SignClientTests.swift +++ b/Example/IntegrationTests/Sign/SignClientTests.swift @@ -26,6 +26,7 @@ final class SignClientTests: XCTestCase { keyValueStorage: keyValueStorage, keychainStorage: keychain, socketFactory: DefaultSocketFactory(), + networkMonitor: NetworkMonitor(), logger: logger ) @@ -91,7 +92,7 @@ final class SignClientTests: XCTestCase { let uri = try! await dappPairingClient.create() try await dapp.connect(requiredNamespaces: requiredNamespaces, topic: uri.topic) try await walletPairingClient.pair(uri: uri) - wait(for: [dappSettlementExpectation, walletSettlementExpectation], timeout: InputConfig.defaultTimeout) + await fulfillment(of: [dappSettlementExpectation, walletSettlementExpectation], timeout: InputConfig.defaultTimeout) } func testSessionReject() async throws { @@ -100,6 +101,7 @@ final class SignClientTests: XCTestCase { class Store { var rejectedProposal: Session.Proposal? } let store = Store() + let semaphore = DispatchSemaphore(value: 0) let uri = try! await dappPairingClient.create() try await dapp.connect(requiredNamespaces: requiredNamespaces, topic: uri.topic) @@ -110,14 +112,16 @@ final class SignClientTests: XCTestCase { do { try await wallet.reject(proposalId: proposal.id, reason: .userRejectedChains) // TODO: Review reason store.rejectedProposal = proposal + semaphore.signal() } catch { XCTFail("\(error)") } } }.store(in: &publishers) dapp.sessionRejectionPublisher.sink { proposal, _ in + semaphore.wait() XCTAssertEqual(store.rejectedProposal, proposal) sessionRejectExpectation.fulfill() // TODO: Assert reason code }.store(in: &publishers) - wait(for: [sessionRejectExpectation], timeout: InputConfig.defaultTimeout) + await fulfillment(of: [sessionRejectExpectation], timeout: InputConfig.defaultTimeout) } func testSessionDelete() async throws { @@ -142,7 +146,7 @@ final class SignClientTests: XCTestCase { let uri = try! await dappPairingClient.create() try await dapp.connect(requiredNamespaces: requiredNamespaces, topic: uri.topic) try await walletPairingClient.pair(uri: uri) - wait(for: [sessionDeleteExpectation], timeout: InputConfig.defaultTimeout) + await fulfillment(of: [sessionDeleteExpectation], timeout: InputConfig.defaultTimeout) } func testSessionPing() async throws { @@ -173,7 +177,7 @@ final class SignClientTests: XCTestCase { try await dapp.connect(requiredNamespaces: requiredNamespaces, topic: uri.topic) try await walletPairingClient.pair(uri: uri) - wait(for: [expectation], timeout: InputConfig.defaultTimeout) + await fulfillment(of: [expectation], timeout: InputConfig.defaultTimeout) } func testSessionRequest() async throws { @@ -197,7 +201,7 @@ final class SignClientTests: XCTestCase { }.store(in: &publishers) dapp.sessionSettlePublisher.sink { [unowned self] settledSession in Task(priority: .high) { - let request = Request(id: RPCID(0), topic: settledSession.topic, method: requestMethod, params: requestParams, chainId: chain, expiry: nil) + let request = try! Request(id: RPCID(0), topic: settledSession.topic, method: requestMethod, params: requestParams, chainId: chain) try await dapp.request(params: request) } }.store(in: &publishers) @@ -223,7 +227,7 @@ final class SignClientTests: XCTestCase { let uri = try! await dappPairingClient.create() try await dapp.connect(requiredNamespaces: requiredNamespaces, topic: uri.topic) try await walletPairingClient.pair(uri: uri) - wait(for: [requestExpectation, responseExpectation], timeout: InputConfig.defaultTimeout) + await fulfillment(of: [requestExpectation, responseExpectation], timeout: InputConfig.defaultTimeout) } func testSessionRequestFailureResponse() async throws { @@ -244,7 +248,7 @@ final class SignClientTests: XCTestCase { }.store(in: &publishers) dapp.sessionSettlePublisher.sink { [unowned self] settledSession in Task(priority: .high) { - let request = Request(id: RPCID(0), topic: settledSession.topic, method: requestMethod, params: requestParams, chainId: chain, expiry: nil) + let request = try! Request(id: RPCID(0), topic: settledSession.topic, method: requestMethod, params: requestParams, chainId: chain) try await dapp.request(params: request) } }.store(in: &publishers) @@ -266,7 +270,7 @@ final class SignClientTests: XCTestCase { let uri = try! await dappPairingClient.create() try await dapp.connect(requiredNamespaces: requiredNamespaces, topic: uri.topic) try await walletPairingClient.pair(uri: uri) - wait(for: [expectation], timeout: InputConfig.defaultTimeout) + await fulfillment(of: [expectation], timeout: InputConfig.defaultTimeout) } func testNewSessionOnExistingPairing() async throws { @@ -304,7 +308,7 @@ final class SignClientTests: XCTestCase { let uri = try! await dappPairingClient.create() try await dapp.connect(requiredNamespaces: requiredNamespaces, topic: uri.topic) try await walletPairingClient.pair(uri: uri) - wait(for: [dappSettlementExpectation, walletSettlementExpectation], timeout: InputConfig.defaultTimeout) + await fulfillment(of: [dappSettlementExpectation, walletSettlementExpectation], timeout: InputConfig.defaultTimeout) } func testSuccessfulSessionUpdateNamespaces() async throws { @@ -328,7 +332,7 @@ final class SignClientTests: XCTestCase { let uri = try! await dappPairingClient.create() try await dapp.connect(requiredNamespaces: requiredNamespaces, topic: uri.topic) try await walletPairingClient.pair(uri: uri) - wait(for: [expectation], timeout: InputConfig.defaultTimeout) + await fulfillment(of: [expectation], timeout: InputConfig.defaultTimeout) } func testSuccessfulSessionExtend() async throws { @@ -357,7 +361,7 @@ final class SignClientTests: XCTestCase { try await dapp.connect(requiredNamespaces: requiredNamespaces, topic: uri.topic) try await walletPairingClient.pair(uri: uri) - wait(for: [expectation], timeout: InputConfig.defaultTimeout) + await fulfillment(of: [expectation], timeout: InputConfig.defaultTimeout) } func testSessionEventSucceeds() async throws { @@ -388,7 +392,7 @@ final class SignClientTests: XCTestCase { try await dapp.connect(requiredNamespaces: requiredNamespaces, topic: uri.topic) try await walletPairingClient.pair(uri: uri) - wait(for: [expectation], timeout: InputConfig.defaultTimeout) + await fulfillment(of: [expectation], timeout: InputConfig.defaultTimeout) } func testSessionEventFails() async throws { @@ -416,7 +420,7 @@ final class SignClientTests: XCTestCase { try await dapp.connect(requiredNamespaces: requiredNamespaces, topic: uri.topic) try await walletPairingClient.pair(uri: uri) - wait(for: [expectation], timeout: InputConfig.defaultTimeout) + await fulfillment(of: [expectation], timeout: InputConfig.defaultTimeout) } func testCaip25SatisfyAllRequiredAllOptionalNamespacesSuccessful() async throws { @@ -494,7 +498,7 @@ final class SignClientTests: XCTestCase { let uri = try! await dappPairingClient.create() try await dapp.connect(requiredNamespaces: requiredNamespaces, optionalNamespaces: optionalNamespaces, topic: uri.topic) try await walletPairingClient.pair(uri: uri) - wait(for: [dappSettlementExpectation, walletSettlementExpectation], timeout: InputConfig.defaultTimeout) + await fulfillment(of: [dappSettlementExpectation, walletSettlementExpectation], timeout: InputConfig.defaultTimeout) } func testCaip25SatisfyAllRequiredNamespacesSuccessful() async throws { @@ -563,7 +567,7 @@ final class SignClientTests: XCTestCase { let uri = try! await dappPairingClient.create() try await dapp.connect(requiredNamespaces: requiredNamespaces, optionalNamespaces: optionalNamespaces, topic: uri.topic) try await walletPairingClient.pair(uri: uri) - wait(for: [dappSettlementExpectation, walletSettlementExpectation], timeout: InputConfig.defaultTimeout) + await fulfillment(of: [dappSettlementExpectation, walletSettlementExpectation], timeout: InputConfig.defaultTimeout) } func testCaip25SatisfyEmptyRequiredNamespacesExtraOptionalNamespacesSuccessful() async throws { @@ -622,7 +626,7 @@ final class SignClientTests: XCTestCase { let uri = try! await dappPairingClient.create() try await dapp.connect(requiredNamespaces: requiredNamespaces, optionalNamespaces: optionalNamespaces, topic: uri.topic) try await walletPairingClient.pair(uri: uri) - wait(for: [dappSettlementExpectation, walletSettlementExpectation], timeout: InputConfig.defaultTimeout) + await fulfillment(of: [dappSettlementExpectation, walletSettlementExpectation], timeout: InputConfig.defaultTimeout) } func testCaip25SatisfyPartiallyRequiredNamespacesFails() async throws { @@ -685,7 +689,7 @@ final class SignClientTests: XCTestCase { let uri = try! await dappPairingClient.create() try await dapp.connect(requiredNamespaces: requiredNamespaces, optionalNamespaces: optionalNamespaces, topic: uri.topic) try await walletPairingClient.pair(uri: uri) - wait(for: [settlementFailedExpectation], timeout: 1) + await fulfillment(of: [settlementFailedExpectation], timeout: InputConfig.defaultTimeout) } func testCaip25SatisfyPartiallyRequiredNamespacesMethodsFails() async throws { @@ -751,6 +755,6 @@ final class SignClientTests: XCTestCase { let uri = try! await dappPairingClient.create() try await dapp.connect(requiredNamespaces: requiredNamespaces, optionalNamespaces: optionalNamespaces, topic: uri.topic) try await walletPairingClient.pair(uri: uri) - wait(for: [settlementFailedExpectation], timeout: 1) + await fulfillment(of: [settlementFailedExpectation], timeout: 1) } } diff --git a/Example/IntegrationTests/Stubs/PushMessage.swift b/Example/IntegrationTests/Stubs/PushMessage.swift index 634d78ea1..68d08b36e 100644 --- a/Example/IntegrationTests/Stubs/PushMessage.swift +++ b/Example/IntegrationTests/Stubs/PushMessage.swift @@ -9,6 +9,7 @@ extension NotifyMessage { body: "body", icon: "https://images.unsplash.com/photo-1581224463294-908316338239?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=250&q=80", url: "https://web3inbox.com", - type: type) + type: type, + sentAt: Date()) } } diff --git a/Example/IntegrationTests/Sync/SyncTests.swift b/Example/IntegrationTests/Sync/SyncTests.swift index adcfdc532..5e19c1345 100644 --- a/Example/IntegrationTests/Sync/SyncTests.swift +++ b/Example/IntegrationTests/Sync/SyncTests.swift @@ -63,6 +63,7 @@ final class SyncTests: XCTestCase { keyValueStorage: RuntimeKeyValueStorage(), keychainStorage: keychain, socketFactory: DefaultSocketFactory(), + networkMonitor: NetworkMonitor(), logger: logger) let networkingInteractor = NetworkingClientFactory.create( relayClient: relayClient, diff --git a/Example/IntegrationTests/XPlatform/Web3Wallet/XPlatformW3WTests.swift b/Example/IntegrationTests/XPlatform/Web3Wallet/XPlatformW3WTests.swift index 3d794d18a..c2f18b4c1 100644 --- a/Example/IntegrationTests/XPlatform/Web3Wallet/XPlatformW3WTests.swift +++ b/Example/IntegrationTests/XPlatform/Web3Wallet/XPlatformW3WTests.swift @@ -33,6 +33,7 @@ final class XPlatformW3WTests: XCTestCase { keyValueStorage: keyValueStorage, keychainStorage: keychain, socketFactory: DefaultSocketFactory(), + networkMonitor: NetworkMonitor(), logger: relayLogger ) diff --git a/Example/PNDecryptionService/NotificationService.swift b/Example/PNDecryptionService/NotificationService.swift index 9fae80990..d3f535f8f 100644 --- a/Example/PNDecryptionService/NotificationService.swift +++ b/Example/PNDecryptionService/NotificationService.swift @@ -75,11 +75,11 @@ class NotificationService: UNNotificationServiceExtension { private func handleNotifyNotification(content: UNNotificationContent, topic: String, ciphertext: String) -> UNMutableNotificationContent { do { let service = NotifyDecryptionService(groupIdentifier: "group.com.walletconnect.sdk") - let (pushMessage, account) = try service.decryptMessage(topic: topic, ciphertext: ciphertext) + let (pushMessage, subscription, account) = try service.decryptMessage(topic: topic, ciphertext: ciphertext) log("message decrypted", account: account, topic: topic, message: pushMessage) - let updatedContent = handle(content: content, pushMessage: pushMessage, topic: topic) + let updatedContent = handle(content: content, pushMessage: pushMessage, subscription: subscription, topic: topic) let mutableContent = updatedContent.mutableCopy() as! UNMutableNotificationContent mutableContent.title = pushMessage.title @@ -114,62 +114,66 @@ class NotificationService: UNNotificationServiceExtension { private extension NotificationService { - func handle(content: UNNotificationContent, pushMessage: NotifyMessage, topic: String) -> UNNotificationContent { - do { - let iconUrl = try pushMessage.icon.asURL() - - let senderThumbnailImageData = try Data(contentsOf: iconUrl) - let senderThumbnailImageFileUrl = try downloadAttachment(data: senderThumbnailImageData, fileName: iconUrl.lastPathComponent) - let senderThumbnailImageFileData = try Data(contentsOf: senderThumbnailImageFileUrl) - let senderAvatar = INImage(imageData: senderThumbnailImageFileData) - - var personNameComponents = PersonNameComponents() - personNameComponents.nickname = pushMessage.title - - let senderPerson = INPerson( - personHandle: INPersonHandle(value: topic, type: .unknown), - nameComponents: personNameComponents, - displayName: pushMessage.title, - image: senderAvatar, - contactIdentifier: nil, - customIdentifier: topic, - isMe: false, - suggestionType: .none - ) - - let selfPerson = INPerson( - personHandle: INPersonHandle(value: "0", type: .unknown), - nameComponents: nil, - displayName: nil, - image: nil, - contactIdentifier: nil, - customIdentifier: nil, - isMe: true, - suggestionType: .none - ) - - let incomingMessagingIntent = INSendMessageIntent( - recipients: [selfPerson], - outgoingMessageType: .outgoingMessageText, - content: pushMessage.body, - speakableGroupName: nil, - conversationIdentifier: pushMessage.type, - serviceName: nil, - sender: senderPerson, - attachments: [] - ) - - incomingMessagingIntent.setImage(senderAvatar, forParameterNamed: \.sender) - - let interaction = INInteraction(intent: incomingMessagingIntent, response: nil) - interaction.direction = .incoming - interaction.donate(completion: nil) - - return try content.updating(from: incomingMessagingIntent) - } - catch { - return content + func handle(content: UNNotificationContent, pushMessage: NotifyMessage, subscription: NotifySubscription, topic: String) -> UNNotificationContent { + + var senderAvatar: INImage? + + if let icon = subscription.messageIcons(ofType: pushMessage.type).md { + do { + let iconUrl = try icon.asURL() + let senderThumbnailImageData = try Data(contentsOf: iconUrl) + let senderThumbnailImageFileUrl = try downloadAttachment(data: senderThumbnailImageData, fileName: iconUrl.lastPathComponent) + let senderThumbnailImageFileData = try Data(contentsOf: senderThumbnailImageFileUrl) + senderAvatar = INImage(imageData: senderThumbnailImageFileData) + } catch { + log("Fetch icon error: \(error)", account: subscription.account, topic: topic, message: pushMessage) + } } + + var personNameComponents = PersonNameComponents() + personNameComponents.nickname = pushMessage.title + + let senderPerson = INPerson( + personHandle: INPersonHandle(value: topic, type: .unknown), + nameComponents: personNameComponents, + displayName: pushMessage.title, + image: senderAvatar, + contactIdentifier: nil, + customIdentifier: topic, + isMe: false, + suggestionType: .none + ) + + let selfPerson = INPerson( + personHandle: INPersonHandle(value: "0", type: .unknown), + nameComponents: nil, + displayName: nil, + image: nil, + contactIdentifier: nil, + customIdentifier: nil, + isMe: true, + suggestionType: .none + ) + + let incomingMessagingIntent = INSendMessageIntent( + recipients: [selfPerson], + outgoingMessageType: .outgoingMessageText, + content: pushMessage.body, + speakableGroupName: nil, + conversationIdentifier: pushMessage.type, + serviceName: nil, + sender: senderPerson, + attachments: [] + ) + + incomingMessagingIntent.setImage(senderAvatar, forParameterNamed: \.sender) + + let interaction = INInteraction(intent: incomingMessagingIntent, response: nil) + interaction.direction = .incoming + interaction.donate(completion: nil) + + let updated = try? content.updating(from: incomingMessagingIntent) + return updated ?? content } func downloadAttachment(data: Data, fileName: String) throws -> URL { diff --git a/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift b/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift index 119a62901..3ac8d75b7 100644 --- a/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift +++ b/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift @@ -44,9 +44,11 @@ final class RelayClientEndToEndTests: XCTestCase { ) let socket = WebSocket(url: urlFactory.create(fallback: false)) let webSocketFactory = WebSocketFactoryMock(webSocket: socket) + let networkMonitor = NetworkMonitor() let dispatcher = Dispatcher( socketFactory: webSocketFactory, relayUrlFactory: urlFactory, + networkMonitor: networkMonitor, socketConnectionType: .manual, logger: logger ) @@ -57,7 +59,8 @@ final class RelayClientEndToEndTests: XCTestCase { keyValueStorage: keyValueStorage, keychainStorage: keychain, socketFactory: DefaultSocketFactory(), - socketConnectionType: .manual, + socketConnectionType: .manual, + networkMonitor: networkMonitor, logger: logger ) let clientId = try! relayClient.getClientId() diff --git a/Example/WalletApp/ApplicationLayer/ConfigurationService.swift b/Example/WalletApp/ApplicationLayer/ConfigurationService.swift index 1267374f7..51d1f0c28 100644 --- a/Example/WalletApp/ApplicationLayer/ConfigurationService.swift +++ b/Example/WalletApp/ApplicationLayer/ConfigurationService.swift @@ -2,9 +2,12 @@ import UIKit import WalletConnectNetworking import WalletConnectNotify import Web3Wallet +import Combine final class ConfigurationService { + private var publishers = Set() + func configure(importAccount: ImportAccount) { Networking.configure( groupIdentifier: "group.com.walletconnect.sdk", @@ -38,6 +41,42 @@ final class ConfigurationService { } LoggingService.instance.startLogging() + Web3Wallet.instance.socketConnectionStatusPublisher + .receive(on: DispatchQueue.main) + .sink { status in + switch status { + case .connected: + AlertPresenter.present(message: "Your web socket has connected", type: .success) + case .disconnected: + AlertPresenter.present(message: "Your web socket is disconnected", type: .warning) + } + }.store(in: &publishers) + + Web3Wallet.instance.logsPublisher + .receive(on: DispatchQueue.main) + .sink { log in + switch log { + case .error(let logMessage): + AlertPresenter.present(message: logMessage.message, type: .error) + default: return + } + }.store(in: &publishers) + + Web3Wallet.instance.pairingExpirationPublisher + .receive(on: DispatchQueue.main) + .sink { pairing in + guard !pairing.active else { return } + AlertPresenter.present(message: "Pairing has expired", type: .warning) + }.store(in: &publishers) + + Web3Wallet.instance.sessionProposalExpirationPublisher.sink { _ in + AlertPresenter.present(message: "Session Proposal has expired", type: .warning) + }.store(in: &publishers) + + Web3Wallet.instance.requestExpirationPublisher.sink { _ in + AlertPresenter.present(message: "Session Request has expired", type: .warning) + }.store(in: &publishers) + Task { do { let params = try await Notify.instance.prepareRegistration(account: importAccount.account, domain: "com.walletconnect") diff --git a/Example/WalletApp/ApplicationLayer/SceneDelegate.swift b/Example/WalletApp/ApplicationLayer/SceneDelegate.swift index c36e6b4f6..ae9208eff 100644 --- a/Example/WalletApp/ApplicationLayer/SceneDelegate.swift +++ b/Example/WalletApp/ApplicationLayer/SceneDelegate.swift @@ -1,7 +1,7 @@ import Auth import SafariServices import UIKit -import WalletConnectPairing +import Web3Wallet final class SceneDelegate: UIResponder, UIWindowSceneDelegate, UNUserNotificationCenterDelegate { var window: UIWindow? @@ -29,7 +29,13 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate, UNUserNotificatio window = UIWindow(windowScene: windowScene) window?.makeKeyAndVisible() - app.uri = WalletConnectURI(connectionOptions: connectionOptions) + do { + let uri = try WalletConnectURI(connectionOptions: connectionOptions) + app.uri = uri + } catch { + print("Error initializing WalletConnectURI: \(error.localizedDescription)") + } + app.requestSent = (connectionOptions.urlContexts.first?.url.absoluteString.replacingOccurrences(of: "walletapp://wc?", with: "") == "requestSent") configurators.configure() @@ -37,18 +43,25 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate, UNUserNotificatio UNUserNotificationCenter.current().delegate = self } + func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { guard let context = URLContexts.first else { return } - let uri = WalletConnectURI(urlContext: context) - - if let uri { + do { + let uri = try WalletConnectURI(urlContext: context) Task { - try await Pair.instance.pair(uri: uri) + try await Web3Wallet.instance.pair(uri: uri) + } + } catch { + if case WalletConnectURI.Errors.expired = error { + AlertPresenter.present(message: error.localizedDescription, type: .error) + } else { + print("Error initializing WalletConnectURI: \(error.localizedDescription)") } } } + func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions { open(notification: notification) return [.sound, .banner, .badge] diff --git a/Example/WalletApp/BusinessLayer/ListingsSertice/ListingsAPI.swift b/Example/WalletApp/BusinessLayer/ListingsSertice/ListingsAPI.swift index c121e1e38..1d3eebf18 100644 --- a/Example/WalletApp/BusinessLayer/ListingsSertice/ListingsAPI.swift +++ b/Example/WalletApp/BusinessLayer/ListingsSertice/ListingsAPI.swift @@ -16,7 +16,7 @@ enum ListingsAPI: HTTPService { } var queryParameters: [String : String]? { - return ["projectId": InputConfig.projectId, "entries": "100"] + return ["projectId": InputConfig.projectId, "isVerified": "true", "isFeatured": "true"] } var additionalHeaderFields: [String : String]? { diff --git a/Example/WalletApp/Common/ActivityIndicatorManager.swift b/Example/WalletApp/Common/ActivityIndicatorManager.swift new file mode 100644 index 000000000..9022a6f41 --- /dev/null +++ b/Example/WalletApp/Common/ActivityIndicatorManager.swift @@ -0,0 +1,42 @@ +import UIKit + +class ActivityIndicatorManager { + static let shared = ActivityIndicatorManager() + private var activityIndicator: UIActivityIndicatorView? + private let serialQueue = DispatchQueue(label: "com.yourapp.activityIndicatorManager") + + private init() {} + + func start() { + serialQueue.async { + self.stopInternal() + + DispatchQueue.main.async { + guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let window = windowScene.windows.first(where: { $0.isKeyWindow }) else { return } + + let activityIndicator = UIActivityIndicatorView(style: .large) + activityIndicator.center = window.center + activityIndicator.color = .blue + activityIndicator.startAnimating() + window.addSubview(activityIndicator) + + self.activityIndicator = activityIndicator + } + } + } + + func stop() { + serialQueue.async { + self.stopInternal() + } + } + + private func stopInternal() { + DispatchQueue.main.sync { + self.activityIndicator?.stopAnimating() + self.activityIndicator?.removeFromSuperview() + self.activityIndicator = nil + } + } +} diff --git a/Example/WalletApp/Common/AlertPresenter.swift b/Example/WalletApp/Common/AlertPresenter.swift new file mode 100644 index 000000000..5da5d4668 --- /dev/null +++ b/Example/WalletApp/Common/AlertPresenter.swift @@ -0,0 +1,35 @@ +import Foundation +import SwiftMessages +import UIKit + +struct AlertPresenter { + enum MessageType { + case warning + case error + case info + case success + } + + static func present(message: String, type: AlertPresenter.MessageType) { + DispatchQueue.main.async { + let view = MessageView.viewFromNib(layout: .cardView) + switch type { + case .warning: + view.configureTheme(.warning, iconStyle: .subtle) + case .error: + view.configureTheme(.error, iconStyle: .subtle) + case .info: + view.configureTheme(.info, iconStyle: .subtle) + case .success: + view.configureTheme(.success, iconStyle: .subtle) + } + view.button?.isHidden = true + view.layoutMarginAdditions = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) + view.configureContent(title: "", body: message) + var config = SwiftMessages.Config() + config.presentationStyle = .top + config.duration = .seconds(seconds: 1.5) + SwiftMessages.show(config: config, view: view) + } + } +} diff --git a/Example/WalletApp/PresentationLayer/Wallet/AuthRequest/AuthRequestRouter.swift b/Example/WalletApp/PresentationLayer/Wallet/AuthRequest/AuthRequestRouter.swift index 76e35da11..d21c62d49 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/AuthRequest/AuthRequestRouter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/AuthRequest/AuthRequestRouter.swift @@ -11,6 +11,5 @@ final class AuthRequestRouter { func dismiss() { viewController.dismiss() - UIApplication.shared.open(URL(string: "showcase://")!) } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/ConnectionDetails/ConnectionDetailsPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/ConnectionDetails/ConnectionDetailsPresenter.swift index 5312123a4..8ba46dc19 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/ConnectionDetails/ConnectionDetailsPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/ConnectionDetails/ConnectionDetailsPresenter.swift @@ -25,11 +25,14 @@ final class ConnectionDetailsPresenter: ObservableObject { func onDelete() { Task { do { + ActivityIndicatorManager.shared.start() try await interactor.disconnectSession(session: session) + ActivityIndicatorManager.shared.stop() DispatchQueue.main.async { self.router.dismiss() } } catch { + ActivityIndicatorManager.shared.stop() print(error) } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift index 762bdf97c..57900bcad 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift @@ -1,5 +1,6 @@ import UIKit import Combine +import SwiftUI final class MainPresenter { private let interactor: MainInteractor @@ -48,9 +49,15 @@ extension MainPresenter { interactor.sessionRequestPublisher .receive(on: DispatchQueue.main) - .sink { [unowned self] request, context in + .sink { [unowned self] (request, context) in + guard let vc = UIApplication.currentWindow.rootViewController?.topController, + vc.restorationIdentifier != SessionRequestModule.restorationIdentifier else { + return + } + router.dismiss() router.present(sessionRequest: request, importAccount: importAccount, sessionContext: context) }.store(in: &disposeBag) + interactor.requestPublisher .receive(on: DispatchQueue.main) diff --git a/Example/WalletApp/PresentationLayer/Wallet/Main/MainRouter.swift b/Example/WalletApp/PresentationLayer/Wallet/Main/MainRouter.swift index 211a1fc62..2f087957f 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Main/MainRouter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Main/MainRouter.swift @@ -41,4 +41,8 @@ final class MainRouter { AuthRequestModule.create(app: app, request: request, importAccount: importAccount, context: context) .presentFullScreen(from: viewController, transparentBackground: true) } + + func dismiss() { + viewController.dismiss() + } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/Notifications/Models/SubscriptionsViewModel.swift b/Example/WalletApp/PresentationLayer/Wallet/Notifications/Models/SubscriptionsViewModel.swift index 6e0f12f42..cced6706c 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Notifications/Models/SubscriptionsViewModel.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Notifications/Models/SubscriptionsViewModel.swift @@ -45,6 +45,7 @@ struct SubscriptionsViewModel: Identifiable { } var hasMessage: Bool { - return messagesCount != 0 + /* return messagesCount != 0 Badge disabled */ + return false } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsView.swift b/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsView.swift index a3146cbd8..7cf63ed8f 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsView.swift @@ -62,6 +62,9 @@ struct NotificationsView: View { .listRowSeparator(.hidden) } .listStyle(PlainListStyle()) + .safeAreaInset(edge: .bottom) { + Spacer().frame(height: 16) + } } .task { try? await presenter.fetch() diff --git a/Example/WalletApp/PresentationLayer/Wallet/PushMessages/Models/NotifyMessageViewModel.swift b/Example/WalletApp/PresentationLayer/Wallet/PushMessages/Models/NotifyMessageViewModel.swift index 9e937154c..342bac31e 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/PushMessages/Models/NotifyMessageViewModel.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/PushMessages/Models/NotifyMessageViewModel.swift @@ -24,4 +24,8 @@ struct NotifyMessageViewModel: Identifiable { var publishedAt: String { return pushMessageRecord.publishedAt.formatted(.relative(presentation: .named, unitsStyle: .wide)) } + + var type: String { + return pushMessageRecord.message.type + } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/PushMessages/SubscriptionInteractor.swift b/Example/WalletApp/PresentationLayer/Wallet/PushMessages/SubscriptionInteractor.swift index 9b0c84ff7..37ea5fcd0 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/PushMessages/SubscriptionInteractor.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/PushMessages/SubscriptionInteractor.swift @@ -30,4 +30,8 @@ final class SubscriptionInteractor { try await Notify.instance.deleteSubscription(topic: subscription.topic) } } + + func fetchHistory(after: String?, limit: Int) async throws -> Bool { + return try await Notify.instance.fetchHistory(subscription: subscription, after: after, limit: limit) + } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/PushMessages/SubscriptionPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/PushMessages/SubscriptionPresenter.swift index 23f9729f9..2f9bb1a21 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/PushMessages/SubscriptionPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/PushMessages/SubscriptionPresenter.swift @@ -4,6 +4,11 @@ import WalletConnectNotify final class SubscriptionPresenter: ObservableObject { + enum LoadingState { + case loading + case idle + } + private var subscription: NotifySubscription private let interactor: SubscriptionInteractor private let router: SubscriptionRouter @@ -11,6 +16,9 @@ final class SubscriptionPresenter: ObservableObject { @Published private var pushMessages: [NotifyMessageRecord] = [] + @Published var loadingState: LoadingState = .idle + @Published var isMoreDataAvailable: Bool = true + var subscriptionViewModel: SubscriptionsViewModel { return SubscriptionsViewModel(subscription: subscription) } @@ -44,11 +52,30 @@ final class SubscriptionPresenter: ObservableObject { } } + func messageIconUrl(message: NotifyMessageViewModel) -> URL? { + let icons = subscription.messageIcons(ofType: message.type) + return try? icons.md?.asURL() + } + func unsubscribe() { interactor.deleteSubscription(subscription) router.dismiss() } + func loadMoreMessages() { + switch loadingState { + case .loading: + break + case .idle: + Task(priority: .high) { @MainActor in + loadingState = .loading + let isLoaded = try? await interactor.fetchHistory(after: messages.last?.id, limit: 50) + isMoreDataAvailable = isLoaded ?? false + loadingState = .idle + } + } + } + @objc func preferencesDidPress() { router.presentPreferences(subscription: subscription) } diff --git a/Example/WalletApp/PresentationLayer/Wallet/PushMessages/SubscriptionView.swift b/Example/WalletApp/PresentationLayer/Wallet/PushMessages/SubscriptionView.swift index 358affe2b..e168a8814 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/PushMessages/SubscriptionView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/PushMessages/SubscriptionView.swift @@ -34,6 +34,11 @@ struct SubscriptionView: View { .listRowSeparator(.hidden) .listRowBackground(Color.clear) } + + if presenter.isMoreDataAvailable { + lastRowView() + .listRowSeparator(.hidden) + } } .listStyle(PlainListStyle()) } @@ -44,15 +49,16 @@ struct SubscriptionView: View { private func notificationView(pushMessage: NotifyMessageViewModel) -> some View { VStack(alignment: .center) { HStack(spacing: 12) { - CacheAsyncImage(url: URL(string: pushMessage.imageUrl)) { phase in + CacheAsyncImage(url: presenter.messageIconUrl(message: pushMessage)) { phase in if let image = phase.image { image .resizable() .frame(width: 48, height: 48) - .background(Color.black) + .background(Color.black.opacity(0.05)) .cornerRadius(10, corners: .allCorners) } else { Color.black + .opacity(0.05) .frame(width: 48, height: 48) .cornerRadius(10, corners: .allCorners) } @@ -72,7 +78,7 @@ struct SubscriptionView: View { .font(.system(size: 11)) } - Text(pushMessage.subtitle) + Text(.init(pushMessage.subtitle)) .foregroundColor(.Foreground175) .font(.system(size: 13)) @@ -161,6 +167,26 @@ struct SubscriptionView: View { .frame(maxWidth: .infinity) .frame(height: 410) } + + func lastRowView() -> some View { + VStack { + switch presenter.loadingState { + case .loading: + HStack { + Spacer() + ProgressView() + Spacer() + } + .padding(.bottom, 24) + case .idle: + EmptyView() + } + } + .frame(height: 50) + .onAppear { + presenter.loadMoreMessages() + } + } } #if DEBUG diff --git a/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalPresenter.swift index 523158eee..e494039a2 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalPresenter.swift @@ -35,9 +35,12 @@ final class SessionProposalPresenter: ObservableObject { @MainActor func onApprove() async throws { do { + ActivityIndicatorManager.shared.start() let showConnected = try await interactor.approve(proposal: sessionProposal, account: importAccount.account) showConnected ? showConnectedSheet.toggle() : router.dismiss() + ActivityIndicatorManager.shared.stop() } catch { + ActivityIndicatorManager.shared.stop() errorMessage = error.localizedDescription showError.toggle() } @@ -46,9 +49,12 @@ final class SessionProposalPresenter: ObservableObject { @MainActor func onReject() async throws { do { + ActivityIndicatorManager.shared.start() try await interactor.reject(proposal: sessionProposal) + ActivityIndicatorManager.shared.stop() router.dismiss() } catch { + ActivityIndicatorManager.shared.stop() errorMessage = error.localizedDescription showError.toggle() } @@ -57,12 +63,31 @@ final class SessionProposalPresenter: ObservableObject { func onConnectedSheetDismiss() { router.dismiss() } + + func dismiss() { + router.dismiss() + } } // MARK: - Private functions private extension SessionProposalPresenter { func setupInitialState() { + Web3Wallet.instance.sessionProposalExpirationPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] proposal in + guard let self = self else { return } + if proposal.id == self.sessionProposal.id { + dismiss() + } + }.store(in: &disposeBag) + Web3Wallet.instance.pairingExpirationPublisher + .receive(on: DispatchQueue.main) + .sink {[weak self] pairing in + if self?.sessionProposal.pairingTopic == pairing.topic { + self?.dismiss() + } + }.store(in: &disposeBag) } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalRouter.swift b/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalRouter.swift index cd9de7971..f03cce6db 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalRouter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalRouter.swift @@ -10,6 +10,8 @@ final class SessionProposalRouter { } func dismiss() { - viewController.dismiss() + DispatchQueue.main.async { [weak self] in + self?.viewController?.dismiss() + } } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalView.swift b/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalView.swift index 19ee52d1e..02f4b0401 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalView.swift @@ -13,6 +13,19 @@ struct SessionProposalView: View { VStack { Spacer() + VStack { + HStack { + Spacer() + Button(action: { + presenter.dismiss() + }) { + Image(systemName: "xmark") + .foregroundColor(.white) + .padding() + } + } + .padding() + } VStack(spacing: 0) { Image("header") .resizable() diff --git a/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestInteractor.swift b/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestInteractor.swift index 59886fbfb..edc2ed4df 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestInteractor.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestInteractor.swift @@ -4,7 +4,7 @@ import Web3Wallet import WalletConnectRouter final class SessionRequestInteractor { - func approve(sessionRequest: Request, importAccount: ImportAccount) async throws -> Bool { + func respondSessionRequest(sessionRequest: Request, importAccount: ImportAccount) async throws -> Bool { do { let result = try Signer.sign(request: sessionRequest, importAccount: importAccount) try await Web3Wallet.instance.respond( @@ -12,7 +12,6 @@ final class SessionRequestInteractor { requestId: sessionRequest.id, response: .response(result) ) - /* Redirect */ let session = getSession(topic: sessionRequest.topic) if let uri = session?.peer.redirect?.native { @@ -26,7 +25,7 @@ final class SessionRequestInteractor { } } - func reject(sessionRequest: Request) async throws { + func respondError(sessionRequest: Request) async throws { try await Web3Wallet.instance.respond( topic: sessionRequest.topic, requestId: sessionRequest.id, diff --git a/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestModule.swift b/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestModule.swift index a12c75573..6cffd00ea 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestModule.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestModule.swift @@ -3,6 +3,7 @@ import SwiftUI import Web3Wallet final class SessionRequestModule { + static let restorationIdentifier = "SessionRequestViewController" @discardableResult static func create(app: Application, sessionRequest: Request, importAccount: ImportAccount, sessionContext: VerifyContext?) -> UIViewController { let router = SessionRequestRouter(app: app) @@ -10,6 +11,7 @@ final class SessionRequestModule { let presenter = SessionRequestPresenter(interactor: interactor, router: router, sessionRequest: sessionRequest, importAccount: importAccount, context: sessionContext) let view = SessionRequestView().environmentObject(presenter) let viewController = SceneViewController(viewModel: presenter, content: view) + viewController.restorationIdentifier = Self.restorationIdentifier router.viewController = viewController diff --git a/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift index d8c00b710..17d2f9b49 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift @@ -43,9 +43,12 @@ final class SessionRequestPresenter: ObservableObject { @MainActor func onApprove() async throws { do { - let showConnected = try await interactor.approve(sessionRequest: sessionRequest, importAccount: importAccount) + ActivityIndicatorManager.shared.start() + let showConnected = try await interactor.respondSessionRequest(sessionRequest: sessionRequest, importAccount: importAccount) showConnected ? showSignedSheet.toggle() : router.dismiss() + ActivityIndicatorManager.shared.stop() } catch { + ActivityIndicatorManager.shared.stop() errorMessage = error.localizedDescription showError.toggle() } @@ -53,8 +56,16 @@ final class SessionRequestPresenter: ObservableObject { @MainActor func onReject() async throws { - try await interactor.reject(sessionRequest: sessionRequest) - router.dismiss() + do { + ActivityIndicatorManager.shared.start() + try await interactor.respondError(sessionRequest: sessionRequest) + ActivityIndicatorManager.shared.stop() + router.dismiss() + } catch { + ActivityIndicatorManager.shared.stop() + errorMessage = error.localizedDescription + showError.toggle() + } } func onSignedSheetDismiss() { @@ -68,7 +79,16 @@ final class SessionRequestPresenter: ObservableObject { // MARK: - Private functions private extension SessionRequestPresenter { - func setupInitialState() {} + func setupInitialState() { + Web3Wallet.instance.requestExpirationPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] request in + guard let self = self else { return } + if request.id == sessionRequest.id { + dismiss() + } + }.store(in: &disposeBag) + } } // MARK: - SceneViewModel diff --git a/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestRouter.swift b/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestRouter.swift index cb7dff530..b4cbce9b5 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestRouter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestRouter.swift @@ -10,6 +10,8 @@ final class SessionRequestRouter { } func dismiss() { - viewController.dismiss() + DispatchQueue.main.async { [weak self] in + self?.viewController?.dismiss() + } } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestView.swift b/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestView.swift index d64b1f82a..ebc9557b2 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestView.swift @@ -11,7 +11,20 @@ struct SessionRequestView: View { VStack { Spacer() - + + VStack { + HStack { + Spacer() + Button(action: { + presenter.dismiss() + }) { + Image(systemName: "xmark") + .foregroundColor(.white) + .padding() + } + } + .padding() + } VStack(spacing: 0) { Image("header") .resizable() @@ -98,7 +111,7 @@ struct SessionRequestView: View { } .alert(presenter.errorMessage, isPresented: $presenter.showError) { Button("OK", role: .cancel) { - presenter.dismiss() +// presenter.dismiss() } } .sheet( diff --git a/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsPresenter.swift index d9c10848d..163799e3c 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsPresenter.swift @@ -1,6 +1,7 @@ import UIKit import Combine import WalletConnectNetworking +import Web3Wallet final class SettingsPresenter: ObservableObject { @@ -46,6 +47,7 @@ final class SettingsPresenter: ObservableObject { guard let account = accountStorage.importAccount?.account else { return } try await interactor.notifyUnregister(account: account) accountStorage.importAccount = nil + try await Web3Wallet.instance.cleanup() UserDefaults.standard.set(nil, forKey: "deviceToken") await router.presentWelcome() } diff --git a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletInteractor.swift b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletInteractor.swift index 3a32be3f8..2b3c7ff4b 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletInteractor.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletInteractor.swift @@ -19,11 +19,7 @@ final class WalletInteractor { func disconnectSession(session: Session) async throws { try await Web3Wallet.instance.disconnect(topic: session.topic) } - - func getPendingProposals() -> [(proposal: Session.Proposal, context: VerifyContext?)] { - Web3Wallet.instance.getPendingProposals() - } - + func getPendingRequests() -> [(request: Request, context: VerifyContext?)] { Web3Wallet.instance.getPendingRequests() } diff --git a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletPresenter.swift index 59d5c1fc2..0affd430f 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletPresenter.swift @@ -13,10 +13,15 @@ final class WalletPresenter: ObservableObject { private let importAccount: ImportAccount private let app: Application - + private var isPairingTimer: Timer? + @Published var sessions = [Session]() - @Published var showPairingLoading = false + @Published var showPairingLoading = false { + didSet { + handlePairingLoadingChanged() + } + } @Published var showError = false @Published var errorMessage = "Error" @Published var showConnectedSheet = false @@ -40,7 +45,7 @@ final class WalletPresenter: ObservableObject { func onAppear() { showPairingLoading = app.requestSent - removePairingIndicator() + setUpPairingIndicatorRemoval() let pendingRequests = interactor.getPendingRequests() if let request = pendingRequests.first(where: { $0.context != nil }) { @@ -53,15 +58,15 @@ final class WalletPresenter: ObservableObject { } func onPasteUri() { - router.presentPaste { [weak self] uri in - guard let uri = WalletConnectURI(string: uri) else { - self?.errorMessage = Errors.invalidUri(uri: uri).localizedDescription + router.presentPaste { [weak self] uriString in + do { + let uri = try WalletConnectURI(uriString: uriString) + print("URI: \(uri)") + self?.pair(uri: uri) + } catch { + self?.errorMessage = error.localizedDescription self?.showError.toggle() - return } - print("URI: \(uri)") - self?.pair(uri: uri) - } onError: { [weak self] error in print(error.localizedDescription) self?.router.dismiss() @@ -69,24 +74,44 @@ final class WalletPresenter: ObservableObject { } func onScanUri() { - router.presentScan { [weak self] uri in - guard let uri = WalletConnectURI(string: uri) else { - self?.errorMessage = Errors.invalidUri(uri: uri).localizedDescription + router.presentScan { [weak self] uriString in + do { + let uri = try WalletConnectURI(uriString: uriString) + print("URI: \(uri)") + self?.pair(uri: uri) + self?.router.dismiss() + } catch { + self?.errorMessage = error.localizedDescription self?.showError.toggle() - return } - print("URI: \(uri)") - self?.pair(uri: uri) - self?.router.dismiss() - } onError: { error in + } onError: { [weak self] error in print(error.localizedDescription) - self.router.dismiss() + self?.router.dismiss() } } + func removeSession(at indexSet: IndexSet) async { if let index = indexSet.first { - try? await interactor.disconnectSession(session: sessions[index]) + do { + ActivityIndicatorManager.shared.start() + try await interactor.disconnectSession(session: sessions[index]) + ActivityIndicatorManager.shared.stop() + } catch { + ActivityIndicatorManager.shared.stop() + sessions = sessions + AlertPresenter.present(message: error.localizedDescription, type: .error) + } + } + } + + private func handlePairingLoadingChanged() { + isPairingTimer?.invalidate() + + if showPairingLoading { + isPairingTimer = Timer.scheduledTimer(withTimeInterval: 10.0, repeats: false) { _ in + AlertPresenter.present(message: "Pairing takes longer then expected, check your internet connection or try again", type: .warning) + } } } } @@ -109,11 +134,8 @@ extension WalletPresenter { private func pair(uri: WalletConnectURI) { Task.detached(priority: .high) { @MainActor [unowned self] in do { - self.showPairingLoading = true - self.removePairingIndicator() try await self.interactor.pair(uri: uri) } catch { - self.showPairingLoading = false self.errorMessage = error.localizedDescription self.showError.toggle() } @@ -126,11 +148,13 @@ extension WalletPresenter { } pair(uri: uri) } - - private func removePairingIndicator() { - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - self.showPairingLoading = false - } + + private func setUpPairingIndicatorRemoval() { + Web3Wallet.instance.pairingStatePublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] isPairing in + self?.showPairingLoading = isPairing + }.store(in: &disposeBag) } } @@ -153,3 +177,4 @@ extension WalletPresenter.Errors: LocalizedError { } } } + diff --git a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletRouter.swift b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletRouter.swift index c9907a0b5..694ddaab7 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletRouter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletRouter.swift @@ -13,7 +13,7 @@ final class WalletRouter { func present(sessionRequest: Request, importAccount: ImportAccount, sessionContext: VerifyContext?) { SessionRequestModule.create(app: app, sessionRequest: sessionRequest, importAccount: importAccount, sessionContext: sessionContext) - .presentFullScreen(from: viewController, transparentBackground: true) + .presentFullScreen(from: UIApplication.currentWindow.rootViewController!, transparentBackground: true) } func present(sessionProposal: Session.Proposal, importAccount: ImportAccount, sessionContext: VerifyContext?) { diff --git a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletView.swift b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletView.swift index db735a7cf..e053493b4 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletView.swift @@ -35,7 +35,7 @@ struct WalletView: View { } .onDelete { indexSet in Task(priority: .high) { - await presenter.removeSession(at: indexSet) + try await presenter.removeSession(at: indexSet) } } } diff --git a/Package.swift b/Package.swift index dcfa42b87..e3c3b7eb8 100644 --- a/Package.swift +++ b/Package.swift @@ -43,13 +43,12 @@ let package = Package( .library( name: "WalletConnectVerify", targets: ["WalletConnectVerify"]), - .library( - name: "WalletConnectHistory", - targets: ["WalletConnectHistory"]), .library( name: "WalletConnectModal", targets: ["WalletConnectModal"]), - + .library( + name: "WalletConnectIdentity", + targets: ["WalletConnectIdentity"]), ], dependencies: [ .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.3.0"), @@ -62,7 +61,7 @@ let package = Package( path: "Sources/WalletConnectSign"), .target( name: "WalletConnectChat", - dependencies: ["WalletConnectIdentity", "WalletConnectSync", "WalletConnectHistory"], + dependencies: ["WalletConnectIdentity", "WalletConnectSync"], path: "Sources/Chat"), .target( name: "Auth", @@ -74,7 +73,7 @@ let package = Package( path: "Sources/Web3Wallet"), .target( name: "WalletConnectNotify", - dependencies: ["WalletConnectPairing", "WalletConnectPush", "WalletConnectIdentity", "WalletConnectSigner", "Database"], + dependencies: ["WalletConnectPairing", "WalletConnectIdentity", "WalletConnectPush", "WalletConnectSigner", "Database"], path: "Sources/WalletConnectNotify"), .target( name: "WalletConnectPush", @@ -92,9 +91,6 @@ let package = Package( .target( name: "WalletConnectPairing", dependencies: ["WalletConnectNetworking"]), - .target( - name: "WalletConnectHistory", - dependencies: ["HTTPClient", "WalletConnectRelay"]), .target( name: "WalletConnectSigner", dependencies: ["WalletConnectNetworking"]), diff --git a/Sources/Chat/Chat.swift b/Sources/Chat/Chat.swift index 2bb3ac56b..003b64335 100644 --- a/Sources/Chat/Chat.swift +++ b/Sources/Chat/Chat.swift @@ -12,8 +12,7 @@ public class Chat { keyserverUrl: keyserverUrl, relayClient: Relay.instance, networkingInteractor: Networking.interactor, - syncClient: Sync.instance, - historyClient: History.instance + syncClient: Sync.instance ) }() diff --git a/Sources/Chat/ChatClientFactory.swift b/Sources/Chat/ChatClientFactory.swift index ebce9f3c3..52bd97024 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, syncClient: SyncClient, historyClient: HistoryClient) -> ChatClient { + static func create(keyserverUrl: String, relayClient: RelayClient, networkingInteractor: NetworkingInteractor, syncClient: SyncClient) -> ChatClient { fatalError("fix access group") let keychain = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk", accessGroup: "") let keyserverURL = URL(string: keyserverUrl)! @@ -13,8 +13,7 @@ public struct ChatClientFactory { keychain: keychain, logger: ConsoleLogger(loggingLevel: .debug), storage: UserDefaults.standard, - syncClient: syncClient, - historyClient: historyClient + syncClient: syncClient ) } @@ -25,12 +24,10 @@ public struct ChatClientFactory { keychain: KeychainStorageProtocol, logger: ConsoleLogging, storage: KeyValueStorage, - syncClient: SyncClient, - historyClient: HistoryClient + syncClient: SyncClient ) -> ChatClient { let kms = KeyManagementService(keychain: keychain) - let serializer = Serializer(kms: kms, logger: logger) - let historyService = HistoryService(historyClient: historyClient, seiralizer: serializer) + let historyService = HistoryService() 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) diff --git a/Sources/Chat/ChatImports.swift b/Sources/Chat/ChatImports.swift index 447ee4a25..24dfe29a5 100644 --- a/Sources/Chat/ChatImports.swift +++ b/Sources/Chat/ChatImports.swift @@ -2,5 +2,4 @@ @_exported import WalletConnectSigner @_exported import WalletConnectIdentity @_exported import WalletConnectSync -@_exported import WalletConnectHistory #endif diff --git a/Sources/Chat/ProtocolServices/History/HistoryService.swift b/Sources/Chat/ProtocolServices/History/HistoryService.swift index ebad3a50d..13f014333 100644 --- a/Sources/Chat/ProtocolServices/History/HistoryService.swift +++ b/Sources/Chat/ProtocolServices/History/HistoryService.swift @@ -2,36 +2,15 @@ import Foundation final class HistoryService { - private let historyClient: HistoryClient - private let seiralizer: Serializing + init() { - init(historyClient: HistoryClient, seiralizer: Serializing) { - self.historyClient = historyClient - self.seiralizer = seiralizer } func register() async throws { - try await historyClient.register(tags: ["2002"]) + fatalError() } 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) - } + fatalError() } } diff --git a/Sources/Database/SQLiteQuery.swift b/Sources/Database/SQLiteQuery.swift index ce2d8d920..ce6078410 100644 --- a/Sources/Database/SQLiteQuery.swift +++ b/Sources/Database/SQLiteQuery.swift @@ -7,7 +7,7 @@ public struct SqliteQuery { for row in rows { values.append(row.encode().values - .map { "'\($0.value)'" } + .map { "'\($0.value.screen())'" } .joined(separator: ", ")) } @@ -34,7 +34,7 @@ public struct SqliteQuery { } public static func select(table: String, where argument: String, equals value: String) -> String { - return "SELECT * FROM \(table) WHERE \(argument) = '\(value)';" + return "SELECT * FROM \(table) WHERE \(argument) = '\(value.screen())';" } public static func delete(table: String) -> String { @@ -42,7 +42,7 @@ public struct SqliteQuery { } public static func delete(table: String, where argument: String, equals value: String) -> String { - return "DELETE FROM \(table) WHERE \(argument) = '\(value)';" + return "DELETE FROM \(table) WHERE \(argument) = '\(value.screen())';" } } @@ -52,3 +52,10 @@ extension SqliteQuery { case rowsNotFound } } + +private extension String { + + func screen() -> String { + return replacingOccurrences(of: "'", with: "''") + } +} diff --git a/Sources/WalletConnectHistory/History.swift b/Sources/WalletConnectHistory/History.swift deleted file mode 100644 index d0954f756..000000000 --- a/Sources/WalletConnectHistory/History.swift +++ /dev/null @@ -1,12 +0,0 @@ -import Foundation - -/// History instatnce wrapper -public class History { - - /// Sync client instance - public static var instance: HistoryClient = { - return HistoryClientFactory.create() - }() - - private init() { } -} diff --git a/Sources/WalletConnectHistory/HistoryAPI.swift b/Sources/WalletConnectHistory/HistoryAPI.swift deleted file mode 100644 index 58f6b0cbd..000000000 --- a/Sources/WalletConnectHistory/HistoryAPI.swift +++ /dev/null @@ -1,56 +0,0 @@ -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" - } - } - - var method: HTTPMethod { - switch self { - case .register: - return .post - case .messages: - return .get - } - } - - var body: Data? { - 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": "Bearer \(jwt)"] - case .messages: - return nil - } - } - - var queryParameters: [String: String]? { - switch self { - case .messages(let payload): - 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 deleted file mode 100644 index 3022a05aa..000000000 --- a/Sources/WalletConnectHistory/HistoryClient.swift +++ /dev/null @@ -1,48 +0,0 @@ -import Foundation - -public final class HistoryClient { - - 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 - } - - public func register(tags: [String]) async throws { - let payload = RegisterPayload(tags: tags, relayUrl: relayUrl) - try await historyNetworkService.registerTags(payload: payload, historyUrl: historyUrl) - } - - public func getMessages(topic: String, count: Int, direction: GetMessagesPayload.Direction) async throws -> [T] { - return try await getRecords(topic: topic, count: count, direction: direction).map { $0.object } - } - - public func getRecords(topic: String, count: Int, direction: GetMessagesPayload.Direction) async throws -> [HistoryRecord] { - let payload = GetMessagesPayload(topic: topic, originId: nil, messageCount: count, direction: direction) - let response = try await historyNetworkService.getMessages(payload: payload, historyUrl: historyUrl) - - return response.messages.compactMap { payload in - do { - let (request, _, _): (RPCRequest, _, _) = try serializer.deserialize( - topic: topic, - encodedEnvelope: payload - ) - - guard - let id = request.id, - let object = try request.params?.get(T.self) - else { return nil } - - return HistoryRecord(id: id, object: object) - } catch { - fatalError(error.localizedDescription) - } - } - } -} diff --git a/Sources/WalletConnectHistory/HistoryClientFactory.swift b/Sources/WalletConnectHistory/HistoryClientFactory.swift deleted file mode 100644 index 26d0d2044..000000000 --- a/Sources/WalletConnectHistory/HistoryClientFactory.swift +++ /dev/null @@ -1,30 +0,0 @@ -import Foundation - -class HistoryClientFactory { - - static func create() -> HistoryClient { - let keychain = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk", accessGroup: "") - let keyValueStorage = UserDefaults.standard - let logger = ConsoleLogger() - return HistoryClientFactory.create( - historyUrl: "https://history.walletconnect.com", - relayUrl: "wss://relay.walletconnect.com", - keyValueStorage: keyValueStorage, - keychain: keychain, - logger: logger - ) - } - - static func create(historyUrl: String, relayUrl: String, keyValueStorage: KeyValueStorage, keychain: KeychainStorageProtocol, logger: ConsoleLogging) -> HistoryClient { - let clientIdStorage = ClientIdStorage(defaults: keyValueStorage, keychain: keychain, logger: logger) - let kms = KeyManagementService(keychain: keychain) - let serializer = Serializer(kms: kms, logger: ConsoleLogger(prefix: "🔐", loggingLevel: .off)) - let historyNetworkService = HistoryNetworkService(clientIdStorage: clientIdStorage) - return HistoryClient( - historyUrl: historyUrl, - relayUrl: relayUrl, - serializer: serializer, - historyNetworkService: historyNetworkService - ) - } -} diff --git a/Sources/WalletConnectHistory/HistoryImports.swift b/Sources/WalletConnectHistory/HistoryImports.swift deleted file mode 100644 index e6d4b859c..000000000 --- a/Sources/WalletConnectHistory/HistoryImports.swift +++ /dev/null @@ -1,4 +0,0 @@ -#if !CocoaPods -@_exported import HTTPClient -@_exported import WalletConnectRelay -#endif diff --git a/Sources/WalletConnectHistory/HistoryNetworkService.swift b/Sources/WalletConnectHistory/HistoryNetworkService.swift deleted file mode 100644 index 906959577..000000000 --- a/Sources/WalletConnectHistory/HistoryNetworkService.swift +++ /dev/null @@ -1,41 +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: 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) - return try authenticator.createAuthToken(url: historyUrl) - } - - func host(from url: String) throws -> String { - guard let host = URL(string: url)?.host else { - throw Errors.couldNotResolveHost - } - return host - } -} diff --git a/Sources/WalletConnectHistory/HistoryRecord.swift b/Sources/WalletConnectHistory/HistoryRecord.swift deleted file mode 100644 index cd2793081..000000000 --- a/Sources/WalletConnectHistory/HistoryRecord.swift +++ /dev/null @@ -1,11 +0,0 @@ -import Foundation - -public struct HistoryRecord { - public let id: RPCID - public let object: Object - - public init(id: RPCID, object: Object) { - self.id = id - self.object = object - } -} diff --git a/Sources/WalletConnectHistory/Types/GetMessagesPayload.swift b/Sources/WalletConnectHistory/Types/GetMessagesPayload.swift deleted file mode 100644 index 7dcc9a08d..000000000 --- a/Sources/WalletConnectHistory/Types/GetMessagesPayload.swift +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index 032bad07e..000000000 --- a/Sources/WalletConnectHistory/Types/GetMessagesResponse.swift +++ /dev/null @@ -1,28 +0,0 @@ -import Foundation - -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 } - } -} diff --git a/Sources/WalletConnectHistory/Types/RegisterPayload.swift b/Sources/WalletConnectHistory/Types/RegisterPayload.swift deleted file mode 100644 index b759c5ce5..000000000 --- a/Sources/WalletConnectHistory/Types/RegisterPayload.swift +++ /dev/null @@ -1,11 +0,0 @@ -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 - } -} diff --git a/Sources/WalletConnectModal/Extensions/View+Backport.swift b/Sources/WalletConnectModal/Extensions/View+Backport.swift index 3c40e44ba..a2ed92408 100644 --- a/Sources/WalletConnectModal/Extensions/View+Backport.swift +++ b/Sources/WalletConnectModal/Extensions/View+Backport.swift @@ -32,7 +32,7 @@ extension View { @ViewBuilder func onTapGestureBackported(count: Int = 1, perform action: @escaping () -> Void) -> some View { - self + self.onTapGesture(count: count, perform: action) } #elseif os(tvOS) diff --git a/Sources/WalletConnectModal/Mocks/Listing+Mocks.swift b/Sources/WalletConnectModal/Mocks/Listing+Mocks.swift index 0c8af0885..721524d4e 100644 --- a/Sources/WalletConnectModal/Mocks/Listing+Mocks.swift +++ b/Sources/WalletConnectModal/Mocks/Listing+Mocks.swift @@ -2,64 +2,40 @@ import Foundation #if DEBUG -extension Listing { - static let stubList: [Listing] = [ - Listing( +extension Wallet { + static let stubList: [Wallet] = [ + Wallet( id: UUID().uuidString, name: "Sample Wallet", - homepage: "https://example.com", + homepage: "https://example.com/cool", + imageId: "0528ee7e-16d1-4089-21e3-bbfb41933100", order: 1, - imageId: UUID().uuidString, - app: Listing.App( - ios: "https://example.com/download-ios", - browser: "https://example.com/download-safari" - ), - mobile: .init( - native: "sampleapp://deeplink", - universal: "https://example.com/universal" - ), - desktop: .init( - native: nil, - universal: "https://example.com/universal" - ) + mobileLink: "https://sample.com/foo/universal", + desktopLink: "sampleapp://deeplink", + webappLink: "https://sample.com/foo/webapp", + appStore: "" ), - Listing( + Wallet( id: UUID().uuidString, - name: "Awesome Wallet", - homepage: "https://example.com/awesome", + name: "Cool Wallet", + homepage: "https://example.com/cool", + imageId: "5195e9db-94d8-4579-6f11-ef553be95100", order: 2, - imageId: UUID().uuidString, - app: Listing.App( - ios: "https://example.com/download-ios", - browser: "https://example.com/download-safari" - ), - mobile: .init( - native: "awesomeapp://deeplink", - universal: "https://example.com/awesome/universal" - ), - desktop: .init( - native: nil, - universal: "https://example.com/awesome/universal" - ) + mobileLink: "awsomeapp://", + desktopLink: "awsomeapp://deeplink", + webappLink: "https://awesome.com/foo/webapp", + appStore: "" ), - Listing( + Wallet( id: UUID().uuidString, name: "Cool Wallet", homepage: "https://example.com/cool", + imageId: "3913df81-63c2-4413-d60b-8ff83cbed500", order: 3, - imageId: UUID().uuidString, - app: Listing.App( - ios: "https://example.com/download-ios", - browser: "https://example.com/download-safari" - ), - mobile: .init( - native: "coolapp://deeplink", - universal: "https://example.com/cool/universal" - ), - desktop: .init( - native: nil, - universal: "https://example.com/cool/universal" - ) + mobileLink: "https://cool.com/foo/universal", + desktopLink: "coolapp://deeplink", + webappLink: "https://cool.com/foo/webapp", + appStore: "" ) ] } diff --git a/Sources/WalletConnectModal/Modal/ModalInteractor.swift b/Sources/WalletConnectModal/Modal/ModalInteractor.swift index 19fc70c51..fe18b4f48 100644 --- a/Sources/WalletConnectModal/Modal/ModalInteractor.swift +++ b/Sources/WalletConnectModal/Modal/ModalInteractor.swift @@ -3,7 +3,7 @@ import Combine import Foundation protocol ModalSheetInteractor { - func getListings() async throws -> [Listing] + func getWallets(page: Int, entries: Int) async throws -> (Int, [Wallet]) func createPairingAndConnect() async throws -> WalletConnectURI? var sessionSettlePublisher: AnyPublisher { get } @@ -11,24 +11,27 @@ protocol ModalSheetInteractor { } final class DefaultModalSheetInteractor: ModalSheetInteractor { - lazy var sessionSettlePublisher: AnyPublisher = WalletConnectModal.instance.sessionSettlePublisher lazy var sessionRejectionPublisher: AnyPublisher<(Session.Proposal, Reason), Never> = WalletConnectModal.instance.sessionRejectionPublisher - func getListings() async throws -> [Listing] { - - let httpClient = HTTPNetworkClient(host: "explorer-api.walletconnect.com") + func getWallets(page: Int, entries: Int) async throws -> (Int, [Wallet]) { + let httpClient = HTTPNetworkClient(host: "api.web3modal.org") let response = try await httpClient.request( - ListingsResponse.self, - at: ExplorerAPI.getListings( - projectId: WalletConnectModal.config.projectId, - metadata: WalletConnectModal.config.metadata, - recommendedIds: WalletConnectModal.config.recommendedWalletIds, - excludedIds: WalletConnectModal.config.excludedWalletIds + GetWalletsResponse.self, + at: Web3ModalAPI.getWallets( + params: Web3ModalAPI.GetWalletsParams( + page: page, + entries: entries, + search: nil, + projectId: WalletConnectModal.config.projectId, + metadata: WalletConnectModal.config.metadata, + recommendedIds: WalletConnectModal.config.recommendedWalletIds, + excludedIds: WalletConnectModal.config.excludedWalletIds + ) ) ) - return response.listings.values.compactMap { $0 } + return (response.count, response.data.compactMap { $0 }) } func createPairingAndConnect() async throws -> WalletConnectURI? { diff --git a/Sources/WalletConnectModal/Modal/ModalSheet.swift b/Sources/WalletConnectModal/Modal/ModalSheet.swift index b09226db0..1bfdbaa59 100644 --- a/Sources/WalletConnectModal/Modal/ModalSheet.swift +++ b/Sources/WalletConnectModal/Modal/ModalSheet.swift @@ -119,14 +119,12 @@ public struct ModalSheet: View { @ViewBuilder private func welcome() -> some View { WalletList( - wallets: .init(get: { - viewModel.filteredWallets - }, set: { _ in }), destination: .init(get: { viewModel.destination }, set: { _ in }), + viewModel: viewModel, navigateTo: viewModel.navigateTo(_:), - onListingTap: { viewModel.onListingTap($0) } + onWalletTap: { viewModel.onWalletTap($0) } ) } diff --git a/Sources/WalletConnectModal/Modal/ModalViewModel.swift b/Sources/WalletConnectModal/Modal/ModalViewModel.swift index 5c274468a..e78aa56e1 100644 --- a/Sources/WalletConnectModal/Modal/ModalViewModel.swift +++ b/Sources/WalletConnectModal/Modal/ModalViewModel.swift @@ -7,7 +7,7 @@ enum Destination: Equatable { case welcome case viewAll case qr - case walletDetail(Listing) + case walletDetail(Wallet) case getWallet var contentTitle: String { @@ -42,17 +42,21 @@ final class ModalViewModel: ObservableObject { @Published private(set) var destinationStack: [Destination] = [.welcome] @Published private(set) var uri: String? - @Published private(set) var wallets: [Listing] = [] - + @Published private(set) var wallets: [Wallet] = [] + @Published var searchTerm: String = "" @Published var toast: Toast? + @Published private(set) var isThereMoreWallets: Bool = true + private var maxPage = Int.max + private var currentPage: Int = 0 + var destination: Destination { destinationStack.last! } - var filteredWallets: [Listing] { + var filteredWallets: [Wallet] { wallets .sortByRecent() .filter(searchTerm: searchTerm) @@ -119,8 +123,8 @@ final class ModalViewModel: ObservableObject { uiApplicationWrapper.openURL(url, nil) } - func onListingTap(_ listing: Listing) { - setLastTimeUsed(listing.id) + func onWalletTap(_ wallet: Wallet) { + setLastTimeUsed(wallet.id) } func onBackButton() { @@ -162,17 +166,32 @@ final class ModalViewModel: ObservableObject { @MainActor func fetchWallets() async { + let entries = 40 + do { - let wallets = try await interactor.getListings() + guard currentPage <= maxPage else { + return + } + + currentPage += 1 + + if currentPage == maxPage { + isThereMoreWallets = false + } + + let (total, wallets) = try await interactor.getWallets(page: currentPage, entries: entries) + maxPage = Int(Double(total / entries).rounded(.up)) + // Small deliberate delay to ensure animations execute properly try await Task.sleep(nanoseconds: 500_000_000) - + loadRecentWallets() checkWhetherInstalled(wallets: wallets) - self.wallets = wallets + self.wallets.append(contentsOf: wallets .sortByOrder() .sortByInstalled() + ) } catch { toast = Toast(style: .error, message: error.localizedDescription) } @@ -181,28 +200,20 @@ final class ModalViewModel: ObservableObject { // MARK: - Sorting and filtering -private extension Array where Element: Listing { - func sortByOrder() -> [Listing] { +private extension Array where Element: Wallet { + func sortByOrder() -> [Wallet] { sorted { - guard let lhs = $0.order else { - return false - } - - guard let rhs = $1.order else { - return true - } - - return lhs < rhs + $0.order < $1.order } } - func sortByInstalled() -> [Listing] { + func sortByInstalled() -> [Wallet] { sorted { lhs, rhs in - if lhs.installed, !rhs.installed { + if lhs.isInstalled, !rhs.isInstalled { return true } - if !lhs.installed, rhs.installed { + if !lhs.isInstalled, rhs.isInstalled { return false } @@ -210,7 +221,7 @@ private extension Array where Element: Listing { } } - func sortByRecent() -> [Listing] { + func sortByRecent() -> [Wallet] { sorted { lhs, rhs in guard let lhsLastTimeUsed = lhs.lastTimeUsed else { return false @@ -224,7 +235,7 @@ private extension Array where Element: Listing { } } - func filter(searchTerm: String) -> [Listing] { + func filter(searchTerm: String) -> [Wallet] { if searchTerm.isEmpty { return self } return filter { @@ -236,18 +247,18 @@ private extension Array where Element: Listing { // MARK: - Recent & Installed Wallets private extension ModalViewModel { - func checkWhetherInstalled(wallets: [Listing]) { + func checkWhetherInstalled(wallets: [Wallet]) { guard let schemes = Bundle.main.object(forInfoDictionaryKey: "LSApplicationQueriesSchemes") as? [String] else { return } wallets.forEach { if - let walletScheme = $0.mobile.native, + let walletScheme = $0.mobileLink, !walletScheme.isEmpty, schemes.contains(walletScheme.replacingOccurrences(of: "://", with: "")) { - $0.installed = uiApplicationWrapper.canOpenURL(URL(string: walletScheme)!) + $0.isInstalled = uiApplicationWrapper.canOpenURL(URL(string: walletScheme)!) } } } @@ -270,40 +281,31 @@ private extension ModalViewModel { // MARK: - Deeplinking protocol WalletDeeplinkHandler { - func openAppstore(wallet: Listing) - func navigateToDeepLink(wallet: Listing, preferUniversal: Bool, preferBrowser: Bool) + func openAppstore(wallet: Wallet) + func navigateToDeepLink(wallet: Wallet, preferBrowser: Bool) } extension ModalViewModel: WalletDeeplinkHandler { - func openAppstore(wallet: Listing) { + func openAppstore(wallet: Wallet) { guard - let storeLinkString = wallet.app.ios, + let storeLinkString = wallet.appStore, let storeLink = URL(string: storeLinkString) else { return } uiApplicationWrapper.openURL(storeLink, nil) } - func navigateToDeepLink(wallet: Listing, preferUniversal: Bool, preferBrowser: Bool) { + func navigateToDeepLink(wallet: Wallet, preferBrowser: Bool) { do { - let nativeScheme = preferBrowser ? nil : wallet.mobile.native - let universalScheme = preferBrowser ? wallet.desktop.universal : wallet.mobile.universal - + let nativeScheme = preferBrowser ? wallet.webappLink : wallet.mobileLink let nativeUrlString = try formatNativeUrlString(nativeScheme) - let universalUrlString = try formatUniversalUrlString(universalScheme) - if let nativeUrl = nativeUrlString?.toURL(), !preferUniversal { + if let nativeUrl = nativeUrlString?.toURL() { uiApplicationWrapper.openURL(nativeUrl) { success in if !success { self.toast = Toast(style: .error, message: DeeplinkErrors.failedToOpen.localizedDescription) } } - } else if let universalUrl = universalUrlString?.toURL() { - uiApplicationWrapper.openURL(universalUrl) { success in - if !success { - self.toast = Toast(style: .error, message: DeeplinkErrors.failedToOpen.localizedDescription) - } - } } else { throw DeeplinkErrors.noWalletLinkFound } diff --git a/Sources/WalletConnectModal/Modal/RecentWalletStorage.swift b/Sources/WalletConnectModal/Modal/RecentWalletStorage.swift index 00ccd5929..87ca09a09 100644 --- a/Sources/WalletConnectModal/Modal/RecentWalletStorage.swift +++ b/Sources/WalletConnectModal/Modal/RecentWalletStorage.swift @@ -7,7 +7,7 @@ final class RecentWalletsStorage { self.defaults = defaults } - var recentWallets: [Listing] { + var recentWallets: [Wallet] { get { loadRecentWallets() } @@ -16,16 +16,16 @@ final class RecentWalletsStorage { } } - func loadRecentWallets() -> [Listing] { + func loadRecentWallets() -> [Wallet] { guard let data = defaults.data(forKey: "recentWallets"), - let wallets = try? JSONDecoder().decode([Listing].self, from: data) + let wallets = try? JSONDecoder().decode([Wallet].self, from: data) else { return [] } - return wallets.filter { listing in - guard let lastTimeUsed = listing.lastTimeUsed else { + return wallets.filter { wallet in + guard let lastTimeUsed = wallet.lastTimeUsed else { assertionFailure("Shouldn't happen we stored wallet without `lastTimeUsed`") return false } @@ -35,9 +35,9 @@ final class RecentWalletsStorage { } } - func saveRecentWallets(_ listings: [Listing]) { + func saveRecentWallets(_ wallets: [Wallet]) { - let subset = Array(listings.filter { + let subset = Array(wallets.filter { $0.lastTimeUsed != nil }.prefix(5)) diff --git a/Sources/WalletConnectModal/Modal/Screens/GetAWalletView.swift b/Sources/WalletConnectModal/Modal/Screens/GetAWalletView.swift index b5f2c7438..0255cd648 100644 --- a/Sources/WalletConnectModal/Modal/Screens/GetAWalletView.swift +++ b/Sources/WalletConnectModal/Modal/Screens/GetAWalletView.swift @@ -1,8 +1,8 @@ import SwiftUI struct GetAWalletView: View { - let wallets: [Listing] - let onWalletTap: (Listing) -> Void + let wallets: [Wallet] + let onWalletTap: (Wallet) -> Void let navigateToExternalLink: (URL) -> Void var body: some View { @@ -71,7 +71,7 @@ struct GetAWalletView: View { struct GetAWalletView_Previews: PreviewProvider { static var previews: some View { GetAWalletView( - wallets: Listing.stubList, + wallets: Wallet.stubList, onWalletTap: { _ in }, navigateToExternalLink: { _ in } ) diff --git a/Sources/WalletConnectModal/Modal/Screens/WalletDetail/WalletDetailViewModel.swift b/Sources/WalletConnectModal/Modal/Screens/WalletDetail/WalletDetailViewModel.swift index 4b146927c..f1ee61ac5 100644 --- a/Sources/WalletConnectModal/Modal/Screens/WalletDetail/WalletDetailViewModel.swift +++ b/Sources/WalletConnectModal/Modal/Screens/WalletDetail/WalletDetailViewModel.swift @@ -15,49 +15,37 @@ final class WalletDetailViewModel: ObservableObject { case didTapAppStore } - let wallet: Listing + let wallet: Wallet let deeplinkHandler: WalletDeeplinkHandler @Published var preferredPlatform: Platform = .native - var showToggle: Bool { wallet.app.browser != nil && wallet.app.ios != nil } - var showUniversalLink: Bool { preferredPlatform == .native && wallet.mobile.universal?.isEmpty == false } - var hasNativeLink: Bool { wallet.mobile.native?.isEmpty == false } + var showToggle: Bool { wallet.webappLink != nil && wallet.appStore != nil } + var showUniversalLink: Bool { preferredPlatform == .native && wallet.mobileLink?.isEmpty == false } + var hasNativeLink: Bool { wallet.mobileLink?.isEmpty == false } init( - wallet: Listing, + wallet: Wallet, deeplinkHandler: WalletDeeplinkHandler ) { self.wallet = wallet self.deeplinkHandler = deeplinkHandler - preferredPlatform = wallet.app.ios != nil ? .native : .browser + preferredPlatform = wallet.appStore != nil ? .native : .browser } func handle(_ event: Event) { switch event { - case .onAppear: - deeplinkHandler.navigateToDeepLink( - wallet: wallet, - preferUniversal: true, - preferBrowser: preferredPlatform == .browser - ) - - case .didTapUniversalLink: - deeplinkHandler.navigateToDeepLink( - wallet: wallet, - preferUniversal: true, - preferBrowser: preferredPlatform == .browser - ) - - case .didTapTryAgain: - deeplinkHandler.navigateToDeepLink( - wallet: wallet, - preferUniversal: false, - preferBrowser: preferredPlatform == .browser - ) - + case .onAppear, .didTapUniversalLink, .didTapTryAgain: + deeplinkToWallet() case .didTapAppStore: deeplinkHandler.openAppstore(wallet: wallet) } } + + func deeplinkToWallet() { + deeplinkHandler.navigateToDeepLink( + wallet: wallet, + preferBrowser: preferredPlatform == .browser + ) + } } diff --git a/Sources/WalletConnectModal/Modal/Screens/WalletList.swift b/Sources/WalletConnectModal/Modal/Screens/WalletList.swift index 55ba54c2a..96efd6a13 100644 --- a/Sources/WalletConnectModal/Modal/Screens/WalletList.swift +++ b/Sources/WalletConnectModal/Modal/Screens/WalletList.swift @@ -1,16 +1,45 @@ import SwiftUI struct WalletList: View { - @Binding var wallets: [Listing] + @Binding var destination: Destination + @ObservedObject var viewModel: ModalViewModel + var navigateTo: (Destination) -> Void - var onListingTap: (Listing) -> Void + var onWalletTap: (Wallet) -> Void @State var numberOfColumns = 4 - @State var availableSize: CGSize = .zero + init( + destination: Binding, + viewModel: ModalViewModel, + navigateTo: @escaping (Destination) -> Void, + onWalletTap: @escaping (Wallet) -> Void, + numberOfColumns: Int = 4, + availableSize: CGSize = .zero, + infiniteScrollLoading: Bool = false + ) { + self._destination = destination + self.viewModel = viewModel + self.navigateTo = navigateTo + self.onWalletTap = onWalletTap + self.numberOfColumns = numberOfColumns + self.availableSize = availableSize + self.infiniteScrollLoading = infiniteScrollLoading + + if #available(iOS 14.0, *) { + // iOS 14 doesn't have extra separators below the list by default. + } else { + // To remove only extra separators below the list: + UITableView.appearance(whenContainedInInstancesOf: [WalletConnectModalSheetController.self]).tableFooterView = UIView() + } + + // To remove all separators including the actual ones: + UITableView.appearance(whenContainedInInstancesOf: [WalletConnectModalSheetController.self]).separatorStyle = .none + } + var body: some View { ZStack { content() @@ -23,6 +52,7 @@ struct WalletList: View { numberOfColumns = Int(round(size.width / 100)) availableSize = size } + } } @@ -47,16 +77,16 @@ struct WalletList: View { VStack { HStack { - ForEach(wallets.prefix(numberOfColumns)) { wallet in + ForEach(viewModel.filteredWallets.prefix(numberOfColumns)) { wallet in gridItem(for: wallet) } } HStack { - ForEach(wallets.dropFirst(numberOfColumns).prefix(max(numberOfColumns - 1, 0))) { wallet in + ForEach(viewModel.filteredWallets.dropFirst(numberOfColumns).prefix(max(numberOfColumns - 1, 0))) { wallet in gridItem(for: wallet) } - if wallets.count > numberOfColumns * 2 { + if viewModel.filteredWallets.count > numberOfColumns * 2 { viewAllItem() .onTapGestureBackported { withAnimation { @@ -67,32 +97,52 @@ struct WalletList: View { } } - if wallets.isEmpty { + if viewModel.filteredWallets.isEmpty { ActivityIndicator(isAnimating: .constant(true)) } } } + @State var infiniteScrollLoading = false + @ViewBuilder private func viewAll() -> some View { ZStack { Spacer().frame(maxWidth: .infinity, maxHeight: 150) - ScrollView(.vertical) { - VStack(alignment: .leading) { - ForEach(Array(stride(from: 0, to: wallets.count, by: numberOfColumns)), id: \.self) { row in - HStack { - ForEach(row ..< (row + numberOfColumns), id: \.self) { index in - if let wallet = wallets[safe: index] { - gridItem(for: wallet) - } + List { + ForEach(Array(stride(from: 0, to: viewModel.filteredWallets.count, by: numberOfColumns)), id: \.self) { row in + HStack { + ForEach(row ..< (row + numberOfColumns), id: \.self) { index in + if let wallet = viewModel.filteredWallets[safe: index] { + gridItem(for: wallet) } } } } - .padding(.vertical) + .listRowInsets(EdgeInsets(top: 0, leading: 24, bottom: 8, trailing: 24)) + .transform { + if #available(iOS 15.0, *) { + $0.listRowSeparator(.hidden) + } + } + + if viewModel.isThereMoreWallets { + Color.clear.frame(height: 100) + .onAppear { + Task { + await viewModel.fetchWallets() + } + } + .transform { + if #available(iOS 15.0, *) { + $0.listRowSeparator(.hidden) + } + } + } } - + .listStyle(.plain) + LinearGradient( stops: [ .init(color: .background1, location: 0.0), @@ -112,7 +162,7 @@ struct WalletList: View { func viewAllItem() -> some View { VStack { VStack(spacing: 3) { - let viewAllWalletsFirstRow = wallets.dropFirst(2 * numberOfColumns - 1).prefix(2) + let viewAllWalletsFirstRow = viewModel.filteredWallets.dropFirst(2 * numberOfColumns - 1).prefix(2) HStack(spacing: 3) { ForEach(viewAllWalletsFirstRow) { wallet in @@ -123,7 +173,7 @@ struct WalletList: View { } .padding(.horizontal, 5) - let viewAllWalletsSecondRow = wallets.dropFirst(2 * numberOfColumns + 1).prefix(2) + let viewAllWalletsSecondRow = viewModel.filteredWallets.dropFirst(2 * numberOfColumns + 1).prefix(2) HStack(spacing: 3) { ForEach(viewAllWalletsSecondRow) { wallet in @@ -155,7 +205,7 @@ struct WalletList: View { } @ViewBuilder - func gridItem(for wallet: Listing) -> some View { + func gridItem(for wallet: Wallet) -> some View { VStack { WalletImage(wallet: wallet) .frame(width: 60, height: 60) @@ -171,7 +221,7 @@ struct WalletList: View { .multilineTextAlignment(.center) Text(wallet.lastTimeUsed != nil ? "RECENT" : "INSTALLED") - .opacity(wallet.lastTimeUsed != nil || wallet.installed ? 1 : 0) + .opacity(wallet.lastTimeUsed != nil || wallet.isInstalled ? 1 : 0) .font(.system(size: 10)) .foregroundColor(.foreground3) .padding(.horizontal, 12) @@ -183,7 +233,7 @@ struct WalletList: View { // Small delay to let detail screen present before actually deeplinking DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - onListingTap(wallet) + onWalletTap(wallet) } } } diff --git a/Sources/WalletConnectModal/Networking/Explorer/ExplorerAPI.swift b/Sources/WalletConnectModal/Networking/Explorer/ExplorerAPI.swift deleted file mode 100644 index f52a3db67..000000000 --- a/Sources/WalletConnectModal/Networking/Explorer/ExplorerAPI.swift +++ /dev/null @@ -1,55 +0,0 @@ -import Foundation - -enum ExplorerAPI: HTTPService { - case getListings( - projectId: String, - metadata: AppMetadata, - recommendedIds: [String], - excludedIds: [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 - } - - var queryParameters: [String: String]? { - switch self { - case let .getListings(projectId, _, recommendedIds, excludedIds): - return [ - "projectId": projectId, - "recommendedIds": recommendedIds.joined(separator: ","), - "excludedIds": excludedIds.joined(separator: ","), - "sdkType": "wcm", - "sdkVersion": EnvironmentInfo.sdkName, - ] - .compactMapValues { value in - value.isEmpty ? nil : value - } - } - } - - var scheme: String { - return "https" - } - - var additionalHeaderFields: [String: String]? { - switch self { - case let .getListings(_, metadata, _, _): - return [ - "Referer": metadata.name - ] - } - } -} diff --git a/Sources/WalletConnectModal/Networking/Explorer/GetIosDataResponse.swift b/Sources/WalletConnectModal/Networking/Explorer/GetIosDataResponse.swift new file mode 100644 index 000000000..31445bebd --- /dev/null +++ b/Sources/WalletConnectModal/Networking/Explorer/GetIosDataResponse.swift @@ -0,0 +1,11 @@ +import Foundation + +struct GetIosDataResponse: Codable { + let count: Int + let data: [WalletMetadata] + + struct WalletMetadata: Codable { + let id: String + let ios_schema: String + } +} diff --git a/Sources/WalletConnectModal/Networking/Explorer/GetWalletsResponse.swift b/Sources/WalletConnectModal/Networking/Explorer/GetWalletsResponse.swift new file mode 100644 index 000000000..02d84ed88 --- /dev/null +++ b/Sources/WalletConnectModal/Networking/Explorer/GetWalletsResponse.swift @@ -0,0 +1,87 @@ +import Foundation + +struct GetWalletsResponse: Codable { + let count: Int + let data: [Wallet] +} + +class Wallet: Codable, Identifiable, Hashable { + let id: String + let name: String + let homepage: String + let imageId: String + let order: Int + let mobileLink: String? + let desktopLink: String? + let webappLink: String? + let appStore: String? + + var lastTimeUsed: Date? + var isInstalled: Bool = false + + enum CodingKeys: String, CodingKey { + case id + case name + case homepage + case imageId = "image_id" + case order + case mobileLink = "mobile_link" + case desktopLink = "desktop_link" + case webappLink = "webapp_link" + case appStore = "app_store" + + // Decorated + case lastTimeUsed + case isInstalled + } + + init( + id: String, + name: String, + homepage: String, + imageId: String, + order: Int, + mobileLink: String? = nil, + desktopLink: String? = nil, + webappLink: String? = nil, + appStore: String? = nil, + lastTimeUsed: Date? = nil, + isInstalled: Bool = false + ) { + self.id = id + self.name = name + self.homepage = homepage + self.imageId = imageId + self.order = order + self.mobileLink = mobileLink + self.desktopLink = desktopLink + self.webappLink = webappLink + self.appStore = appStore + self.lastTimeUsed = lastTimeUsed + self.isInstalled = isInstalled + } + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.id = try container.decode(String.self, forKey: .id) + self.name = try container.decode(String.self, forKey: .name) + self.homepage = try container.decode(String.self, forKey: .homepage) + self.imageId = try container.decode(String.self, forKey: .imageId) + self.order = try container.decode(Int.self, forKey: .order) + self.mobileLink = try container.decodeIfPresent(String.self, forKey: .mobileLink) + self.desktopLink = try container.decodeIfPresent(String.self, forKey: .desktopLink) + self.webappLink = try container.decodeIfPresent(String.self, forKey: .webappLink) + self.appStore = try container.decodeIfPresent(String.self, forKey: .appStore) + self.lastTimeUsed = try container.decodeIfPresent(Date.self, forKey: .lastTimeUsed) + self.isInstalled = try container.decodeIfPresent(Bool.self, forKey: .isInstalled) ?? false + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + hasher.combine(name) + } + + static func == (lhs: Wallet, rhs: Wallet) -> Bool { + lhs.id == rhs.id && lhs.name == rhs.name + } +} diff --git a/Sources/WalletConnectModal/Networking/Explorer/ListingsResponse.swift b/Sources/WalletConnectModal/Networking/Explorer/ListingsResponse.swift deleted file mode 100644 index 0ddd4446c..000000000 --- a/Sources/WalletConnectModal/Networking/Explorer/ListingsResponse.swift +++ /dev/null @@ -1,74 +0,0 @@ -import Foundation - -struct ListingsResponse: Codable { - let listings: [String: Listing] -} - -class Listing: Codable, Hashable, Identifiable { - init( - id: String, - name: String, - homepage: String, - order: Int? = nil, - imageId: String, - app: Listing.App, - mobile: Listing.Links, - desktop: Listing.Links, - lastTimeUsed: Date? = nil, - installed: Bool = false - ) { - self.id = id - self.name = name - self.homepage = homepage - self.order = order - self.imageId = imageId - self.app = app - self.mobile = mobile - self.desktop = desktop - self.lastTimeUsed = lastTimeUsed - self.installed = installed - } - - func hash(into hasher: inout Hasher) { - hasher.combine(id) - hasher.combine(name) - } - - static func == (lhs: Listing, rhs: Listing) -> Bool { - lhs.id == rhs.id && lhs.name == rhs.name - } - - let id: String - let name: String - let homepage: String - let order: Int? - let imageId: String - let app: App - let mobile: Links - let desktop: Links - - var lastTimeUsed: Date? - var installed: Bool = false - - private enum CodingKeys: String, CodingKey { - case id - case name - case homepage - case order - case imageId = "image_id" - case app - case mobile - case desktop - case lastTimeUsed - } - - struct App: Codable, Hashable { - let ios: String? - let browser: String? - } - - struct Links: Codable, Hashable { - let native: String? - let universal: String? - } -} diff --git a/Sources/WalletConnectModal/Networking/Explorer/Web3ModalAPI.swift b/Sources/WalletConnectModal/Networking/Explorer/Web3ModalAPI.swift new file mode 100644 index 000000000..e2c63128a --- /dev/null +++ b/Sources/WalletConnectModal/Networking/Explorer/Web3ModalAPI.swift @@ -0,0 +1,84 @@ +import Foundation + +enum Web3ModalAPI: HTTPService { + struct GetWalletsParams { + let page: Int + let entries: Int + let search: String? + let projectId: String + let metadata: AppMetadata + let recommendedIds: [String] + let excludedIds: [String] + } + + struct GetIosDataParams { + let projectId: String + let metadata: AppMetadata + } + + case getWallets(params: GetWalletsParams) + case getIosData(params: GetIosDataParams) + + var path: String { + switch self { + case .getWallets: return "/getWallets" + case .getIosData: return "/getIosData" + } + } + + var method: HTTPMethod { + switch self { + case .getWallets: return .get + case .getIosData: return .get + } + } + + var body: Data? { + nil + } + + var queryParameters: [String: String]? { + switch self { + case let .getWallets(params): + return [ + "page": "\(params.page)", + "entries": "\(params.entries)", + "search": params.search ?? "", + "recommendedIds": params.recommendedIds.joined(separator: ","), + "excludedIds": params.excludedIds.joined(separator: ","), + "platform": "ios", + ] + .compactMapValues { value in + value.isEmpty ? nil : value + } + case let .getIosData(params): + return [ + "projectId": params.projectId, + "metadata": params.metadata.name + ] + } + } + + var scheme: String { + return "https" + } + + var additionalHeaderFields: [String: String]? { + switch self { + case let .getWallets(params): + return [ + "x-project-id": params.projectId, + "x-sdk-version": WalletConnectModal.Config.sdkVersion, + "x-sdk-type": WalletConnectModal.Config.sdkType, + "Referer": params.metadata.name + ] + case let .getIosData(params): + return [ + "x-project-id": params.projectId, + "x-sdk-version": WalletConnectModal.Config.sdkVersion, + "x-sdk-type": WalletConnectModal.Config.sdkType, + "Referer": params.metadata.name + ] + } + } +} diff --git a/Sources/WalletConnectModal/UI/WalletImage.swift b/Sources/WalletConnectModal/UI/WalletImage.swift index cd70dae0a..9b142eab0 100644 --- a/Sources/WalletConnectModal/UI/WalletImage.swift +++ b/Sources/WalletConnectModal/UI/WalletImage.swift @@ -10,7 +10,7 @@ struct WalletImage: View { @Environment(\.projectId) var projectId - var wallet: Listing? + var wallet: Wallet? var size: Size = .medium var body: some View { @@ -24,7 +24,7 @@ struct WalletImage: View { } } - private func imageURL(for wallet: Listing?) -> URL? { + private func imageURL(for wallet: Wallet?) -> URL? { guard let wallet else { return nil } diff --git a/Sources/WalletConnectModal/WalletConnectModal.swift b/Sources/WalletConnectModal/WalletConnectModal.swift index 87085fcf5..c74e5c884 100644 --- a/Sources/WalletConnectModal/WalletConnectModal.swift +++ b/Sources/WalletConnectModal/WalletConnectModal.swift @@ -34,6 +34,9 @@ public class WalletConnectModal { }() struct Config { + static let sdkVersion: String = "swift-\(EnvironmentInfo.packageVersion)" + static let sdkType = "wcm" + let projectId: String var metadata: AppMetadata var sessionParams: SessionParams diff --git a/Sources/WalletConnectNetworking/NetworkInteracting.swift b/Sources/WalletConnectNetworking/NetworkInteracting.swift index 6bad90646..a79814399 100644 --- a/Sources/WalletConnectNetworking/NetworkInteracting.swift +++ b/Sources/WalletConnectNetworking/NetworkInteracting.swift @@ -42,6 +42,15 @@ public protocol NetworkInteracting { subscription: @escaping (ResponseSubscriptionPayload) async throws -> Void ) + func awaitResponse( + request: RPCRequest, + topic: String, + method: ProtocolMethod, + requestOfType: Request.Type, + responseOfType: Response.Type, + envelopeType: Envelope.EnvelopeType + ) async throws -> Response + func getClientId() throws -> String } diff --git a/Sources/WalletConnectNetworking/NetworkingInteractor.swift b/Sources/WalletConnectNetworking/NetworkingInteractor.swift index f311cdc5b..d086b8db8 100644 --- a/Sources/WalletConnectNetworking/NetworkingInteractor.swift +++ b/Sources/WalletConnectNetworking/NetworkingInteractor.swift @@ -29,8 +29,6 @@ public class NetworkingInteractor: NetworkInteracting { public var networkConnectionStatusPublisher: AnyPublisher public var socketConnectionStatusPublisher: AnyPublisher - - private let networkMonitor: NetworkMonitoring public init( relayClient: RelayClient, @@ -43,8 +41,7 @@ public class NetworkingInteractor: NetworkInteracting { self.rpcHistory = rpcHistory self.logger = logger self.socketConnectionStatusPublisher = relayClient.socketConnectionStatusPublisher - self.networkMonitor = NetworkMonitor() - self.networkConnectionStatusPublisher = networkMonitor.networkConnectionStatusPublisher + self.networkConnectionStatusPublisher = relayClient.networkConnectionStatusPublisher setupRelaySubscribtion() } @@ -139,6 +136,45 @@ public class NetworkingInteractor: NetworkInteracting { .eraseToAnyPublisher() } + public func awaitResponse( + request: RPCRequest, + topic: String, + method: ProtocolMethod, + requestOfType: Request.Type, + responseOfType: Response.Type, + envelopeType: Envelope.EnvelopeType + ) async throws -> Response { + return try await withCheckedThrowingContinuation { [unowned self] continuation in + var response, error: AnyCancellable? + + let cancel: () -> Void = { + response?.cancel() + error?.cancel() + } + + response = responseSubscription(on: method) + .sink { (payload: ResponseSubscriptionPayload) in + cancel() + continuation.resume(with: .success(payload.response)) + } + + error = responseErrorSubscription(on: method) + .sink { (payload: ResponseSubscriptionErrorPayload) in + cancel() + continuation.resume(throwing: payload.error) + } + + Task(priority: .high) { + do { + try await self.request(request, topic: topic, protocolMethod: method, envelopeType: envelopeType) + } catch { + cancel() + continuation.resume(throwing: error) + } + } + } + } + public func responseSubscription(on request: ProtocolMethod) -> AnyPublisher, Never> { return responsePublisher .filter { rpcRequest in @@ -166,14 +202,28 @@ public class NetworkingInteractor: NetworkInteracting { public func request(_ request: RPCRequest, topic: String, protocolMethod: ProtocolMethod, envelopeType: Envelope.EnvelopeType) async throws { try rpcHistory.set(request, forTopic: topic, emmitedBy: .local) - let message = try serializer.serialize(topic: topic, encodable: request, envelopeType: envelopeType) - try await relayClient.publish(topic: topic, payload: message, tag: protocolMethod.requestConfig.tag, prompt: protocolMethod.requestConfig.prompt, ttl: protocolMethod.requestConfig.ttl) + + do { + let message = try serializer.serialize(topic: topic, encodable: request, envelopeType: envelopeType) + + try await relayClient.publish(topic: topic, + payload: message, + tag: protocolMethod.requestConfig.tag, + prompt: protocolMethod.requestConfig.prompt, + ttl: protocolMethod.requestConfig.ttl) + } catch { + if let id = request.id { + rpcHistory.delete(id: id) + } + throw error + } } public func respond(topic: String, response: RPCResponse, protocolMethod: ProtocolMethod, envelopeType: Envelope.EnvelopeType) async throws { - try rpcHistory.resolve(response) + try rpcHistory.validate(response) let message = try serializer.serialize(topic: topic, encodable: response, envelopeType: envelopeType) try await relayClient.publish(topic: topic, payload: message, tag: protocolMethod.responseConfig.tag, prompt: protocolMethod.responseConfig.prompt, ttl: protocolMethod.responseConfig.ttl) + try rpcHistory.resolve(response) } public func respondSuccess(topic: String, requestId: RPCID, protocolMethod: ProtocolMethod, envelopeType: Envelope.EnvelopeType) async throws { @@ -216,8 +266,7 @@ public class NetworkingInteractor: NetworkInteracting { private func handleResponse(topic: String, response: RPCResponse, publishedAt: Date, derivedTopic: String?) { do { - try rpcHistory.resolve(response) - let record = rpcHistory.get(recordId: response.id!)! + let record = try rpcHistory.resolve(response) responsePublisherSubject.send((topic, record.request, response, publishedAt, derivedTopic)) } catch { logger.debug("Handle json rpc response error: \(error)") diff --git a/Sources/WalletConnectNotify/Client/Wallet/HistoryService.swift b/Sources/WalletConnectNotify/Client/Wallet/HistoryService.swift new file mode 100644 index 000000000..7cece784e --- /dev/null +++ b/Sources/WalletConnectNotify/Client/Wallet/HistoryService.swift @@ -0,0 +1,46 @@ +import Foundation + +public final class HistoryService { + + private let keyserver: URL + private let networkingClient: NetworkInteracting + private let identityClient: IdentityClient + + init(keyserver: URL, networkingClient: NetworkInteracting, identityClient: IdentityClient) { + self.keyserver = keyserver + self.networkingClient = networkingClient + self.identityClient = identityClient + } + + public func fetchHistory(account: Account, topic: String, appAuthenticationKey: String, host: String, after: String?, limit: Int) async throws -> [NotifyMessage] { + let dappAuthKey = try DIDKey(did: appAuthenticationKey) + let app = DIDWeb(host: host) + + let requestPayload = NotifyGetNotificationsRequestPayload( + account: account, + keyserver: keyserver.absoluteString, + dappAuthKey: dappAuthKey, + app: app, + limit: UInt64(limit), + after: after + ) + + let wrapper = try identityClient.signAndCreateWrapper(payload: requestPayload, account: account) + + let protocolMethod = NotifyGetNotificationsProtocolMethod() + let request = RPCRequest(method: protocolMethod.method, params: wrapper) + + let response = try await networkingClient.awaitResponse( + request: request, + topic: topic, + method: protocolMethod, + requestOfType: NotifyGetNotificationsRequestPayload.Wrapper.self, + responseOfType: NotifyGetNotificationsResponsePayload.Wrapper.self, + envelopeType: .type0 + ) + + let (responsePayload, _) = try NotifyGetNotificationsResponsePayload.decodeAndVerify(from: response) + + return responsePayload.messages + } +} diff --git a/Sources/WalletConnectNotify/Client/Wallet/NotifyClient.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifyClient.swift index ba30e6956..6b5aa22fc 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/NotifyClient.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifyClient.swift @@ -17,7 +17,7 @@ public class NotifyClient { return logger.logsPublisher } - private let deleteNotifySubscriptionRequester: DeleteNotifySubscriptionRequester + private let notifyDeleteSubscriptionRequester: NotifyDeleteSubscriptionRequester private let notifySubscribeRequester: NotifySubscribeRequester public let logger: ConsoleLogging @@ -25,30 +25,35 @@ public class NotifyClient { private let keyserverURL: URL private let pushClient: PushClient private let identityClient: IdentityClient + private let historyService: HistoryService private let notifyStorage: NotifyStorage private let notifyAccountProvider: NotifyAccountProvider private let notifyMessageSubscriber: NotifyMessageSubscriber private let resubscribeService: NotifyResubscribeService private let notifySubscribeResponseSubscriber: NotifySubscribeResponseSubscriber + private let notifyDeleteSubscriptionSubscriber: NotifyDeleteSubscriptionSubscriber private let notifyUpdateRequester: NotifyUpdateRequester private let notifyUpdateResponseSubscriber: NotifyUpdateResponseSubscriber private let subscriptionsAutoUpdater: SubscriptionsAutoUpdater private let notifyWatchSubscriptionsResponseSubscriber: NotifyWatchSubscriptionsResponseSubscriber private let notifyWatcherAgreementKeysProvider: NotifyWatcherAgreementKeysProvider private let notifySubscriptionsChangedRequestSubscriber: NotifySubscriptionsChangedRequestSubscriber + private let notifySubscriptionsUpdater: NotifySubsctiptionsUpdater private let subscriptionWatcher: SubscriptionWatcher init(logger: ConsoleLogging, keyserverURL: URL, kms: KeyManagementServiceProtocol, identityClient: IdentityClient, + historyService: HistoryService, pushClient: PushClient, notifyMessageSubscriber: NotifyMessageSubscriber, notifyStorage: NotifyStorage, - deleteNotifySubscriptionRequester: DeleteNotifySubscriptionRequester, + notifyDeleteSubscriptionRequester: NotifyDeleteSubscriptionRequester, resubscribeService: NotifyResubscribeService, notifySubscribeRequester: NotifySubscribeRequester, notifySubscribeResponseSubscriber: NotifySubscribeResponseSubscriber, + notifyDeleteSubscriptionSubscriber: NotifyDeleteSubscriptionSubscriber, notifyUpdateRequester: NotifyUpdateRequester, notifyUpdateResponseSubscriber: NotifyUpdateResponseSubscriber, notifyAccountProvider: NotifyAccountProvider, @@ -56,18 +61,21 @@ public class NotifyClient { notifyWatchSubscriptionsResponseSubscriber: NotifyWatchSubscriptionsResponseSubscriber, notifyWatcherAgreementKeysProvider: NotifyWatcherAgreementKeysProvider, notifySubscriptionsChangedRequestSubscriber: NotifySubscriptionsChangedRequestSubscriber, + notifySubscriptionsUpdater: NotifySubsctiptionsUpdater, subscriptionWatcher: SubscriptionWatcher ) { self.logger = logger self.keyserverURL = keyserverURL self.pushClient = pushClient self.identityClient = identityClient + self.historyService = historyService self.notifyMessageSubscriber = notifyMessageSubscriber self.notifyStorage = notifyStorage - self.deleteNotifySubscriptionRequester = deleteNotifySubscriptionRequester + self.notifyDeleteSubscriptionRequester = notifyDeleteSubscriptionRequester self.resubscribeService = resubscribeService self.notifySubscribeRequester = notifySubscribeRequester self.notifySubscribeResponseSubscriber = notifySubscribeResponseSubscriber + self.notifyDeleteSubscriptionSubscriber = notifyDeleteSubscriptionSubscriber self.notifyUpdateRequester = notifyUpdateRequester self.notifyUpdateResponseSubscriber = notifyUpdateResponseSubscriber self.notifyAccountProvider = notifyAccountProvider @@ -75,6 +83,7 @@ public class NotifyClient { self.notifyWatchSubscriptionsResponseSubscriber = notifyWatchSubscriptionsResponseSubscriber self.notifyWatcherAgreementKeysProvider = notifyWatcherAgreementKeysProvider self.notifySubscriptionsChangedRequestSubscriber = notifySubscriptionsChangedRequestSubscriber + self.notifySubscriptionsUpdater = notifySubscriptionsUpdater self.subscriptionWatcher = subscriptionWatcher } @@ -122,7 +131,7 @@ public class NotifyClient { } public func deleteSubscription(topic: String) async throws { - try await deleteNotifySubscriptionRequester.delete(topic: topic) + try await notifyDeleteSubscriptionRequester.delete(topic: topic) } public func deleteNotifyMessage(id: String) { @@ -144,6 +153,25 @@ public class NotifyClient { public func messagesPublisher(topic: String) -> AnyPublisher<[NotifyMessageRecord], Never> { return notifyStorage.messagesPublisher(topic: topic) } + + public func fetchHistory(subscription: NotifySubscription, after: String?, limit: Int) async throws -> Bool { + let messages = try await historyService.fetchHistory( + account: subscription.account, + topic: subscription.topic, + appAuthenticationKey: subscription.appAuthenticationKey, + host: subscription.metadata.url, + after: after, + limit: limit + ) + + let records = messages.map { message in + return NotifyMessageRecord(topic: subscription.topic, message: message, publishedAt: message.sentAt) + } + + try notifyStorage.setMessages(records) + + return messages.count == limit + } } private extension NotifyClient { @@ -162,7 +190,7 @@ private extension NotifyClient { extension NotifyClient { public var subscriptionChangedPublisher: AnyPublisher<[NotifySubscription], Never> { - return notifySubscriptionsChangedRequestSubscriber.subscriptionChangedPublisher + return notifySubscriptionsUpdater.subscriptionChangedPublisher } public func register(deviceToken: String) async throws { diff --git a/Sources/WalletConnectNotify/Client/Wallet/NotifyClientFactory.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifyClientFactory.swift index 3a08a5727..c18b8749f 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/NotifyClientFactory.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifyClientFactory.swift @@ -2,13 +2,12 @@ import Foundation public struct NotifyClientFactory { - public static func create(projectId: String, groupIdentifier: String, networkInteractor: NetworkInteracting, pairingRegisterer: PairingRegisterer, pushClient: PushClient, crypto: CryptoProvider, notifyHost: String, explorerHost: String) -> NotifyClient { + public static func create(projectId: String, groupIdentifier: String, networkInteractor: NetworkInteracting, pushClient: PushClient, crypto: CryptoProvider, notifyHost: String, explorerHost: String) -> NotifyClient { let logger = ConsoleLogger(prefix: "🔔",loggingLevel: .debug) let keyserverURL = URL(string: "https://keys.walletconnect.com")! let keychainStorage = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk", accessGroup: groupIdentifier) let groupKeychainService = GroupKeychainStorage(serviceIdentifier: groupIdentifier) - let databasePath = databasePath(appGroup: groupIdentifier, database: "notify_v\(version).db") - let sqlite = DiskSqlite(path: databasePath) + let sqlite = NotifySqliteFactory.create(appGroup: groupIdentifier) return NotifyClientFactory.create( projectId: projectId, @@ -18,7 +17,6 @@ public struct NotifyClientFactory { keychainStorage: keychainStorage, groupKeychainStorage: groupKeychainService, networkInteractor: networkInteractor, - pairingRegisterer: pairingRegisterer, pushClient: pushClient, crypto: crypto, notifyHost: notifyHost, @@ -34,7 +32,6 @@ public struct NotifyClientFactory { keychainStorage: KeychainStorageProtocol, groupKeychainStorage: KeychainStorageProtocol, networkInteractor: NetworkInteracting, - pairingRegisterer: PairingRegisterer, pushClient: PushClient, crypto: CryptoProvider, notifyHost: String, @@ -47,63 +44,56 @@ public struct NotifyClientFactory { let identityClient = IdentityClientFactory.create(keyserver: keyserverURL, keychain: keychainStorage, logger: logger) let notifyMessageSubscriber = NotifyMessageSubscriber(keyserver: keyserverURL, networkingInteractor: networkInteractor, identityClient: identityClient, notifyStorage: notifyStorage, crypto: crypto, logger: logger) let webDidResolver = NotifyWebDidResolver() - let deleteNotifySubscriptionRequester = DeleteNotifySubscriptionRequester(keyserver: keyserverURL, networkingInteractor: networkInteractor, identityClient: identityClient, kms: kms, logger: logger, notifyStorage: notifyStorage) + let notifyDeleteSubscriptionRequester = NotifyDeleteSubscriptionRequester(keyserver: keyserverURL, networkingInteractor: networkInteractor, identityClient: identityClient, logger: logger, notifyStorage: notifyStorage) let resubscribeService = NotifyResubscribeService(networkInteractor: networkInteractor, notifyStorage: notifyStorage, logger: logger) let notifyConfigProvider = NotifyConfigProvider(projectId: projectId, explorerHost: explorerHost) let notifySubscribeRequester = NotifySubscribeRequester(keyserverURL: keyserverURL, networkingInteractor: networkInteractor, identityClient: identityClient, logger: logger, kms: kms, webDidResolver: webDidResolver, notifyConfigProvider: notifyConfigProvider) - let notifySubscribeResponseSubscriber = NotifySubscribeResponseSubscriber(networkingInteractor: networkInteractor, kms: kms, logger: logger, groupKeychainStorage: groupKeychainStorage, notifyStorage: notifyStorage, notifyConfigProvider: notifyConfigProvider) + let notifySubscriptionsUpdater = NotifySubsctiptionsUpdater(networkingInteractor: networkInteractor, kms: kms, logger: logger, notifyStorage: notifyStorage, groupKeychainStorage: groupKeychainStorage) + + let notifySubscriptionsBuilder = NotifySubscriptionsBuilder(notifyConfigProvider: notifyConfigProvider) + + let notifySubscribeResponseSubscriber = NotifySubscribeResponseSubscriber(networkingInteractor: networkInteractor, logger: logger, notifySubscriptionsBuilder: notifySubscriptionsBuilder, notifySubscriptionsUpdater: notifySubscriptionsUpdater) let notifyUpdateRequester = NotifyUpdateRequester(keyserverURL: keyserverURL, identityClient: identityClient, networkingInteractor: networkInteractor, notifyConfigProvider: notifyConfigProvider, logger: logger, notifyStorage: notifyStorage) - let notifyUpdateResponseSubscriber = NotifyUpdateResponseSubscriber(networkingInteractor: networkInteractor, logger: logger, notifyConfigProvider: notifyConfigProvider, notifyStorage: notifyStorage) + let notifyUpdateResponseSubscriber = NotifyUpdateResponseSubscriber(networkingInteractor: networkInteractor, logger: logger, notifySubscriptionsBuilder: notifySubscriptionsBuilder, notifySubscriptionsUpdater: notifySubscriptionsUpdater) let subscriptionsAutoUpdater = SubscriptionsAutoUpdater(notifyUpdateRequester: notifyUpdateRequester, logger: logger, notifyStorage: notifyStorage) let notifyWatcherAgreementKeysProvider = NotifyWatcherAgreementKeysProvider(kms: kms) let notifyWatchSubscriptionsRequester = NotifyWatchSubscriptionsRequester(keyserverURL: keyserverURL, networkingInteractor: networkInteractor, identityClient: identityClient, logger: logger, webDidResolver: webDidResolver, notifyAccountProvider: notifyAccountProvider, notifyWatcherAgreementKeysProvider: notifyWatcherAgreementKeysProvider, notifyHost: notifyHost) - let notifySubscriptionsBuilder = NotifySubscriptionsBuilder(notifyConfigProvider: notifyConfigProvider) - let notifyWatchSubscriptionsResponseSubscriber = NotifyWatchSubscriptionsResponseSubscriber(networkingInteractor: networkInteractor, kms: kms, logger: logger, notifyStorage: notifyStorage, groupKeychainStorage: groupKeychainStorage, notifySubscriptionsBuilder: notifySubscriptionsBuilder) - let notifySubscriptionsChangedRequestSubscriber = NotifySubscriptionsChangedRequestSubscriber(keyserver: keyserverURL, networkingInteractor: networkInteractor, kms: kms, identityClient: identityClient, logger: logger, groupKeychainStorage: groupKeychainStorage, notifyStorage: notifyStorage, notifySubscriptionsBuilder: notifySubscriptionsBuilder) + let notifyWatchSubscriptionsResponseSubscriber = NotifyWatchSubscriptionsResponseSubscriber(networkingInteractor: networkInteractor, logger: logger, notifySubscriptionsBuilder: notifySubscriptionsBuilder, notifySubscriptionsUpdater: notifySubscriptionsUpdater) + let notifySubscriptionsChangedRequestSubscriber = NotifySubscriptionsChangedRequestSubscriber(keyserver: keyserverURL, networkingInteractor: networkInteractor, identityClient: identityClient, logger: logger, notifySubscriptionsUpdater: notifySubscriptionsUpdater, notifySubscriptionsBuilder: notifySubscriptionsBuilder) let subscriptionWatcher = SubscriptionWatcher(notifyWatchSubscriptionsRequester: notifyWatchSubscriptionsRequester, logger: logger) + let historyService = HistoryService(keyserver: keyserverURL, networkingClient: networkInteractor, identityClient: identityClient) + let notifyDeleteSubscriptionSubscriber = NotifyDeleteSubscriptionSubscriber(networkingInteractor: networkInteractor, kms: kms, logger: logger, notifySubscriptionsBuilder: notifySubscriptionsBuilder, notifySubscriptionsUpdater: notifySubscriptionsUpdater) return NotifyClient( logger: logger, keyserverURL: keyserverURL, kms: kms, - identityClient: identityClient, + identityClient: identityClient, + historyService: historyService, pushClient: pushClient, notifyMessageSubscriber: notifyMessageSubscriber, notifyStorage: notifyStorage, - deleteNotifySubscriptionRequester: deleteNotifySubscriptionRequester, + notifyDeleteSubscriptionRequester: notifyDeleteSubscriptionRequester, resubscribeService: resubscribeService, notifySubscribeRequester: notifySubscribeRequester, - notifySubscribeResponseSubscriber: notifySubscribeResponseSubscriber, + notifySubscribeResponseSubscriber: notifySubscribeResponseSubscriber, + notifyDeleteSubscriptionSubscriber: notifyDeleteSubscriptionSubscriber, notifyUpdateRequester: notifyUpdateRequester, notifyUpdateResponseSubscriber: notifyUpdateResponseSubscriber, notifyAccountProvider: notifyAccountProvider, subscriptionsAutoUpdater: subscriptionsAutoUpdater, notifyWatchSubscriptionsResponseSubscriber: notifyWatchSubscriptionsResponseSubscriber, notifyWatcherAgreementKeysProvider: notifyWatcherAgreementKeysProvider, - notifySubscriptionsChangedRequestSubscriber: notifySubscriptionsChangedRequestSubscriber, + notifySubscriptionsChangedRequestSubscriber: notifySubscriptionsChangedRequestSubscriber, + notifySubscriptionsUpdater: notifySubscriptionsUpdater, subscriptionWatcher: subscriptionWatcher ) } - - static func databasePath(appGroup: String, database: String) -> String { - guard let path = FileManager.default - .containerURL(forSecurityApplicationGroupIdentifier: appGroup)? - .appendingPathComponent(database) else { - - fatalError("Database path not exists") - } - - return path.absoluteString - } - - static var version: String { - return "1" - } } diff --git a/Sources/WalletConnectNotify/Client/Wallet/NotifyDecryptionService.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifyDecryptionService.swift index 272a37cea..e53b53042 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/NotifyDecryptionService.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifyDecryptionService.swift @@ -3,29 +3,36 @@ import Foundation public class NotifyDecryptionService { enum Errors: Error { case malformedNotifyMessage + case subsctiptionNotFound } private let serializer: Serializing + private let database: NotifyDatabase private static let notifyTags: [UInt] = [4002] - init(serializer: Serializing) { + init(serializer: Serializing, database: NotifyDatabase) { self.serializer = serializer + self.database = database } public init(groupIdentifier: String) { let keychainStorage = GroupKeychainStorage(serviceIdentifier: groupIdentifier) let kms = KeyManagementService(keychain: keychainStorage) - self.serializer = Serializer(kms: kms, logger: ConsoleLogger(prefix: "🔐", loggingLevel: .off)) + let logger = ConsoleLogger(prefix: "🔐", loggingLevel: .off) + let sqlite = NotifySqliteFactory.create(appGroup: groupIdentifier) + self.serializer = Serializer(kms: kms, logger: logger) + self.database = NotifyDatabase(sqlite: sqlite, logger: logger) } public static func canHandle(tag: UInt) -> Bool { return notifyTags.contains(tag) } - public func decryptMessage(topic: String, ciphertext: String) throws -> (NotifyMessage, Account) { + public func decryptMessage(topic: String, ciphertext: String) throws -> (NotifyMessage, NotifySubscription, Account) { let (rpcRequest, _, _): (RPCRequest, String?, Data) = try serializer.deserialize(topic: topic, encodedEnvelope: ciphertext) guard let params = rpcRequest.params else { throw Errors.malformedNotifyMessage } let wrapper = try params.get(NotifyMessagePayload.Wrapper.self) let (messagePayload, _) = try NotifyMessagePayload.decodeAndVerify(from: wrapper) - return (messagePayload.message, messagePayload.account) + guard let subscription = database.getSubscription(topic: topic) else { throw Errors.subsctiptionNotFound } + return (messagePayload.message, subscription, messagePayload.account) } } diff --git a/Sources/WalletConnectNotify/Client/Wallet/NotifyImageUrls.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifyImageUrls.swift index 124dc1def..c5963bae2 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/NotifyImageUrls.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifyImageUrls.swift @@ -1,7 +1,21 @@ import Foundation public struct NotifyImageUrls: Codable, Equatable { + public let sm: String? public let md: String? public let lg: String? + + public init(sm: String? = nil, md: String? = nil, lg: String? = nil) { + self.sm = sm + self.md = md + self.lg = lg + } + + public init?(icons: [String]) { + guard icons.count == 3 else { return nil } + self.sm = icons[0] + self.md = icons[1] + self.lg = icons[2] + } } diff --git a/Sources/WalletConnectNotify/Client/Wallet/NotifyMessageRecord.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifyMessageRecord.swift index 9fc7b1c2b..97b9a3567 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/NotifyMessageRecord.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifyMessageRecord.swift @@ -18,16 +18,19 @@ public struct NotifyMessageRecord: Codable, Equatable, SqliteRow { public init(decoder: SqliteRowDecoder) throws { self.topic = try decoder.decodeString(at: 1) + let sentAt = try decoder.decodeDate(at: 7) + self.message = NotifyMessage( id: try decoder.decodeString(at: 0), title: try decoder.decodeString(at: 2), body: try decoder.decodeString(at: 3), icon: try decoder.decodeString(at: 4), url: try decoder.decodeString(at: 5), - type: try decoder.decodeString(at: 6) + type: try decoder.decodeString(at: 6), + sentAt: sentAt ) - self.publishedAt = try decoder.decodeDate(at: 7) + self.publishedAt = sentAt } public func encode() -> SqliteRowEncoder { diff --git a/Sources/WalletConnectNotify/Client/Wallet/NotifySqliteFactory.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifySqliteFactory.swift new file mode 100644 index 000000000..cd1b0dd59 --- /dev/null +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifySqliteFactory.swift @@ -0,0 +1,25 @@ +import Foundation + +struct NotifySqliteFactory { + + static func create(appGroup: String) -> Sqlite { + let databasePath = databasePath(appGroup: appGroup, database: "notify_v\(version).db") + let sqlite = DiskSqlite(path: databasePath) + return sqlite + } + + static func databasePath(appGroup: String, database: String) -> String { + guard let path = FileManager.default + .containerURL(forSecurityApplicationGroupIdentifier: appGroup)? + .appendingPathComponent(database) else { + + fatalError("Database path not exists") + } + + return path.absoluteString + } + + static var version: String { + return "1" + } +} diff --git a/Sources/WalletConnectNotify/Client/Wallet/NotifyStorage.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifyStorage.swift index cca2c3e6a..e26667ec6 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/NotifyStorage.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifyStorage.swift @@ -122,6 +122,10 @@ final class NotifyStorage: NotifyStoring { func setMessage(_ message: NotifyMessageRecord) throws { try database.save(message: message) } + + func setMessages(_ messages: [NotifyMessageRecord]) throws { + try database.save(messages: messages) + } } private extension NotifyStorage { diff --git a/Sources/WalletConnectNotify/Client/Wallet/NotifySubsctiptionsUpdater.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifySubsctiptionsUpdater.swift new file mode 100644 index 000000000..6b48492d0 --- /dev/null +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifySubsctiptionsUpdater.swift @@ -0,0 +1,60 @@ +import Foundation +import Combine + +final class NotifySubsctiptionsUpdater { + private let networkingInteractor: NetworkInteracting + private let kms: KeyManagementServiceProtocol + private let logger: ConsoleLogging + private let notifyStorage: NotifyStorage + private let groupKeychainStorage: KeychainStorageProtocol + + private let subscriptionChangedSubject = PassthroughSubject<[NotifySubscription], Never>() + + var subscriptionChangedPublisher: AnyPublisher<[NotifySubscription], Never> { + return subscriptionChangedSubject.eraseToAnyPublisher() + } + + init(networkingInteractor: NetworkInteracting, kms: KeyManagementServiceProtocol, logger: ConsoleLogging, notifyStorage: NotifyStorage, groupKeychainStorage: KeychainStorageProtocol) { + self.networkingInteractor = networkingInteractor + self.kms = kms + self.logger = logger + self.notifyStorage = notifyStorage + self.groupKeychainStorage = groupKeychainStorage + } + + func update(subscriptions newSubscriptions: [NotifySubscription], for account: Account) async throws { + let oldSubscriptions = notifyStorage.getSubscriptions(account: account) + + subscriptionChangedSubject.send(newSubscriptions) + + try Task.checkCancellation() + + let subscriptions = oldSubscriptions.difference(from: newSubscriptions) + + logger.debug("Received: \(newSubscriptions.count), changed: \(subscriptions.count)") + + if subscriptions.count > 0 { + try notifyStorage.replaceAllSubscriptions(newSubscriptions) + + for subscription in newSubscriptions { + let symKey = try SymmetricKey(hex: subscription.symKey) + try groupKeychainStorage.add(symKey, forKey: subscription.topic) + try kms.setSymmetricKey(symKey, for: subscription.topic) + } + + let topicsToSubscribe = newSubscriptions.map { $0.topic } + + let oldTopics = Set(oldSubscriptions.map { $0.topic }) + let topicsToUnsubscribe = Array(oldTopics.subtracting(topicsToSubscribe)) + + try await networkingInteractor.batchUnsubscribe(topics: topicsToUnsubscribe) + try await networkingInteractor.batchSubscribe(topics: topicsToSubscribe) + + try Task.checkCancellation() + + logger.debug("Updated Subscriptions by Subscriptions Changed Request", properties: [ + "topics": newSubscriptions.map { $0.topic }.joined(separator: ",") + ]) + } + } +} diff --git a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyDelete/DeleteNotifySubscriptionRequester.swift b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyDelete/NotifyDeleteSubscriptionRequester.swift similarity index 84% rename from Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyDelete/DeleteNotifySubscriptionRequester.swift rename to Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyDelete/NotifyDeleteSubscriptionRequester.swift index b9c86009c..762f4948d 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyDelete/DeleteNotifySubscriptionRequester.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyDelete/NotifyDeleteSubscriptionRequester.swift @@ -1,13 +1,12 @@ import Foundation -class DeleteNotifySubscriptionRequester { +class NotifyDeleteSubscriptionRequester { enum Errors: Error { case notifySubscriptionNotFound } private let keyserver: URL private let networkingInteractor: NetworkInteracting private let identityClient: IdentityClient - private let kms: KeyManagementServiceProtocol private let logger: ConsoleLogging private let notifyStorage: NotifyStorage @@ -15,14 +14,12 @@ class DeleteNotifySubscriptionRequester { keyserver: URL, networkingInteractor: NetworkInteracting, identityClient: IdentityClient, - kms: KeyManagementServiceProtocol, logger: ConsoleLogging, notifyStorage: NotifyStorage ) { self.keyserver = keyserver self.networkingInteractor = networkingInteractor self.identityClient = identityClient - self.kms = kms self.logger = logger self.notifyStorage = notifyStorage } @@ -49,15 +46,11 @@ class DeleteNotifySubscriptionRequester { try notifyStorage.deleteSubscription(topic: topic) try notifyStorage.deleteMessages(topic: topic) - networkingInteractor.unsubscribe(topic: topic) - - logger.debug("Subscription removed, topic: \(topic)") - - kms.deleteSymmetricKey(for: topic) + logger.debug("Subscription delete request sent, topic: \(topic)") } } -private extension DeleteNotifySubscriptionRequester { +private extension NotifyDeleteSubscriptionRequester { func createJWTWrapper(dappPubKey: DIDKey, reason: String, app: DIDWeb, account: Account) throws -> NotifyDeletePayload.Wrapper { let jwtPayload = NotifyDeletePayload(account: account, keyserver: keyserver, dappPubKey: dappPubKey, app: app) diff --git a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyDelete/NotifyDeleteSubscriptionSubscriber.swift b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyDelete/NotifyDeleteSubscriptionSubscriber.swift new file mode 100644 index 000000000..3768b6697 --- /dev/null +++ b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyDelete/NotifyDeleteSubscriptionSubscriber.swift @@ -0,0 +1,51 @@ +import Foundation +import Combine + +class NotifyDeleteSubscriptionSubscriber { + + private let networkingInteractor: NetworkInteracting + private let kms: KeyManagementServiceProtocol + private let logger: ConsoleLogging + private let notifySubscriptionsBuilder: NotifySubscriptionsBuilder + private let notifySubscriptionsUpdater: NotifySubsctiptionsUpdater + + init( + networkingInteractor: NetworkInteracting, + kms: KeyManagementServiceProtocol, + logger: ConsoleLogging, + notifySubscriptionsBuilder: NotifySubscriptionsBuilder, + notifySubscriptionsUpdater: NotifySubsctiptionsUpdater + ) { + self.networkingInteractor = networkingInteractor + self.kms = kms + self.logger = logger + self.notifySubscriptionsBuilder = notifySubscriptionsBuilder + self.notifySubscriptionsUpdater = notifySubscriptionsUpdater + + subscribeForDeleteResponse() + } +} + +private extension NotifyDeleteSubscriptionSubscriber { + + func subscribeForDeleteResponse() { + networkingInteractor.subscribeOnResponse( + protocolMethod: NotifyDeleteProtocolMethod(), + requestOfType: NotifyDeletePayload.Wrapper.self, + responseOfType: NotifyDeleteResponsePayload.Wrapper.self, + errorHandler: logger + ) { [unowned self] payload in + + let (responsePayload, _) = try NotifyDeleteResponsePayload.decodeAndVerify(from: payload.response) + + let subscriptions = try await notifySubscriptionsBuilder.buildSubscriptions(responsePayload.subscriptions) + + try await notifySubscriptionsUpdater.update(subscriptions: subscriptions, for: responsePayload.account) + + logger.debug("Received Notify Delete response") + + networkingInteractor.unsubscribe(topic: payload.topic) + kms.deleteSymmetricKey(for: payload.topic) + } + } +} diff --git a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifySubscriptionsChanged/NotifySubscriptionsChangedRequestSubscriber.swift b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifySubscriptionsChanged/NotifySubscriptionsChangedRequestSubscriber.swift index a7c9cdd95..5125fc9f4 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifySubscriptionsChanged/NotifySubscriptionsChangedRequestSubscriber.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifySubscriptionsChanged/NotifySubscriptionsChangedRequestSubscriber.swift @@ -5,36 +5,25 @@ class NotifySubscriptionsChangedRequestSubscriber { private let keyserver: URL private let networkingInteractor: NetworkInteracting private let identityClient: IdentityClient - private let kms: KeyManagementServiceProtocol private let logger: ConsoleLogging - private let groupKeychainStorage: KeychainStorageProtocol - private let notifyStorage: NotifyStorage + private let notifySubscriptionsUpdater: NotifySubsctiptionsUpdater private let notifySubscriptionsBuilder: NotifySubscriptionsBuilder - - private let subscriptionChangedSubject = PassthroughSubject<[NotifySubscription], Never>() - - var subscriptionChangedPublisher: AnyPublisher<[NotifySubscription], Never> { - return subscriptionChangedSubject.eraseToAnyPublisher() - } init( keyserver: URL, networkingInteractor: NetworkInteracting, - kms: KeyManagementServiceProtocol, identityClient: IdentityClient, logger: ConsoleLogging, - groupKeychainStorage: KeychainStorageProtocol, - notifyStorage: NotifyStorage, + notifySubscriptionsUpdater: NotifySubsctiptionsUpdater, notifySubscriptionsBuilder: NotifySubscriptionsBuilder ) { self.keyserver = keyserver self.networkingInteractor = networkingInteractor - self.kms = kms self.logger = logger self.identityClient = identityClient - self.groupKeychainStorage = groupKeychainStorage - self.notifyStorage = notifyStorage + self.notifySubscriptionsUpdater = notifySubscriptionsUpdater self.notifySubscriptionsBuilder = notifySubscriptionsBuilder + subscribeForNofifyChangedRequests() } @@ -44,54 +33,20 @@ class NotifySubscriptionsChangedRequestSubscriber { protocolMethod: NotifySubscriptionsChangedProtocolMethod(), requestOfType: NotifySubscriptionsChangedRequestPayload.Wrapper.self, errorHandler: logger) { [unowned self] payload in + logger.debug("Received Subscriptions Changed Request") let (jwtPayload, _) = try NotifySubscriptionsChangedRequestPayload.decodeAndVerify(from: payload.request) - let account = jwtPayload.account - - // TODO: varify signature with notify server diddoc authentication key - - let oldSubscriptions = notifyStorage.getSubscriptions(account: account) - let newSubscriptions = try await notifySubscriptionsBuilder.buildSubscriptions(jwtPayload.subscriptions) - - subscriptionChangedSubject.send(newSubscriptions) - - try Task.checkCancellation() - let subscriptions = oldSubscriptions.difference(from: newSubscriptions) + let subscriptions = try await notifySubscriptionsBuilder.buildSubscriptions(jwtPayload.subscriptions) - logger.debug("Received: \(newSubscriptions.count), changed: \(subscriptions.count)") - - if subscriptions.count > 0 { - try notifyStorage.replaceAllSubscriptions(newSubscriptions) - - for subscription in newSubscriptions { - let symKey = try SymmetricKey(hex: subscription.symKey) - try groupKeychainStorage.add(symKey, forKey: subscription.topic) - try kms.setSymmetricKey(symKey, for: subscription.topic) - } - - let topics = newSubscriptions.map { $0.topic } - - try await networkingInteractor.batchSubscribe(topics: topics) - - try Task.checkCancellation() - - var logProperties = ["rpcId": payload.id.string] - for (index, subscription) in newSubscriptions.enumerated() { - let key = "subscription_\(index + 1)" - logProperties[key] = subscription.topic - } - - logger.debug("Updated Subscriptions by Subscriptions Changed Request", properties: logProperties) - } + try await notifySubscriptionsUpdater.update(subscriptions: subscriptions, for: jwtPayload.account) try await respond(topic: payload.topic, account: jwtPayload.account, rpcId: payload.id, notifyServerAuthenticationKey: jwtPayload.notifyServerAuthenticationKey) } } private func respond(topic: String, account: Account, rpcId: RPCID, notifyServerAuthenticationKey: DIDKey) async throws { - let receiptPayload = NotifySubscriptionsChangedResponsePayload(account: account, keyserver: keyserver, notifyServerAuthenticationKey: notifyServerAuthenticationKey) let wrapper = try identityClient.signAndCreateWrapper( diff --git a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyUpdate/NotifyUpdateResponseSubscriber.swift b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyUpdate/NotifyUpdateResponseSubscriber.swift index d9b2bafbd..76b2a539b 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyUpdate/NotifyUpdateResponseSubscriber.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyUpdate/NotifyUpdateResponseSubscriber.swift @@ -2,21 +2,23 @@ import Foundation import Combine class NotifyUpdateResponseSubscriber { + private let networkingInteractor: NetworkInteracting - private var publishers = [AnyCancellable]() private let logger: ConsoleLogging - private let notifyStorage: NotifyStorage - private let nofityConfigProvider: NotifyConfigProvider + private let notifySubscriptionsBuilder: NotifySubscriptionsBuilder + private let notifySubscriptionsUpdater: NotifySubsctiptionsUpdater - init(networkingInteractor: NetworkInteracting, - logger: ConsoleLogging, - notifyConfigProvider: NotifyConfigProvider, - notifyStorage: NotifyStorage + init( + networkingInteractor: NetworkInteracting, + logger: ConsoleLogging, + notifySubscriptionsBuilder: NotifySubscriptionsBuilder, + notifySubscriptionsUpdater: NotifySubsctiptionsUpdater ) { self.networkingInteractor = networkingInteractor self.logger = logger - self.notifyStorage = notifyStorage - self.nofityConfigProvider = notifyConfigProvider + self.notifySubscriptionsBuilder = notifySubscriptionsBuilder + self.notifySubscriptionsUpdater = notifySubscriptionsUpdater + subscribeForUpdateResponse() } @@ -24,10 +26,6 @@ class NotifyUpdateResponseSubscriber { } private extension NotifyUpdateResponseSubscriber { - enum Errors: Error { - case subscriptionDoesNotExist - case selectedScopeNotFound - } func subscribeForUpdateResponse() { networkingInteractor.subscribeOnResponse( @@ -37,7 +35,11 @@ private extension NotifyUpdateResponseSubscriber { errorHandler: logger ) { [unowned self] payload in - let _ = try NotifyUpdateResponsePayload.decodeAndVerify(from: payload.response) + let (responsePayload, _) = try NotifyUpdateResponsePayload.decodeAndVerify(from: payload.response) + + let subscriptions = try await notifySubscriptionsBuilder.buildSubscriptions(responsePayload.subscriptions) + + try await notifySubscriptionsUpdater.update(subscriptions: subscriptions, for: responsePayload.account) logger.debug("Received Notify Update response") } diff --git a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyWatchSubscriptions/NotifyWatchSubscriptionsResponseSubscriber.swift b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyWatchSubscriptions/NotifyWatchSubscriptionsResponseSubscriber.swift index dccdcbdd7..34239ec13 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyWatchSubscriptions/NotifyWatchSubscriptionsResponseSubscriber.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyWatchSubscriptions/NotifyWatchSubscriptionsResponseSubscriber.swift @@ -3,25 +3,20 @@ import Combine class NotifyWatchSubscriptionsResponseSubscriber { private let networkingInteractor: NetworkInteracting - private let kms: KeyManagementServiceProtocol private let logger: ConsoleLogging - private let notifyStorage: NotifyStorage - private let groupKeychainStorage: KeychainStorageProtocol private let notifySubscriptionsBuilder: NotifySubscriptionsBuilder + private let notifySubscriptionsUpdater: NotifySubsctiptionsUpdater init(networkingInteractor: NetworkInteracting, - kms: KeyManagementServiceProtocol, logger: ConsoleLogging, - notifyStorage: NotifyStorage, - groupKeychainStorage: KeychainStorageProtocol, - notifySubscriptionsBuilder: NotifySubscriptionsBuilder + notifySubscriptionsBuilder: NotifySubscriptionsBuilder, + notifySubscriptionsUpdater: NotifySubsctiptionsUpdater ) { self.networkingInteractor = networkingInteractor - self.kms = kms self.logger = logger - self.notifyStorage = notifyStorage - self.groupKeychainStorage = groupKeychainStorage self.notifySubscriptionsBuilder = notifySubscriptionsBuilder + self.notifySubscriptionsUpdater = notifySubscriptionsUpdater + subscribeForWatchSubscriptionsResponse() } @@ -32,45 +27,15 @@ class NotifyWatchSubscriptionsResponseSubscriber { requestOfType: NotifyWatchSubscriptionsPayload.Wrapper.self, responseOfType: NotifyWatchSubscriptionsResponsePayload.Wrapper.self, errorHandler: logger) { [unowned self] payload in + logger.debug("Received Notify Watch Subscriptions response") + let (requestPayload, _) = try NotifyWatchSubscriptionsPayload.decodeAndVerify(from: payload.request) let (responsePayload, _) = try NotifyWatchSubscriptionsResponsePayload.decodeAndVerify(from: payload.response) - let (watchSubscriptionPayloadRequest, _) = try NotifyWatchSubscriptionsPayload.decodeAndVerify(from: payload.request) - - let account = watchSubscriptionPayloadRequest.subscriptionAccount - // TODO: varify signature with notify server diddoc authentication key - - let oldSubscriptions = notifyStorage.getSubscriptions(account: account) - let newSubscriptions = try await notifySubscriptionsBuilder.buildSubscriptions(responsePayload.subscriptions) - - try Task.checkCancellation() - - let subscriptions = oldSubscriptions.difference(from: newSubscriptions) - - logger.debug("Received: \(newSubscriptions.count), changed: \(subscriptions.count)") - - if subscriptions.count > 0 { - // TODO: unsubscribe for oldSubscriptions topics that are not included in new subscriptions - try notifyStorage.replaceAllSubscriptions(newSubscriptions) - - for subscription in newSubscriptions { - let symKey = try SymmetricKey(hex: subscription.symKey) - try groupKeychainStorage.add(symKey, forKey: subscription.topic) - try kms.setSymmetricKey(symKey, for: subscription.topic) - } - - try await networkingInteractor.batchSubscribe(topics: newSubscriptions.map { $0.topic }) - - try Task.checkCancellation() - var logProperties = [String: String]() - for (index, subscription) in newSubscriptions.enumerated() { - let key = "subscription_\(index + 1)" - logProperties[key] = subscription.topic - } + let subscriptions = try await notifySubscriptionsBuilder.buildSubscriptions(responsePayload.subscriptions) - logger.debug("Updated Subscriptions with Watch Subscriptions Update, number of subscriptions: \(newSubscriptions.count)", properties: logProperties) - } + try await notifySubscriptionsUpdater.update(subscriptions: subscriptions, for: requestPayload.subscriptionAccount) } } diff --git a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_pushSubscribe/NotifySubscribeResponseSubscriber.swift b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_pushSubscribe/NotifySubscribeResponseSubscriber.swift index d8aa56a39..d42e8b1f0 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_pushSubscribe/NotifySubscribeResponseSubscriber.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_pushSubscribe/NotifySubscribeResponseSubscriber.swift @@ -2,31 +2,22 @@ import Foundation import Combine class NotifySubscribeResponseSubscriber { - enum Errors: Error { - case couldNotCreateSubscription - } private let networkingInteractor: NetworkInteracting - private let kms: KeyManagementServiceProtocol - private var publishers = [AnyCancellable]() private let logger: ConsoleLogging - private let notifyStorage: NotifyStorage - private let groupKeychainStorage: KeychainStorageProtocol - private let notifyConfigProvider: NotifyConfigProvider + private let notifySubscriptionsBuilder: NotifySubscriptionsBuilder + private let notifySubscriptionsUpdater: NotifySubsctiptionsUpdater init(networkingInteractor: NetworkInteracting, - kms: KeyManagementServiceProtocol, logger: ConsoleLogging, - groupKeychainStorage: KeychainStorageProtocol, - notifyStorage: NotifyStorage, - notifyConfigProvider: NotifyConfigProvider + notifySubscriptionsBuilder: NotifySubscriptionsBuilder, + notifySubscriptionsUpdater: NotifySubsctiptionsUpdater ) { self.networkingInteractor = networkingInteractor - self.kms = kms self.logger = logger - self.groupKeychainStorage = groupKeychainStorage - self.notifyStorage = notifyStorage - self.notifyConfigProvider = notifyConfigProvider + self.notifySubscriptionsBuilder = notifySubscriptionsBuilder + self.notifySubscriptionsUpdater = notifySubscriptionsUpdater + subscribeForSubscriptionResponse() } @@ -39,7 +30,12 @@ class NotifySubscribeResponseSubscriber { ) { [unowned self] payload in logger.debug("Received Notify Subscribe response") - let _ = try NotifySubscriptionResponsePayload.decodeAndVerify(from: payload.response) + let (requestPayload, _) = try NotifySubscriptionPayload.decodeAndVerify(from: payload.request) + let (responsePayload, _) = try NotifySubscriptionResponsePayload.decodeAndVerify(from: payload.response) + + let subscriptions = try await notifySubscriptionsBuilder.buildSubscriptions(responsePayload.subscriptions) + + try await notifySubscriptionsUpdater.update(subscriptions: subscriptions, for: requestPayload.subscriptionAccount) logger.debug("NotifySubscribeResponseSubscriber: unsubscribing from response topic: \(payload.topic)") diff --git a/Sources/WalletConnectNotify/Notify.swift b/Sources/WalletConnectNotify/Notify.swift index 9ed189307..3bb7b41f1 100644 --- a/Sources/WalletConnectNotify/Notify.swift +++ b/Sources/WalletConnectNotify/Notify.swift @@ -10,7 +10,6 @@ public class Notify { projectId: Networking.projectId, groupIdentifier: Networking.groupIdentifier, networkInteractor: Networking.interactor, - pairingRegisterer: Pair.registerer, pushClient: Push.instance, crypto: config.crypto, notifyHost: config.notifyHost, diff --git a/Sources/WalletConnectNotify/ProtocolMethods/NotifyGetNotificationsProtocolMethod.swift b/Sources/WalletConnectNotify/ProtocolMethods/NotifyGetNotificationsProtocolMethod.swift new file mode 100644 index 000000000..b86c57a1a --- /dev/null +++ b/Sources/WalletConnectNotify/ProtocolMethods/NotifyGetNotificationsProtocolMethod.swift @@ -0,0 +1,9 @@ +import Foundation + +struct NotifyGetNotificationsProtocolMethod: ProtocolMethod { + let method: String = "wc_notifyGetNotifications" + + let requestConfig: RelayConfig = RelayConfig(tag: 4014, prompt: false, ttl: 300) + + let responseConfig: RelayConfig = RelayConfig(tag: 4015, prompt: false, ttl: 300) +} diff --git a/Sources/WalletConnectNotify/Types/DataStructures/NotifyMessage.swift b/Sources/WalletConnectNotify/Types/DataStructures/NotifyMessage.swift index e61d46223..f34549757 100644 --- a/Sources/WalletConnectNotify/Types/DataStructures/NotifyMessage.swift +++ b/Sources/WalletConnectNotify/Types/DataStructures/NotifyMessage.swift @@ -7,13 +7,30 @@ public struct NotifyMessage: Codable, Equatable { public let icon: String public let url: String public let type: String + public let sent_at: UInt64 - public init(id: String, title: String, body: String, icon: String, url: String, type: String) { + public var sentAt: Date { + return Date(milliseconds: sent_at) + } + + public init(id: String, title: String, body: String, icon: String?, url: String?, type: String, sentAt: Date) { self.id = id self.title = title self.body = body - self.icon = icon - self.url = url + self.icon = icon ?? "" + self.url = url ?? "" self.type = type + self.sent_at = UInt64(sentAt.millisecondsSince1970) + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.id = try container.decode(String.self, forKey: .id) + self.title = try container.decode(String.self, forKey: .title) + self.body = try container.decode(String.self, forKey: .body) + self.icon = try container.decodeIfPresent(String.self, forKey: .icon) ?? "" + self.url = try container.decodeIfPresent(String.self, forKey: .url) ?? "" + self.type = try container.decode(String.self, forKey: .type) + self.sent_at = try container.decode(UInt64.self, forKey: .sent_at) } } diff --git a/Sources/WalletConnectNotify/Types/DataStructures/NotifySubscription.swift b/Sources/WalletConnectNotify/Types/DataStructures/NotifySubscription.swift index 0cc7f313e..54be8d136 100644 --- a/Sources/WalletConnectNotify/Types/DataStructures/NotifySubscription.swift +++ b/Sources/WalletConnectNotify/Types/DataStructures/NotifySubscription.swift @@ -15,6 +15,10 @@ public struct NotifySubscription: Codable, Equatable, SqliteRow { return "\(account.absoluteString)-\(metadata.url)" } + public func messageIcons(ofType type: String) -> NotifyImageUrls { + return scope[type]?.imageUrls ?? NotifyImageUrls(icons: metadata.icons) ?? NotifyImageUrls() + } + public init(decoder: SqliteRowDecoder) throws { self.topic = try decoder.decodeString(at: 0) self.account = try Account(decoder.decodeString(at: 1))! diff --git a/Sources/WalletConnectNotify/Types/JWTPayloads/NotifySubscriptionResponsePayload.swift b/Sources/WalletConnectNotify/Types/JWTPayloads/NotifySubscriptionResponsePayload.swift index 8ed7e775e..d9f04440a 100644 --- a/Sources/WalletConnectNotify/Types/JWTPayloads/NotifySubscriptionResponsePayload.swift +++ b/Sources/WalletConnectNotify/Types/JWTPayloads/NotifySubscriptionResponsePayload.swift @@ -18,6 +18,8 @@ struct NotifySubscriptionResponsePayload: JWTClaimsCodable { let sub: String /// Dapp's domain url let app: String + /// array of Notify Subscriptions + let sbs: [NotifyServerSubscription] static var action: String? { return "notify_subscription_response" @@ -39,22 +41,16 @@ struct NotifySubscriptionResponsePayload: JWTClaimsCodable { let account: Account let selfPubKey: DIDKey let app: String + let subscriptions: [NotifyServerSubscription] init(claims: Claims) throws { self.account = try Account(DIDPKHString: claims.sub) self.selfPubKey = try DIDKey(did: claims.aud) self.app = claims.app + self.subscriptions = claims.sbs } func encode(iss: String) throws -> Claims { - return Claims( - iat: defaultIat(), - exp: expiry(days: 1), - act: Claims.action, - iss: iss, - aud: selfPubKey.did(variant: .ED25519), - sub: account.did, - app: app - ) + fatalError("Client is not supposed to encode this JWT payload") } } diff --git a/Sources/WalletConnectNotify/Types/JWTPayloads/NotifyUpdateResponsePayload.swift b/Sources/WalletConnectNotify/Types/JWTPayloads/NotifyUpdateResponsePayload.swift index a03ca61f8..7e49d08c3 100644 --- a/Sources/WalletConnectNotify/Types/JWTPayloads/NotifyUpdateResponsePayload.swift +++ b/Sources/WalletConnectNotify/Types/JWTPayloads/NotifyUpdateResponsePayload.swift @@ -16,6 +16,8 @@ struct NotifyUpdateResponsePayload: JWTClaimsCodable { let aud: String /// Blockchain account that notify subscription has been proposed for -`did:pkh` let sub: String + /// array of Notify Server Subscriptions + let sbs: [NotifyServerSubscription] /// Dapp's domain url let app: String @@ -39,22 +41,16 @@ struct NotifyUpdateResponsePayload: JWTClaimsCodable { let account: Account let selfPubKey: DIDKey let app: DIDWeb + let subscriptions: [NotifyServerSubscription] init(claims: Claims) throws { self.account = try Account(DIDPKHString: claims.sub) self.selfPubKey = try DIDKey(did: claims.aud) self.app = try DIDWeb(did: claims.app) + self.subscriptions = claims.sbs } func encode(iss: String) throws -> Claims { - return Claims( - iat: defaultIat(), - exp: expiry(days: 1), - act: Claims.action, - iss: iss, - aud: selfPubKey.did(variant: .ED25519), - sub: account.did, - app: app.did - ) + fatalError() } } diff --git a/Sources/WalletConnectNotify/Types/JWTPayloads/NotifyDeletePayload.swift b/Sources/WalletConnectNotify/Types/JWTPayloads/notify_delete/NotifyDeletePayload.swift similarity index 100% rename from Sources/WalletConnectNotify/Types/JWTPayloads/NotifyDeletePayload.swift rename to Sources/WalletConnectNotify/Types/JWTPayloads/notify_delete/NotifyDeletePayload.swift diff --git a/Sources/WalletConnectNotify/Types/JWTPayloads/NotifyDeleteResponsePayload.swift b/Sources/WalletConnectNotify/Types/JWTPayloads/notify_delete/NotifyDeleteResponsePayload.swift similarity index 84% rename from Sources/WalletConnectNotify/Types/JWTPayloads/NotifyDeleteResponsePayload.swift rename to Sources/WalletConnectNotify/Types/JWTPayloads/notify_delete/NotifyDeleteResponsePayload.swift index b9f97cc43..ac7f76e0d 100644 --- a/Sources/WalletConnectNotify/Types/JWTPayloads/NotifyDeleteResponsePayload.swift +++ b/Sources/WalletConnectNotify/Types/JWTPayloads/notify_delete/NotifyDeleteResponsePayload.swift @@ -16,6 +16,8 @@ struct NotifyDeleteResponsePayload: JWTClaimsCodable { let aud: String /// Blockchain account that notify subscription has been proposed for -`did:pkh` let sub: String + /// array of Notify Server Subscriptions + let sbs: [NotifyServerSubscription] /// Dapp's domain url let app: String @@ -39,22 +41,16 @@ struct NotifyDeleteResponsePayload: JWTClaimsCodable { let account: Account let selfPubKey: DIDKey let app: DIDWeb + let subscriptions: [NotifyServerSubscription] init(claims: Claims) throws { self.account = try Account(DIDPKHString: claims.sub) self.selfPubKey = try DIDKey(did: claims.aud) self.app = try DIDWeb(did: claims.app) + self.subscriptions = claims.sbs } func encode(iss: String) throws -> Claims { - return Claims( - iat: defaultIat(), - exp: expiry(days: 1), - act: Claims.action, - iss: iss, - aud: selfPubKey.did(variant: .ED25519), - sub: account.did, - app: app.did - ) + fatalError() } } diff --git a/Sources/WalletConnectNotify/Types/JWTPayloads/notify_get_notifications/NotifyGetNotificationsRequestPayload.swift b/Sources/WalletConnectNotify/Types/JWTPayloads/notify_get_notifications/NotifyGetNotificationsRequestPayload.swift new file mode 100644 index 000000000..d54825d85 --- /dev/null +++ b/Sources/WalletConnectNotify/Types/JWTPayloads/notify_get_notifications/NotifyGetNotificationsRequestPayload.swift @@ -0,0 +1,70 @@ +import Foundation + +struct NotifyGetNotificationsRequestPayload: JWTClaimsCodable { + + struct Claims: JWTClaims { + let iat: UInt64 + let exp: UInt64 + let sub: String + let act: String? // - `notify_get_notifications` + let iss: String // - did:key of client identity key + let ksu: String // - key server for identity key verification + let aud: String // - did:key of dapp authentication key + let app: String // - did:web of app domain that this request is associated with - Example: `did:web:app.example.com` + let lmt: UInt64 // - the max number of notifications to return. Maximum value is 50. + let aft: String? // - the notification ID to start returning messages after. Null to start with the most recent notification + let urf: Bool + + static var action: String? { + return "notify_get_notifications" + } + } + + struct Wrapper: JWTWrapper { + let auth: String + + init(jwtString: String) { + self.auth = jwtString + } + + var jwtString: String { + return auth + } + } + + let account: Account + let keyserver: String + let dappAuthKey: DIDKey + let app: DIDWeb + let limit: UInt64 + let after: String? + + init(account: Account, keyserver: String, dappAuthKey: DIDKey, app: DIDWeb, limit: UInt64, after: String? = nil) { + self.account = account + self.keyserver = keyserver + self.dappAuthKey = dappAuthKey + self.app = app + self.limit = limit + self.after = after + } + + init(claims: Claims) throws { + fatalError() + } + + func encode(iss: String) throws -> Claims { + return Claims( + iat: defaultIat(), + exp: expiry(days: 1), + sub: account.did, + act: Claims.action, + iss: iss, + ksu: keyserver, + aud: dappAuthKey.did(variant: .ED25519), + app: app.did, + lmt: limit, + aft: after, + urf: false + ) + } +} diff --git a/Sources/WalletConnectNotify/Types/JWTPayloads/notify_get_notifications/NotifyGetNotificationsResponsePayload.swift b/Sources/WalletConnectNotify/Types/JWTPayloads/notify_get_notifications/NotifyGetNotificationsResponsePayload.swift new file mode 100644 index 000000000..cf8cb2243 --- /dev/null +++ b/Sources/WalletConnectNotify/Types/JWTPayloads/notify_get_notifications/NotifyGetNotificationsResponsePayload.swift @@ -0,0 +1,39 @@ +import Foundation + +struct NotifyGetNotificationsResponsePayload: JWTClaimsCodable { + + struct Claims: JWTClaims { + let iat: UInt64 + let exp: UInt64 + let act: String? // - `notify_get_notifications_response` + let iss: String // - did:key of client identity key + let aud: String // - did:key of Notify Server authentication key + let nfs: [NotifyMessage] // array of [Notify Notifications](./data-structures.md#notify-notification) + + static var action: String? { + return "notify_get_notifications_response" + } + } + + struct Wrapper: JWTWrapper { + let auth: String + + init(jwtString: String) { + self.auth = jwtString + } + + var jwtString: String { + return auth + } + } + + let messages: [NotifyMessage] + + init(claims: Claims) throws { + self.messages = claims.nfs + } + + func encode(iss: String) throws -> Claims { + fatalError() + } +} diff --git a/Sources/WalletConnectPairing/PairingClient.swift b/Sources/WalletConnectPairing/PairingClient.swift index 16d8f0076..5637ba3e6 100644 --- a/Sources/WalletConnectPairing/PairingClient.swift +++ b/Sources/WalletConnectPairing/PairingClient.swift @@ -9,7 +9,14 @@ public class PairingClient: PairingRegisterer, PairingInteracting, PairingClient pairingDeleteRequestSubscriber.deletePublisherSubject.eraseToAnyPublisher() } + public var pairingStatePublisher: AnyPublisher { + return pairingStateProvider.pairingStatePublisher + } + public let socketConnectionStatusPublisher: AnyPublisher + public var pairingExpirationPublisher: AnyPublisher { + return expirationService.pairingExpirationPublisher + } private let pairingStorage: WCPairingStorage private let walletPairService: WalletPairService @@ -25,6 +32,7 @@ public class PairingClient: PairingRegisterer, PairingInteracting, PairingClient private let resubscribeService: PairingResubscribeService private let expirationService: ExpirationService private let pairingDeleteRequestSubscriber: PairingDeleteRequestSubscriber + private let pairingStateProvider: PairingStateProvider private let cleanupService: PairingCleanupService @@ -47,7 +55,8 @@ public class PairingClient: PairingRegisterer, PairingInteracting, PairingClient cleanupService: PairingCleanupService, pingService: PairingPingService, socketConnectionStatusPublisher: AnyPublisher, - pairingsProvider: PairingsProvider + pairingsProvider: PairingsProvider, + pairingStateProvider: PairingStateProvider ) { self.pairingStorage = pairingStorage self.appPairService = appPairService @@ -64,6 +73,7 @@ public class PairingClient: PairingRegisterer, PairingInteracting, PairingClient self.pingService = pingService self.pairingRequestsSubscriber = pairingRequestsSubscriber self.pairingsProvider = pairingsProvider + self.pairingStateProvider = pairingStateProvider setUpPublishers() setUpExpiration() } diff --git a/Sources/WalletConnectPairing/PairingClientFactory.swift b/Sources/WalletConnectPairing/PairingClientFactory.swift index 902ba7d28..22840bed3 100644 --- a/Sources/WalletConnectPairing/PairingClientFactory.swift +++ b/Sources/WalletConnectPairing/PairingClientFactory.swift @@ -36,6 +36,7 @@ public struct PairingClientFactory { let expirationService = ExpirationService(pairingStorage: pairingStore, networkInteractor: networkingClient, kms: kms) let resubscribeService = PairingResubscribeService(networkInteractor: networkingClient, pairingStorage: pairingStore) let pairingDeleteRequestSubscriber = PairingDeleteRequestSubscriber(networkingInteractor: networkingClient, kms: kms, pairingStorage: pairingStore, logger: logger) + let pairingStateProvider = PairingStateProvider(pairingStorage: pairingStore) return PairingClient( pairingStorage: pairingStore, @@ -52,7 +53,8 @@ public struct PairingClientFactory { cleanupService: cleanupService, pingService: pingService, socketConnectionStatusPublisher: networkingClient.socketConnectionStatusPublisher, - pairingsProvider: pairingsProvider + pairingsProvider: pairingsProvider, + pairingStateProvider: pairingStateProvider ) } } diff --git a/Sources/WalletConnectPairing/PairingClientProtocol.swift b/Sources/WalletConnectPairing/PairingClientProtocol.swift index 7edd05b30..905c1b4e8 100644 --- a/Sources/WalletConnectPairing/PairingClientProtocol.swift +++ b/Sources/WalletConnectPairing/PairingClientProtocol.swift @@ -3,6 +3,8 @@ import Combine public protocol PairingClientProtocol { var logsPublisher: AnyPublisher {get} var pairingDeletePublisher: AnyPublisher<(code: Int, message: String), Never> {get} + var pairingStatePublisher: AnyPublisher {get} + var pairingExpirationPublisher: AnyPublisher {get} func pair(uri: WalletConnectURI) async throws func disconnect(topic: String) async throws func getPairings() -> [Pairing] diff --git a/Sources/WalletConnectPairing/Services/App/AppPairService.swift b/Sources/WalletConnectPairing/Services/App/AppPairService.swift index 7dd5deb09..0ecb3a64e 100644 --- a/Sources/WalletConnectPairing/Services/App/AppPairService.swift +++ b/Sources/WalletConnectPairing/Services/App/AppPairService.swift @@ -15,8 +15,9 @@ actor AppPairService { let topic = String.generateTopic() try await networkingInteractor.subscribe(topic: topic) let symKey = try! kms.createSymmetricKey(topic) - let pairing = WCPairing(topic: topic) - let uri = WalletConnectURI(topic: topic, symKey: symKey.hexRepresentation, relay: pairing.relay) + let relay = RelayProtocolOptions(protocol: "irn", data: nil) + let uri = WalletConnectURI(topic: topic, symKey: symKey.hexRepresentation, relay: relay) + let pairing = WCPairing(uri: uri) pairingStorage.setPairing(pairing) return uri } diff --git a/Sources/WalletConnectPairing/Services/Common/ExpirationService.swift b/Sources/WalletConnectPairing/Services/Common/ExpirationService.swift index 4a8850cbe..0df4caf7e 100644 --- a/Sources/WalletConnectPairing/Services/Common/ExpirationService.swift +++ b/Sources/WalletConnectPairing/Services/Common/ExpirationService.swift @@ -1,9 +1,14 @@ import Foundation +import Combine final class ExpirationService { private let pairingStorage: WCPairingStorage private let networkInteractor: NetworkInteracting private let kms: KeyManagementServiceProtocol + private let pairingExpirationPublisherSubject: PassthroughSubject = .init() + var pairingExpirationPublisher: AnyPublisher { + pairingExpirationPublisherSubject.eraseToAnyPublisher() + } init(pairingStorage: WCPairingStorage, networkInteractor: NetworkInteracting, kms: KeyManagementServiceProtocol) { self.pairingStorage = pairingStorage @@ -15,6 +20,10 @@ final class ExpirationService { pairingStorage.onPairingExpiration = { [weak self] pairing in self?.kms.deleteSymmetricKey(for: pairing.topic) self?.networkInteractor.unsubscribe(topic: pairing.topic) + + DispatchQueue.main.async { + self?.pairingExpirationPublisherSubject.send(Pairing(pairing)) + } } } } diff --git a/Sources/WalletConnectPairing/Services/Common/PairingStateProvider.swift b/Sources/WalletConnectPairing/Services/Common/PairingStateProvider.swift new file mode 100644 index 000000000..ff917e33d --- /dev/null +++ b/Sources/WalletConnectPairing/Services/Common/PairingStateProvider.swift @@ -0,0 +1,33 @@ +import Combine +import Foundation + +class PairingStateProvider { + private let pairingStorage: WCPairingStorage + private var pairingStatePublisherSubject = PassthroughSubject() + private var checkTimer: Timer? + private var lastPairingState: Bool? + + public var pairingStatePublisher: AnyPublisher { + pairingStatePublisherSubject.eraseToAnyPublisher() + } + + public init(pairingStorage: WCPairingStorage) { + self.pairingStorage = pairingStorage + setupPairingStateCheckTimer() + } + + private func setupPairingStateCheckTimer() { + checkTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [unowned self] _ in + checkPairingState() + } + } + + private func checkPairingState() { + let pairingStateActive = !pairingStorage.getAll().allSatisfy { $0.active || $0.requestReceived } + + if lastPairingState != pairingStateActive { + pairingStatePublisherSubject.send(pairingStateActive) + lastPairingState = pairingStateActive + } + } +} diff --git a/Sources/WalletConnectPairing/Types/AppMetadata.swift b/Sources/WalletConnectPairing/Types/AppMetadata.swift index 4fcfee409..0ab317f12 100644 --- a/Sources/WalletConnectPairing/Types/AppMetadata.swift +++ b/Sources/WalletConnectPairing/Types/AppMetadata.swift @@ -70,3 +70,17 @@ public struct AppMetadata: Codable, Equatable { self.redirect = redirect } } + +#if DEBUG +public extension AppMetadata { + static func stub() -> AppMetadata { + AppMetadata( + name: "Wallet Connect", + description: "A protocol to connect blockchain wallets to dapps.", + url: "https://walletconnect.com/", + icons: [], + redirect: AppMetadata.Redirect(native: "", universal: nil) + ) + } +} +#endif diff --git a/Sources/WalletConnectPairing/Types/Pairing.swift b/Sources/WalletConnectPairing/Types/Pairing.swift index 03ed01a41..9edd37941 100644 --- a/Sources/WalletConnectPairing/Types/Pairing.swift +++ b/Sources/WalletConnectPairing/Types/Pairing.swift @@ -6,16 +6,20 @@ public struct Pairing { public let topic: String public let peer: AppMetadata? public let expiryDate: Date - - public init(topic: String, peer: AppMetadata?, expiryDate: Date) { - self.topic = topic - self.peer = peer - self.expiryDate = expiryDate - } + public let active: Bool init(_ pairing: WCPairing) { self.topic = pairing.topic self.peer = pairing.peerMetadata self.expiryDate = pairing.expiryDate + self.active = pairing.active + } +} + +#if DEBUG +extension Pairing { + static func stub(expiryDate: Date = Date(timeIntervalSinceNow: 10000), topic: String = String.generateTopic()) -> Pairing { + Pairing(WCPairing.stub(expiryDate: expiryDate, topic: topic)) } } +#endif diff --git a/Sources/WalletConnectPairing/Types/WCPairing.swift b/Sources/WalletConnectPairing/Types/WCPairing.swift index d87bd8946..a44ee01fa 100644 --- a/Sources/WalletConnectPairing/Types/WCPairing.swift +++ b/Sources/WalletConnectPairing/Types/WCPairing.swift @@ -27,29 +27,12 @@ public struct WCPairing: SequenceObject { 30 * .day } - public init(topic: String, relay: RelayProtocolOptions, peerMetadata: AppMetadata, isActive: Bool = false, requestReceived: Bool = false, expiryDate: Date) { - self.topic = topic - self.relay = relay - self.peerMetadata = peerMetadata - self.active = isActive - self.requestReceived = requestReceived - self.expiryDate = expiryDate - } - - public init(topic: String) { - self.topic = topic - self.relay = RelayProtocolOptions(protocol: "irn", data: nil) - self.active = false - self.requestReceived = false - self.expiryDate = Self.dateInitializer().advanced(by: Self.timeToLiveInactive) - } - public init(uri: WalletConnectURI) { self.topic = uri.topic self.relay = uri.relay self.active = false self.requestReceived = false - self.expiryDate = Self.dateInitializer().advanced(by: Self.timeToLiveInactive) + self.expiryDate = Date(timeIntervalSince1970: TimeInterval(uri.expiryTimestamp)) } public mutating func activate() { @@ -75,3 +58,31 @@ public struct WCPairing: SequenceObject { expiryDate = newExpiryDate } } + +#if DEBUG +extension WCPairing { + static func stub(expiryDate: Date = Date(timeIntervalSinceNow: 10000), isActive: Bool = true, topic: String = String.generateTopic()) -> WCPairing { + WCPairing(topic: topic, relay: RelayProtocolOptions.stub(), peerMetadata: AppMetadata.stub(), isActive: isActive, expiryDate: expiryDate) + } + + init(topic: String, relay: RelayProtocolOptions, peerMetadata: AppMetadata, isActive: Bool = false, requestReceived: Bool = false, expiryDate: Date) { + self.topic = topic + self.relay = relay + self.peerMetadata = peerMetadata + self.active = isActive + self.requestReceived = requestReceived + self.expiryDate = expiryDate + } +} + +extension WalletConnectURI { + public static func stub() -> WalletConnectURI { + WalletConnectURI( + topic: String.generateTopic(), + symKey: SymmetricKey().hexRepresentation, + relay: RelayProtocolOptions(protocol: "", data: nil) + ) + } +} + +#endif diff --git a/Sources/WalletConnectRelay/Dispatching.swift b/Sources/WalletConnectRelay/Dispatching.swift index bf3ef2c92..7d13bc39f 100644 --- a/Sources/WalletConnectRelay/Dispatching.swift +++ b/Sources/WalletConnectRelay/Dispatching.swift @@ -3,6 +3,7 @@ import Combine protocol Dispatching { var onMessage: ((String) -> Void)? { get set } + var networkConnectionStatusPublisher: AnyPublisher { get } var socketConnectionStatusPublisher: AnyPublisher { get } func send(_ string: String, completion: @escaping (Error?) -> Void) func protectedSend(_ string: String, completion: @escaping (Error?) -> Void) @@ -17,8 +18,9 @@ final class Dispatcher: NSObject, Dispatching { var socketConnectionHandler: SocketConnectionHandler private let relayUrlFactory: RelayUrlFactory + private let networkMonitor: NetworkMonitoring private let logger: ConsoleLogging - + private let defaultTimeout: Int = 5 /// The property is used to determine whether relay.walletconnect.org will be used /// in case relay.walletconnect.com doesn't respond for some reason (most likely due to being blocked in the user's location). @@ -30,15 +32,21 @@ final class Dispatcher: NSObject, Dispatching { socketConnectionStatusPublisherSubject.eraseToAnyPublisher() } + var networkConnectionStatusPublisher: AnyPublisher { + networkMonitor.networkConnectionStatusPublisher + } + private let concurrentQueue = DispatchQueue(label: "com.walletconnect.sdk.dispatcher", attributes: .concurrent) init( socketFactory: WebSocketFactory, relayUrlFactory: RelayUrlFactory, + networkMonitor: NetworkMonitoring, socketConnectionType: SocketConnectionType, logger: ConsoleLogging ) { self.relayUrlFactory = relayUrlFactory + self.networkMonitor = networkMonitor self.logger = logger let socket = socketFactory.create(with: relayUrlFactory.create(fallback: fallback)) @@ -60,7 +68,7 @@ final class Dispatcher: NSObject, Dispatching { func send(_ string: String, completion: @escaping (Error?) -> Void) { guard socket.isConnected else { - completion(NetworkError.webSocketNotConnected) + completion(NetworkError.connectionFailed) return } socket.write(string: string) { @@ -69,20 +77,22 @@ final class Dispatcher: NSObject, Dispatching { } func protectedSend(_ string: String, completion: @escaping (Error?) -> Void) { - guard !socket.isConnected else { + guard !socket.isConnected || !networkMonitor.isConnected else { return send(string, completion: completion) } var cancellable: AnyCancellable? - cancellable = socketConnectionStatusPublisher - .filter { $0 == .connected } + cancellable = Publishers.CombineLatest(socketConnectionStatusPublisher, networkConnectionStatusPublisher) + .filter { $0.0 == .connected && $0.1 == .connected } .setFailureType(to: NetworkError.self) - .timeout(.seconds(defaultTimeout), scheduler: concurrentQueue, customError: { .webSocketNotConnected }) + .timeout(.seconds(defaultTimeout), scheduler: concurrentQueue, customError: { .connectionFailed }) .sink(receiveCompletion: { [unowned self] result in switch result { case .failure(let error): cancellable?.cancel() - self.handleFallbackIfNeeded(error: error) + if !socket.isConnected { + handleFallbackIfNeeded(error: error) + } completion(error) case .finished: break } @@ -137,7 +147,7 @@ extension Dispatcher { } private func handleFallbackIfNeeded(error: NetworkError) { - if error == .webSocketNotConnected && socket.request.url?.host == NetworkConstants.defaultUrl { + if error == .connectionFailed && socket.request.url?.host == NetworkConstants.defaultUrl { logger.debug("[WebSocket] - Fallback to \(NetworkConstants.fallbackUrl)") fallback = true socket.request.url = relayUrlFactory.create(fallback: fallback) diff --git a/Sources/WalletConnectRelay/Misc/NetworkError.swift b/Sources/WalletConnectRelay/Misc/NetworkError.swift index f31340bbd..e0a66c2c1 100644 --- a/Sources/WalletConnectRelay/Misc/NetworkError.swift +++ b/Sources/WalletConnectRelay/Misc/NetworkError.swift @@ -1,13 +1,13 @@ import Foundation enum NetworkError: Error, Equatable { - case webSocketNotConnected + case connectionFailed case sendMessageFailed(Error) case receiveMessageFailure(Error) static func == (lhs: NetworkError, rhs: NetworkError) -> Bool { switch (lhs, rhs) { - case (.webSocketNotConnected, .webSocketNotConnected): return true + case (.connectionFailed, .connectionFailed): return true case (.sendMessageFailed, .sendMessageFailed): return true case (.receiveMessageFailure, .receiveMessageFailure): return true default: return false @@ -22,8 +22,8 @@ extension NetworkError: LocalizedError { var localizedDescription: String { switch self { - case .webSocketNotConnected: - return "Web socket is not connected to any URL." + case .connectionFailed: + return "Web socket is not connected to any URL or networking connection error" case .sendMessageFailed(let error): return "Failed to send a message through the web socket: \(error)" case .receiveMessageFailure(let error): diff --git a/Sources/WalletConnectRelay/NetworkMonitoring.swift b/Sources/WalletConnectRelay/NetworkMonitoring.swift index 1d3932db5..e6c6b4477 100644 --- a/Sources/WalletConnectRelay/NetworkMonitoring.swift +++ b/Sources/WalletConnectRelay/NetworkMonitoring.swift @@ -8,6 +8,7 @@ public enum NetworkConnectionStatus { } public protocol NetworkMonitoring: AnyObject { + var isConnected: Bool { get } var networkConnectionStatusPublisher: AnyPublisher { get } } @@ -16,7 +17,11 @@ public final class NetworkMonitor: NetworkMonitoring { private let workerQueue = DispatchQueue(label: "com.walletconnect.sdk.network.monitor") private let networkConnectionStatusPublisherSubject = CurrentValueSubject(.connected) - + + public var isConnected: Bool { + return networkConnectionStatusPublisherSubject.value == .connected + } + public var networkConnectionStatusPublisher: AnyPublisher { networkConnectionStatusPublisherSubject .share() diff --git a/Sources/WalletConnectRelay/PackageConfig.json b/Sources/WalletConnectRelay/PackageConfig.json index c16d55635..ec1cdfd4c 100644 --- a/Sources/WalletConnectRelay/PackageConfig.json +++ b/Sources/WalletConnectRelay/PackageConfig.json @@ -1 +1 @@ -{"version": "1.11.0"} +{"version": "1.12.0"} diff --git a/Sources/WalletConnectRelay/RelayClient.swift b/Sources/WalletConnectRelay/RelayClient.swift index 441f40314..5ff6135fd 100644 --- a/Sources/WalletConnectRelay/RelayClient.swift +++ b/Sources/WalletConnectRelay/RelayClient.swift @@ -27,6 +27,10 @@ public final class RelayClient { dispatcher.socketConnectionStatusPublisher } + public var networkConnectionStatusPublisher: AnyPublisher { + dispatcher.networkConnectionStatusPublisher + } + private let messagePublisherSubject = PassthroughSubject<(topic: String, message: String, publishedAt: Date), Never>() private let subscriptionResponsePublisherSubject = PassthroughSubject<(RPCID?, [String]), Never>() @@ -34,6 +38,11 @@ public final class RelayClient { subscriptionResponsePublisherSubject.eraseToAnyPublisher() } + private let requestAcknowledgePublisherSubject = PassthroughSubject() + private var requestAcknowledgePublisher: AnyPublisher { + requestAcknowledgePublisherSubject.eraseToAnyPublisher() + } + private let clientIdStorage: ClientIdStoring private var dispatcher: Dispatching @@ -86,13 +95,35 @@ public final class RelayClient { try dispatcher.disconnect(closeCode: closeCode) } - /// Completes when networking client sends a request, error if it fails on client side + /// Completes with an acknowledgement from the relay network public func publish(topic: String, payload: String, tag: Int, prompt: Bool, ttl: Int) async throws { - let request = Publish(params: .init(topic: topic, message: payload, ttl: ttl, prompt: prompt, tag: tag)) - .asRPCRequest() + let request = Publish(params: .init(topic: topic, message: payload, ttl: ttl, prompt: prompt, tag: tag)).asRPCRequest() let message = try request.asJSONEncodedString() - logger.debug("Publishing payload on topic: \(topic)") + + logger.debug("[Publish] Sending payload on topic: \(topic)") + try await dispatcher.protectedSend(message) + + return try await withUnsafeThrowingContinuation { continuation in + var cancellable: AnyCancellable? + cancellable = requestAcknowledgePublisher + .filter { $0 == request.id } + .setFailureType(to: RelayError.self) + .timeout(.seconds(10), scheduler: concurrentQueue, customError: { .requestTimeout }) + .sink(receiveCompletion: { [unowned self] result in + switch result { + case .failure(let error): + cancellable?.cancel() + logger.debug("[Publish] Relay request timeout for topic: \(topic)") + continuation.resume(throwing: error) + case .finished: break + } + }, receiveValue: { [unowned self] _ in + cancellable?.cancel() + logger.debug("[Publish] Published payload on topic: \(topic)") + continuation.resume(returning: ()) + }) + } } public func subscribe(topic: String) async throws { @@ -138,9 +169,9 @@ public final class RelayClient { } } - public func unsubscribe(topic: String, completion: @escaping ((Error?) -> Void)) { + public func unsubscribe(topic: String, completion: ((Error?) -> Void)?) { guard let subscriptionId = subscriptions[topic] else { - completion(Errors.subscriptionIdNotFound) + completion?(Errors.subscriptionIdNotFound) return } logger.debug("Unsubscribing from topic: \(topic)") @@ -152,12 +183,12 @@ public final class RelayClient { dispatcher.protectedSend(message) { [weak self] error in if let error = error { self?.logger.debug("Failed to unsubscribe from topic") - completion(error) + completion?(error) } else { self?.concurrentQueue.async(flags: .barrier) { self?.subscriptions[topic] = nil } - completion(nil) + completion?(nil) } } } @@ -204,7 +235,9 @@ public final class RelayClient { } else if let response = tryDecode(RPCResponse.self, from: payload) { switch response.outcome { case .response(let anyCodable): - if let subscriptionId = try? anyCodable.get(String.self) { + if let _ = try? anyCodable.get(Bool.self) { + requestAcknowledgePublisherSubject.send(response.id) + } else if let subscriptionId = try? anyCodable.get(String.self) { subscriptionResponsePublisherSubject.send((response.id, [subscriptionId])) } else if let subscriptionIds = try? anyCodable.get([String].self) { subscriptionResponsePublisherSubject.send((response.id, subscriptionIds)) diff --git a/Sources/WalletConnectRelay/RelayClientFactory.swift b/Sources/WalletConnectRelay/RelayClientFactory.swift index 98066e6c8..b59a50d29 100644 --- a/Sources/WalletConnectRelay/RelayClientFactory.swift +++ b/Sources/WalletConnectRelay/RelayClientFactory.swift @@ -20,6 +20,8 @@ public struct RelayClientFactory { let logger = ConsoleLogger(prefix: "🚄" ,loggingLevel: .off) + let networkMonitor = NetworkMonitor() + return RelayClientFactory.create( relayHost: relayHost, projectId: projectId, @@ -27,6 +29,7 @@ public struct RelayClientFactory { keychainStorage: keychainStorage, socketFactory: socketFactory, socketConnectionType: socketConnectionType, + networkMonitor: networkMonitor, logger: logger ) } @@ -39,6 +42,7 @@ public struct RelayClientFactory { keychainStorage: KeychainStorageProtocol, socketFactory: WebSocketFactory, socketConnectionType: SocketConnectionType = .automatic, + networkMonitor: NetworkMonitoring, logger: ConsoleLogging ) -> RelayClient { @@ -52,9 +56,11 @@ public struct RelayClientFactory { projectId: projectId, socketAuthenticator: socketAuthenticator ) + let dispatcher = Dispatcher( socketFactory: socketFactory, - relayUrlFactory: relayUrlFactory, + relayUrlFactory: relayUrlFactory, + networkMonitor: networkMonitor, socketConnectionType: socketConnectionType, logger: logger ) diff --git a/Sources/WalletConnectRelay/RelayError.swift b/Sources/WalletConnectRelay/RelayError.swift new file mode 100644 index 000000000..39d725d7c --- /dev/null +++ b/Sources/WalletConnectRelay/RelayError.swift @@ -0,0 +1,16 @@ +import Foundation + +enum RelayError: Error, LocalizedError { + case requestTimeout + + var errorDescription: String? { + return localizedDescription + } + + var localizedDescription: String { + switch self { + case .requestTimeout: + return "Relay request timeout" + } + } +} diff --git a/Sources/WalletConnectRouter/Router/Router.swift b/Sources/WalletConnectRouter/Router/Router.swift index 75a227b31..97db89059 100644 --- a/Sources/WalletConnectRouter/Router/Router.swift +++ b/Sources/WalletConnectRouter/Router/Router.swift @@ -1,5 +1,5 @@ +#if os(iOS) import UIKit - public struct WalletConnectRouter { public static func goBack(uri: String) { if #available(iOS 17, *) { @@ -13,3 +13,4 @@ public struct WalletConnectRouter { } } } +#endif diff --git a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift index 8c994c094..899b384cc 100644 --- a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift @@ -3,13 +3,14 @@ import Combine final class ApproveEngine { enum Errors: Error { - case wrongRequestParams + case proposalNotFound case relayNotFound - case proposalPayloadsNotFound case pairingNotFound case sessionNotFound case agreementMissingOrInvalid case networkNotConnected + case proposalExpired + case emtySessionNamespacesForbidden } var onSessionProposal: ((Session.Proposal, VerifyContext?) -> Void)? @@ -27,6 +28,7 @@ final class ApproveEngine { private let metadata: AppMetadata private let kms: KeyManagementServiceProtocol private let logger: ConsoleLogging + private let rpcHistory: RPCHistory private var publishers = Set() @@ -41,7 +43,8 @@ final class ApproveEngine { logger: ConsoleLogging, pairingStore: WCPairingStorage, sessionStore: WCSessionStorage, - verifyClient: VerifyClientProtocol + verifyClient: VerifyClientProtocol, + rpcHistory: RPCHistory ) { self.networkingInteractor = networkingInteractor self.proposalPayloadsStore = proposalPayloadsStore @@ -54,6 +57,7 @@ final class ApproveEngine { self.pairingStore = pairingStore self.sessionStore = sessionStore self.verifyClient = verifyClient + self.rpcHistory = rpcHistory setupRequestSubscriptions() setupResponseSubscriptions() @@ -62,21 +66,28 @@ final class ApproveEngine { func approveProposal(proposerPubKey: String, validating sessionNamespaces: [String: SessionNamespace], sessionProperties: [String: String]? = nil) async throws { logger.debug("Approving session proposal") + + guard !sessionNamespaces.isEmpty else { throw Errors.emtySessionNamespacesForbidden } + guard let payload = try proposalPayloadsStore.get(key: proposerPubKey) else { - throw Errors.wrongRequestParams + throw Errors.proposalNotFound } - + + let proposal = payload.request + + guard !proposal.isExpired() else { + logger.debug("Proposal has expired, topic: \(payload.topic)") + proposalPayloadsStore.delete(forKey: proposerPubKey) + throw Errors.proposalExpired + } + let networkConnectionStatus = await resolveNetworkConnectionStatus() guard networkConnectionStatus == .connected else { throw Errors.networkNotConnected } - let proposal = payload.request let pairingTopic = payload.topic - proposalPayloadsStore.delete(forKey: proposerPubKey) - verifyContextStore.delete(forKey: proposerPubKey) - try Namespace.validate(sessionNamespaces) try Namespace.validateApproved(sessionNamespaces, against: proposal.requiredNamespaces) @@ -97,13 +108,13 @@ final class ApproveEngine { let result = SessionType.ProposeResponse(relay: relay, responderPublicKey: selfPublicKey.hexRepresentation) let response = RPCResponse(id: payload.id, result: result) - async let proposeResponse: () = networkingInteractor.respond( + async let proposeResponseTask: () = networkingInteractor.respond( topic: payload.topic, response: response, protocolMethod: SessionProposeProtocolMethod() ) - async let settleRequest: () = settle( + async let settleRequestTask: WCSession = settle( topic: sessionTopic, proposal: proposal, namespaces: sessionNamespaces, @@ -111,10 +122,16 @@ final class ApproveEngine { pairingTopic: pairingTopic ) - _ = try await [proposeResponse, settleRequest] + _ = try await proposeResponseTask + let session: WCSession = try await settleRequestTask + sessionStore.setSession(session) + onSessionSettle?(session.publicRepresentation()) logger.debug("Session proposal response and settle request have been sent") + proposalPayloadsStore.delete(forKey: proposerPubKey) + verifyContextStore.delete(forKey: proposerPubKey) + pairingRegisterer.activate( pairingTopic: payload.topic, peerMetadata: payload.request.proposer.metadata @@ -123,15 +140,23 @@ final class ApproveEngine { func reject(proposerPubKey: String, reason: SignReasonCode) async throws { guard let payload = try proposalPayloadsStore.get(key: proposerPubKey) else { - throw Errors.proposalPayloadsNotFound + throw Errors.proposalNotFound } + + try await networkingInteractor.respondError(topic: payload.topic, requestId: payload.id, protocolMethod: SessionProposeProtocolMethod(), reason: reason) + + if let pairingTopic = rpcHistory.get(recordId: payload.id)?.topic, + let pairing = pairingStore.getPairing(forTopic: pairingTopic), + !pairing.active { + pairingStore.delete(topic: pairingTopic) + } + proposalPayloadsStore.delete(forKey: proposerPubKey) verifyContextStore.delete(forKey: proposerPubKey) - try await networkingInteractor.respondError(topic: payload.topic, requestId: payload.id, protocolMethod: SessionProposeProtocolMethod(), reason: reason) - // TODO: Delete pairing if inactive + } - func settle(topic: String, proposal: SessionProposal, namespaces: [String: SessionNamespace], sessionProperties: [String: String]? = nil, pairingTopic: String) async throws { + func settle(topic: String, proposal: SessionProposal, namespaces: [String: SessionNamespace], sessionProperties: [String: String]? = nil, pairingTopic: String) async throws -> WCSession { guard let agreementKeys = kms.getAgreementSecret(for: topic) else { throw Errors.agreementMissingOrInvalid } @@ -169,7 +194,6 @@ final class ApproveEngine { logger.debug("Sending session settle request") - sessionStore.setSession(session) let protocolMethod = SessionSettleProtocolMethod() let request = RPCRequest(method: protocolMethod.method, params: settleParams) @@ -178,7 +202,7 @@ final class ApproveEngine { async let settleRequest: () = networkingInteractor.request(request, topic: topic, protocolMethod: protocolMethod) _ = try await [settleRequest, subscription] - onSessionSettle?(session.publicRepresentation()) + return session } } @@ -408,8 +432,22 @@ private extension ApproveEngine { extension ApproveEngine.Errors: LocalizedError { var errorDescription: String? { switch self { - case .networkNotConnected: return "Action failed. You seem to be offline" - default: return "" + case .proposalNotFound: + return "Proposal not found." + case .relayNotFound: + return "Relay not found." + case .pairingNotFound: + return "Pairing not found." + case .sessionNotFound: + return "Session not found." + case .agreementMissingOrInvalid: + return "Agreement missing or invalid." + case .networkNotConnected: + return "Network not connected." + case .proposalExpired: + return "Proposal expired." + case .emtySessionNamespacesForbidden: + return "Session Namespaces Cannot Be Empty" } } } diff --git a/Sources/WalletConnectSign/Engine/Common/PendingProposalsProvider.swift b/Sources/WalletConnectSign/Engine/Common/PendingProposalsProvider.swift new file mode 100644 index 000000000..4af7e3808 --- /dev/null +++ b/Sources/WalletConnectSign/Engine/Common/PendingProposalsProvider.swift @@ -0,0 +1,49 @@ +import Foundation +import Combine + +class PendingProposalsProvider { + + private let proposalPayloadsStore: CodableStore> + private let verifyContextStore: CodableStore + private var publishers = Set() + private let pendingProposalsPublisherSubject = CurrentValueSubject<[(proposal: Session.Proposal, context: VerifyContext?)], Never>([]) + + var pendingProposalsPublisher: AnyPublisher<[(proposal: Session.Proposal, context: VerifyContext?)], Never> { + return pendingProposalsPublisherSubject.eraseToAnyPublisher() + } + + internal init( + proposalPayloadsStore: CodableStore>, + verifyContextStore: CodableStore) + { + self.proposalPayloadsStore = proposalPayloadsStore + self.verifyContextStore = verifyContextStore + updatePendingProposals() + setUpPendingProposalsPublisher() + } + + private func updatePendingProposals() { + let proposalsWithVerifyContext = getPendingProposals() + pendingProposalsPublisherSubject.send(proposalsWithVerifyContext) + } + + func setUpPendingProposalsPublisher() { + proposalPayloadsStore.storeUpdatePublisher.sink { [unowned self] _ in + updatePendingProposals() + }.store(in: &publishers) + } + + private func getPendingProposals() -> [(proposal: Session.Proposal, context: VerifyContext?)] { + let proposals = proposalPayloadsStore.getAll() + return proposals.map { ($0.request.publicRepresentation(pairingTopic: $0.topic), try? verifyContextStore.get(key: $0.request.proposer.publicKey)) } + } + + public func getPendingProposals(topic: String? = nil) -> [(proposal: Session.Proposal, context: VerifyContext?)] { + if let topic = topic { + return getPendingProposals().filter { $0.proposal.pairingTopic == topic } + } else { + return getPendingProposals() + } + } + +} diff --git a/Sources/WalletConnectSign/Engine/Common/ProposalExpiryWatcher.swift b/Sources/WalletConnectSign/Engine/Common/ProposalExpiryWatcher.swift new file mode 100644 index 000000000..94bc407ca --- /dev/null +++ b/Sources/WalletConnectSign/Engine/Common/ProposalExpiryWatcher.swift @@ -0,0 +1,41 @@ +import Foundation +import Combine + +class ProposalExpiryWatcher { + + private let sessionProposalExpirationPublisherSubject: PassthroughSubject = .init() + private let rpcHistory: RPCHistory + + var sessionProposalExpirationPublisher: AnyPublisher { + return sessionProposalExpirationPublisherSubject.eraseToAnyPublisher() + } + + private let proposalPayloadsStore: CodableStore> + private var checkTimer: Timer? + + internal init( + proposalPayloadsStore: CodableStore>, + rpcHistory: RPCHistory + ) { + self.proposalPayloadsStore = proposalPayloadsStore + self.rpcHistory = rpcHistory + setUpExpiryCheckTimer() + } + + func setUpExpiryCheckTimer() { + checkTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true) { [unowned self] _ in + checkForProposalsExpiry() + } + } + + func checkForProposalsExpiry() { + let proposals = proposalPayloadsStore.getAll() + proposals.forEach { proposalPayload in + let pairingTopic = proposalPayload.topic + guard proposalPayload.request.isExpired() else { return } + sessionProposalExpirationPublisherSubject.send(proposalPayload.request.publicRepresentation(pairingTopic: pairingTopic)) + proposalPayloadsStore.delete(forKey: proposalPayload.request.proposer.publicKey) + rpcHistory.delete(id: proposalPayload.id) + } + } +} diff --git a/Sources/WalletConnectSign/Engine/Common/RequestsExpiryWatcher.swift b/Sources/WalletConnectSign/Engine/Common/RequestsExpiryWatcher.swift new file mode 100644 index 000000000..a1a2726df --- /dev/null +++ b/Sources/WalletConnectSign/Engine/Common/RequestsExpiryWatcher.swift @@ -0,0 +1,41 @@ + +import Foundation +import Combine + +class RequestsExpiryWatcher { + + private let requestExpirationPublisherSubject: PassthroughSubject = .init() + private let rpcHistory: RPCHistory + private let historyService: HistoryService + + var requestExpirationPublisher: AnyPublisher { + return requestExpirationPublisherSubject.eraseToAnyPublisher() + } + + private var checkTimer: Timer? + + internal init( + proposalPayloadsStore: CodableStore>, + rpcHistory: RPCHistory, + historyService: HistoryService + ) { + self.rpcHistory = rpcHistory + self.historyService = historyService + setUpExpiryCheckTimer() + } + + func setUpExpiryCheckTimer() { + checkTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true) { [unowned self] _ in + checkForRequestExpiry() + } + } + + func checkForRequestExpiry() { + let requests = historyService.getPendingRequestsWithRecordId() + requests.forEach { (request: Request, recordId: RPCID) in + guard request.isExpired() else { return } + requestExpirationPublisherSubject.send(request) + rpcHistory.delete(id: recordId) + } + } +} diff --git a/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift index f2598b074..daf11e954 100644 --- a/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift @@ -7,12 +7,16 @@ final class SessionEngine { } var onSessionsUpdate: (([Session]) -> Void)? - var onSessionRequest: ((Request, VerifyContext?) -> Void)? var onSessionResponse: ((Response) -> Void)? var onSessionRejected: ((String, SessionType.Reason) -> Void)? var onSessionDelete: ((String, SessionType.Reason) -> Void)? var onEventReceived: ((String, Session.Event, Blockchain?) -> Void)? + var sessionRequestPublisher: AnyPublisher<(request: Request, context: VerifyContext?), Never> { + return sessionRequestsProvider.sessionRequestPublisher + } + + private let sessionStore: WCSessionStorage private let networkingInteractor: NetworkInteracting private let historyService: HistoryService @@ -21,6 +25,7 @@ final class SessionEngine { private let kms: KeyManagementServiceProtocol private var publishers = [AnyCancellable]() private let logger: ConsoleLogging + private let sessionRequestsProvider: SessionRequestsProvider init( networkingInteractor: NetworkInteracting, @@ -29,7 +34,8 @@ final class SessionEngine { verifyClient: VerifyClientProtocol, kms: KeyManagementServiceProtocol, sessionStore: WCSessionStorage, - logger: ConsoleLogging + logger: ConsoleLogging, + sessionRequestsProvider: SessionRequestsProvider ) { self.networkingInteractor = networkingInteractor self.historyService = historyService @@ -38,12 +44,17 @@ final class SessionEngine { self.kms = kms self.sessionStore = sessionStore self.logger = logger + self.sessionRequestsProvider = sessionRequestsProvider setupConnectionSubscriptions() setupRequestSubscriptions() setupResponseSubscriptions() setupUpdateSubscriptions() setupExpirationSubscriptions() + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in + guard let self = self else {return} + sessionRequestsProvider.emitRequestIfPending() + } } func hasSession(for topic: String) -> Bool { @@ -63,9 +74,10 @@ final class SessionEngine { guard session.hasPermission(forMethod: request.method, onChain: request.chainId) else { throw WalletConnectError.invalidPermissions } - let chainRequest = SessionType.RequestParams.Request(method: request.method, params: request.params, expiry: request.expiry) + let chainRequest = SessionType.RequestParams.Request(method: request.method, params: request.params, expiryTimestamp: request.expiryTimestamp) let sessionRequestParams = SessionType.RequestParams(request: chainRequest, chainId: request.chainId) - let protocolMethod = SessionRequestProtocolMethod(ttl: request.calculateTtl()) + let ttl = try request.calculateTtl() + let protocolMethod = SessionRequestProtocolMethod(ttl: ttl) let rpcRequest = RPCRequest(method: protocolMethod.method, params: sessionRequestParams, rpcid: request.id) try await networkingInteractor.request(rpcRequest, topic: request.topic, protocolMethod: SessionRequestProtocolMethod()) } @@ -94,6 +106,10 @@ final class SessionEngine { protocolMethod: protocolMethod ) verifyContextStore.delete(forKey: requestId.string) + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in + guard let self = self else {return} + sessionRequestsProvider.emitRequestIfPending() + } } func emit(topic: String, event: SessionType.EventParams.Event, chainId: Blockchain) async throws { @@ -228,7 +244,7 @@ private extension SessionEngine { method: payload.request.request.method, params: payload.request.request.params, chainId: payload.request.chainId, - expiry: payload.request.request.expiry + expiryTimestamp: payload.request.request.expiryTimestamp ) guard let session = sessionStore.getSession(forTopic: topic) else { return respondError(payload: payload, reason: .noSessionForTopic, protocolMethod: protocolMethod) @@ -248,12 +264,12 @@ private extension SessionEngine { let response = try await verifyClient.verifyOrigin(assertionId: assertionId) let verifyContext = verifyClient.createVerifyContext(origin: response.origin, domain: session.peerParticipant.metadata.url, isScam: response.isScam) verifyContextStore.set(verifyContext, forKey: request.id.string) - onSessionRequest?(request, verifyContext) + + sessionRequestsProvider.emitRequestIfPending() } catch { let verifyContext = verifyClient.createVerifyContext(origin: nil, domain: session.peerParticipant.metadata.url, isScam: nil) verifyContextStore.set(verifyContext, forKey: request.id.string) - onSessionRequest?(request, verifyContext) - return + sessionRequestsProvider.emitRequestIfPending() } } } diff --git a/Sources/WalletConnectSign/Namespace.swift b/Sources/WalletConnectSign/Namespace.swift index 6de73e1cc..08ded8326 100644 --- a/Sources/WalletConnectSign/Namespace.swift +++ b/Sources/WalletConnectSign/Namespace.swift @@ -1,8 +1,26 @@ -public enum AutoNamespacesError: Error { +import Foundation + +public enum AutoNamespacesError: Error, LocalizedError { case requiredChainsNotSatisfied case requiredAccountsNotSatisfied case requiredMethodsNotSatisfied case requiredEventsNotSatisfied + case emtySessionNamespacesForbidden + + public var errorDescription: String? { + switch self { + case .requiredChainsNotSatisfied: + return "The required chains are not satisfied." + case .requiredAccountsNotSatisfied: + return "The required accounts are not satisfied." + case .requiredMethodsNotSatisfied: + return "The required methods are not satisfied." + case .requiredEventsNotSatisfied: + return "The required events are not satisfied." + case .emtySessionNamespacesForbidden: + return "Empty session namespaces are not allowed." + } + } } public struct ProposalNamespace: Equatable, Codable { @@ -148,9 +166,6 @@ enum SessionProperties { public enum AutoNamespaces { /// For a wallet to build session proposal structure by provided supported chains, methods, events & accounts. - /// - Parameters: - /// - proposalId: Session Proposal id - /// - namespaces: namespaces for given session, needs to contain at least required namespaces proposed by dApp. public static func build( sessionProposal: Session.Proposal, chains: [Blockchain], @@ -325,7 +340,8 @@ public enum AutoNamespaces { } } } - + guard !sessionNamespaces.isEmpty else { throw AutoNamespacesError.emtySessionNamespacesForbidden } + return sessionNamespaces } } diff --git a/Sources/WalletConnectSign/Request.swift b/Sources/WalletConnectSign/Request.swift index 1cae4e0cd..3898d45df 100644 --- a/Sources/WalletConnectSign/Request.swift +++ b/Sources/WalletConnectSign/Request.swift @@ -1,62 +1,72 @@ import Foundation public struct Request: Codable, Equatable { + public enum Errors: Error { + case invalidTtl + case requestExpired + } + public let id: RPCID public let topic: String public let method: String public let params: AnyCodable public let chainId: Blockchain - public let expiry: UInt64? + public var expiryTimestamp: UInt64? + + // TTL bounds + static let minTtl: TimeInterval = 300 // 5 minutes + static let maxTtl: TimeInterval = 604800 // 7 days + + + /// - Parameters: + /// - topic: topic of a session + /// - method: request method + /// - params: request params + /// - chainId: chain id + /// - ttl: ttl of a request, will be used to calculate expiry, 10 minutes by default + public init(topic: String, method: String, params: AnyCodable, chainId: Blockchain, ttl: TimeInterval = 300) throws { + guard ttl >= Request.minTtl && ttl <= Request.maxTtl else { + throw Errors.invalidTtl + } + + let calculatedExpiry = UInt64(Date().timeIntervalSince1970) + UInt64(ttl) + self.init(id: RPCID(JsonRpcID.generate()), topic: topic, method: method, params: params, chainId: chainId, expiryTimestamp: calculatedExpiry) + } - internal init(id: RPCID, topic: String, method: String, params: AnyCodable, chainId: Blockchain, expiry: UInt64?) { + init(id: RPCID, topic: String, method: String, params: C, chainId: Blockchain, ttl: TimeInterval = 300) throws where C: Codable { + guard ttl >= Request.minTtl && ttl <= Request.maxTtl else { + throw Errors.invalidTtl + } + + let calculatedExpiry = UInt64(Date().timeIntervalSince1970) + UInt64(ttl) + self.init(id: id, topic: topic, method: method, params: AnyCodable(params), chainId: chainId, expiryTimestamp: calculatedExpiry) + } + + internal init(id: RPCID, topic: String, method: String, params: AnyCodable, chainId: Blockchain, expiryTimestamp: UInt64?) { self.id = id self.topic = topic self.method = method self.params = params self.chainId = chainId - self.expiry = expiry - } - - public init(topic: String, method: String, params: AnyCodable, chainId: Blockchain, expiry: UInt64? = nil) { - self.init(id: RPCID(JsonRpcID.generate()), topic: topic, method: method, params: params, chainId: chainId, expiry: expiry) - } - - init(id: RPCID, topic: String, method: String, params: C, chainId: Blockchain, expiry: UInt64?) where C: Codable { - self.init(id: id, topic: topic, method: method, params: AnyCodable(params), chainId: chainId, expiry: expiry) + self.expiryTimestamp = expiryTimestamp } func isExpired(currentDate: Date = Date()) -> Bool { - guard let expiry = expiry else { return false } - + guard let expiry = expiryTimestamp else { return false } let expiryDate = Date(timeIntervalSince1970: TimeInterval(expiry)) - - guard - abs(currentDate.distance(to: expiryDate)) < Constants.maxExpiry, - abs(currentDate.distance(to: expiryDate)) > Constants.minExpiry - else { return true } - return expiryDate < currentDate } - func calculateTtl(currentDate: Date = Date()) -> Int { - guard let expiry = expiry else { return SessionRequestProtocolMethod.defaultTtl } - + func calculateTtl(currentDate: Date = Date()) throws -> Int { + guard let expiry = expiryTimestamp else { return Int(Self.minTtl) } + let expiryDate = Date(timeIntervalSince1970: TimeInterval(expiry)) - let diff = expiryDate - currentDate.timeIntervalSince1970 - - guard - diff.timeIntervalSince1970 < Constants.maxExpiry, - diff.timeIntervalSince1970 > Constants.minExpiry - else { return SessionRequestProtocolMethod.defaultTtl } - - return Int(diff.timeIntervalSince1970) - } -} + let diff = expiryDate.timeIntervalSince(currentDate) -private extension Request { + guard diff > 0 else { + throw Errors.requestExpired + } - struct Constants { - static let minExpiry: TimeInterval = 300 // 5 minutes - static let maxExpiry: TimeInterval = 604800 // 7 days + return Int(diff) } } diff --git a/Sources/WalletConnectSign/Services/HistoryService.swift b/Sources/WalletConnectSign/Services/HistoryService.swift index 2a5974471..dba1bdbb7 100644 --- a/Sources/WalletConnectSign/Services/HistoryService.swift +++ b/Sources/WalletConnectSign/Services/HistoryService.swift @@ -3,98 +3,69 @@ import Foundation final class HistoryService { private let history: RPCHistory - private let proposalPayloadsStore: CodableStore> private let verifyContextStore: CodableStore init( history: RPCHistory, - proposalPayloadsStore: CodableStore>, verifyContextStore: CodableStore ) { self.history = history - self.proposalPayloadsStore = proposalPayloadsStore self.verifyContextStore = verifyContextStore } public func getSessionRequest(id: RPCID) -> (request: Request, context: VerifyContext?)? { guard let record = history.get(recordId: id) else { return nil } - guard let request = mapRequestRecord(record) else { + guard let (request, recordId, _) = mapRequestRecord(record) else { return nil } - return (request, try? verifyContextStore.get(key: request.id.string)) + return (request, try? verifyContextStore.get(key: recordId.string)) } - + func getPendingRequests() -> [(request: Request, context: VerifyContext?)] { - let requests = history.getPending() - .compactMap { mapRequestRecord($0) } - .filter { !$0.isExpired() } - return requests.map { ($0, try? verifyContextStore.get(key: $0.id.string)) } + getPendingRequestsSortedByTimestamp() } - func getPendingRequests(topic: String) -> [(request: Request, context: VerifyContext?)] { - return getPendingRequests().filter { $0.request.topic == topic } - } - - func getPendingProposals() -> [(proposal: Session.Proposal, context: VerifyContext?)] { - let pendingHistory = history.getPending() - - let requestSubscriptionPayloads = pendingHistory - .compactMap { record -> RequestSubscriptionPayload? in - guard let proposalParams = mapProposeParams(record) else { - return nil + func getPendingRequestsSortedByTimestamp() -> [(request: Request, context: VerifyContext?)] { + let requests = history.getPending() + .compactMap { mapRequestRecord($0) } + .filter { !$0.0.isExpired() } + .sorted { + switch ($0.2, $1.2) { + case let (date1?, date2?): return date1 < date2 // Both dates are present + case (nil, _): return false // First date is nil, so it should go last + case (_, nil): return true // Second date is nil, so the first one should come first } - return RequestSubscriptionPayload(id: record.id, topic: record.topic, request: proposalParams, decryptedPayload: Data(), publishedAt: Date(), derivedTopic: nil) } - - requestSubscriptionPayloads.forEach { - let proposal = $0.request - proposalPayloadsStore.set($0, forKey: proposal.proposer.publicKey) - } - - let proposals = pendingHistory - .compactMap { mapProposalRecord($0) } - - return proposals.map { ($0, try? verifyContextStore.get(key: $0.proposal.proposer.publicKey)) } + .map { (request: $0.0, context: try? verifyContextStore.get(key: $0.1.string)) } + + return requests + } + + func getPendingRequestsWithRecordId() -> [(request: Request, recordId: RPCID)] { + return history.getPending() + .compactMap { mapRequestRecord($0) } + .map { (request: $0.0, recordId: $0.1) } } - - func getPendingProposals(topic: String) -> [(proposal: Session.Proposal, context: VerifyContext?)] { - return getPendingProposals().filter { $0.proposal.pairingTopic == topic } + + func getPendingRequests(topic: String) -> [(request: Request, context: VerifyContext?)] { + return getPendingRequestsSortedByTimestamp().filter { $0.request.topic == topic } } } private extension HistoryService { - func mapRequestRecord(_ record: RPCHistory.Record) -> Request? { + func mapRequestRecord(_ record: RPCHistory.Record) -> (Request, RPCID, Date?)? { guard let request = try? record.request.params?.get(SessionType.RequestParams.self) else { return nil } - return Request( + let mappedRequest = Request( id: record.id, topic: record.topic, method: request.request.method, params: request.request.params, chainId: request.chainId, - expiry: request.request.expiry - ) - } - - func mapProposeParams(_ record: RPCHistory.Record) -> SessionType.ProposeParams? { - guard let proposal = try? record.request.params?.get(SessionType.ProposeParams.self) - else { return nil } - return proposal - } - - func mapProposalRecord(_ record: RPCHistory.Record) -> Session.Proposal? { - guard let proposal = try? record.request.params?.get(SessionType.ProposeParams.self) - else { return nil } - - return Session.Proposal( - id: proposal.proposer.publicKey, - pairingTopic: record.topic, - proposer: proposal.proposer.metadata, - requiredNamespaces: proposal.requiredNamespaces, - optionalNamespaces: proposal.optionalNamespaces ?? [:], - sessionProperties: proposal.sessionProperties, - proposal: proposal + expiryTimestamp: request.request.expiryTimestamp ) + + return (mappedRequest, record.id, record.timestamp) } } diff --git a/Sources/WalletConnectSign/Services/SignCleanupService.swift b/Sources/WalletConnectSign/Services/SignCleanupService.swift index abee34063..5c2a6ec1c 100644 --- a/Sources/WalletConnectSign/Services/SignCleanupService.swift +++ b/Sources/WalletConnectSign/Services/SignCleanupService.swift @@ -7,13 +7,16 @@ final class SignCleanupService { private let kms: KeyManagementServiceProtocol private let sessionTopicToProposal: CodableStore private let networkInteractor: NetworkInteracting + private let rpcHistory: RPCHistory - init(pairingStore: WCPairingStorage, sessionStore: WCSessionStorage, kms: KeyManagementServiceProtocol, sessionTopicToProposal: CodableStore, networkInteractor: NetworkInteracting) { + init(pairingStore: WCPairingStorage, sessionStore: WCSessionStorage, kms: KeyManagementServiceProtocol, sessionTopicToProposal: CodableStore, networkInteractor: NetworkInteracting, + rpcHistory: RPCHistory) { self.pairingStore = pairingStore self.sessionStore = sessionStore self.sessionTopicToProposal = sessionTopicToProposal self.networkInteractor = networkInteractor self.kms = kms + self.rpcHistory = rpcHistory } func cleanup() async throws { @@ -39,6 +42,7 @@ private extension SignCleanupService { pairingStore.deleteAll() sessionStore.deleteAll() sessionTopicToProposal.deleteAll() + rpcHistory.deleteAll() try kms.deleteAll() } } diff --git a/Sources/WalletConnectSign/Session.swift b/Sources/WalletConnectSign/Session.swift index 7e0d24fb4..06b2500f7 100644 --- a/Sources/WalletConnectSign/Session.swift +++ b/Sources/WalletConnectSign/Session.swift @@ -28,7 +28,11 @@ extension Session { // TODO: Refactor internal objects to manage only needed data internal let proposal: SessionProposal - + + func isExpired() -> Bool { + return proposal.isExpired() + } + init( id: String, pairingTopic: String, diff --git a/Sources/WalletConnectSign/Sign/SessionRequestsProvider.swift b/Sources/WalletConnectSign/Sign/SessionRequestsProvider.swift new file mode 100644 index 000000000..7838b65b7 --- /dev/null +++ b/Sources/WalletConnectSign/Sign/SessionRequestsProvider.swift @@ -0,0 +1,20 @@ +import Combine +import Foundation + +class SessionRequestsProvider { + private let historyService: HistoryService + private var sessionRequestPublisherSubject = PassthroughSubject<(request: Request, context: VerifyContext?), Never>() + public var sessionRequestPublisher: AnyPublisher<(request: Request, context: VerifyContext?), Never> { + sessionRequestPublisherSubject.eraseToAnyPublisher() + } + + init(historyService: HistoryService) { + self.historyService = historyService + } + + func emitRequestIfPending() { + if let oldestRequest = self.historyService.getPendingRequestsSortedByTimestamp().first { + self.sessionRequestPublisherSubject.send(oldestRequest) + } + } +} diff --git a/Sources/WalletConnectSign/Sign/SignClient.swift b/Sources/WalletConnectSign/Sign/SignClient.swift index b87864de1..0122f0f82 100644 --- a/Sources/WalletConnectSign/Sign/SignClient.swift +++ b/Sources/WalletConnectSign/Sign/SignClient.swift @@ -24,7 +24,7 @@ public final class SignClient: SignClientProtocol { /// /// In most cases event will be emited on wallet public var sessionRequestPublisher: AnyPublisher<(request: Request, context: VerifyContext?), Never> { - sessionRequestPublisherSubject.eraseToAnyPublisher() + sessionEngine.sessionRequestPublisher } /// Publisher that sends web socket connection status @@ -99,6 +99,20 @@ public final class SignClient: SignClientProtocol { return logger.logsPublisher } + /// Publisher that sends session proposal expiration + public var sessionProposalExpirationPublisher: AnyPublisher { + return proposalExpiryWatcher.sessionProposalExpirationPublisher + } + + public var pendingProposalsPublisher: AnyPublisher<[(proposal: Session.Proposal, context: VerifyContext?)], Never> { + return pendingProposalsProvider.pendingProposalsPublisher + } + + public var requestExpirationPublisher: AnyPublisher { + return requestsExpiryWatcher.requestExpirationPublisher + } + + /// An object that loggs SDK's errors and info messages public let logger: ConsoleLogging @@ -119,9 +133,11 @@ public final class SignClient: SignClientProtocol { private let appProposeService: AppProposeService private let historyService: HistoryService private let cleanupService: SignCleanupService + private let proposalExpiryWatcher: ProposalExpiryWatcher + private let pendingProposalsProvider: PendingProposalsProvider + private let requestsExpiryWatcher: RequestsExpiryWatcher private let sessionProposalPublisherSubject = PassthroughSubject<(proposal: Session.Proposal, context: VerifyContext?), Never>() - private let sessionRequestPublisherSubject = PassthroughSubject<(request: Request, context: VerifyContext?), Never>() private let socketConnectionStatusPublisherSubject = PassthroughSubject() private let sessionSettlePublisherSubject = PassthroughSubject() private let sessionDeletePublisherSubject = PassthroughSubject<(String, Reason), Never>() @@ -152,7 +168,10 @@ public final class SignClient: SignClientProtocol { disconnectService: DisconnectService, historyService: HistoryService, cleanupService: SignCleanupService, - pairingClient: PairingClient + pairingClient: PairingClient, + proposalExpiryWatcher: ProposalExpiryWatcher, + pendingProposalsProvider: PendingProposalsProvider, + requestsExpiryWatcher: RequestsExpiryWatcher ) { self.logger = logger self.networkingClient = networkingClient @@ -170,6 +189,9 @@ public final class SignClient: SignClientProtocol { self.cleanupService = cleanupService self.disconnectService = disconnectService self.pairingClient = pairingClient + self.proposalExpiryWatcher = proposalExpiryWatcher + self.pendingProposalsProvider = pendingProposalsProvider + self.requestsExpiryWatcher = requestsExpiryWatcher setUpConnectionObserving() setUpEnginesCallbacks() @@ -303,21 +325,9 @@ public final class SignClient: SignClientProtocol { return historyService.getPendingRequests() } } - - /// Query pending proposals - /// - Returns: Pending proposals received from peer with `wc_sessionPropose` protocol method - public func getPendingProposals(topic: String? = nil) -> [(proposal: Session.Proposal, context: VerifyContext?)] { - if let topic = topic { - return historyService.getPendingProposals(topic: topic) - } else { - return historyService.getPendingProposals() - } - } - /// - Parameter id: id of a wc_sessionRequest jsonrpc request - /// - Returns: json rpc record object for given id or nil if record for give id does not exits - public func getSessionRequestRecord(id: RPCID) -> (request: Request, context: VerifyContext?)? { - return historyService.getSessionRequest(id: id) + public func getPendingProposals(topic: String? = nil) -> [(proposal: Session.Proposal, context: VerifyContext?)] { + pendingProposalsProvider.getPendingProposals() } /// Delete all stored data such as: pairings, sessions, keys @@ -348,9 +358,6 @@ public final class SignClient: SignClientProtocol { approveEngine.onSessionSettle = { [unowned self] settledSession in sessionSettlePublisherSubject.send(settledSession) } - sessionEngine.onSessionRequest = { [unowned self] (sessionRequest, context) in - sessionRequestPublisherSubject.send((sessionRequest, context)) - } sessionEngine.onSessionDelete = { [unowned self] topic, reason in sessionDeletePublisherSubject.send((topic, reason)) } diff --git a/Sources/WalletConnectSign/Sign/SignClientFactory.swift b/Sources/WalletConnectSign/Sign/SignClientFactory.swift index 1d9adf073..7bba8b44b 100644 --- a/Sources/WalletConnectSign/Sign/SignClientFactory.swift +++ b/Sources/WalletConnectSign/Sign/SignClientFactory.swift @@ -41,9 +41,10 @@ public struct SignClientFactory { let sessionStore = SessionStorage(storage: SequenceStore(store: .init(defaults: keyValueStorage, identifier: SignStorageIdentifiers.sessions.rawValue))) let proposalPayloadsStore = CodableStore>(defaults: RuntimeKeyValueStorage(), identifier: SignStorageIdentifiers.proposals.rawValue) let verifyContextStore = CodableStore(defaults: keyValueStorage, identifier: VerifyStorageIdentifiers.context.rawValue) - let historyService = HistoryService(history: rpcHistory, proposalPayloadsStore: proposalPayloadsStore, verifyContextStore: verifyContextStore) + let historyService = HistoryService(history: rpcHistory, verifyContextStore: verifyContextStore) let verifyClient = VerifyClientFactory.create() - let sessionEngine = SessionEngine(networkingInteractor: networkingClient, historyService: historyService, verifyContextStore: verifyContextStore, verifyClient: verifyClient, kms: kms, sessionStore: sessionStore, logger: logger) + let sessionRequestsProvider = SessionRequestsProvider(historyService: historyService) + let sessionEngine = SessionEngine(networkingInteractor: networkingClient, historyService: historyService, verifyContextStore: verifyContextStore, verifyClient: verifyClient, kms: kms, sessionStore: sessionStore, logger: logger, sessionRequestsProvider: sessionRequestsProvider) let nonControllerSessionStateMachine = NonControllerSessionStateMachine(networkingInteractor: networkingClient, kms: kms, sessionStore: sessionStore, logger: logger) let controllerSessionStateMachine = ControllerSessionStateMachine(networkingInteractor: networkingClient, kms: kms, sessionStore: sessionStore, logger: logger) let sessionExtendRequester = SessionExtendRequester(sessionStore: sessionStore, networkingInteractor: networkingClient) @@ -61,14 +62,18 @@ public struct SignClientFactory { logger: logger, pairingStore: pairingStore, sessionStore: sessionStore, - verifyClient: verifyClient + verifyClient: verifyClient, + rpcHistory: rpcHistory ) - let cleanupService = SignCleanupService(pairingStore: pairingStore, sessionStore: sessionStore, kms: kms, sessionTopicToProposal: sessionTopicToProposal, networkInteractor: networkingClient) + let cleanupService = SignCleanupService(pairingStore: pairingStore, sessionStore: sessionStore, kms: kms, sessionTopicToProposal: sessionTopicToProposal, networkInteractor: networkingClient, rpcHistory: rpcHistory) let deleteSessionService = DeleteSessionService(networkingInteractor: networkingClient, kms: kms, sessionStore: sessionStore, logger: logger) let disconnectService = DisconnectService(deleteSessionService: deleteSessionService, sessionStorage: sessionStore) let sessionPingService = SessionPingService(sessionStorage: sessionStore, networkingInteractor: networkingClient, logger: logger) let pairingPingService = PairingPingService(pairingStorage: pairingStore, networkingInteractor: networkingClient, logger: logger) let appProposerService = AppProposeService(metadata: metadata, networkingInteractor: networkingClient, kms: kms, logger: logger) + let proposalExpiryWatcher = ProposalExpiryWatcher(proposalPayloadsStore: proposalPayloadsStore, rpcHistory: rpcHistory) + let pendingProposalsProvider = PendingProposalsProvider(proposalPayloadsStore: proposalPayloadsStore, verifyContextStore: verifyContextStore) + let requestsExpiryWatcher = RequestsExpiryWatcher(proposalPayloadsStore: proposalPayloadsStore, rpcHistory: rpcHistory, historyService: historyService) let client = SignClient( logger: logger, @@ -86,7 +91,10 @@ public struct SignClientFactory { disconnectService: disconnectService, historyService: historyService, cleanupService: cleanupService, - pairingClient: pairingClient + pairingClient: pairingClient, + proposalExpiryWatcher: proposalExpiryWatcher, + pendingProposalsProvider: pendingProposalsProvider, + requestsExpiryWatcher: requestsExpiryWatcher ) return client } diff --git a/Sources/WalletConnectSign/Sign/SignClientProtocol.swift b/Sources/WalletConnectSign/Sign/SignClientProtocol.swift index 2b174ddec..4aecfac04 100644 --- a/Sources/WalletConnectSign/Sign/SignClientProtocol.swift +++ b/Sources/WalletConnectSign/Sign/SignClientProtocol.swift @@ -12,6 +12,9 @@ public protocol SignClientProtocol { var sessionRejectionPublisher: AnyPublisher<(Session.Proposal, Reason), Never> { get } var sessionEventPublisher: AnyPublisher<(event: Session.Event, sessionTopic: String, chainId: Blockchain?), Never> { get } var logsPublisher: AnyPublisher {get} + var sessionProposalExpirationPublisher: AnyPublisher { get } + var pendingProposalsPublisher: AnyPublisher<[(proposal: Session.Proposal, context: VerifyContext?)], Never> { get } + var requestExpirationPublisher: AnyPublisher { get } func connect(requiredNamespaces: [String: ProposalNamespace], optionalNamespaces: [String: ProposalNamespace]?, sessionProperties: [String: String]?, topic: String) async throws func request(params: Request) async throws @@ -27,5 +30,5 @@ public protocol SignClientProtocol { func getPendingRequests(topic: String?) -> [(request: Request, context: VerifyContext?)] func getPendingProposals(topic: String?) -> [(proposal: Session.Proposal, context: VerifyContext?)] - func getSessionRequestRecord(id: RPCID) -> (request: Request, context: VerifyContext?)? } + diff --git a/Sources/WalletConnectSign/SignDecryptionService.swift b/Sources/WalletConnectSign/SignDecryptionService.swift index 8b0e82125..588d07337 100644 --- a/Sources/WalletConnectSign/SignDecryptionService.swift +++ b/Sources/WalletConnectSign/SignDecryptionService.swift @@ -37,7 +37,7 @@ public class SignDecryptionService { method: request.request.method, params: request.request.params, chainId: request.chainId, - expiry: request.request.expiry + expiryTimestamp: request.request.expiryTimestamp ) return request diff --git a/Sources/WalletConnectSign/Types/Session/SessionProposal.swift b/Sources/WalletConnectSign/Types/Session/SessionProposal.swift index fa1ee979a..2723ef46c 100644 --- a/Sources/WalletConnectSign/Types/Session/SessionProposal.swift +++ b/Sources/WalletConnectSign/Types/Session/SessionProposal.swift @@ -1,11 +1,28 @@ import Foundation struct SessionProposal: Codable, Equatable { + let relays: [RelayProtocolOptions] let proposer: Participant let requiredNamespaces: [String: ProposalNamespace] let optionalNamespaces: [String: ProposalNamespace]? let sessionProperties: [String: String]? + let expiryTimestamp: UInt64? + + static let proposalTtl: TimeInterval = 300 // 5 minutes + + internal init(relays: [RelayProtocolOptions], + proposer: Participant, + requiredNamespaces: [String : ProposalNamespace], + optionalNamespaces: [String : ProposalNamespace]? = nil, + sessionProperties: [String : String]? = nil) { + self.relays = relays + self.proposer = proposer + self.requiredNamespaces = requiredNamespaces + self.optionalNamespaces = optionalNamespaces + self.sessionProperties = sessionProperties + self.expiryTimestamp = UInt64(Date().timeIntervalSince1970 + Self.proposalTtl) + } func publicRepresentation(pairingTopic: String) -> Session.Proposal { return Session.Proposal( @@ -18,4 +35,12 @@ struct SessionProposal: Codable, Equatable { proposal: self ) } + + func isExpired(currentDate: Date = Date()) -> Bool { + guard let expiry = expiryTimestamp else { return false } + + let expiryDate = Date(timeIntervalSince1970: TimeInterval(expiry)) + + return expiryDate < currentDate + } } diff --git a/Sources/WalletConnectSign/Types/Session/SessionType.swift b/Sources/WalletConnectSign/Types/Session/SessionType.swift index cc838f084..d4411aa9a 100644 --- a/Sources/WalletConnectSign/Types/Session/SessionType.swift +++ b/Sources/WalletConnectSign/Types/Session/SessionType.swift @@ -43,7 +43,7 @@ internal enum SessionType { struct Request: Codable, Equatable { let method: String let params: AnyCodable - let expiry: UInt64? + let expiryTimestamp: UInt64? } } diff --git a/Sources/WalletConnectSign/Types/Session/WCSession.swift b/Sources/WalletConnectSign/Types/Session/WCSession.swift index 30d237a85..a24b13691 100644 --- a/Sources/WalletConnectSign/Types/Session/WCSession.swift +++ b/Sources/WalletConnectSign/Types/Session/WCSession.swift @@ -65,7 +65,7 @@ struct WCSession: SequenceObject, Equatable { events: Set, accounts: Set, acknowledged: Bool, - expiry: Int64 + expiryTimestamp: Int64 ) { self.topic = topic self.pairingTopic = pairingTopic @@ -78,7 +78,7 @@ struct WCSession: SequenceObject, Equatable { self.sessionProperties = sessionProperties self.requiredNamespaces = requiredNamespaces self.acknowledged = acknowledged - self.expiryDate = Date(timeIntervalSince1970: TimeInterval(expiry)) + self.expiryDate = Date(timeIntervalSince1970: TimeInterval(expiryTimestamp)) } #endif diff --git a/Sources/WalletConnectUtils/RPCHistory/RPCHistory.swift b/Sources/WalletConnectUtils/RPCHistory/RPCHistory.swift index 4fc00aebe..ff6bda280 100644 --- a/Sources/WalletConnectUtils/RPCHistory/RPCHistory.swift +++ b/Sources/WalletConnectUtils/RPCHistory/RPCHistory.swift @@ -1,3 +1,5 @@ +import Foundation + public final class RPCHistory { public struct Record: Codable { @@ -9,51 +11,74 @@ public final class RPCHistory { public let topic: String let origin: Origin public let request: RPCRequest - public var response: RPCResponse? + public let response: RPCResponse? + public var timestamp: Date? } - enum HistoryError: Error { + enum HistoryError: Error, LocalizedError { case unidentifiedRequest case unidentifiedResponse case requestDuplicateNotAllowed case responseDuplicateNotAllowed case requestMatchingResponseNotFound + var errorDescription: String? { + switch self { + case .unidentifiedRequest: + return "Unidentified request." + case .unidentifiedResponse: + return "Unidentified response." + case .requestDuplicateNotAllowed: + return "Request duplicates are not allowed." + case .responseDuplicateNotAllowed: + return "Response duplicates are not allowed." + case .requestMatchingResponseNotFound: + return "Matching request for the response not found." + } + } } + private let storage: CodableStore init(keyValueStore: CodableStore) { self.storage = keyValueStore + + removeOutdated() } public func get(recordId: RPCID) -> Record? { try? storage.get(key: recordId.string) } - public func set(_ request: RPCRequest, forTopic topic: String, emmitedBy origin: Record.Origin) throws { + public func set(_ request: RPCRequest, forTopic topic: String, emmitedBy origin: Record.Origin, time: TimeProvider = DefaultTimeProvider()) throws { guard let id = request.id else { throw HistoryError.unidentifiedRequest } guard get(recordId: id) == nil else { throw HistoryError.requestDuplicateNotAllowed } - let record = Record(id: id, topic: topic, origin: origin, request: request) + let record = Record(id: id, topic: topic, origin: origin, request: request, response: nil, timestamp: time.currentDate) storage.set(record, forKey: "\(record.id)") } @discardableResult public func resolve(_ response: RPCResponse) throws -> Record { + let record = try validate(response) + storage.delete(forKey: "\(record.id)") + return record + } + + @discardableResult + public func validate(_ response: RPCResponse) throws -> Record { guard let id = response.id else { throw HistoryError.unidentifiedResponse } - guard var record = get(recordId: id) else { + guard let record = get(recordId: id) else { throw HistoryError.requestMatchingResponseNotFound } guard record.response == nil else { throw HistoryError.responseDuplicateNotAllowed } - record.response = response - storage.set(record, forKey: "\(record.id)") return record } @@ -94,4 +119,28 @@ public final class RPCHistory { public func getPending() -> [Record] { storage.getAll().filter { $0.response == nil } } + + public func deleteAll() { + storage.deleteAll() + } +} + +extension RPCHistory { + + func removeOutdated() { + let records = storage.getAll() + + let thirtyDays: TimeInterval = 30*86400 + + for var record in records { + if let timestamp = record.timestamp { + if timestamp.distance(to: Date()) > thirtyDays { + storage.delete(forKey: record.id.string) + } + } else { + record.timestamp = Date() + storage.set(record, forKey: "\(record.id)") + } + } + } } diff --git a/Sources/WalletConnectUtils/RelayProtocolOptions.swift b/Sources/WalletConnectUtils/RelayProtocolOptions.swift index e437e9342..5a19d9f05 100644 --- a/Sources/WalletConnectUtils/RelayProtocolOptions.swift +++ b/Sources/WalletConnectUtils/RelayProtocolOptions.swift @@ -9,3 +9,11 @@ public struct RelayProtocolOptions: Codable, Equatable { self.data = data } } + +#if DEBUG +public extension RelayProtocolOptions { + static func stub() -> RelayProtocolOptions { + RelayProtocolOptions(protocol: "", data: nil) + } +} +#endif diff --git a/Sources/WalletConnectUtils/SequenceStore.swift b/Sources/WalletConnectUtils/SequenceStore.swift index 618ba411e..94591af29 100644 --- a/Sources/WalletConnectUtils/SequenceStore.swift +++ b/Sources/WalletConnectUtils/SequenceStore.swift @@ -12,10 +12,13 @@ public final class SequenceStore where T: SequenceObject { private let store: CodableStore private let dateInitializer: () -> Date + private var expiryMonitorTimer: Timer? + public init(store: CodableStore, dateInitializer: @escaping () -> Date = Date.init) { self.store = store self.dateInitializer = dateInitializer + startExpiryMonitor() } public func hasSequence(forTopic topic: String) -> Bool { @@ -46,6 +49,19 @@ public final class SequenceStore where T: SequenceObject { store.deleteAll() onSequenceUpdate?() } + + // MARK: Expiry Monitor + + private func startExpiryMonitor() { + expiryMonitorTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true) { [weak self] _ in + self?.checkAllSequencesForExpiry() + } + } + + private func checkAllSequencesForExpiry() { + let allSequences = getAll() + allSequences.forEach { _ = verifyExpiry(on: $0) } + } } // MARK: Privates diff --git a/Sources/WalletConnectUtils/TimeProvider.swift b/Sources/WalletConnectUtils/TimeProvider.swift new file mode 100644 index 000000000..86732b6f0 --- /dev/null +++ b/Sources/WalletConnectUtils/TimeProvider.swift @@ -0,0 +1,12 @@ +import Foundation + +public protocol TimeProvider { + var currentDate: Date { get } +} + +public struct DefaultTimeProvider: TimeProvider { + public init() {} + public var currentDate: Date { + return Date() + } +} diff --git a/Sources/WalletConnectUtils/WalletConnectURI.swift b/Sources/WalletConnectUtils/WalletConnectURI.swift index 2f068a67f..4e8701b50 100644 --- a/Sources/WalletConnectUtils/WalletConnectURI.swift +++ b/Sources/WalletConnectUtils/WalletConnectURI.swift @@ -1,13 +1,18 @@ import Foundation public struct WalletConnectURI: Equatable { + public enum Errors: Error { + case expired + case invalidFormat + } public let topic: String public let version: String public let symKey: String public let relay: RelayProtocolOptions + public let expiryTimestamp: UInt64 public var absoluteString: String { - return "wc:\(topic)@\(version)?symKey=\(symKey)&\(relayQuery)" + return "wc:\(topic)@\(version)?symKey=\(symKey)&\(relayQuery)&expiryTimestamp=\(expiryTimestamp)" } public var deeplinkUri: String { @@ -20,12 +25,26 @@ public struct WalletConnectURI: Equatable { self.topic = topic self.symKey = symKey self.relay = relay + + // Only after all properties are initialized, you can use self or its methods + self.expiryTimestamp = fiveMinutesFromNow } + @available(*, deprecated, message: "Use the throwing initializer instead") public init?(string: String) { - guard let components = Self.parseURIComponents(from: string) else { + do { + try self.init(uriString: string) + } catch { + print("Initialization failed: \(error.localizedDescription)") return nil } + } + + public init(uriString: String) throws { + let decodedString = uriString.removingPercentEncoding ?? uriString + guard let components = Self.parseURIComponents(from: decodedString) else { + throw Errors.invalidFormat + } let query: [String: String]? = components.queryItems?.reduce(into: [:]) { $0[$1.name] = $1.value } guard @@ -34,22 +53,31 @@ public struct WalletConnectURI: Equatable { let symKey = query?["symKey"], let relayProtocol = query?["relay-protocol"] else { - return nil + throw Errors.invalidFormat } + let relayData = query?["relay-data"] + // Check if expiryTimestamp is provided and valid + if let expiryTimestampString = query?["expiryTimestamp"], + let expiryTimestamp = UInt64(expiryTimestampString), + expiryTimestamp <= UInt64(Date().timeIntervalSince1970) { + throw Errors.expired + } + self.version = version self.topic = topic self.symKey = symKey self.relay = RelayProtocolOptions(protocol: relayProtocol, data: relayData) + // Set expiryTimestamp to 5 minutes in the future if not included in the uri + self.expiryTimestamp = UInt64(query?["expiryTimestamp"] ?? "") ?? fiveMinutesFromNow + } - - public init?(deeplinkUri: URL) { - if let deeplinkUri = deeplinkUri.query?.replacingOccurrences(of: "uri=", with: "") { - self.init(string: deeplinkUri) - } else { - return nil - } + + + public init(deeplinkUri: URL) throws { + let uriString = deeplinkUri.query?.replacingOccurrences(of: "uri=", with: "") ?? "" + try self.init(uriString: uriString) } private var relayQuery: String { @@ -61,34 +89,64 @@ public struct WalletConnectURI: Equatable { } private static func parseURIComponents(from string: String) -> URLComponents? { - guard string.hasPrefix("wc:") else { + let decodedString = string.removingPercentEncoding ?? string + guard decodedString.hasPrefix("wc:") else { return nil } - let urlString = !string.hasPrefix("wc://") ? string.replacingOccurrences(of: "wc:", with: "wc://") : string + let urlString = !decodedString.hasPrefix("wc://") ? decodedString.replacingOccurrences(of: "wc:", with: "wc://") : decodedString return URLComponents(string: urlString) } } +extension WalletConnectURI.Errors: LocalizedError { + public var errorDescription: String? { + switch self { + case .expired: + return NSLocalizedString("The WalletConnect Pairing URI has expired.", comment: "Expired URI Error") + case .invalidFormat: + return NSLocalizedString("The format of the WalletConnect Pairing URI is invalid.", comment: "Invalid Format URI Error") + } + } +} + + +fileprivate var fiveMinutesFromNow: UInt64 { + return UInt64(Date().timeIntervalSince1970) + 5 * 60 +} + + #if canImport(UIKit) import UIKit extension WalletConnectURI { - public init?(connectionOptions: UIScene.ConnectionOptions) { + public init(connectionOptions: UIScene.ConnectionOptions) throws { if let uri = connectionOptions.urlContexts.first?.url.query?.replacingOccurrences(of: "uri=", with: "") { - self.init(string: uri) + try self.init(uriString: uri) } else { - return nil + throw Errors.invalidFormat } } - public init?(urlContext: UIOpenURLContext) { + public init(urlContext: UIOpenURLContext) throws { if let uri = urlContext.url.query?.replacingOccurrences(of: "uri=", with: "") { - self.init(string: uri) + try self.init(uriString: uri) } else { - return nil + throw Errors.invalidFormat } } } +#endif +#if DEBUG +extension WalletConnectURI { + init(topic: String, symKey: String, relay: RelayProtocolOptions, expiryTimestamp: UInt64) { + self.version = "2" + self.topic = topic + self.symKey = symKey + self.relay = relay + self.expiryTimestamp = expiryTimestamp + } + +} #endif diff --git a/Sources/Web3Wallet/Web3WalletClient.swift b/Sources/Web3Wallet/Web3WalletClient.swift index 5b70a374a..0930b8443 100644 --- a/Sources/Web3Wallet/Web3WalletClient.swift +++ b/Sources/Web3Wallet/Web3WalletClient.swift @@ -67,12 +67,33 @@ public class Web3WalletClient { pairingClient.pairingDeletePublisher } + public var pairingStatePublisher: AnyPublisher { + pairingClient.pairingStatePublisher + } + + public var pairingExpirationPublisher: AnyPublisher { + return pairingClient.pairingExpirationPublisher + } + public var logsPublisher: AnyPublisher { return signClient.logsPublisher .merge(with: pairingClient.logsPublisher) .eraseToAnyPublisher() } + /// Publisher that sends session proposal expiration + public var sessionProposalExpirationPublisher: AnyPublisher { + return signClient.sessionProposalExpirationPublisher + } + + public var pendingProposalsPublisher: AnyPublisher<[(proposal: Session.Proposal, context: VerifyContext?)], Never> { + return signClient.pendingProposalsPublisher + } + + public var requestExpirationPublisher: AnyPublisher { + return signClient.requestExpirationPublisher + } + // MARK: - Private Properties private let authClient: AuthClientProtocol private let signClient: SignClientProtocol @@ -203,19 +224,11 @@ public class Web3WalletClient { public func getPendingRequests(topic: String? = nil) -> [(request: Request, context: VerifyContext?)] { signClient.getPendingRequests(topic: topic) } - - /// Query pending proposals - /// - Returns: Pending proposals received from peer with `wc_sessionPropose` protocol method + public func getPendingProposals(topic: String? = nil) -> [(proposal: Session.Proposal, context: VerifyContext?)] { signClient.getPendingProposals(topic: topic) } - - /// - Parameter id: id of a wc_sessionRequest jsonrpc request - /// - Returns: json rpc record object for given id or nil if record for give id does not exits - public func getSessionRequestRecord(id: RPCID) -> (request: Request, context: VerifyContext?)? { - signClient.getSessionRequestRecord(id: id) - } - + /// Query pending authentication requests /// - Returns: Pending authentication requests public func getPendingRequests() throws -> [(AuthRequest, VerifyContext?)] { diff --git a/Tests/RelayerTests/DispatcherTests.swift b/Tests/RelayerTests/DispatcherTests.swift index 8d86455df..331bd640d 100644 --- a/Tests/RelayerTests/DispatcherTests.swift +++ b/Tests/RelayerTests/DispatcherTests.swift @@ -62,6 +62,7 @@ final class DispatcherTests: XCTestCase { networkMonitor = NetworkMonitoringMock() let defaults = RuntimeKeyValueStorage() let logger = ConsoleLoggerMock() + let networkMonitor = NetworkMonitoringMock() let keychainStorageMock = DispatcherKeychainStorageMock() let clientIdStorage = ClientIdStorage(defaults: defaults, keychain: keychainStorageMock, logger: logger) let socketAuthenticator = ClientIdAuthenticator(clientIdStorage: clientIdStorage) @@ -72,7 +73,8 @@ final class DispatcherTests: XCTestCase { ) sut = Dispatcher( socketFactory: webSocketFactory, - relayUrlFactory: relayUrlFactory, + relayUrlFactory: relayUrlFactory, + networkMonitor: networkMonitor, socketConnectionType: .manual, logger: ConsoleLoggerMock() ) diff --git a/Tests/RelayerTests/Helpers/Error+Extension.swift b/Tests/RelayerTests/Helpers/Error+Extension.swift index 901d2d829..76dd92672 100644 --- a/Tests/RelayerTests/Helpers/Error+Extension.swift +++ b/Tests/RelayerTests/Helpers/Error+Extension.swift @@ -24,7 +24,7 @@ extension Error { extension NetworkError { var isWebSocketError: Bool { - guard case .webSocketNotConnected = self else { return false } + guard case .connectionFailed = self else { return false } return true } diff --git a/Tests/RelayerTests/Mocks/DispatcherMock.swift b/Tests/RelayerTests/Mocks/DispatcherMock.swift index d5088bf61..869e3a0f9 100644 --- a/Tests/RelayerTests/Mocks/DispatcherMock.swift +++ b/Tests/RelayerTests/Mocks/DispatcherMock.swift @@ -4,11 +4,15 @@ import Combine @testable import WalletConnectRelay class DispatcherMock: Dispatching { + private var publishers = Set() private let socketConnectionStatusPublisherSubject = CurrentValueSubject(.disconnected) var socketConnectionStatusPublisher: AnyPublisher { return socketConnectionStatusPublisherSubject.eraseToAnyPublisher() } + var networkConnectionStatusPublisher: AnyPublisher { + return Just(.connected).eraseToAnyPublisher() + } var sent = false var lastMessage: String = "" diff --git a/Tests/RelayerTests/Mocks/NetworkMonitoringMock.swift b/Tests/RelayerTests/Mocks/NetworkMonitoringMock.swift index 1095d1677..bfbad58cf 100644 --- a/Tests/RelayerTests/Mocks/NetworkMonitoringMock.swift +++ b/Tests/RelayerTests/Mocks/NetworkMonitoringMock.swift @@ -4,6 +4,10 @@ import Combine @testable import WalletConnectRelay class NetworkMonitoringMock: NetworkMonitoring { + var isConnected: Bool { + return true + } + var networkConnectionStatusPublisher: AnyPublisher { networkConnectionStatusPublisherSubject.eraseToAnyPublisher() } diff --git a/Tests/TestingUtils/NetworkingInteractorMock.swift b/Tests/TestingUtils/NetworkingInteractorMock.swift index 27fa35a5c..a1fa196ad 100644 --- a/Tests/TestingUtils/NetworkingInteractorMock.swift +++ b/Tests/TestingUtils/NetworkingInteractorMock.swift @@ -134,6 +134,36 @@ public class NetworkingInteractorMock: NetworkInteracting { }.store(in: &publishers) } + public func awaitResponse( + request: RPCRequest, + topic: String, + method: ProtocolMethod, + requestOfType: Request.Type, + responseOfType: Response.Type, + envelopeType: Envelope.EnvelopeType + ) async throws -> Response { + + try await self.request(request, topic: topic, protocolMethod: method, envelopeType: envelopeType) + + return try await withCheckedThrowingContinuation { [unowned self] continuation in + var response, error: AnyCancellable? + + response = responseSubscription(on: method) + .sink { (payload: ResponseSubscriptionPayload) in + response?.cancel() + error?.cancel() + continuation.resume(with: .success(payload.response)) + } + + error = responseErrorSubscription(on: method) + .sink { (payload: ResponseSubscriptionErrorPayload) in + response?.cancel() + error?.cancel() + continuation.resume(throwing: payload.error) + } + } + } + public func subscribe(topic: String) async throws { defer { onSubscribeCalled?() } subscriptions.append(topic) diff --git a/Tests/TestingUtils/Stubs/AppMetadata+Stub.swift b/Tests/TestingUtils/Stubs/AppMetadata+Stub.swift deleted file mode 100644 index ffa368454..000000000 --- a/Tests/TestingUtils/Stubs/AppMetadata+Stub.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Foundation -import WalletConnectPairing - -public extension AppMetadata { - static func stub() -> AppMetadata { - AppMetadata( - name: "Wallet Connect", - description: "A protocol to connect blockchain wallets to dapps.", - url: "https://walletconnect.com/", - icons: [], - redirect: AppMetadata.Redirect(native: "", universal: nil) - ) - } -} diff --git a/Tests/TestingUtils/Stubs/RelayProtocolOptions+Stub.swift b/Tests/TestingUtils/Stubs/RelayProtocolOptions+Stub.swift deleted file mode 100644 index bd3db0839..000000000 --- a/Tests/TestingUtils/Stubs/RelayProtocolOptions+Stub.swift +++ /dev/null @@ -1,8 +0,0 @@ -import WalletConnectUtils - -extension RelayProtocolOptions { - - public static func stub() -> RelayProtocolOptions { - RelayProtocolOptions(protocol: "", data: nil) - } -} diff --git a/Tests/TestingUtils/Stubs/WalletConnectURI+Stub.swift b/Tests/TestingUtils/Stubs/WalletConnectURI+Stub.swift deleted file mode 100644 index 67ee5dfe9..000000000 --- a/Tests/TestingUtils/Stubs/WalletConnectURI+Stub.swift +++ /dev/null @@ -1,13 +0,0 @@ -import WalletConnectKMS -import WalletConnectUtils - -extension WalletConnectURI { - - public static func stub(isController: Bool = false) -> WalletConnectURI { - WalletConnectURI( - topic: String.generateTopic(), - symKey: SymmetricKey().hexRepresentation, - relay: RelayProtocolOptions(protocol: "", data: nil) - ) - } -} diff --git a/Tests/WalletConnectModalTests/ExplorerAPITests.swift b/Tests/WalletConnectModalTests/ExplorerAPITests.swift index 14f0f6bf5..26bdb83e9 100644 --- a/Tests/WalletConnectModalTests/ExplorerAPITests.swift +++ b/Tests/WalletConnectModalTests/ExplorerAPITests.swift @@ -6,18 +6,31 @@ final class ExplorerAPITests: XCTestCase { func testCorrectMappingOfWalletIds() throws { - let request = ExplorerAPI - .getListings(projectId: "123", metadata: .stub(), recommendedIds: ["foo", "bar"], excludedIds: ["boo", "far"]) + let request = Web3ModalAPI + .getWallets( + params: .init( + page: 2, + entries: 40, + search: "", + projectId: "123", + metadata: .stub(), + recommendedIds: ["foo", "bar"], + excludedIds: ["boo", "far"] + ) + ) .resolve(for: "www.google.com") XCTAssertEqual(request?.allHTTPHeaderFields?["Referer"], "Wallet Connect") + XCTAssertEqual(request?.allHTTPHeaderFields?["x-sdk-version"], WalletConnectModal.Config.sdkVersion) + XCTAssertEqual(request?.allHTTPHeaderFields?["x-sdk-type"], "wcm") + XCTAssertEqual(request?.allHTTPHeaderFields?["x-project-id"], "123") XCTAssertEqual(request?.url?.queryParameters, [ - "projectId": "123", "recommendedIds": "foo,bar", + "page": "2", + "entries": "40", + "platform": "ios", "excludedIds": "boo,far", - "sdkVersion": EnvironmentInfo.sdkName, - "sdkType": "wcm" ]) } } diff --git a/Tests/WalletConnectModalTests/Mocks/ModalSheetInteractorMock.swift b/Tests/WalletConnectModalTests/Mocks/ModalSheetInteractorMock.swift index 182e50d0f..23ed24b76 100644 --- a/Tests/WalletConnectModalTests/Mocks/ModalSheetInteractorMock.swift +++ b/Tests/WalletConnectModalTests/Mocks/ModalSheetInteractorMock.swift @@ -1,24 +1,24 @@ import Combine import Foundation import WalletConnectSign -import WalletConnectUtils +@testable import WalletConnectUtils @testable import WalletConnectModal @testable import WalletConnectSign final class ModalSheetInteractorMock: ModalSheetInteractor { - var listings: [Listing] + var wallets: [Wallet] - init(listings: [Listing] = Listing.stubList) { - self.listings = listings + init(wallets: [Wallet] = Wallet.stubList) { + self.wallets = wallets } - func getListings() async throws -> [Listing] { - listings + func getWallets(page: Int, entries: Int) async throws -> (Int, [Wallet]) { + (1, wallets) } func createPairingAndConnect() async throws -> WalletConnectURI? { - .init(topic: "foo", symKey: "bar", relay: .init(protocol: "irn", data: nil)) + .init(topic: "foo", symKey: "bar", relay: .init(protocol: "irn", data: nil), expiryTimestamp: 1706001526) } var sessionSettlePublisher: AnyPublisher { diff --git a/Tests/WalletConnectModalTests/ModalViewModelTests.swift b/Tests/WalletConnectModalTests/ModalViewModelTests.swift index 55de25cc9..a7ec21f6d 100644 --- a/Tests/WalletConnectModalTests/ModalViewModelTests.swift +++ b/Tests/WalletConnectModalTests/ModalViewModelTests.swift @@ -17,44 +17,28 @@ final class ModalViewModelTests: XCTestCase { sut = .init( isShown: .constant(true), - interactor: ModalSheetInteractorMock(listings: [ - Listing( + interactor: ModalSheetInteractorMock(wallets: [ + Wallet( id: "1", name: "Sample App", - homepage: "https://example.com", + homepage: "https://example.com/cool", + imageId: "0528ee7e-16d1-4089-21e3-bbfb41933100", order: 1, - imageId: "1", - app: Listing.App( - ios: "https://example.com/download-ios", - browser: "https://example.com/wallet" - ), - mobile: Listing.Links( - native: nil, - universal: "https://example.com/universal" - ), - desktop: Listing.Links( - native: nil, - universal: "https://example.com/universal" - ) + mobileLink: "https://example.com/universal/", + desktopLink: "sampleapp://deeplink", + webappLink: "https://sample.com/foo/webapp", + appStore: "" ), - Listing( + Wallet( id: "2", name: "Awesome App", - homepage: "https://example.com/awesome", + homepage: "https://example.com/cool", + imageId: "5195e9db-94d8-4579-6f11-ef553be95100", order: 2, - imageId: "2", - app: Listing.App( - ios: "https://example.com/download-ios", - browser: "https://example.com/wallet" - ), - mobile: Listing.Links( - native: "awesomeapp://deeplink", - universal: "https://awesome.com/awesome/universal" - ), - desktop: Listing.Links( - native: "awesomeapp://deeplink", - universal: "https://awesome.com/awesome/desktop/universal" - ) + mobileLink: "awesomeapp://deeplink", + desktopLink: "awesomeapp://deeplink", + webappLink: "https://awesome.com/awesome/universal/", + appStore: "" ), ]), uiApplicationWrapper: .init( @@ -82,49 +66,49 @@ final class ModalViewModelTests: XCTestCase { await sut.fetchWallets() await sut.createURI() - XCTAssertEqual(sut.uri, "wc:foo@2?symKey=bar&relay-protocol=irn") + XCTAssertEqual(sut.uri, "wc:foo@2?symKey=bar&relay-protocol=irn&expiryTimestamp=1706001526") XCTAssertEqual(sut.wallets.count, 2) XCTAssertEqual(sut.wallets.map(\.id), ["1", "2"]) XCTAssertEqual(sut.wallets.map(\.name), ["Sample App", "Awesome App"]) - expectation = XCTestExpectation(description: "Wait for openUrl to be called") + expectation = XCTestExpectation(description: "Wait for openUrl to be called using native link") - sut.navigateToDeepLink(wallet: sut.wallets[0], preferUniversal: true, preferBrowser: false) + sut.navigateToDeepLink(wallet: sut.wallets[1], preferBrowser: false) XCTWaiter.wait(for: [expectation], timeout: 3) XCTAssertEqual( openURLFuncTest.currentValue, - URL(string: "https://example.com/universal/wc?uri=wc%3Afoo%402%3FsymKey%3Dbar%26relay-protocol%3Dirn")! + URL(string: "awesomeapp://deeplinkwc?uri=wc%3Afoo%402%3FsymKey%3Dbar%26relay-protocol%3Dirn%26expiryTimestamp%3D1706001526")! ) expectation = XCTestExpectation(description: "Wait for openUrl to be called using universal link") - sut.navigateToDeepLink(wallet: sut.wallets[1], preferUniversal: false, preferBrowser: false) + sut.navigateToDeepLink(wallet: sut.wallets[1], preferBrowser: false) XCTWaiter.wait(for: [expectation], timeout: 3) XCTAssertEqual( openURLFuncTest.currentValue, - URL(string: "awesomeapp://deeplinkwc?uri=wc%3Afoo%402%3FsymKey%3Dbar%26relay-protocol%3Dirn")! + URL(string: "awesomeapp://deeplinkwc?uri=wc%3Afoo%402%3FsymKey%3Dbar%26relay-protocol%3Dirn%26expiryTimestamp%3D1706001526")! ) - expectation = XCTestExpectation(description: "Wait for openUrl to be called using native link") + expectation = XCTestExpectation(description: "Wait for openUrl to be called using webapp link") - sut.navigateToDeepLink(wallet: sut.wallets[1], preferUniversal: true, preferBrowser: false) + sut.navigateToDeepLink(wallet: sut.wallets[1], preferBrowser: true) XCTWaiter.wait(for: [expectation], timeout: 3) XCTAssertEqual( openURLFuncTest.currentValue, - URL(string: "https://awesome.com/awesome/universal/wc?uri=wc%3Afoo%402%3FsymKey%3Dbar%26relay-protocol%3Dirn")! + URL(string: "https://awesome.com/awesome/universal/wc?uri=wc%3Afoo%402%3FsymKey%3Dbar%26relay-protocol%3Dirn%26expiryTimestamp%3D1706001526")! ) expectation = XCTestExpectation(description: "Wait for openUrl to be called using native link") - sut.navigateToDeepLink(wallet: sut.wallets[1], preferUniversal: false, preferBrowser: true) + sut.navigateToDeepLink(wallet: sut.wallets[1], preferBrowser: true) XCTWaiter.wait(for: [expectation], timeout: 3) XCTAssertEqual( openURLFuncTest.currentValue, - URL(string: "https://awesome.com/awesome/desktop/universal/wc?uri=wc%3Afoo%402%3FsymKey%3Dbar%26relay-protocol%3Dirn")! + URL(string: "https://awesome.com/awesome/universal/wc?uri=wc%3Afoo%402%3FsymKey%3Dbar%26relay-protocol%3Dirn%26expiryTimestamp%3D1706001526")! ) } } diff --git a/Tests/WalletConnectPairingTests/AppPairActivationServiceTests.swift b/Tests/WalletConnectPairingTests/AppPairActivationServiceTests.swift index 9cfd39b60..601862ad6 100644 --- a/Tests/WalletConnectPairingTests/AppPairActivationServiceTests.swift +++ b/Tests/WalletConnectPairingTests/AppPairActivationServiceTests.swift @@ -22,8 +22,8 @@ final class AppPairActivationServiceTests: XCTestCase { } func testActivate() { - let topic = "topic" - let pairing = WCPairing(topic: topic) + let pairing = WCPairing(uri: WalletConnectURI.stub()) + let topic = pairing.topic let date = pairing.expiryDate storageMock.setPairing(pairing) diff --git a/Tests/WalletConnectPairingTests/WCPairingTests.swift b/Tests/WalletConnectPairingTests/WCPairingTests.swift index f180efee2..8565b5bfa 100644 --- a/Tests/WalletConnectPairingTests/WCPairingTests.swift +++ b/Tests/WalletConnectPairingTests/WCPairingTests.swift @@ -23,21 +23,21 @@ final class WCPairingTests: XCTestCase { } func testInitInactiveFromTopic() { - let pairing = WCPairing(topic: "") + let pairing = WCPairing(uri: WalletConnectURI.stub()) let inactiveExpiry = referenceDate.advanced(by: WCPairing.timeToLiveInactive) XCTAssertFalse(pairing.active) - XCTAssertEqual(pairing.expiryDate, inactiveExpiry) + XCTAssertEqual(pairing.expiryDate.timeIntervalSince1970, inactiveExpiry.timeIntervalSince1970, accuracy: 1) } func testInitInactiveFromURI() { let pairing = WCPairing(uri: WalletConnectURI.stub()) let inactiveExpiry = referenceDate.advanced(by: WCPairing.timeToLiveInactive) XCTAssertFalse(pairing.active) - XCTAssertEqual(pairing.expiryDate, inactiveExpiry) + XCTAssertEqual(pairing.expiryDate.timeIntervalSince1970, inactiveExpiry.timeIntervalSince1970, accuracy: 1) } func testUpdateExpiryForTopic() { - var pairing = WCPairing(topic: "") + var pairing = WCPairing(uri: WalletConnectURI.stub()) let activeExpiry = referenceDate.advanced(by: WCPairing.timeToLiveActive) try? pairing.updateExpiry() XCTAssertEqual(pairing.expiryDate, activeExpiry) @@ -51,7 +51,7 @@ final class WCPairingTests: XCTestCase { } func testActivateTopic() { - var pairing = WCPairing(topic: "") + var pairing = WCPairing(uri: WalletConnectURI.stub()) let activeExpiry = referenceDate.advanced(by: WCPairing.timeToLiveActive) XCTAssertFalse(pairing.active) pairing.activate() diff --git a/Tests/WalletConnectSignTests/AppProposalServiceTests.swift b/Tests/WalletConnectSignTests/AppProposalServiceTests.swift index bdc8c7180..5e4b7da61 100644 --- a/Tests/WalletConnectSignTests/AppProposalServiceTests.swift +++ b/Tests/WalletConnectSignTests/AppProposalServiceTests.swift @@ -5,7 +5,7 @@ import JSONRPC @testable import TestingUtils @testable import WalletConnectKMS @testable import WalletConnectPairing -import WalletConnectUtils +@testable import WalletConnectUtils func deriveTopic(publicKey: String, privateKey: AgreementPrivateKey) -> String { try! KeyManagementService.generateAgreementKey(from: privateKey, peerPublicKey: publicKey).derivedTopic() @@ -59,6 +59,12 @@ final class AppProposalServiceTests: XCTestCase { kms: cryptoMock, logger: logger ) + let history = RPCHistory( + keyValueStore: .init( + defaults: RuntimeKeyValueStorage(), + identifier: "" + ) + ) approveEngine = ApproveEngine( networkingInteractor: networkingInteractor, proposalPayloadsStore: .init(defaults: RuntimeKeyValueStorage(), identifier: ""), @@ -70,7 +76,8 @@ final class AppProposalServiceTests: XCTestCase { logger: logger, pairingStore: storageMock, sessionStore: WCSessionStorageMock(), - verifyClient: VerifyClientMock() + verifyClient: VerifyClientMock(), + rpcHistory: history ) } diff --git a/Tests/WalletConnectSignTests/ApproveEngineTests.swift b/Tests/WalletConnectSignTests/ApproveEngineTests.swift index de84c86d2..c840d0ad0 100644 --- a/Tests/WalletConnectSignTests/ApproveEngineTests.swift +++ b/Tests/WalletConnectSignTests/ApproveEngineTests.swift @@ -1,12 +1,12 @@ import XCTest import Combine import JSONRPC -import WalletConnectUtils -import WalletConnectPairing import WalletConnectNetworking +@testable import WalletConnectPairing @testable import WalletConnectSign @testable import TestingUtils @testable import WalletConnectKMS +@testable import WalletConnectUtils final class ApproveEngineTests: XCTestCase { @@ -33,6 +33,12 @@ final class ApproveEngineTests: XCTestCase { proposalPayloadsStore = CodableStore>(defaults: RuntimeKeyValueStorage(), identifier: "") verifyContextStore = CodableStore(defaults: RuntimeKeyValueStorage(), identifier: "") sessionTopicToProposal = CodableStore(defaults: RuntimeKeyValueStorage(), identifier: "") + let history = RPCHistory( + keyValueStore: .init( + defaults: RuntimeKeyValueStorage(), + identifier: "" + ) + ) engine = ApproveEngine( networkingInteractor: networkingInteractor, proposalPayloadsStore: proposalPayloadsStore, @@ -44,7 +50,8 @@ final class ApproveEngineTests: XCTestCase { logger: ConsoleLoggerMock(), pairingStore: pairingStorageMock, sessionStore: sessionStorageMock, - verifyClient: VerifyClientMock() + verifyClient: VerifyClientMock(), + rpcHistory: history ) } @@ -73,6 +80,7 @@ final class ApproveEngineTests: XCTestCase { XCTAssertTrue(networkingInteractor.didCallSubscribe) XCTAssert(cryptoMock.hasAgreementSecret(for: topicB), "Responder must store agreement key for topic B") XCTAssertEqual(networkingInteractor.didRespondOnTopic!, topicA, "Responder must respond on topic A") + XCTAssertTrue(sessionStorageMock.hasSession(forTopic: topicB), "Responder must persist session on topic B") XCTAssertTrue(pairingRegisterer.isActivateCalled) } @@ -98,8 +106,7 @@ final class ApproveEngineTests: XCTestCase { let topicB = String.generateTopic() cryptoMock.setAgreementSecret(agreementKeys, topic: topicB) let proposal = SessionProposal.stub(proposerPubKey: AgreementPrivateKey().publicKey.hexRepresentation) - try await engine.settle(topic: topicB, proposal: proposal, namespaces: SessionNamespace.stubDictionary(), pairingTopic: "") - XCTAssertTrue(sessionStorageMock.hasSession(forTopic: topicB), "Responder must persist session on topic B") + _ = try await engine.settle(topic: topicB, proposal: proposal, namespaces: SessionNamespace.stubDictionary(), pairingTopic: "") XCTAssert(networkingInteractor.didSubscribe(to: topicB), "Responder must subscribe for topic B") XCTAssertTrue(networkingInteractor.didCallRequest, "Responder must send session settle payload on topic B") } diff --git a/Tests/WalletConnectSignTests/AutoNamespacesValidationTests.swift b/Tests/WalletConnectSignTests/AutoNamespacesValidationTests.swift index 1942c9d75..8e2d5260c 100644 --- a/Tests/WalletConnectSignTests/AutoNamespacesValidationTests.swift +++ b/Tests/WalletConnectSignTests/AutoNamespacesValidationTests.swift @@ -999,4 +999,28 @@ final class AutoNamespacesValidationTests: XCTestCase { ] XCTAssertEqual(sessionNamespaces, expectedNamespaces) } + + func testBuildThrowsWhenSessionNamespacesAreEmpty() { + let sessionProposal = Session.Proposal( + id: "", + pairingTopic: "", + proposer: AppMetadata(name: "", description: "", url: "", icons: [], redirect: AppMetadata.Redirect(native: "", universal: nil)), + requiredNamespaces: [:], + optionalNamespaces: [:], + sessionProperties: nil, + proposal: SessionProposal(relays: [], proposer: Participant(publicKey: "", metadata: AppMetadata(name: "", description: "", url: "", icons: [], redirect: AppMetadata.Redirect(native: "", universal: nil))), requiredNamespaces: [:], optionalNamespaces: [:], sessionProperties: [:]) + ) + + XCTAssertThrowsError(try AutoNamespaces.build( + sessionProposal: sessionProposal, + chains: [], + methods: [], + events: [], + accounts: [] + ), "Expected to throw AutoNamespacesError.emtySessionNamespacesForbidden, but it did not") { error in + guard case AutoNamespacesError.emtySessionNamespacesForbidden = error else { + return XCTFail("Unexpected error type: \(error)") + } + } + } } diff --git a/Tests/WalletConnectSignTests/SessionEngineTests.swift b/Tests/WalletConnectSignTests/SessionEngineTests.swift index 23f1e420b..e3a42232e 100644 --- a/Tests/WalletConnectSignTests/SessionEngineTests.swift +++ b/Tests/WalletConnectSignTests/SessionEngineTests.swift @@ -7,14 +7,12 @@ final class SessionEngineTests: XCTestCase { var networkingInteractor: NetworkingInteractorMock! var sessionStorage: WCSessionStorageMock! - var proposalPayloadsStore: CodableStore>! var verifyContextStore: CodableStore! var engine: SessionEngine! override func setUp() { networkingInteractor = NetworkingInteractorMock() sessionStorage = WCSessionStorageMock() - proposalPayloadsStore = CodableStore>(defaults: RuntimeKeyValueStorage(), identifier: "") verifyContextStore = CodableStore(defaults: RuntimeKeyValueStorage(), identifier: "") engine = SessionEngine( networkingInteractor: networkingInteractor, @@ -25,14 +23,23 @@ final class SessionEngineTests: XCTestCase { identifier: "" ) ), - proposalPayloadsStore: proposalPayloadsStore, verifyContextStore: verifyContextStore ), verifyContextStore: verifyContextStore, verifyClient: VerifyClientMock(), kms: KeyManagementServiceMock(), sessionStore: sessionStorage, - logger: ConsoleLoggerMock() + logger: ConsoleLoggerMock(), + sessionRequestsProvider: SessionRequestsProvider( + historyService: HistoryService( + history: RPCHistory( + keyValueStore: .init( + defaults: RuntimeKeyValueStorage(), + identifier: "" + ) + ), + verifyContextStore: verifyContextStore + )) ) } diff --git a/Tests/WalletConnectSignTests/SessionProposalTests.swift b/Tests/WalletConnectSignTests/SessionProposalTests.swift new file mode 100644 index 000000000..e490ba5be --- /dev/null +++ b/Tests/WalletConnectSignTests/SessionProposalTests.swift @@ -0,0 +1,50 @@ +import XCTest +@testable import WalletConnectSign + +class SessionProposalTests: XCTestCase { + + func testProposalNotExpiredImmediately() { + let proposal = SessionProposal.stub() + XCTAssertFalse(proposal.isExpired(), "Proposal should not be expired immediately after creation.") + } + + func testProposalExpired() { + let proposal = SessionProposal.stub() + let expiredDate = Date(timeIntervalSince1970: TimeInterval(proposal.expiryTimestamp! + 1)) + XCTAssertTrue(proposal.isExpired(currentDate: expiredDate), "Proposal should be expired after the expiry time.") + } + + func testProposalNotExpiredJustBeforeExpiry() { + let proposal = SessionProposal.stub() + let justBeforeExpiryDate = Date(timeIntervalSince1970: TimeInterval(proposal.expiryTimestamp! - 1)) + XCTAssertFalse(proposal.isExpired(currentDate: justBeforeExpiryDate), "Proposal should not be expired just before the expiry time.") + } + + // for backward compatibility + func testDecodingWithoutExpiry() throws { + let json = """ + { + "relays": [], + "proposer": { + "publicKey": "testKey", + "metadata": { + "name": "Wallet Connect", + "description": "A protocol to connect blockchain wallets to dapps.", + "url": "https://walletconnect.com/", + "icons": [] + } + }, + "requiredNamespaces": {}, + "optionalNamespaces": {}, + "sessionProperties": {} + } + """.data(using: .utf8)! + + let decoder = JSONDecoder() + let proposal = try decoder.decode(SessionProposal.self, from: json) + + // Assertions + XCTAssertNotNil(proposal, "Proposal should be successfully decoded even without an expiry field.") + XCTAssertNil(proposal.expiryTimestamp, "Expiry should be nil if not provided in JSON.") + } +} diff --git a/Tests/WalletConnectSignTests/SessionRequestTests.swift b/Tests/WalletConnectSignTests/SessionRequestTests.swift index e89ff347d..3321f2fe1 100644 --- a/Tests/WalletConnectSignTests/SessionRequestTests.swift +++ b/Tests/WalletConnectSignTests/SessionRequestTests.swift @@ -1,83 +1,71 @@ import XCTest @testable import WalletConnectSign -final class SessionRequestTests: XCTestCase { +class RequestTests: XCTestCase { - func testRequestTtlDefault() { - let request = Request.stub() - - XCTAssertEqual(request.calculateTtl(), SessionRequestProtocolMethod.defaultTtl) + func testInitWithValidTtl() { + XCTAssertNoThrow(try Request.stub(ttl: 3600)) // 1 hour } - func testRequestTtlExtended() { - let currentDate = Date(timeIntervalSince1970: 0) - let expiry = currentDate.advanced(by: 500) - let request = Request.stub(expiry: UInt64(expiry.timeIntervalSince1970)) - - XCTAssertEqual(request.calculateTtl(currentDate: currentDate), 500) + func testInitWithInvalidTtlTooShort() { + XCTAssertThrowsError(try Request.stub(ttl: 100)) { error in // Less than minTtl + XCTAssertEqual(error as? Request.Errors, Request.Errors.invalidTtl) + } } - func testRequestTtlNotExtendedMinValidation() { - let currentDate = Date(timeIntervalSince1970: 0) - let expiry = currentDate.advanced(by: 200) - let request = Request.stub(expiry: UInt64(expiry.timeIntervalSince1970)) - - XCTAssertEqual(request.calculateTtl(currentDate: currentDate), SessionRequestProtocolMethod.defaultTtl) + func testInitWithInvalidTtlTooLong() { + XCTAssertThrowsError(try Request.stub(ttl: 700000)) { error in // More than maxTtl + XCTAssertEqual(error as? Request.Errors, Request.Errors.invalidTtl) + } } - func testRequestTtlNotExtendedMaxValidation() { - let currentDate = Date(timeIntervalSince1970: 0) - let expiry = currentDate.advanced(by: 700000) - let request = Request.stub(expiry: UInt64(expiry.timeIntervalSince1970)) - - XCTAssertEqual(request.calculateTtl(currentDate: currentDate), SessionRequestProtocolMethod.defaultTtl) - } - - func testIsExpiredDefault() { - let request = Request.stub() - + func testIsExpiredForNonExpiredRequest() { + let request = try! Request.stub(ttl: 3600) // 1 hour XCTAssertFalse(request.isExpired()) } - func testIsExpiredTrue() { - let currentDate = Date(timeIntervalSince1970: 500) - let expiry = Date(timeIntervalSince1970: 0) - let request = Request.stub(expiry: UInt64(expiry.timeIntervalSince1970)) - XCTAssertTrue(request.isExpired(currentDate: currentDate)) + func testIsExpiredForExpiredRequest() { + let pastTimestamp = UInt64(Date().timeIntervalSince1970) - 3600 // 1 hour ago + let request = Request.stubWithExpiry(expiry: pastTimestamp) + XCTAssertTrue(request.isExpired()) } - func testIsExpiredTrueMinValidation() { - let currentDate = Date(timeIntervalSince1970: 500) - let expiry = Date(timeIntervalSince1970: 600) - let request = Request.stub(expiry: UInt64(expiry.timeIntervalSince1970)) - XCTAssertTrue(request.isExpired(currentDate: currentDate)) + func testCalculateTtlForNonExpiredRequest() { + let request = try! Request.stub(ttl: 3600) // 1 hour + XCTAssertNoThrow(try request.calculateTtl()) } - func testIsExpiredTrueMaxValidation() { - let currentDate = Date(timeIntervalSince1970: 500) - let expiry = Date(timeIntervalSince1970: 700000) - let request = Request.stub(expiry: UInt64(expiry.timeIntervalSince1970)) - XCTAssertTrue(request.isExpired(currentDate: currentDate)) + func testCalculateTtlForExpiredRequest() { + let pastTimestamp = UInt64(Date().timeIntervalSince1970) - 3600 // 1 hour ago + let request = Request.stubWithExpiry(expiry: pastTimestamp) + XCTAssertThrowsError(try request.calculateTtl()) { error in + XCTAssertEqual(error as? Request.Errors, Request.Errors.requestExpired) + } } +} - func testIsExpiredFalse() { - let currentDate = Date(timeIntervalSince1970: 0) - let expiry = Date(timeIntervalSince1970: 500) - let request = Request.stub(expiry: UInt64(expiry.timeIntervalSince1970)) - XCTAssertFalse(request.isExpired(currentDate: currentDate)) - } -} private extension Request { - static func stub(expiry: UInt64? = nil) -> Request { + static func stub(ttl: TimeInterval = 300) throws -> Request { + return try Request( + topic: "topic", + method: "method", + params: AnyCodable("params"), + chainId: Blockchain("eip155:1")!, + ttl: ttl + ) + } + + static func stubWithExpiry(expiry: UInt64) -> Request { return Request( + id: RPCID(JsonRpcID.generate()), topic: "topic", method: "method", params: AnyCodable("params"), chainId: Blockchain("eip155:1")!, - expiry: expiry + expiryTimestamp: expiry ) } } diff --git a/Tests/WalletConnectSignTests/Stub/Session+Stub.swift b/Tests/WalletConnectSignTests/Stub/Session+Stub.swift index 37fcfdef9..67bc7045d 100644 --- a/Tests/WalletConnectSignTests/Stub/Session+Stub.swift +++ b/Tests/WalletConnectSignTests/Stub/Session+Stub.swift @@ -31,7 +31,7 @@ extension WCSession { events: [], accounts: Account.stubSet(), acknowledged: acknowledged, - expiry: Int64(expiryDate.timeIntervalSince1970)) + expiryTimestamp: Int64(expiryDate.timeIntervalSince1970)) } } diff --git a/Tests/WalletConnectSignTests/Stub/Stubs.swift b/Tests/WalletConnectSignTests/Stub/Stubs.swift index 2f7961e22..9c5de2ccb 100644 --- a/Tests/WalletConnectSignTests/Stub/Stubs.swift +++ b/Tests/WalletConnectSignTests/Stub/Stubs.swift @@ -4,19 +4,7 @@ import JSONRPC import WalletConnectKMS import WalletConnectUtils import TestingUtils -import WalletConnectPairing - -extension Pairing { - static func stub(expiryDate: Date = Date(timeIntervalSinceNow: 10000), topic: String = String.generateTopic()) -> Pairing { - Pairing(topic: topic, peer: nil, expiryDate: expiryDate) - } -} - -extension WCPairing { - static func stub(expiryDate: Date = Date(timeIntervalSinceNow: 10000), isActive: Bool = true, topic: String = String.generateTopic()) -> WCPairing { - WCPairing(topic: topic, relay: RelayProtocolOptions.stub(), peerMetadata: AppMetadata.stub(), isActive: isActive, expiryDate: expiryDate) - } -} +@testable import WalletConnectPairing extension ProposalNamespace { static func stubDictionary() -> [String: ProposalNamespace] { @@ -68,7 +56,7 @@ extension RPCRequest { static func stubRequest(method: String, chainId: Blockchain, expiry: UInt64? = nil) -> RPCRequest { let params = SessionType.RequestParams( - request: SessionType.RequestParams.Request(method: method, params: AnyCodable(EmptyCodable()), expiry: expiry), + request: SessionType.RequestParams.Request(method: method, params: AnyCodable(EmptyCodable()), expiryTimestamp: expiry), chainId: chainId) return RPCRequest(method: SessionRequestProtocolMethod().method, params: params) } diff --git a/Tests/WalletConnectUtilsTests/RPCHistoryTests.swift b/Tests/WalletConnectUtilsTests/RPCHistoryTests.swift index 37b05ccdd..b4023eaff 100644 --- a/Tests/WalletConnectUtilsTests/RPCHistoryTests.swift +++ b/Tests/WalletConnectUtilsTests/RPCHistoryTests.swift @@ -35,10 +35,8 @@ final class RPCHistoryTests: XCTestCase { try sut.set(requestB, forTopic: String.randomTopic(), emmitedBy: .local) try sut.resolve(responseA) try sut.resolve(responseB) - let recordA = sut.get(recordId: requestA.id!) - let recordB = sut.get(recordId: requestB.id!) - XCTAssertEqual(recordA?.response, responseA) - XCTAssertEqual(recordB?.response, responseB) + XCTAssertNil(sut.get(recordId: requestA.id!)) + XCTAssertNil(sut.get(recordId: requestB.id!)) } func testDelete() throws { @@ -95,7 +93,7 @@ final class RPCHistoryTests: XCTestCase { } func testResolveDuplicateResponse() throws { - let expectedError = RPCHistory.HistoryError.responseDuplicateNotAllowed + let expectedError = RPCHistory.HistoryError.requestMatchingResponseNotFound let request = RPCRequest.stub() let responseA = RPCResponse(matchingRequest: request, result: true) @@ -107,4 +105,27 @@ final class RPCHistoryTests: XCTestCase { XCTAssertEqual(expectedError, error as? RPCHistory.HistoryError) } } + + func testRemoveOutdated() throws { + let request1 = RPCRequest.stub() + let request2 = RPCRequest.stub() + + let time1 = TestTimeProvider(currentDate: .distantPast) + let time2 = TestTimeProvider(currentDate: Date()) + + try sut.set(request1, forTopic: .randomTopic(), emmitedBy: .local, time: time1) + try sut.set(request2, forTopic: .randomTopic(), emmitedBy: .local, time: time2) + + XCTAssertEqual(sut.get(recordId: request1.id!)?.request, request1) + XCTAssertEqual(sut.get(recordId: request2.id!)?.request, request2) + + sut.removeOutdated() + + XCTAssertEqual(sut.get(recordId: request1.id!)?.request, nil) + XCTAssertEqual(sut.get(recordId: request2.id!)?.request, request2) + } + + struct TestTimeProvider: TimeProvider { + var currentDate: Date + } } diff --git a/Tests/WalletConnectUtilsTests/WalletConnectURITests.swift b/Tests/WalletConnectUtilsTests/WalletConnectURITests.swift index 5f65b5c28..be9913382 100644 --- a/Tests/WalletConnectUtilsTests/WalletConnectURITests.swift +++ b/Tests/WalletConnectUtilsTests/WalletConnectURITests.swift @@ -5,11 +5,11 @@ private func stubURI() -> (uri: WalletConnectURI, string: String) { let topic = Data.randomBytes(count: 32).toHexString() let symKey = Data.randomBytes(count: 32).toHexString() let protocolName = "irn" - let uriString = "wc:\(topic)@2?symKey=\(symKey)&relay-protocol=\(protocolName)" let uri = WalletConnectURI( topic: topic, symKey: symKey, relay: RelayProtocolOptions(protocol: protocolName, data: nil)) + let uriString = uri.absoluteString return (uri, uriString) } @@ -17,26 +17,26 @@ final class WalletConnectURITests: XCTestCase { // MARK: - Init URI with string - func testInitURIToString() { + func testInitURIToString() throws { let input = stubURI() let uriString = input.uri.absoluteString - let outputURI = WalletConnectURI(string: uriString) + let outputURI = try WalletConnectURI(uriString: uriString) XCTAssertEqual(input.uri, outputURI) - XCTAssertEqual(input.string, outputURI?.absoluteString) + XCTAssertEqual(input.string, outputURI.absoluteString) } - func testInitStringToURI() { + func testInitStringToURI() throws { let inputURIString = stubURI().string - let uri = WalletConnectURI(string: inputURIString) - let outputURIString = uri?.absoluteString + let uri = try WalletConnectURI(uriString: inputURIString) + let outputURIString = uri.absoluteString XCTAssertEqual(inputURIString, outputURIString) } - func testInitStringToURIAlternate() { + func testInitStringToURIAlternate() throws { let expectedString = stubURI().string let inputURIString = expectedString.replacingOccurrences(of: "wc:", with: "wc://") - let uri = WalletConnectURI(string: inputURIString) - let outputURIString = uri?.absoluteString + let uri = try WalletConnectURI(uriString: inputURIString) + let outputURIString = uri.absoluteString XCTAssertEqual(expectedString, outputURIString) } @@ -44,27 +44,63 @@ final class WalletConnectURITests: XCTestCase { func testInitFailsBadScheme() { let inputURIString = stubURI().string.replacingOccurrences(of: "wc:", with: "") - let uri = WalletConnectURI(string: inputURIString) - XCTAssertNil(uri) + XCTAssertThrowsError(try WalletConnectURI(uriString: inputURIString)) } func testInitFailsMalformedURL() { let inputURIString = "wc://<" - let uri = WalletConnectURI(string: inputURIString) - XCTAssertNil(uri) + XCTAssertThrowsError(try WalletConnectURI(uriString: inputURIString)) } func testInitFailsNoSymKeyParam() { let input = stubURI() let inputURIString = input.string.replacingOccurrences(of: "symKey=\(input.uri.symKey)", with: "") - let uri = WalletConnectURI(string: inputURIString) - XCTAssertNil(uri) + XCTAssertThrowsError(try WalletConnectURI(uriString: inputURIString)) } func testInitFailsNoRelayParam() { let input = stubURI() let inputURIString = input.string.replacingOccurrences(of: "&relay-protocol=\(input.uri.relay.protocol)", with: "") - let uri = WalletConnectURI(string: inputURIString) - XCTAssertNil(uri) + XCTAssertThrowsError(try WalletConnectURI(uriString: inputURIString)) } + + func testInitHandlesURLEncodedString() throws { + let input = stubURI() + let encodedURIString = input.string + .addingPercentEncoding(withAllowedCharacters: .alphanumerics) ?? "" + let uri = try WalletConnectURI(uriString: encodedURIString) + + // Assert that the initializer can handle encoded URI and it matches the expected URI + XCTAssertEqual(input.uri, uri) + XCTAssertEqual(input.string, uri.absoluteString) + } + + // MARK: - Expiry Logic Tests + + func testExpiryTimestampIsSet() { + let uri = stubURI().uri + XCTAssertNotNil(uri.expiryTimestamp) + XCTAssertTrue(uri.expiryTimestamp > UInt64(Date().timeIntervalSince1970)) + } + + func testInitFailsIfURIExpired() { + let input = stubURI() + // Create a URI string with an expired timestamp + let expiredTimestamp = UInt64(Date().timeIntervalSince1970) - 300 // 5 minutes in the past + let expiredURIString = "wc:\(input.uri.topic)@\(input.uri.version)?symKey=\(input.uri.symKey)&relay-protocol=\(input.uri.relay.protocol)&expiryTimestamp=\(expiredTimestamp)" + XCTAssertThrowsError(try WalletConnectURI(uriString: expiredURIString)) + } + + // Test compatibility with old clients that don't include expiryTimestamp in their uri + func testDefaultExpiryTimestampIfNotIncluded() throws { + let input = stubURI().string + // Remove expiryTimestamp from the URI string + let uriStringWithoutExpiry = input.replacingOccurrences(of: "&expiryTimestamp=\(stubURI().uri.expiryTimestamp)", with: "") + let uri = try WalletConnectURI(uriString: uriStringWithoutExpiry) + + // Check if the expiryTimestamp is set to 5 minutes in the future + let expectedExpiryTimestamp = UInt64(Date().timeIntervalSince1970) + 5 * 60 + XCTAssertTrue(uri.expiryTimestamp >= expectedExpiryTimestamp) + } + } diff --git a/Tests/Web3WalletTests/Mocks/PairingClientMock.swift b/Tests/Web3WalletTests/Mocks/PairingClientMock.swift index 0f0012d59..3e752da08 100644 --- a/Tests/Web3WalletTests/Mocks/PairingClientMock.swift +++ b/Tests/Web3WalletTests/Mocks/PairingClientMock.swift @@ -4,6 +4,17 @@ import Combine @testable import WalletConnectPairing final class PairingClientMock: PairingClientProtocol { + var pairingStatePublisher: AnyPublisher { + pairingStatePublisherSubject.eraseToAnyPublisher() + } + var pairingStatePublisherSubject = PassthroughSubject() + + var pairingExpirationPublisher: AnyPublisher { + return pairingExpirationPublisherSubject.eraseToAnyPublisher() + } + var pairingExpirationPublisherSubject = PassthroughSubject() + + var pairingDeletePublisher: AnyPublisher<(code: Int, message: String), Never> { pairingDeletePublisherSubject.eraseToAnyPublisher() } @@ -29,6 +40,6 @@ final class PairingClientMock: PairingClientProtocol { } func getPairings() -> [Pairing] { - return [Pairing(topic: "", peer: nil, expiryDate: Date())] + return [Pairing(WCPairing.stub())] } } diff --git a/Tests/Web3WalletTests/Mocks/SignClientMock.swift b/Tests/Web3WalletTests/Mocks/SignClientMock.swift index de4b3cb22..f144ef727 100644 --- a/Tests/Web3WalletTests/Mocks/SignClientMock.swift +++ b/Tests/Web3WalletTests/Mocks/SignClientMock.swift @@ -22,7 +22,7 @@ final class SignClientMock: SignClientProtocol { var requestCalled = false private let metadata = AppMetadata(name: "", description: "", url: "", icons: [], redirect: AppMetadata.Redirect(native: "", universal: nil)) - private let request = WalletConnectSign.Request(id: .left(""), topic: "", method: "", params: "", chainId: Blockchain("eip155:1")!, expiry: nil) + private let request = WalletConnectSign.Request(id: .left(""), topic: "", method: "", params: AnyCodable(""), chainId: Blockchain("eip155:1")!, expiryTimestamp: nil) private let response = WalletConnectSign.Response(id: RPCID(1234567890123456789), topic: "", chainId: "", result: .response(AnyCodable(any: ""))) var sessionProposalPublisher: AnyPublisher<(proposal: WalletConnectSign.Session.Proposal, context: VerifyContext?), Never> { @@ -63,7 +63,17 @@ final class SignClientMock: SignClientProtocol { return Result.Publisher(("topic", ReasonMock())) .eraseToAnyPublisher() } - + + var pendingProposalsPublisher: AnyPublisher<[(proposal: WalletConnectSign.Session.Proposal, context: WalletConnectSign.VerifyContext?)], Never> { + return Result.Publisher([]) + .eraseToAnyPublisher() + } + + var requestExpirationPublisher: AnyPublisher { + Result.Publisher(request) + .eraseToAnyPublisher() + } + var sessionEventPublisher: AnyPublisher<(event: WalletConnectSign.Session.Event, sessionTopic: String, chainId: WalletConnectUtils.Blockchain?), Never> { return Result.Publisher( ( @@ -74,7 +84,11 @@ final class SignClientMock: SignClientProtocol { ) .eraseToAnyPublisher() } - + + var sessionProposalExpirationPublisher: AnyPublisher { + fatalError() + } + var sessionRejectionPublisher: AnyPublisher<(Session.Proposal, Reason), Never> { let sessionProposal = Session.Proposal( id: "", @@ -138,11 +152,7 @@ final class SignClientMock: SignClientProtocol { func getPendingRequests(topic: String?) -> [(request: WalletConnectSign.Request, context: WalletConnectSign.VerifyContext?)] { return [(request, nil)] } - - func getSessionRequestRecord(id: JSONRPC.RPCID) -> (request: WalletConnectSign.Request, context: WalletConnectSign.VerifyContext?)? { - return (request, nil) - } - + func cleanup() async throws { cleanupCalled = true } diff --git a/Tests/Web3WalletTests/Web3WalletTests.swift b/Tests/Web3WalletTests/Web3WalletTests.swift index 39165751d..520639d83 100644 --- a/Tests/Web3WalletTests/Web3WalletTests.swift +++ b/Tests/Web3WalletTests/Web3WalletTests.swift @@ -249,11 +249,6 @@ final class Web3WalletTests: XCTestCase { XCTAssertEqual(1, pendingRequests.count) } - func testSessionRequestRecordCalledAndNotNil() async { - let sessionRequestRecord = web3WalletClient.getSessionRequestRecord(id: .left("")) - XCTAssertNotNil(sessionRequestRecord) - } - func testAuthPendingRequestsCalledAndNotEmpty() async { let pendingRequests = try! web3WalletClient.getPendingRequests() XCTAssertEqual(1, pendingRequests.count) diff --git a/WalletConnectSwiftV2.podspec b/WalletConnectSwiftV2.podspec index c3cf47387..5a294b701 100644 --- a/WalletConnectSwiftV2.podspec +++ b/WalletConnectSwiftV2.podspec @@ -101,17 +101,10 @@ 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|