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|