diff --git a/Example/DApp/ Accounts/AccountsViewController.swift b/Example/DApp/ Accounts/AccountsViewController.swift index b1a85fbc2..620fc400b 100644 --- a/Example/DApp/ Accounts/AccountsViewController.swift +++ b/Example/DApp/ Accounts/AccountsViewController.swift @@ -9,7 +9,6 @@ struct AccountDetails { final class AccountsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { - let client = ClientDelegate.shared.client let session: Session var accountsDetails: [AccountDetails] = [] var onDisconnect: (()->())? @@ -54,7 +53,7 @@ final class AccountsViewController: UIViewController, UITableViewDataSource, UIT private func disconnect() { Task { do { - try await client.disconnect(topic: session.topic, reason: Reason(code: 0, message: "disconnect")) + try await Auth.instance.disconnect(topic: session.topic, reason: Reason(code: 0, message: "disconnect")) DispatchQueue.main.async { [weak self] in self?.onDisconnect?() } diff --git a/Example/DApp/AccountRequest/AccountRequestViewController.swift b/Example/DApp/AccountRequest/AccountRequestViewController.swift index 862d26dc9..33ae48423 100644 --- a/Example/DApp/AccountRequest/AccountRequestViewController.swift +++ b/Example/DApp/AccountRequest/AccountRequestViewController.swift @@ -7,7 +7,6 @@ import WalletConnectUtils class AccountRequestViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { private let session: Session - private let client: AuthClient = ClientDelegate.shared.client private let chainId: String private let account: String private let methods = ["eth_sendTransaction", "personal_sign", "eth_signTypedData"] @@ -61,7 +60,7 @@ class AccountRequestViewController: UIViewController, UITableViewDelegate, UITab let request = Request(topic: session.topic, method: method, params: requestParams, chainId: Blockchain(chainId)!) Task { do { - try await client.request(params: request) + try await Auth.instance.request(params: request) DispatchQueue.main.async { [weak self] in self?.presentConfirmationAlert() } diff --git a/Example/DApp/ClientDelegate.swift b/Example/DApp/ClientDelegate.swift deleted file mode 100644 index 228572e59..000000000 --- a/Example/DApp/ClientDelegate.swift +++ /dev/null @@ -1,49 +0,0 @@ -import WalletConnectAuth -import WalletConnectRelay - -class ClientDelegate: AuthClientDelegate { - var client: AuthClient - var onSessionSettled: ((Session)->())? - var onSessionResponse: ((Response)->())? - var onSessionDelete: (()->())? - - static var shared: ClientDelegate = ClientDelegate() - private init() { - let metadata = AppMetadata( - name: "Swift Dapp", - description: "a description", - url: "wallet.connect", - icons: ["https://gblobscdn.gitbook.com/spaces%2F-LJJeCjcLrr53DcT1Ml7%2Favatar.png?alt=media"]) - let relayClient = RelayClient(relayHost: "relay.walletconnect.com", projectId: "8ba9ee138960775e5231b70cc5ef1c3a") - self.client = AuthClient(metadata: metadata, relayClient: relayClient) - client.logger.setLogging(level: .debug) - client.delegate = self - } - - func didConnect() { - print("Client connected") - } - - func didSettle(session: Session) { - onSessionSettled?(session) - } - - func didDelete(sessionTopic: String, reason: Reason) { - onSessionDelete?() - } - - func didReceive(sessionResponse: Response) { - onSessionResponse?(sessionResponse) - } - - func didUpdate(sessionTopic: String, accounts: Set) { - } - - func didReject(proposal: Session.Proposal, reason: Reason) { - - } - - func didUpdate(sessionTopic: String, namespaces: [String : SessionNamespace]) { - - } -} diff --git a/Example/DApp/Connect/ConnectViewController.swift b/Example/DApp/Connect/ConnectViewController.swift index 8fbda2efe..7384de1cd 100644 --- a/Example/DApp/Connect/ConnectViewController.swift +++ b/Example/DApp/Connect/ConnectViewController.swift @@ -5,7 +5,7 @@ import WalletConnectAuth class ConnectViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { let uriString: String - let activePairings: [Pairing] = ClientDelegate.shared.client.getSettledPairings() + let activePairings: [Pairing] = Auth.instance.getSettledPairings() let segmentedControl = UISegmentedControl(items: ["Pairings", "New Pairing"]) init(uri: String) { @@ -99,7 +99,7 @@ class ConnectViewController: UIViewController, UITableViewDataSource, UITableVie let methods: Set = ["eth_sendTransaction", "personal_sign", "eth_signTypedData"] let namespaces: [String: ProposalNamespace] = ["eip155": ProposalNamespace(chains: blockchains, methods: methods, events: [], extensions: nil)] Task { - _ = try await ClientDelegate.shared.client.connect(requiredNamespaces: namespaces, topic: pairingTopic) + _ = try await Auth.instance.connect(requiredNamespaces: namespaces, topic: pairingTopic) connectWithExampleWallet() } } diff --git a/Example/DApp/ResponseViewController.swift b/Example/DApp/ResponseViewController.swift index 34fe0d56a..c7c4b45b4 100644 --- a/Example/DApp/ResponseViewController.swift +++ b/Example/DApp/ResponseViewController.swift @@ -24,7 +24,7 @@ class ResponseViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - let record = ClientDelegate.shared.client.getSessionRequestRecord(id: response.result.id)! + let record = Auth.instance.getSessionRequestRecord(id: response.result.id)! switch response.result { case .response(let response): responseView.nameLabel.text = "Received Response\n\(record.request.method)" diff --git a/Example/DApp/SceneDelegate.swift b/Example/DApp/SceneDelegate.swift index 0128d48d2..73a85be39 100644 --- a/Example/DApp/SceneDelegate.swift +++ b/Example/DApp/SceneDelegate.swift @@ -1,10 +1,12 @@ import UIKit import WalletConnectAuth +import Combine class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? + private var publishers = [AnyCancellable]() func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { @@ -13,13 +15,24 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). guard let windowScene = (scene as? UIWindowScene) else { return } window = UIWindow(windowScene: windowScene) - ClientDelegate.shared.onSessionDelete = { [unowned self] in - showSelectChainScreen() - } - ClientDelegate.shared.onSessionResponse = { [unowned self] response in + let metadata = AppMetadata( + name: "Swift Dapp", + description: "a description", + url: "wallet.connect", + icons: ["https://gblobscdn.gitbook.com/spaces%2F-LJJeCjcLrr53DcT1Ml7%2Favatar.png?alt=media"]) + Auth.configure(Auth.Config(metadata: metadata, projectId: "8ba9ee138960775e5231b70cc5ef1c3a")) + Auth.instance.sessionDeletePublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] _ in + showSelectChainScreen() + }.store(in: &publishers) + + Auth.instance.sessionResponsePublisher.sink { [unowned self] response in presentResponse(for: response) - } - if let session = ClientDelegate.shared.client.getSettledSessions().first { + }.store(in: &publishers) + + + if let session = Auth.instance.getSettledSessions().first { showAccountsScreen(session) } else { showSelectChainScreen() diff --git a/Example/DApp/SelectChain/SelectChainViewController.swift b/Example/DApp/SelectChain/SelectChainViewController.swift index db41a3eec..f54a687d0 100644 --- a/Example/DApp/SelectChain/SelectChainViewController.swift +++ b/Example/DApp/SelectChain/SelectChainViewController.swift @@ -3,6 +3,7 @@ import Foundation import WalletConnectAuth import UIKit +import Combine struct Chain { let name: String @@ -13,23 +14,22 @@ class SelectChainViewController: UIViewController, UITableViewDataSource { private let selectChainView: SelectChainView = { SelectChainView() }() + private var publishers = [AnyCancellable]() + let chains = [Chain(name: "Ethereum", id: "eip155:1"), Chain(name: "Polygon", id: "eip155:137")] - let client = ClientDelegate.shared.client var onSessionSettled: ((Session)->())? override func viewDidLoad() { super.viewDidLoad() navigationItem.title = "Available Chains" selectChainView.tableView.dataSource = self selectChainView.connectButton.addTarget(self, action: #selector(connect), for: .touchUpInside) - ClientDelegate.shared.onSessionSettled = { [unowned self] session in + Auth.instance.sessionSettlePublisher.sink {[unowned self] session in onSessionSettled?(session) - } + }.store(in: &publishers) } override func loadView() { view = selectChainView - - } @objc @@ -39,7 +39,7 @@ class SelectChainViewController: UIViewController, UITableViewDataSource { let blockchains: Set = [Blockchain("eip155:1")!, Blockchain("eip155:137")!] let namespaces: [String: ProposalNamespace] = ["eip155": ProposalNamespace(chains: blockchains, methods: methods, events: [], extensions: nil)] Task { - let uri = try await client.connect(requiredNamespaces: namespaces) + let uri = try await Auth.instance.connect(requiredNamespaces: namespaces) showConnectScreen(uriString: uri!) } } diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index 1c0e6c716..e950d8d7b 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -40,7 +40,6 @@ 84CE64392798228D00142511 /* Web3 in Frameworks */ = {isa = PBXBuildFile; productRef = 84CE64382798228D00142511 /* Web3 */; }; 84CE643D2798322600142511 /* ConnectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CE643C2798322600142511 /* ConnectViewController.swift */; }; 84CE6444279AB5AD00142511 /* SelectChainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CE6443279AB5AD00142511 /* SelectChainViewController.swift */; }; - 84CE6446279ABBF300142511 /* ClientDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CE6445279ABBF300142511 /* ClientDelegate.swift */; }; 84CE6448279AE68600142511 /* AccountRequestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CE6447279AE68600142511 /* AccountRequestViewController.swift */; }; 84CE644B279EA1FA00142511 /* AccountRequestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CE644A279EA1FA00142511 /* AccountRequestView.swift */; }; 84CE644E279ED2FF00142511 /* SelectChainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CE644D279ED2FF00142511 /* SelectChainView.swift */; }; @@ -113,7 +112,6 @@ 84CE642C27981DF000142511 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 84CE643C2798322600142511 /* ConnectViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectViewController.swift; sourceTree = ""; }; 84CE6443279AB5AD00142511 /* SelectChainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectChainViewController.swift; sourceTree = ""; }; - 84CE6445279ABBF300142511 /* ClientDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientDelegate.swift; sourceTree = ""; }; 84CE6447279AE68600142511 /* AccountRequestViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountRequestViewController.swift; sourceTree = ""; }; 84CE644A279EA1FA00142511 /* AccountRequestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountRequestView.swift; sourceTree = ""; }; 84CE644D279ED2FF00142511 /* SelectChainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectChainView.swift; sourceTree = ""; }; @@ -276,7 +274,6 @@ 84CE642027981DED00142511 /* SceneDelegate.swift */, 84CE645427A29D4C00142511 /* ResponseViewController.swift */, 84CE6449279EA1E600142511 /* AccountRequest */, - 84CE6445279ABBF300142511 /* ClientDelegate.swift */, 84CE644C279ED2EC00142511 /* SelectChain */, 84CE6450279ED41D00142511 /* Connect */, 84CE644F279ED3FB00142511 /* Accounts */, @@ -540,7 +537,6 @@ files = ( 84CE6430279820F600142511 /* AccountsViewController.swift in Sources */, 84CE645527A29D4D00142511 /* ResponseViewController.swift in Sources */, - 84CE6446279ABBF300142511 /* ClientDelegate.swift in Sources */, 84CE641F27981DED00142511 /* AppDelegate.swift in Sources */, 84CE6452279ED42B00142511 /* ConnectView.swift in Sources */, 84CE6448279AE68600142511 /* AccountRequestViewController.swift in Sources */, @@ -733,7 +729,7 @@ CODE_SIGN_ENTITLEMENTS = Wallet.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 11; DEVELOPMENT_TEAM = W5R8AG9K22; INFOPLIST_FILE = ExampleApp/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -758,7 +754,7 @@ CODE_SIGN_ENTITLEMENTS = Wallet.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 11; DEVELOPMENT_TEAM = W5R8AG9K22; INFOPLIST_FILE = ExampleApp/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -783,7 +779,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_TEAM = W5R8AG9K22; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = DApp/Info.plist; @@ -817,7 +813,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_TEAM = W5R8AG9K22; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = DApp/Info.plist; diff --git a/Example/ExampleApp/SceneDelegate.swift b/Example/ExampleApp/SceneDelegate.swift index de23ce81c..bbe9aeaa9 100644 --- a/Example/ExampleApp/SceneDelegate.swift +++ b/Example/ExampleApp/SceneDelegate.swift @@ -1,10 +1,19 @@ import UIKit +import WalletConnectAuth class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + + let metadata = AppMetadata( + name: "Example Wallet", + description: "wallet description", + url: "example.wallet", + icons: ["https://gblobscdn.gitbook.com/spaces%2F-LJJeCjcLrr53DcT1Ml7%2Favatar.png?alt=media"]) + Auth.configure(Auth.Config(metadata: metadata, projectId: "8ba9ee138960775e5231b70cc5ef1c3a")) + guard let windowScene = (scene as? UIWindowScene) else { return } window = UIWindow(windowScene: windowScene) window?.rootViewController = UITabBarController.createExampleApp() @@ -21,7 +30,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { vc.onClientConnected = { Task { do { - try await vc.client.pair(uri: wcUri) + try await Auth.instance.pair(uri: wcUri) } catch { print(error) } diff --git a/Example/ExampleApp/SessionDetails/SessionDetailsViewController.swift b/Example/ExampleApp/SessionDetails/SessionDetailsViewController.swift index d745268eb..44af78efb 100644 --- a/Example/ExampleApp/SessionDetails/SessionDetailsViewController.swift +++ b/Example/ExampleApp/SessionDetails/SessionDetailsViewController.swift @@ -7,10 +7,9 @@ final class SessionDetailsViewController: UIViewController, UITableViewDelegate, SessionDetailsView() }() private var sessionInfo: SessionInfo - private let client: AuthClient private let session: Session - init(_ session: Session, _ client: AuthClient) { - let pendingRequests = client.getPendingRequests(topic: session.topic).map{$0.method} + init(_ session: Session) { + let pendingRequests = Auth.instance.getPendingRequests(topic: session.topic).map{$0.method} let chains = Array(session.namespaces.values.flatMap { n in n.accounts.map{$0.blockchain.absoluteString}}) let methods = Array(session.namespaces.values.first?.methods ?? []) // TODO: Rethink how to show this info on example app self.sessionInfo = SessionInfo(name: session.peer.name, @@ -20,7 +19,6 @@ final class SessionDetailsViewController: UIViewController, UITableViewDelegate, chains: chains, methods: methods, pendingRequests: pendingRequests) - self.client = client self.session = session super.init(nibName: nil, bundle: nil) } @@ -51,7 +49,7 @@ final class SessionDetailsViewController: UIViewController, UITableViewDelegate, @objc private func ping() { - client.ping(topic: session.topic) { result in + Auth.instance.ping(topic: session.topic) { result in switch result { case .success(): print("received ping response") @@ -101,7 +99,7 @@ final class SessionDetailsViewController: UIViewController, UITableViewDelegate, func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { if indexPath.section == 2 { - let pendingRequests = client.getPendingRequests(topic: session.topic) + let pendingRequests = Auth.instance.getPendingRequests(topic: session.topic) showSessionRequest(pendingRequests[indexPath.row]) } } @@ -111,18 +109,18 @@ final class SessionDetailsViewController: UIViewController, UITableViewDelegate, requestVC.onSign = { [unowned self] in let result = Signer.signEth(request: sessionRequest) let response = JSONRPCResponse(id: sessionRequest.id, result: result) - client.respond(topic: sessionRequest.topic, response: .response(response)) + Auth.instance.respond(topic: sessionRequest.topic, response: .response(response)) reloadTable() } requestVC.onReject = { [unowned self] in - client.respond(topic: sessionRequest.topic, response: .error(JSONRPCErrorResponse(id: sessionRequest.id, error: JSONRPCErrorResponse.Error(code: 0, message: "")))) + Auth.instance.respond(topic: sessionRequest.topic, response: .error(JSONRPCErrorResponse(id: sessionRequest.id, error: JSONRPCErrorResponse.Error(code: 0, message: "")))) reloadTable() } present(requestVC, animated: true) } func reloadTable() { - let pendingRequests = client.getPendingRequests(topic: session.topic).map{$0.method} + let pendingRequests = Auth.instance.getPendingRequests(topic: session.topic).map{$0.method} let chains = Array(session.namespaces.values.flatMap { n in n.accounts.map{$0.blockchain.absoluteString}}) let methods = Array(session.namespaces.values.first?.methods ?? []) // TODO: Rethink how to show this info on example app self.sessionInfo = SessionInfo(name: session.peer.name, diff --git a/Example/ExampleApp/Wallet/WalletViewController.swift b/Example/ExampleApp/Wallet/WalletViewController.swift index 74ff05f68..12ae3a833 100644 --- a/Example/ExampleApp/Wallet/WalletViewController.swift +++ b/Example/ExampleApp/Wallet/WalletViewController.swift @@ -3,26 +3,15 @@ import WalletConnectAuth import WalletConnectUtils import Web3 import CryptoSwift +import Combine final class WalletViewController: UIViewController { - - let client: AuthClient = { - let metadata = AppMetadata( - name: "Example Wallet", - description: "wallet description", - url: "example.wallet", - icons: ["https://gblobscdn.gitbook.com/spaces%2F-LJJeCjcLrr53DcT1Ml7%2Favatar.png?alt=media"]) - return AuthClient( - metadata: metadata, - projectId: "8ba9ee138960775e5231b70cc5ef1c3a", - relayHost: "relay.walletconnect.com" - ) - }() lazy var account = Signer.privateKey.address.hex(eip55: true) var sessionItems: [ActiveSessionItem] = [] var currentProposal: Session.Proposal? var onClientConnected: (()->())? - + private var publishers = [AnyCancellable]() + private let walletView: WalletView = { WalletView() }() @@ -39,10 +28,9 @@ final class WalletViewController: UIViewController { walletView.tableView.dataSource = self walletView.tableView.delegate = self - let settledSessions = client.getSettledSessions() + let settledSessions = Auth.instance.getSettledSessions() sessionItems = getActiveSessionItem(for: settledSessions) - client.delegate = self - client.logger.setLogging(level: .debug) + setUpAuthSubscribing() } @objc @@ -67,7 +55,7 @@ final class WalletViewController: UIViewController { } private func showSessionDetailsViewController(_ session: Session) { - let vc = SessionDetailsViewController(session, client) + let vc = SessionDetailsViewController(session) navigationController?.pushViewController(vc, animated: true) } @@ -76,11 +64,11 @@ final class WalletViewController: UIViewController { requestVC.onSign = { [unowned self] in let result = Signer.signEth(request: sessionRequest) let response = JSONRPCResponse(id: sessionRequest.id, result: result) - client.respond(topic: sessionRequest.topic, response: .response(response)) + Auth.instance.respond(topic: sessionRequest.topic, response: .response(response)) reloadSessionDetailsIfNeeded() } requestVC.onReject = { [unowned self] in - client.respond(topic: sessionRequest.topic, response: .error(JSONRPCErrorResponse(id: sessionRequest.id, error: JSONRPCErrorResponse.Error(code: 0, message: "")))) + Auth.instance.respond(topic: sessionRequest.topic, response: .error(JSONRPCErrorResponse(id: sessionRequest.id, error: JSONRPCErrorResponse.Error(code: 0, message: "")))) reloadSessionDetailsIfNeeded() } reloadSessionDetailsIfNeeded() @@ -97,7 +85,7 @@ final class WalletViewController: UIViewController { print("[RESPONDER] Pairing to: \(uri)") Task { do { - try await client.pair(uri: uri) + try await Auth.instance.pair(uri: uri) } catch { print("[PROPOSER] Pairing connect error: \(error)") } @@ -122,7 +110,7 @@ extension WalletViewController: UITableViewDataSource, UITableViewDelegate { let item = sessionItems[indexPath.row] Task { do { - try await client.disconnect(topic: item.topic, reason: Reason(code: 0, message: "disconnect")) + try await Auth.instance.disconnect(topic: item.topic, reason: Reason(code: 0, message: "disconnect")) DispatchQueue.main.async { [weak self] in self?.sessionItems.remove(at: indexPath.row) tableView.deleteRows(at: [indexPath], with: .automatic) @@ -141,7 +129,7 @@ extension WalletViewController: UITableViewDataSource, UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { print("did select row \(indexPath)") let itemTopic = sessionItems[indexPath.row].topic - if let session = client.getSettledSessions().first{$0.topic == itemTopic} { + if let session = Auth.instance.getSettledSessions().first{$0.topic == itemTopic} { showSessionDetailsViewController(session) } } @@ -173,61 +161,56 @@ extension WalletViewController: ProposalViewControllerDelegate { let sessionNamespace = SessionNamespace(accounts: accounts, methods: proposalNamespace.methods, events: proposalNamespace.events, extensions: extensions) sessionNamespaces[caip2Namespace] = sessionNamespace } - try! client.approve(proposalId: proposal.id, namespaces: sessionNamespaces) + try! Auth.instance.approve(proposalId: proposal.id, namespaces: sessionNamespaces) } func didRejectSession() { print("did reject session") let proposal = currentProposal! currentProposal = nil - client.reject(proposal: proposal, reason: .disapprovedChains) + Auth.instance.reject(proposal: proposal, reason: .disapprovedChains) } } -extension WalletViewController: AuthClientDelegate { - func didConnect() { - onClientConnected?() - print("Client connected") - - } - - func didUpdate(sessionTopic: String, namespaces: [String : SessionNamespace]) { - - } - - // TODO: Adapt proposal data to be used on the view - func didReceive(sessionProposal: Session.Proposal) { - print("[RESPONDER] WC: Did receive session proposal") -// let appMetadata = sessionProposal.proposer -// let info = SessionInfo( -// name: appMetadata.name, -// descriptionText: appMetadata.description, -// dappURL: appMetadata.url, -// iconURL: appMetadata.icons.first ?? "", -// chains: Array(sessionProposal.namespaces.first?.chains.map { $0.absoluteString } ?? []), -// methods: Array(sessionProposal.namespaces.first?.methods ?? []), pendingRequests: []) - currentProposal = sessionProposal - DispatchQueue.main.async { // FIXME: Delegate being called from background thread - self.showSessionProposal(Proposal(proposal: sessionProposal)) // FIXME: Remove mock - } - } +extension WalletViewController { + func setUpAuthSubscribing() { + Auth.instance.socketConnectionStatusPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] status in + if status == .connected { + self?.onClientConnected?() + print("Client connected") + } + }.store(in: &publishers) - func didSettle(session: Session) { - reloadActiveSessions() - } + // TODO: Adapt proposal data to be used on the view + Auth.instance.sessionProposalPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] sessionProposal in + print("[RESPONDER] WC: Did receive session proposal") + self?.currentProposal = sessionProposal + self?.showSessionProposal(Proposal(proposal: sessionProposal)) // FIXME: Remove mock + }.store(in: &publishers) - func didReceive(sessionRequest: Request) { - DispatchQueue.main.async { [weak self] in - self?.showSessionRequest(sessionRequest) - } - print("[RESPONDER] WC: Did receive session request") - } - - func didDelete(sessionTopic: String, reason: Reason) { - reloadActiveSessions() - DispatchQueue.main.async { [unowned self] in - navigationController?.popToRootViewController(animated: true) - } + Auth.instance.sessionSettlePublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.reloadActiveSessions() + }.store(in: &publishers) + + Auth.instance.sessionRequestPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] sessionRequest in + print("[RESPONDER] WC: Did receive session request") + self?.showSessionRequest(sessionRequest) + }.store(in: &publishers) + + Auth.instance.sessionDeletePublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] sessionRequest in + self?.reloadActiveSessions() + self?.navigationController?.popToRootViewController(animated: true) + }.store(in: &publishers) } private func getActiveSessionItem(for settledSessions: [Session]) -> [ActiveSessionItem] { @@ -242,7 +225,7 @@ extension WalletViewController: AuthClientDelegate { } private func reloadActiveSessions() { - let settledSessions = client.getSettledSessions() + let settledSessions = Auth.instance.getSettledSessions() let activeSessions = getActiveSessionItem(for: settledSessions) DispatchQueue.main.async { // FIXME: Delegate being called from background thread self.sessionItems = activeSessions diff --git a/Sources/WalletConnectAuth/Auth/Auth.swift b/Sources/WalletConnectAuth/Auth/Auth.swift new file mode 100644 index 000000000..00faa0fc1 --- /dev/null +++ b/Sources/WalletConnectAuth/Auth/Auth.swift @@ -0,0 +1,250 @@ + +import Foundation +import WalletConnectUtils +import WalletConnectRelay +import Combine + +public class Auth { + public static let instance = Auth() + + private static var config: Config? + private let client: AuthClient + private let relayClient: RelayClient + + private init() { + guard let config = Auth.config else { + fatalError("Error - you must configure before accessing Auth.instance") + } + relayClient = RelayClient(relayHost: "relay.walletconnect.com", projectId: config.projectId, socketConnectionType: config.socketConnectionType) + client = AuthClient(metadata: config.metadata, relayClient: relayClient) + client.delegate = self + } + + static public func configure(_ config: Config) { + Auth.config = config + } + + var sessionProposalPublisherSubject = PassthroughSubject() + public var sessionProposalPublisher: AnyPublisher { + sessionProposalPublisherSubject.eraseToAnyPublisher() + } + + var sessionRequestPublisherSubject = PassthroughSubject() + public var sessionRequestPublisher: AnyPublisher { + sessionRequestPublisherSubject.eraseToAnyPublisher() + } + + var socketConnectionStatusPublisherSubject = PassthroughSubject() + public var socketConnectionStatusPublisher: AnyPublisher { + socketConnectionStatusPublisherSubject.eraseToAnyPublisher() + } + + var sessionSettlePublisherSubject = PassthroughSubject() + public var sessionSettlePublisher: AnyPublisher { + sessionSettlePublisherSubject.eraseToAnyPublisher() + } + + var sessionDeletePublisherSubject = PassthroughSubject<(String, Reason), Never>() + public var sessionDeletePublisher: AnyPublisher<(String, Reason), Never> { + sessionDeletePublisherSubject.eraseToAnyPublisher() + } + + var sessionResponsePublisherSubject = PassthroughSubject() + public var sessionResponsePublisher: AnyPublisher { + sessionResponsePublisherSubject.eraseToAnyPublisher() + } + + var sessionRejectionPublisherSubject = PassthroughSubject<(Session.Proposal, Reason), Never>() + public var sessionRejectionPublisher: AnyPublisher<(Session.Proposal, Reason), Never> { + sessionRejectionPublisherSubject.eraseToAnyPublisher() + } + + var sessionUpdatePublisherSubject = PassthroughSubject<(sessionTopic: String, namespaces: [String : SessionNamespace]), Never>() + public var sessionUpdatePublisher: AnyPublisher<(sessionTopic: String, namespaces: [String : SessionNamespace]), Never> { + sessionUpdatePublisherSubject.eraseToAnyPublisher() + } + + var sessionEventPublisherSubject = PassthroughSubject<(event: Session.Event, sessionTopic: String, chainId: Blockchain?), Never>() + public var sessionEventPublisher: AnyPublisher<(event: Session.Event, sessionTopic: String, chainId: Blockchain?), Never> { + sessionEventPublisherSubject.eraseToAnyPublisher() + } + + var sessionUpdateExpiryPublisherSubject = PassthroughSubject<(sessionTopic: String, expiry: Date), Never>() + public var sessionUpdateExpiryPublisher: AnyPublisher<(sessionTopic: String, expiry: Date), Never> { + sessionUpdateExpiryPublisherSubject.eraseToAnyPublisher() + } +} + +extension Auth: AuthClientDelegate { + + public func didReceive(sessionProposal: Session.Proposal) { + sessionProposalPublisherSubject.send(sessionProposal) + } + + public func didReceive(sessionRequest: Request) { + sessionRequestPublisherSubject.send(sessionRequest) + } + + public func didReceive(sessionResponse: Response) { + sessionResponsePublisherSubject.send(sessionResponse) + } + + public func didDelete(sessionTopic: String, reason: Reason) { + sessionDeletePublisherSubject.send((sessionTopic, reason)) + } + + public func didUpdate(sessionTopic: String, namespaces: [String : SessionNamespace]) { + sessionUpdatePublisherSubject.send((sessionTopic, namespaces)) + } + + public func didUpdate(sessionTopic: String, expiry: Date) { + sessionUpdateExpiryPublisherSubject.send((sessionTopic, expiry)) + } + + public func didSettle(session: Session) { + sessionSettlePublisherSubject.send(session) + } + + public func didReceive(event: Session.Event, sessionTopic: String, chainId: Blockchain?) { + sessionEventPublisherSubject.send((event, sessionTopic, chainId)) + } + + public func didReject(proposal: Session.Proposal, reason: Reason) { + sessionRejectionPublisherSubject.send((proposal, reason)) + } + + public func didChangeSocketConnectionStatus(_ status: SocketConnectionStatus) { + socketConnectionStatusPublisherSubject.send(status) + } +} + +extension Auth { + + /// For the Proposer to propose a session to a responder. + /// Function will create pending pairing sequence or propose a session on existing pairing. When responder client approves pairing, session is be proposed automatically by your client. + /// - Parameter sessionPermissions: The session permissions the responder will be requested for. + /// - Parameter topic: Optional parameter - use it if you already have an established pairing with peer client. + /// - Returns: Pairing URI that should be shared with responder out of bound. Common way is to present it as a QR code. Pairing URI will be nil if you are going to establish a session on existing Pairing and `topic` function parameter was provided. + public func connect(requiredNamespaces: [String : ProposalNamespace], topic: String? = nil) async throws -> String? { + try await client.connect(requiredNamespaces: requiredNamespaces) + } + + /// For responder to receive a session proposal from a proposer + /// Responder should call this function in order to accept peer's pairing proposal and be able to subscribe for future session proposals. + /// - Parameter uri: Pairing URI that is commonly presented as a QR code by a dapp. + /// + /// Should Error: + /// - When URI is invalid format or missing params + /// - When topic is already in use + public func pair(uri: String) async throws { + try await client.pair(uri: uri) + } + + /// For the responder to approve a session proposal. + /// - Parameters: + /// - proposal: Session Proposal received from peer client in a WalletConnect delegate function: `didReceive(sessionProposal: Session.Proposal)` + /// - accounts: A Set of accounts that the dapp will be allowed to request methods executions on. + /// - methods: A Set of methods that the dapp will be allowed to request. + /// - events: A Set of events + public func approve(proposalId: String, namespaces: [String : SessionNamespace]) throws { + try client.approve(proposalId: proposalId, namespaces: namespaces) + } + + /// For the responder to reject a session proposal. + /// - Parameters: + /// - proposal: Session Proposal received from peer client in a WalletConnect delegate. + /// - reason: Reason why the session proposal was rejected. Conforms to CAIP25. + public func reject(proposal: Session.Proposal, reason: RejectionReason) { + client.reject(proposal: proposal, reason: reason) + } + + /// For the responder to update session methods + /// - Parameters: + /// - topic: Topic of the session that is intended to be updated. + /// - methods: Sets of methods that will replace existing ones. + public func update(topic: String, namespaces: [String : SessionNamespace]) async throws { + try await client.update(topic: topic, namespaces: namespaces) + } + + /// For controller to update expiry of a session + /// - Parameters: + /// - topic: Topic of the Session, it can be a pairing or a session topic. + /// - ttl: Time in seconds that a target session is expected to be extended for. Must be greater than current time to expire and than 7 days + public func extend(topic: String) async throws { + try await client.extend(topic: topic) + } + + /// For the proposer to send JSON-RPC requests to responding peer. + /// - Parameters: + /// - params: Parameters defining request and related session + public func request(params: Request) async throws { + try await client.request(params: params) + } + + /// For the responder to respond on pending peer's session JSON-RPC Request + /// - Parameters: + /// - topic: Topic of the session for which the request was received. + /// - response: Your JSON RPC response or an error. + public func respond(topic: String, response: JsonRpcResult) { + client.respond(topic: topic, response: response) + } + + /// Ping method allows to check if client's peer is online and is subscribing for your sequence topic + /// + /// Should Error: + /// - When the session topic is not found + /// - When the response is neither result or error + /// - When the peer fails to respond within timeout + /// + /// - Parameters: + /// - topic: Topic of the sequence, it can be a pairing or a session topic. + /// - completion: Result will be success on response or error on timeout. -- TODO: timeout + public func ping(topic: String, completion: @escaping ((Result) -> ())) { + client.ping(topic: topic, completion: completion) + } + + /// - Parameters: + /// - topic: Session topic + /// - params: Event Parameters + /// - completion: calls a handler upon completion + public func emit(topic: String, event: Session.Event, chainId: Blockchain) async throws { + try await client.emit(topic: topic, event: event, chainId: chainId) + } + + /// - Parameters: + /// - topic: Session topic that you want to delete + /// - reason: Reason of session deletion + public func disconnect(topic: String, reason: Reason) async throws { + try await client.disconnect(topic: topic, reason: reason) + } + + /// - Returns: All settled sessions that are active + public func getSettledSessions() -> [Session] { + client.getSettledSessions() + } + + /// - Returns: All settled pairings that are active + public func getSettledPairings() -> [Pairing] { + client.getSettledPairings() + } + + /// - Returns: Pending requests received with wc_sessionRequest + /// - Parameter topic: topic representing session for which you want to get pending requests. If nil, you will receive pending requests for all active sessions. + public func getPendingRequests(topic: String? = nil) -> [Request] { + client.getPendingRequests(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: Int64) -> WalletConnectUtils.JsonRpcRecord? { + client.getSessionRequestRecord(id: id) + } + + public func connect() throws { + try relayClient.connect() + } + + public func disconnect(closeCode: URLSessionWebSocketTask.CloseCode) throws { + try relayClient.disconnect(closeCode: closeCode) + } +} diff --git a/Sources/WalletConnectAuth/AuthClient.swift b/Sources/WalletConnectAuth/Auth/AuthClient.swift similarity index 99% rename from Sources/WalletConnectAuth/AuthClient.swift rename to Sources/WalletConnectAuth/Auth/AuthClient.swift index 8df37b7bf..3490d2ea1 100644 --- a/Sources/WalletConnectAuth/AuthClient.swift +++ b/Sources/WalletConnectAuth/Auth/AuthClient.swift @@ -103,12 +103,7 @@ public final class AuthClient { private func setUpConnectionObserving(relayClient: RelayClient) { relayClient.socketConnectionStatusPublisher.sink { [weak self] status in - switch status { - case .connected: - self?.delegate?.didConnect() - case .disconnected: - self?.delegate?.didDisconnect() - } + self?.delegate?.didChangeSocketConnectionStatus(status) }.store(in: &publishers) } diff --git a/Sources/WalletConnectAuth/AuthClientDelegate.swift b/Sources/WalletConnectAuth/Auth/AuthClientDelegate.swift similarity index 96% rename from Sources/WalletConnectAuth/AuthClientDelegate.swift rename to Sources/WalletConnectAuth/Auth/AuthClientDelegate.swift index c247f9f89..2e7a7e5ac 100644 --- a/Sources/WalletConnectAuth/AuthClientDelegate.swift +++ b/Sources/WalletConnectAuth/Auth/AuthClientDelegate.swift @@ -1,5 +1,6 @@ import Foundation +import WalletConnectRelay /// A protocol that defines methods that AuthClient instance call on it's delegate to handle sequences level events public protocol AuthClientDelegate: AnyObject { @@ -52,10 +53,7 @@ public protocol AuthClientDelegate: AnyObject { func didReject(proposal: Session.Proposal, reason: Reason) /// Tells the delegate that client has connected WebSocket - func didConnect() - - /// Tells the delegate that client has disconnected WebSocket - func didDisconnect() + func didChangeSocketConnectionStatus(_ status: SocketConnectionStatus) } public extension AuthClientDelegate { diff --git a/Sources/WalletConnectAuth/Auth/AuthConfig.swift b/Sources/WalletConnectAuth/Auth/AuthConfig.swift new file mode 100644 index 000000000..3817a5a85 --- /dev/null +++ b/Sources/WalletConnectAuth/Auth/AuthConfig.swift @@ -0,0 +1,16 @@ +import WalletConnectRelay +import Foundation + +public extension Auth { + struct Config { + let metadata: AppMetadata + let projectId: String + let socketConnectionType: SocketConnectionType + + public init(metadata: AppMetadata, projectId: String, socketConnectionType: SocketConnectionType = .automatic) { + self.metadata = metadata + self.projectId = projectId + self.socketConnectionType = socketConnectionType + } + } +} diff --git a/Sources/WalletConnectAuth/Auth/SocketConnectionStatus.swift b/Sources/WalletConnectAuth/Auth/SocketConnectionStatus.swift new file mode 100644 index 000000000..f9cf90b1e --- /dev/null +++ b/Sources/WalletConnectAuth/Auth/SocketConnectionStatus.swift @@ -0,0 +1,3 @@ +import WalletConnectRelay + +public typealias SocketConnectionStatus = WalletConnectRelay.SocketConnectionStatus diff --git a/Tests/IntegrationTests/ClientDelegate.swift b/Tests/IntegrationTests/ClientDelegate.swift index 867b8a4df..3ec563aea 100644 --- a/Tests/IntegrationTests/ClientDelegate.swift +++ b/Tests/IntegrationTests/ClientDelegate.swift @@ -3,6 +3,10 @@ import Foundation @testable import WalletConnectAuth class ClientDelegate: AuthClientDelegate { + func didChangeSocketConnectionStatus(_ status: SocketConnectionStatus) { + onConnected?() + } + var client: AuthClient var onSessionSettled: ((Session)->())? var onConnected: (()->())? @@ -49,8 +53,6 @@ class ClientDelegate: AuthClientDelegate { func didReceive(sessionResponse: Response) { onSessionResponse?(sessionResponse) } - func didConnect() { - onConnected?() - } + func didDisconnect() {} }