diff --git a/Example/DApp/Sign/Connect/ConnectViewController.swift b/Example/DApp/Sign/Connect/ConnectViewController.swift index 0d4932cc7..3e3da5422 100644 --- a/Example/DApp/Sign/Connect/ConnectViewController.swift +++ b/Example/DApp/Sign/Connect/ConnectViewController.swift @@ -95,11 +95,11 @@ class ConnectViewController: UIViewController, UITableViewDataSource, UITableVie ), "solana": ProposalNamespace( chains: [ - Blockchain("solana:4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ")!, + Blockchain("solana:4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ")! ], methods: [ "solana_signMessage", - "solana_signTransaction", + "solana_signTransaction" ], events: [], extensions: nil ) ] diff --git a/Example/DApp/Sign/SelectChain/SelectChainViewController.swift b/Example/DApp/Sign/SelectChain/SelectChainViewController.swift index f0881e48f..be310e2f9 100644 --- a/Example/DApp/Sign/SelectChain/SelectChainViewController.swift +++ b/Example/DApp/Sign/SelectChain/SelectChainViewController.swift @@ -50,11 +50,11 @@ class SelectChainViewController: UIViewController, UITableViewDataSource { ), "solana": ProposalNamespace( chains: [ - Blockchain("solana:4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ")!, + Blockchain("solana:4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ")! ], methods: [ "solana_signMessage", - "solana_signTransaction", + "solana_signTransaction" ], events: [], extensions: nil ) ] diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index 6e5062005..3630866ec 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -27,11 +27,17 @@ 7694A5262874296A0001257E /* RegistryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7694A5252874296A0001257E /* RegistryTests.swift */; }; 76B149F02821C03B00F05F91 /* Proposal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76B149EF2821C03B00F05F91 /* Proposal.swift */; }; 76B6E39F2807A3B6004DF775 /* WalletViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76B6E39E2807A3B6004DF775 /* WalletViewController.swift */; }; + 840BCF142949B9F900CB0655 /* WalletConnectPush in Frameworks */ = {isa = PBXBuildFile; productRef = 840BCF132949B9F900CB0655 /* WalletConnectPush */; }; + 840BCF162949C6C100CB0655 /* WalletConnectEcho in Frameworks */ = {isa = PBXBuildFile; productRef = 840BCF152949C6C100CB0655 /* WalletConnectEcho */; }; + 8439CB89293F658E00F2F2E2 /* PushMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8439CB88293F658E00F2F2E2 /* PushMessage.swift */; }; 8448F1D427E4726F0000B866 /* WalletConnect in Frameworks */ = {isa = PBXBuildFile; productRef = 8448F1D327E4726F0000B866 /* WalletConnect */; }; 84494388278D9C1B00CC26BB /* UIAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84494387278D9C1B00CC26BB /* UIAlertController.swift */; }; + 845B8D8C2934B36C0084A966 /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845B8D8B2934B36C0084A966 /* Account.swift */; }; 8460DCFC274F98A10081F94C /* RequestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8460DCFB274F98A10081F94C /* RequestViewController.swift */; }; 847CF3AF28E3141700F1D760 /* WalletConnectPush in Frameworks */ = {isa = PBXBuildFile; productRef = 847CF3AE28E3141700F1D760 /* WalletConnectPush */; settings = {ATTRIBUTES = (Required, ); }; }; + 8485617D295079730064877B /* WalletConnectPairing in Frameworks */ = {isa = PBXBuildFile; productRef = 8485617C295079730064877B /* WalletConnectPairing */; }; 849D7A90292665D3006A2BD4 /* WalletConnectVerify in Frameworks */ = {isa = PBXBuildFile; productRef = 849D7A8F292665D3006A2BD4 /* WalletConnectVerify */; }; + 849D7A93292E2169006A2BD4 /* PushTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849D7A92292E2169006A2BD4 /* PushTests.swift */; }; 84AA01DB28CF0CD7005D48D8 /* XCTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AA01DA28CF0CD7005D48D8 /* XCTest.swift */; }; 84CE641F27981DED00142511 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CE641E27981DED00142511 /* AppDelegate.swift */; }; 84CE642127981DED00142511 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CE642027981DED00142511 /* SceneDelegate.swift */; }; @@ -191,6 +197,7 @@ A5E22D222840C8D300E36487 /* WalletEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E22D212840C8D300E36487 /* WalletEngine.swift */; }; A5E22D242840C8DB00E36487 /* SafariEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E22D232840C8DB00E36487 /* SafariEngine.swift */; }; A5E22D2C2840EAC300E36487 /* XCUIElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E22D2B2840EAC300E36487 /* XCUIElement.swift */; }; + C5DD5BE1294E09E3008FD3A4 /* Web3Wallet in Frameworks */ = {isa = PBXBuildFile; productRef = C5DD5BE0294E09E3008FD3A4 /* Web3Wallet */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -237,8 +244,11 @@ 7694A5252874296A0001257E /* RegistryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistryTests.swift; sourceTree = ""; }; 76B149EF2821C03B00F05F91 /* Proposal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Proposal.swift; sourceTree = ""; }; 76B6E39E2807A3B6004DF775 /* WalletViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletViewController.swift; sourceTree = ""; }; + 8439CB88293F658E00F2F2E2 /* PushMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushMessage.swift; sourceTree = ""; }; 84494387278D9C1B00CC26BB /* UIAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIAlertController.swift; sourceTree = ""; }; + 845B8D8B2934B36C0084A966 /* Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = ""; }; 8460DCFB274F98A10081F94C /* RequestViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestViewController.swift; sourceTree = ""; }; + 849D7A92292E2169006A2BD4 /* PushTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushTests.swift; sourceTree = ""; }; 84AA01DA28CF0CD7005D48D8 /* XCTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCTest.swift; sourceTree = ""; }; 84CE641C27981DED00142511 /* DApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 84CE641E27981DED00142511 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -393,7 +403,10 @@ A5AE354728A1A2AC0059AE8A /* Web3 in Frameworks */, A5434023291E6A270068F706 /* SolanaSwift in Frameworks */, 849D7A90292665D3006A2BD4 /* WalletConnectVerify in Frameworks */, + 840BCF162949C6C100CB0655 /* WalletConnectEcho in Frameworks */, + 8485617D295079730064877B /* WalletConnectPairing in Frameworks */, 764E1D5826F8DBAB00A1FB15 /* WalletConnect in Frameworks */, + 840BCF142949B9F900CB0655 /* WalletConnectPush in Frameworks */, A5D85226286333D500DAF5C3 /* Starscream in Frameworks */, A5C4DD8728A2DE88006A626D /* WalletConnectRouter in Frameworks */, ); @@ -437,6 +450,7 @@ 847CF3AF28E3141700F1D760 /* WalletConnectPush in Frameworks */, A5C8BE85292FE20B006CC85C /* Web3 in Frameworks */, 84DDB4ED28ABB663003D66ED /* WalletConnectAuth in Frameworks */, + C5DD5BE1294E09E3008FD3A4 /* Web3Wallet in Frameworks */, A5E03E01286466EA00888481 /* WalletConnectChat in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -567,6 +581,14 @@ path = SessionDetails; sourceTree = ""; }; + 849D7A91292E2115006A2BD4 /* Push */ = { + isa = PBXGroup; + children = ( + 849D7A92292E2169006A2BD4 /* PushTests.swift */, + ); + path = Push; + sourceTree = ""; + }; 84CE641D27981DED00142511 /* DApp */ = { isa = PBXGroup; children = ( @@ -1095,6 +1117,7 @@ A5E03DEE286464DB00888481 /* IntegrationTests */ = { isa = PBXGroup; children = ( + 849D7A91292E2115006A2BD4 /* Push */, 84CEC64728D8A98900D081A8 /* Pairing */, A5E03E0B28646AA500888481 /* Relay */, A5E03E0A28646A8A00888481 /* Stubs */, @@ -1130,6 +1153,8 @@ A5E03E1028646F8000888481 /* KeychainStorageMock.swift */, A5E03DFC286465D100888481 /* Stubs.swift */, 84FE684528ACDB4700C893FF /* RequestParams.swift */, + 845B8D8B2934B36C0084A966 /* Account.swift */, + 8439CB88293F658E00F2F2E2 /* PushMessage.swift */, ); path = Stubs; sourceTree = ""; @@ -1181,6 +1206,9 @@ A5C4DD8628A2DE88006A626D /* WalletConnectRouter */, A5434022291E6A270068F706 /* SolanaSwift */, 849D7A8F292665D3006A2BD4 /* WalletConnectVerify */, + 840BCF132949B9F900CB0655 /* WalletConnectPush */, + 840BCF152949C6C100CB0655 /* WalletConnectEcho */, + 8485617C295079730064877B /* WalletConnectPairing */, ); productName = ExampleApp; productReference = 764E1D3C26F8D3FC00A1FB15 /* WalletConnect Wallet.app */; @@ -1271,6 +1299,7 @@ 84DDB4EC28ABB663003D66ED /* WalletConnectAuth */, 847CF3AE28E3141700F1D760 /* WalletConnectPush */, A5C8BE84292FE20B006CC85C /* Web3 */, + C5DD5BE0294E09E3008FD3A4 /* Web3Wallet */, ); productName = IntegrationTests; productReference = A5E03DED286464DB00888481 /* IntegrationTests.xctest */; @@ -1547,10 +1576,13 @@ files = ( 84CEC64628D89D6B00D081A8 /* PairingTests.swift in Sources */, 767DC83528997F8E00080FA9 /* EthSendTransaction.swift in Sources */, + 8439CB89293F658E00F2F2E2 /* PushMessage.swift in Sources */, A518B31428E33A6500A2CE93 /* InputConfig.swift in Sources */, A541959E2934BFEF0035AD19 /* CacaoSignerTests.swift in Sources */, A59CF4F6292F83D50031A42F /* DefaultSignerFactory.swift in Sources */, A5E03E03286466F400888481 /* ChatTests.swift in Sources */, + 849D7A93292E2169006A2BD4 /* PushTests.swift in Sources */, + 845B8D8C2934B36C0084A966 /* Account.swift in Sources */, 84D2A66628A4F51E0088AE09 /* AuthTests.swift in Sources */, 84FE684628ACDB4700C893FF /* RequestParams.swift in Sources */, 7694A5262874296A0001257E /* RegistryTests.swift in Sources */, @@ -2077,6 +2109,14 @@ isa = XCSwiftPackageProductDependency; productName = WalletConnect; }; + 840BCF132949B9F900CB0655 /* WalletConnectPush */ = { + isa = XCSwiftPackageProductDependency; + productName = WalletConnectPush; + }; + 840BCF152949C6C100CB0655 /* WalletConnectEcho */ = { + isa = XCSwiftPackageProductDependency; + productName = WalletConnectEcho; + }; 8448F1D327E4726F0000B866 /* WalletConnect */ = { isa = XCSwiftPackageProductDependency; productName = WalletConnect; @@ -2085,6 +2125,10 @@ isa = XCSwiftPackageProductDependency; productName = WalletConnectPush; }; + 8485617C295079730064877B /* WalletConnectPairing */ = { + isa = XCSwiftPackageProductDependency; + productName = WalletConnectPairing; + }; 849D7A8F292665D3006A2BD4 /* WalletConnectVerify */ = { isa = XCSwiftPackageProductDependency; productName = WalletConnectVerify; @@ -2162,6 +2206,10 @@ isa = XCSwiftPackageProductDependency; productName = WalletConnectChat; }; + C5DD5BE0294E09E3008FD3A4 /* Web3Wallet */ = { + isa = XCSwiftPackageProductDependency; + productName = Web3Wallet; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 764E1D3426F8D3FC00A1FB15 /* Project object */; diff --git a/Example/ExampleApp/AppDelegate.swift b/Example/ExampleApp/AppDelegate.swift index 8476d254c..8e4872106 100644 --- a/Example/ExampleApp/AppDelegate.swift +++ b/Example/ExampleApp/AppDelegate.swift @@ -1,9 +1,39 @@ import UIKit +import UserNotifications +import WalletConnectNetworking +import WalletConnectEcho +import WalletConnectPairing +import WalletConnectPush +import Combine @main class AppDelegate: UIResponder, UIApplicationDelegate { + private var publishers = [AnyCancellable]() func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + registerForPushNotifications() + + + let metadata = AppMetadata( + name: "Example Wallet", + description: "wallet description", + url: "example.wallet", + icons: ["https://avatars.githubusercontent.com/u/37784886"]) + + Networking.configure(projectId: InputConfig.projectId, socketFactory: DefaultSocketFactory()) + Pair.configure(metadata: metadata) + + let clientId = try! Networking.interactor.getClientId() + let sanitizedClientId = clientId.replacingOccurrences(of: "did:key:", with: "") + + Echo.configure(projectId: InputConfig.projectId, clientId: sanitizedClientId) + Push.wallet.requestPublisher.sink { id, _ in + Task(priority: .high) { try! await Push.wallet.approve(id: id) } + }.store(in: &publishers) + Push.wallet.pushMessagePublisher.sink { pm in + print(pm) + }.store(in: &publishers) + return true } @@ -15,4 +45,61 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { } + + func getNotificationSettings() { + UNUserNotificationCenter.current().getNotificationSettings { settings in + print("Notification settings: \(settings)") + guard settings.authorizationStatus == .authorized else { return } + DispatchQueue.main.async { + UIApplication.shared.registerForRemoteNotifications() + } + } + } + + func registerForPushNotifications() { + UNUserNotificationCenter.current() + .requestAuthorization( + options: [.alert, .sound, .badge]) { [weak self] granted, _ in + print("Permission granted: \(granted)") + guard granted else { return } + self?.getNotificationSettings() + #if targetEnvironment(simulator) + +// let clientId = try! Networking.interactor.socketConnectionStatusPublisher +// .first {$0 == .connected} +// .sink(receiveValue: { status in +// let deviceToken = InputConfig.simulatorIdentifier +// assert(deviceToken != "SIMULATOR_IDENTIFIER", "Please set your Simulator identifier") +// Task(priority: .high) { +// try await Echo.instance.register(deviceToken: deviceToken) +// } +// }) + #endif + } + } + + func modelIdentifier() -> String { + if let simulatorModelIdentifier = ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] { return simulatorModelIdentifier } + var sysinfo = utsname() + uname(&sysinfo) // ignore return value + return String(bytes: Data(bytes: &sysinfo.machine, count: Int(_SYS_NAMELEN)), encoding: .ascii)!.trimmingCharacters(in: .controlCharacters) + } + + func application( + _ application: UIApplication, + didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data + ) { + Task(priority: .high) { + try await Echo.instance.register(deviceToken: deviceToken) + } + } + + func application( + _ application: UIApplication, + didFailToRegisterForRemoteNotificationsWithError error: Error + ) { + // TODO: when is this invoked? + print("Failed to register: \(error)") + } + } diff --git a/Example/ExampleApp/Common/InputConfig.swift b/Example/ExampleApp/Common/InputConfig.swift index 53931721a..a5440b2c7 100644 --- a/Example/ExampleApp/Common/InputConfig.swift +++ b/Example/ExampleApp/Common/InputConfig.swift @@ -5,6 +5,11 @@ struct InputConfig { static var projectId: String { return config(for: "PROJECT_ID")! } + #if targetEnvironment(simulator) + static var simulatorIdentifier: String { + return config(for: "SIMULATOR_IDENTIFIER")! + } + #endif private static func config(for key: String) -> String? { return Bundle.main.object(forInfoDictionaryKey: key) as? String diff --git a/Example/ExampleApp/Info.plist b/Example/ExampleApp/Info.plist index 4d76b146a..97535e087 100644 --- a/Example/ExampleApp/Info.plist +++ b/Example/ExampleApp/Info.plist @@ -4,6 +4,8 @@ PROJECT_ID $(PROJECT_ID) + SIMULATOR_IDENTIFIER + $(SIMULATOR_IDENTIFIER) CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName diff --git a/Example/ExampleApp/SceneDelegate.swift b/Example/ExampleApp/SceneDelegate.swift index c3ddd5743..7e1f7a85c 100644 --- a/Example/ExampleApp/SceneDelegate.swift +++ b/Example/ExampleApp/SceneDelegate.swift @@ -5,21 +5,15 @@ import WalletConnectSign import WalletConnectNetworking import WalletConnectRelay import WalletConnectPairing +import Combine class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? + private var publishers = [AnyCancellable]() 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://avatars.githubusercontent.com/u/37784886"]) - - Networking.configure(projectId: InputConfig.projectId, socketFactory: DefaultSocketFactory()) - Pair.configure(metadata: metadata) #if DEBUG if CommandLine.arguments.contains("-cleanInstall") { try? Sign.instance.cleanup() diff --git a/Example/ExampleApp/Shared/Signer/SOLSigner.swift b/Example/ExampleApp/Shared/Signer/SOLSigner.swift index 557d0177f..f9f328069 100644 --- a/Example/ExampleApp/Shared/Signer/SOLSigner.swift +++ b/Example/ExampleApp/Shared/Signer/SOLSigner.swift @@ -28,7 +28,7 @@ struct SOLSigner { } } -fileprivate struct SolSignTransaction: Codable { +private struct SolSignTransaction: Codable { let instructions: [TransactionInstruction] let recentBlockhash: String let feePayer: PublicKey diff --git a/Example/ExampleApp/Wallet/WalletViewController.swift b/Example/ExampleApp/Wallet/WalletViewController.swift index b20a21ea0..da1d92cad 100644 --- a/Example/ExampleApp/Wallet/WalletViewController.swift +++ b/Example/ExampleApp/Wallet/WalletViewController.swift @@ -8,12 +8,14 @@ import CryptoSwift import Combine final class WalletViewController: UIViewController { + lazy var accounts = [ "eip155": ETHSigner.address, "solana": SOLSigner.address ] var sessionItems: [ActiveSessionItem] = [] + var currentProposal: Session.Proposal? private var publishers = [AnyCancellable]() @@ -36,11 +38,15 @@ final class WalletViewController: UIViewController { walletView.tableView.dataSource = self walletView.tableView.delegate = self - let settledSessions = Sign.instance.getSessions() - sessionItems = getActiveSessionItem(for: settledSessions) + + setUpSessions() setUpAuthSubscribing() } + private func setUpSessions() { + reloadSessions(Sign.instance.getSessions()) + } + @objc private func showScanner() { let scannerViewController = ScannerViewController() @@ -178,10 +184,6 @@ extension WalletViewController: UITableViewDataSource, UITableViewDelegate { Task { do { try await Sign.instance.disconnect(topic: item.topic) - DispatchQueue.main.async { [weak self] in - self?.sessionItems.remove(at: indexPath.row) - tableView.deleteRows(at: [indexPath], with: .automatic) - } } catch { print(error) } @@ -258,12 +260,6 @@ extension WalletViewController { self?.showSessionProposal(Proposal(proposal: sessionProposal)) // FIXME: Remove mock }.store(in: &publishers) - Sign.instance.sessionSettlePublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - self?.reloadActiveSessions() - }.store(in: &publishers) - Sign.instance.sessionRequestPublisher .receive(on: DispatchQueue.main) .sink { [weak self] sessionRequest in @@ -274,9 +270,14 @@ extension WalletViewController { Sign.instance.sessionDeletePublisher .receive(on: DispatchQueue.main) .sink { [weak self] _ in - self?.reloadActiveSessions() self?.navigationController?.popToRootViewController(animated: true) }.store(in: &publishers) + + Sign.instance.sessionsPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] sessions in + self?.reloadSessions(sessions) + }.store(in: &publishers) } private func getActiveSessionItem(for settledSessions: [Session]) -> [ActiveSessionItem] { @@ -290,12 +291,8 @@ extension WalletViewController { } } - private func reloadActiveSessions() { - let settledSessions = Sign.instance.getSessions() - let activeSessions = getActiveSessionItem(for: settledSessions) - DispatchQueue.main.async { // FIXME: Delegate being called from background thread - self.sessionItems = activeSessions - self.walletView.tableView.reloadData() - } + private func reloadSessions(_ sessions: [Session]) { + sessionItems = getActiveSessionItem(for: sessions) + walletView.tableView.reloadData() } } diff --git a/Example/IntegrationTests/Pairing/PairingTests.swift b/Example/IntegrationTests/Pairing/PairingTests.swift index a3724cb67..1952af7e4 100644 --- a/Example/IntegrationTests/Pairing/PairingTests.swift +++ b/Example/IntegrationTests/Pairing/PairingTests.swift @@ -5,7 +5,8 @@ import WalletConnectUtils import WalletConnectRelay import Combine import WalletConnectNetworking -import WalletConnectPush +import WalletConnectEcho +@testable import WalletConnectPush @testable import WalletConnectPairing final class PairingTests: XCTestCase { @@ -13,20 +14,19 @@ final class PairingTests: XCTestCase { var appPairingClient: PairingClient! var walletPairingClient: PairingClient! - var appPushClient: PushClient! - var walletPushClient: PushClient! + var appPushClient: DappPushClient! + var walletPushClient: WalletPushClient! var pairingStorage: PairingStorage! private var publishers = [AnyCancellable]() - func makeClients(prefix: String) -> (PairingClient, PushClient) { + func makeClientDependencies(prefix: String) -> (PairingClient, NetworkInteracting, KeychainStorageProtocol, KeyValueStorage) { let keychain = KeychainStorageMock() let keyValueStorage = RuntimeKeyValueStorage() let relayLogger = ConsoleLogger(suffix: prefix + " [Relay]", loggingLevel: .debug) let pairingLogger = ConsoleLogger(suffix: prefix + " [Pairing]", loggingLevel: .debug) - let pushLogger = ConsoleLogger(suffix: prefix + " [Push]", loggingLevel: .debug) let networkingLogger = ConsoleLogger(suffix: prefix + " [Networking]", loggingLevel: .debug) let relayClient = RelayClient( @@ -49,50 +49,50 @@ final class PairingTests: XCTestCase { keychainStorage: keychain, networkingClient: networkingClient) - let pushClient = PushClientFactory.create( - logger: pushLogger, - keyValueStorage: keyValueStorage, - keychainStorage: keychain, - networkingClient: networkingClient, - pairingClient: pairingClient) - - return (pairingClient, pushClient) + return (pairingClient, networkingClient, keychain, keyValueStorage) } - func makePairingClient(prefix: String) -> PairingClient { - let keychain = KeychainStorageMock() - let logger = ConsoleLogger(suffix: prefix, loggingLevel: .debug) - let keyValueStorage = RuntimeKeyValueStorage() - - let relayClient = RelayClient( - relayHost: InputConfig.relayHost, - projectId: InputConfig.projectId, - keychainStorage: keychain, - socketFactory: DefaultSocketFactory(), - logger: logger) + func makeDappClients() { + let prefix = "🤖 Dapp: " + let (pairingClient, networkingInteractor, keychain, keyValueStorage) = makeClientDependencies(prefix: prefix) + let pushLogger = ConsoleLogger(suffix: prefix + " [Push]", loggingLevel: .debug) + appPairingClient = pairingClient + appPushClient = DappPushClientFactory.create(metadata: AppMetadata(name: name, description: "", url: "", icons: [""]), + logger: pushLogger, + keyValueStorage: keyValueStorage, + keychainStorage: keychain, + networkInteractor: networkingInteractor) + } - let networkingClient = NetworkingClientFactory.create( - relayClient: relayClient, - logger: logger, - keychainStorage: keychain, - keyValueStorage: keyValueStorage) + func makeWalletClients() { + let prefix = "🐶 Wallet: " + let (pairingClient, networkingInteractor, keychain, keyValueStorage) = makeClientDependencies(prefix: prefix) + let pushLogger = ConsoleLogger(suffix: prefix + " [Push]", loggingLevel: .debug) + walletPairingClient = pairingClient + let echoClient = EchoClientFactory.create(projectId: "", clientId: "") + walletPushClient = WalletPushClientFactory.create(logger: pushLogger, + keyValueStorage: keyValueStorage, + keychainStorage: keychain, + networkInteractor: networkingInteractor, + pairingRegisterer: pairingClient, + echoClient: echoClient) + } - let pairingClient = PairingClientFactory.create( - logger: logger, - keyValueStorage: keyValueStorage, - keychainStorage: keychain, - networkingClient: networkingClient) + func makeWalletPairingClient() { + let prefix = "🐶 Wallet: " + let (pairingClient, _, _, _) = makeClientDependencies(prefix: prefix) + walletPairingClient = pairingClient + } - return pairingClient + override func setUp() { + makeDappClients() } func testProposePushOnPairing() async { + makeWalletClients() let expectation = expectation(description: "propose push on pairing") - (appPairingClient, appPushClient) = makeClients(prefix: "🤖 App") - (walletPairingClient, walletPushClient) = makeClients(prefix: "🐶 Wallet") - - walletPushClient.proposalPublisher.sink { _ in + walletPushClient.requestPublisher.sink { _ in expectation.fulfill() }.store(in: &publishers) @@ -100,17 +100,14 @@ final class PairingTests: XCTestCase { try! await walletPairingClient.pair(uri: uri) - try! await appPushClient.propose(topic: uri.topic) + try! await appPushClient.request(account: Account.stub(), topic: uri.topic) wait(for: [expectation], timeout: InputConfig.defaultTimeout) } func testPing() async { let expectation = expectation(description: "expects ping response") - - (appPairingClient, appPushClient) = makeClients(prefix: "🤖 App") - (walletPairingClient, walletPushClient) = makeClients(prefix: "🐶 Wallet") - + makeWalletClients() let uri = try! await appPairingClient.create() try! await walletPairingClient.pair(uri: uri) try! await walletPairingClient.ping(topic: uri.topic) @@ -123,13 +120,11 @@ final class PairingTests: XCTestCase { } func testResponseErrorForMethodUnregistered() async { - (appPairingClient, appPushClient) = makeClients(prefix: "🤖 App") - walletPairingClient = makePairingClient(prefix: "🐶 Wallet") - + makeWalletPairingClient() let expectation = expectation(description: "wallet responds unsupported method for unregistered method") appPushClient.responsePublisher.sink { (_, response) in - XCTAssertEqual(response, .failure(WalletConnectPairing.PairError(code: 10001)!)) + XCTAssertEqual(response, .failure(PushError(code: 10001)!)) expectation.fulfill() }.store(in: &publishers) @@ -137,10 +132,9 @@ final class PairingTests: XCTestCase { try! await walletPairingClient.pair(uri: uri) - try! await appPushClient.propose(topic: uri.topic) + try! await appPushClient.request(account: Account.stub(), topic: uri.topic) wait(for: [expectation], timeout: InputConfig.defaultTimeout) - } func testDisconnect() { diff --git a/Example/IntegrationTests/Push/PushTests.swift b/Example/IntegrationTests/Push/PushTests.swift new file mode 100644 index 000000000..e004b6abd --- /dev/null +++ b/Example/IntegrationTests/Push/PushTests.swift @@ -0,0 +1,227 @@ +import Foundation +import XCTest +import WalletConnectUtils +@testable import WalletConnectKMS +import WalletConnectRelay +import Combine +import WalletConnectNetworking +import WalletConnectEcho +@testable import WalletConnectPush +@testable import WalletConnectPairing + +final class PushTests: XCTestCase { + + var dappPairingClient: PairingClient! + var walletPairingClient: PairingClient! + + var dappPushClient: DappPushClient! + var walletPushClient: WalletPushClient! + + var pairingStorage: PairingStorage! + + private var publishers = [AnyCancellable]() + + func makeClientDependencies(prefix: String) -> (PairingClient, NetworkInteracting, KeychainStorageProtocol, KeyValueStorage) { + let keychain = KeychainStorageMock() + let keyValueStorage = RuntimeKeyValueStorage() + + let relayLogger = ConsoleLogger(suffix: prefix + " [Relay]", loggingLevel: .debug) + let pairingLogger = ConsoleLogger(suffix: prefix + " [Pairing]", loggingLevel: .debug) + let networkingLogger = ConsoleLogger(suffix: prefix + " [Networking]", loggingLevel: .debug) + + let relayClient = RelayClient( + relayHost: InputConfig.relayHost, + projectId: InputConfig.projectId, + keyValueStorage: RuntimeKeyValueStorage(), + keychainStorage: keychain, + socketFactory: DefaultSocketFactory(), + logger: relayLogger) + + let networkingClient = NetworkingClientFactory.create( + relayClient: relayClient, + logger: networkingLogger, + keychainStorage: keychain, + keyValueStorage: keyValueStorage) + + let pairingClient = PairingClientFactory.create( + logger: pairingLogger, + keyValueStorage: keyValueStorage, + keychainStorage: keychain, + networkingClient: networkingClient) + + return (pairingClient, networkingClient, keychain, keyValueStorage) + } + + func makeDappClients() { + let prefix = "🦄 Dapp: " + let (pairingClient, networkingInteractor, keychain, keyValueStorage) = makeClientDependencies(prefix: prefix) + let pushLogger = ConsoleLogger(suffix: prefix + " [Push]", loggingLevel: .debug) + dappPairingClient = pairingClient + dappPushClient = DappPushClientFactory.create(metadata: AppMetadata(name: name, description: "", url: "", icons: [""]), + logger: pushLogger, + keyValueStorage: keyValueStorage, + keychainStorage: keychain, + networkInteractor: networkingInteractor) + } + + func makeWalletClients() { + let prefix = "🦋 Wallet: " + let (pairingClient, networkingInteractor, keychain, keyValueStorage) = makeClientDependencies(prefix: prefix) + let pushLogger = ConsoleLogger(suffix: prefix + " [Push]", loggingLevel: .debug) + walletPairingClient = pairingClient + let echoClient = EchoClientFactory.create(projectId: "", clientId: "") + walletPushClient = WalletPushClientFactory.create(logger: pushLogger, + keyValueStorage: keyValueStorage, + keychainStorage: keychain, + networkInteractor: networkingInteractor, + pairingRegisterer: pairingClient, + echoClient: echoClient) + } + + override func setUp() { + makeDappClients() + makeWalletClients() + } + + func testRequestPush() async { + let expectation = expectation(description: "expects to receive push request") + + let uri = try! await dappPairingClient.create() + try! await walletPairingClient.pair(uri: uri) + try! await dappPushClient.request(account: Account.stub(), topic: uri.topic) + + walletPushClient.requestPublisher.sink { (_, _) in + expectation.fulfill() + } + .store(in: &publishers) + wait(for: [expectation], timeout: InputConfig.defaultTimeout) + } + + func testWalletApprovesPushRequest() async { + let expectation = expectation(description: "expects dapp to receive successful response") + + let uri = try! await dappPairingClient.create() + try! await walletPairingClient.pair(uri: uri) + try! await dappPushClient.request(account: Account.stub(), topic: uri.topic) + + walletPushClient.requestPublisher.sink { [unowned self] (id, _) in + + Task(priority: .high) { try! await walletPushClient.approve(id: id) } + }.store(in: &publishers) + + dappPushClient.responsePublisher.sink { (_, result) in + guard case .success = result else { + XCTFail() + return + } + expectation.fulfill() + }.store(in: &publishers) + + wait(for: [expectation], timeout: InputConfig.defaultTimeout) + } + + func testWalletRejectsPushRequest() async { + let expectation = expectation(description: "expects dapp to receive error response") + + let uri = try! await dappPairingClient.create() + try! await walletPairingClient.pair(uri: uri) + try! await dappPushClient.request(account: Account.stub(), topic: uri.topic) + + walletPushClient.requestPublisher.sink { [unowned self] (id, _) in + + Task(priority: .high) { try! await walletPushClient.reject(id: id) } + }.store(in: &publishers) + + dappPushClient.responsePublisher.sink { (_, result) in + guard case .failure = result else { + XCTFail() + return + } + expectation.fulfill() + }.store(in: &publishers) + + wait(for: [expectation], timeout: InputConfig.defaultTimeout) + } + + func testDappSendsPushMessage() async { + let expectation = expectation(description: "expects wallet to receive push message") + let pushMessage = PushMessage.stub() + let uri = try! await dappPairingClient.create() + try! await walletPairingClient.pair(uri: uri) + try! await dappPushClient.request(account: Account.stub(), topic: uri.topic) + + walletPushClient.requestPublisher.sink { [unowned self] (id, _) in + Task(priority: .high) { try! await walletPushClient.approve(id: id) } + }.store(in: &publishers) + + dappPushClient.responsePublisher.sink { [unowned self] (_, result) in + guard case .success(let subscription) = result else { + XCTFail() + return + } + Task(priority: .userInitiated) { try! await dappPushClient.notify(topic: subscription.topic, message: pushMessage) } + }.store(in: &publishers) + + walletPushClient.pushMessagePublisher.sink { receivedPushMessage in + XCTAssertEqual(pushMessage, receivedPushMessage) + expectation.fulfill() + }.store(in: &publishers) + + wait(for: [expectation], timeout: InputConfig.defaultTimeout) + + } + + func testWalletDeletePushSubscription() async { + let expectation = expectation(description: "expects to delete push subscription") + let uri = try! await dappPairingClient.create() + try! await walletPairingClient.pair(uri: uri) + try! await dappPushClient.request(account: Account.stub(), topic: uri.topic) + var subscriptionTopic: String! + + walletPushClient.requestPublisher.sink { [unowned self] (id, _) in + Task(priority: .high) { try! await walletPushClient.approve(id: id) } + }.store(in: &publishers) + + dappPushClient.responsePublisher.sink { [unowned self] (_, result) in + guard case .success(let subscription) = result else { + XCTFail() + return + } + subscriptionTopic = subscription.topic + Task(priority: .userInitiated) { try! await walletPushClient.delete(topic: subscription.topic)} + }.store(in: &publishers) + + dappPushClient.deleteSubscriptionPublisher.sink { topic in + XCTAssertEqual(subscriptionTopic, topic) + expectation.fulfill() + }.store(in: &publishers) + wait(for: [expectation], timeout: InputConfig.defaultTimeout) + } + + func testDappDeletePushSubscription() async { + let expectation = expectation(description: "expects to delete push subscription") + let uri = try! await dappPairingClient.create() + try! await walletPairingClient.pair(uri: uri) + try! await dappPushClient.request(account: Account.stub(), topic: uri.topic) + var subscriptionTopic: String! + + walletPushClient.requestPublisher.sink { [unowned self] (id, _) in + Task(priority: .high) { try! await walletPushClient.approve(id: id) } + }.store(in: &publishers) + + dappPushClient.responsePublisher.sink { [unowned self] (_, result) in + guard case .success(let subscription) = result else { + XCTFail() + return + } + subscriptionTopic = subscription.topic + Task(priority: .userInitiated) { try! await dappPushClient.delete(topic: subscription.topic)} + }.store(in: &publishers) + + walletPushClient.deleteSubscriptionPublisher.sink { topic in + XCTAssertEqual(subscriptionTopic, topic) + expectation.fulfill() + }.store(in: &publishers) + wait(for: [expectation], timeout: InputConfig.defaultTimeout) + } +} diff --git a/Example/IntegrationTests/Stubs/Account.swift b/Example/IntegrationTests/Stubs/Account.swift new file mode 100644 index 000000000..3363fc459 --- /dev/null +++ b/Example/IntegrationTests/Stubs/Account.swift @@ -0,0 +1,8 @@ +import Foundation +import WalletConnectUtils + +extension Account { + static func stub() -> Account { + return Account(chainIdentifier: "eip155:1", address: "0x724d0D2DaD3fbB0C168f947B87Fa5DBe36F1A8bf")! + } +} diff --git a/Example/IntegrationTests/Stubs/PushMessage.swift b/Example/IntegrationTests/Stubs/PushMessage.swift new file mode 100644 index 000000000..db9f5d870 --- /dev/null +++ b/Example/IntegrationTests/Stubs/PushMessage.swift @@ -0,0 +1,8 @@ +import Foundation +import WalletConnectPush + +extension PushMessage { + static func stub() -> PushMessage { + return PushMessage(title: "test_push_message", body: "", icon: "", url: "") + } +} diff --git a/Example/Wallet.entitlements b/Example/Wallet.entitlements index 44da28df4..55235d503 100644 --- a/Example/Wallet.entitlements +++ b/Example/Wallet.entitlements @@ -2,6 +2,8 @@ + aps-environment + development com.apple.developer.associated-domains applinks:walletconnect.com diff --git a/Package.swift b/Package.swift index a9f2af394..7110440bc 100644 --- a/Package.swift +++ b/Package.swift @@ -19,12 +19,18 @@ let package = Package( .library( name: "WalletConnectAuth", targets: ["Auth"]), + .library( + name: "Web3Wallet", + targets: ["Web3Wallet"]), .library( name: "WalletConnectPairing", targets: ["WalletConnectPairing"]), .library( name: "WalletConnectPush", targets: ["WalletConnectPush"]), + .library( + name: "WalletConnectEcho", + targets: ["WalletConnectEcho"]), .library( name: "WalletConnectRouter", targets: ["WalletConnectRouter"]), @@ -49,10 +55,18 @@ let package = Package( name: "Auth", dependencies: ["WalletConnectPairing"], path: "Sources/Auth"), + .target( + name: "Web3Wallet", + dependencies: ["Auth", "WalletConnectSign"], + path: "Sources/Web3Wallet"), .target( name: "WalletConnectPush", - dependencies: ["WalletConnectPairing"], + dependencies: ["WalletConnectPairing", "WalletConnectEcho", "WalletConnectNetworking"], path: "Sources/WalletConnectPush"), + .target( + name: "WalletConnectEcho", + dependencies: ["WalletConnectNetworking"], + path: "Sources/WalletConnectEcho"), .target( name: "WalletConnectRelay", dependencies: ["WalletConnectKMS"], @@ -85,7 +99,10 @@ let package = Package( dependencies: ["WalletConnectUtils"]), .testTarget( name: "WalletConnectSignTests", - dependencies: ["WalletConnectSign", "TestingUtils"]), + dependencies: ["WalletConnectSign", "WalletConnectUtils", "TestingUtils"]), + .testTarget( + name: "Web3WalletTests", + dependencies: ["Web3Wallet", "TestingUtils"]), .testTarget( name: "WalletConnectPairingTests", dependencies: ["WalletConnectPairing", "TestingUtils"]), diff --git a/README.md b/README.md index 3b269c948..bf481bd7a 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,9 @@ Follow instructions from *Configuration.xcconfig* and configure PROJECT_ID with ``` // Uncomment next line and paste your project id. Get this on: https://cloud.walletconnect.com/sign-in // PROJECT_ID = YOUR_PROJECT_ID +// To use Push Notifications on the Simulator you need to grab the simulator identifier +// from Window->Devices and Simulators->Simulator you're using->Identifier +SIMULATOR_IDENTIFIER = YOUR_SIMULATOR_IDENTIFIER ``` ## Example App open `Example/ExampleApp.xcodeproj` diff --git a/Sources/Auth/AuthClient.swift b/Sources/Auth/AuthClient.swift index 43847d3ef..9ea611488 100644 --- a/Sources/Auth/AuthClient.swift +++ b/Sources/Auth/AuthClient.swift @@ -6,7 +6,7 @@ import Combine /// Cannot be instantiated outside of the SDK /// /// Access via `Auth.instance` -public class AuthClient { +public class AuthClient: AuthClientProtocol { // MARK: - Public Properties diff --git a/Sources/Auth/AuthClientProtocol.swift b/Sources/Auth/AuthClientProtocol.swift new file mode 100644 index 000000000..2604ac2f2 --- /dev/null +++ b/Sources/Auth/AuthClientProtocol.swift @@ -0,0 +1,10 @@ +import Foundation +import Combine + +public protocol AuthClientProtocol { + var authRequestPublisher: AnyPublisher { get } + + func formatMessage(payload: AuthPayload, address: String) throws -> String + func respond(requestId: RPCID, signature: CacaoSignature, from account: Account) async throws + func getPendingRequests(account: Account) throws -> [AuthRequest] +} diff --git a/Sources/Auth/Services/App/AppRespondSubscriber.swift b/Sources/Auth/Services/App/AppRespondSubscriber.swift index 2aaaba9e9..d070880f5 100644 --- a/Sources/Auth/Services/App/AppRespondSubscriber.swift +++ b/Sources/Auth/Services/App/AppRespondSubscriber.swift @@ -37,7 +37,8 @@ class AppRespondSubscriber { networkingInteractor.responseSubscription(on: AuthRequestProtocolMethod()) .sink { [unowned self] (payload: ResponseSubscriptionPayload) in - pairingRegisterer.activate(pairingTopic: payload.topic) + pairingRegisterer.activate(pairingTopic: payload.topic, peerMetadata: nil) + networkingInteractor.unsubscribe(topic: payload.topic) let requestId = payload.id diff --git a/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift index b47f1cbf2..67b541fd2 100644 --- a/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift +++ b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift @@ -15,8 +15,7 @@ class WalletRequestSubscriber { logger: ConsoleLogging, kms: KeyManagementServiceProtocol, walletErrorResponder: WalletErrorResponder, - pairingRegisterer: PairingRegisterer) - { + pairingRegisterer: PairingRegisterer) { self.networkingInteractor = networkingInteractor self.logger = logger self.kms = kms @@ -29,7 +28,11 @@ class WalletRequestSubscriber { pairingRegisterer.register(method: AuthRequestProtocolMethod()) .sink { [unowned self] (payload: RequestSubscriptionPayload) in logger.debug("WalletRequestSubscriber: Received request") - pairingRegisterer.activate(pairingTopic: payload.topic) + + pairingRegisterer.activate( + pairingTopic: payload.topic, + peerMetadata: payload.request.requester.metadata + ) let request = AuthRequest(id: payload.id, payload: payload.request.payloadParams) onRequest?(request) diff --git a/Sources/Chat/Types/UserAccount.swift b/Sources/Chat/Types/UserAccount.swift index 3aa512cf8..e659bd0b9 100644 --- a/Sources/Chat/Types/UserAccount.swift +++ b/Sources/Chat/Types/UserAccount.swift @@ -1,4 +1,3 @@ - struct UserAccount: Codable { let account: Account let publicKey: String diff --git a/Sources/WalletConnectEcho/DecryptionService.swift b/Sources/WalletConnectEcho/DecryptionService.swift new file mode 100644 index 000000000..f2297f84e --- /dev/null +++ b/Sources/WalletConnectEcho/DecryptionService.swift @@ -0,0 +1,13 @@ +import WalletConnectKMS + +class DecryptionService { + private let serializer: Serializing + + init(serializer: Serializing) { + self.serializer = serializer + } + + public func decryptMessage(topic: String, ciphertext: String) throws -> String { + try serializer.deserialize(topic: topic, encodedEnvelope: ciphertext) + } +} diff --git a/Sources/WalletConnectEcho/Echo.swift b/Sources/WalletConnectEcho/Echo.swift new file mode 100644 index 000000000..b876ef135 --- /dev/null +++ b/Sources/WalletConnectEcho/Echo.swift @@ -0,0 +1,25 @@ +import Foundation + +public class Echo { + + public static var instance: EchoClient = { + guard let config = Echo.config else { + fatalError("Error - you must call Echo.configure(_:) before accessing the shared instance.") + } + + return EchoClientFactory.create( + projectId: config.projectId, + clientId: config.clientId) + }() + + private static var config: Config? + + private init() { } + + /// Echo instance config method + /// - Parameters: + /// - tenantId: + static public func configure(projectId: String, clientId: String) { + Echo.config = Echo.Config(clientId: clientId, projectId: projectId) + } +} diff --git a/Sources/WalletConnectEcho/EchoClient.swift b/Sources/WalletConnectEcho/EchoClient.swift new file mode 100644 index 000000000..6f8dd022d --- /dev/null +++ b/Sources/WalletConnectEcho/EchoClient.swift @@ -0,0 +1,21 @@ +import Foundation +import WalletConnectNetworking + +public class EchoClient { + private let registerService: EchoRegisterService + private let decryptionService: DecryptionService + + init(registerService: EchoRegisterService, + decryptionService: DecryptionService) { + self.registerService = registerService + self.decryptionService = decryptionService + } + + public func register(deviceToken: Data) async throws { + try await registerService.register(deviceToken: deviceToken) + } + + public func decryptMessage(topic: String, ciphertext: String) throws -> String { + try decryptionService.decryptMessage(topic: topic, ciphertext: ciphertext) + } +} diff --git a/Sources/WalletConnectEcho/EchoClientFactory.swift b/Sources/WalletConnectEcho/EchoClientFactory.swift new file mode 100644 index 000000000..0cda812cd --- /dev/null +++ b/Sources/WalletConnectEcho/EchoClientFactory.swift @@ -0,0 +1,33 @@ +import Foundation +import WalletConnectNetworking + +public struct EchoClientFactory { + public static func create(projectId: String, clientId: String) -> EchoClient { + + let keychainStorage = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk") + + return EchoClientFactory.create( + projectId: projectId, + clientId: clientId, + keychainStorage: keychainStorage) + } + + static func create(projectId: String, + clientId: String, + keychainStorage: KeychainStorageProtocol) -> EchoClient { + + let httpClient = HTTPNetworkClient(host: "echo.walletconnect.com") + + let registerService = EchoRegisterService(httpClient: httpClient, projectId: projectId, clientId: clientId) + + let kms = KeyManagementService(keychain: keychainStorage) + + let serializer = Serializer(kms: kms) + + let decryptionService = DecryptionService(serializer: serializer) + + return EchoClient( + registerService: registerService, + decryptionService: decryptionService) + } +} diff --git a/Sources/WalletConnectEcho/EchoConfig.swift b/Sources/WalletConnectEcho/EchoConfig.swift new file mode 100644 index 000000000..e0a0cecbc --- /dev/null +++ b/Sources/WalletConnectEcho/EchoConfig.swift @@ -0,0 +1,8 @@ +import Foundation + +extension Echo { + struct Config { + let clientId: String + let projectId: String + } +} diff --git a/Sources/WalletConnectEcho/Register/EchoRegisterService.swift b/Sources/WalletConnectEcho/Register/EchoRegisterService.swift new file mode 100644 index 000000000..d907656fd --- /dev/null +++ b/Sources/WalletConnectEcho/Register/EchoRegisterService.swift @@ -0,0 +1,30 @@ +import Foundation +import WalletConnectNetworking + +actor EchoRegisterService { + private let httpClient: HTTPClient + private let projectId: String + private let clientId: String + + enum Errors: Error { + case registrationFailed + } + + init(httpClient: HTTPClient, projectId: String, clientId: String) { + self.httpClient = httpClient + self.clientId = clientId + self.projectId = projectId + } + + func register(deviceToken: Data) async throws { + let tokenParts = deviceToken.map { data in String(format: "%02.2hhx", data) } + let token = tokenParts.joined() + let response = try await httpClient.request( + EchoResponse.self, + at: EchoAPI.register(clientId: clientId, token: token, projectId: projectId) + ) + guard response.status == .ok else { + throw Errors.registrationFailed + } + } +} diff --git a/Sources/WalletConnectEcho/Register/EchoService.swift b/Sources/WalletConnectEcho/Register/EchoService.swift new file mode 100644 index 000000000..c792cc098 --- /dev/null +++ b/Sources/WalletConnectEcho/Register/EchoService.swift @@ -0,0 +1,46 @@ +import Foundation +import WalletConnectNetworking + +enum EchoAPI: HTTPService { + case register(clientId: String, token: String, projectId: String) + case unregister(clientId: String, projectId: String) + + var path: String { + switch self { + case .register(_, _, let projectId): + return "/\(projectId)/clients" + case .unregister(let clientId, let projectId): + return "/\(projectId)/clients\(clientId)" + } + } + + var method: HTTPMethod { + switch self { + case .register: + return .post + case .unregister: + return .delete + } + } + + var body: Data? { + switch self { + case .register(let clientId, let token, _): + return try? JSONEncoder().encode([ + "client_id": clientId, + "type": "apns", + "token": token + ]) + case .unregister: + return nil + } + } + + var queryParameters: [String: String]? { + return nil + } + + var scheme: String { + return "https" + } +} diff --git a/Sources/WalletConnectEcho/Register/EcoResponse.swift b/Sources/WalletConnectEcho/Register/EcoResponse.swift new file mode 100644 index 000000000..4c50053a0 --- /dev/null +++ b/Sources/WalletConnectEcho/Register/EcoResponse.swift @@ -0,0 +1,10 @@ +import Foundation + +struct EchoResponse: Codable { + enum Status: String, Codable { + case ok = "OK" + case failed = "FAILED" + } + + let status: Status +} diff --git a/Sources/WalletConnectNetworking/HTTPClient/HTTPService.swift b/Sources/WalletConnectNetworking/HTTPClient/HTTPService.swift index f1b4ee2f6..1a1a75b62 100644 --- a/Sources/WalletConnectNetworking/HTTPClient/HTTPService.swift +++ b/Sources/WalletConnectNetworking/HTTPClient/HTTPService.swift @@ -3,6 +3,7 @@ import Foundation public enum HTTPMethod: String { case get = "GET" case post = "POST" + case delete = "DELETE" } public protocol HTTPService { diff --git a/Sources/WalletConnectNetworking/NetworkInteracting.swift b/Sources/WalletConnectNetworking/NetworkInteracting.swift index c9374830f..ea759adfe 100644 --- a/Sources/WalletConnectNetworking/NetworkInteracting.swift +++ b/Sources/WalletConnectNetworking/NetworkInteracting.swift @@ -6,6 +6,7 @@ public protocol NetworkInteracting { var requestPublisher: AnyPublisher<(topic: String, request: RPCRequest), Never> { get } func subscribe(topic: String) async throws func unsubscribe(topic: String) + func batchUnsubscribe(topics: [String]) async throws func request(_ request: RPCRequest, topic: String, protocolMethod: ProtocolMethod, envelopeType: Envelope.EnvelopeType) async throws func requestNetworkAck(_ request: RPCRequest, topic: String, protocolMethod: ProtocolMethod) async throws func respond(topic: String, response: RPCResponse, protocolMethod: ProtocolMethod, envelopeType: Envelope.EnvelopeType) async throws @@ -23,6 +24,8 @@ public protocol NetworkInteracting { func responseErrorSubscription( on request: ProtocolMethod ) -> AnyPublisher, Never> + + func getClientId() throws -> String } extension NetworkInteracting { diff --git a/Sources/WalletConnectNetworking/NetworkingClient.swift b/Sources/WalletConnectNetworking/NetworkingClient.swift index e7b31e0d3..150208766 100644 --- a/Sources/WalletConnectNetworking/NetworkingClient.swift +++ b/Sources/WalletConnectNetworking/NetworkingClient.swift @@ -5,5 +5,4 @@ public protocol NetworkingClient { var socketConnectionStatusPublisher: AnyPublisher { get } func connect() throws func disconnect(closeCode: URLSessionWebSocketTask.CloseCode) throws - func getClientId() throws -> String } diff --git a/Sources/WalletConnectNetworking/NetworkingInteractor.swift b/Sources/WalletConnectNetworking/NetworkingInteractor.swift index 4aa117c12..9e72ba110 100644 --- a/Sources/WalletConnectNetworking/NetworkingInteractor.swift +++ b/Sources/WalletConnectNetworking/NetworkingInteractor.swift @@ -56,6 +56,11 @@ public class NetworkingInteractor: NetworkInteracting { } } + public func batchUnsubscribe(topics: [String]) async throws { + try await relayClient.batchUnsubscribe(topics: topics) + rpcHistory.deleteAll(forTopics: topics) + } + public func requestSubscription(on request: ProtocolMethod) -> AnyPublisher, Never> { return requestPublisher .filter { rpcRequest in @@ -131,6 +136,10 @@ public class NetworkingInteractor: NetworkInteracting { try await respond(topic: topic, response: response, protocolMethod: protocolMethod, envelopeType: envelopeType) } + public func getClientId() throws -> String { + try relayClient.getClientId() + } + public func respondError(topic: String, requestId: RPCID, protocolMethod: ProtocolMethod, reason: Reason, envelopeType: Envelope.EnvelopeType) async throws { let error = JSONRPCError(code: reason.code, message: reason.message) let response = RPCResponse(id: requestId, error: error) @@ -175,8 +184,4 @@ extension NetworkingInteractor: NetworkingClient { public func disconnect(closeCode: URLSessionWebSocketTask.CloseCode) throws { try relayClient.disconnect(closeCode: closeCode) } - - public func getClientId() throws -> String { - try relayClient.getClientId() - } } diff --git a/Sources/WalletConnectPairing/PairingClient.swift b/Sources/WalletConnectPairing/PairingClient.swift index 9346c983c..41aabf244 100644 --- a/Sources/WalletConnectPairing/PairingClient.swift +++ b/Sources/WalletConnectPairing/PairingClient.swift @@ -1,7 +1,7 @@ import Foundation import Combine -public class PairingClient: PairingRegisterer, PairingInteracting { +public class PairingClient: PairingRegisterer, PairingInteracting, PairingClientProtocol { public var pingResponsePublisher: AnyPublisher<(String), Never> { pingResponsePublisherSubject.eraseToAnyPublisher() } @@ -10,7 +10,6 @@ public class PairingClient: PairingRegisterer, PairingInteracting { private let walletPairService: WalletPairService private let appPairService: AppPairService private let appPairActivateService: AppPairActivationService - private let appUpdateMetadataService: AppUpdateMetadataService private var pingResponsePublisherSubject = PassthroughSubject() private let logger: ConsoleLogging private let pingService: PairingPingService @@ -32,7 +31,6 @@ public class PairingClient: PairingRegisterer, PairingInteracting { expirationService: ExpirationService, pairingRequestsSubscriber: PairingRequestsSubscriber, appPairActivateService: AppPairActivationService, - appUpdateMetadataService: AppUpdateMetadataService, cleanupService: PairingCleanupService, pingService: PairingPingService, socketConnectionStatusPublisher: AnyPublisher, @@ -45,7 +43,6 @@ public class PairingClient: PairingRegisterer, PairingInteracting { self.logger = logger self.deletePairingService = deletePairingService self.appPairActivateService = appPairActivateService - self.appUpdateMetadataService = appUpdateMetadataService self.resubscribeService = resubscribeService self.expirationService = expirationService self.cleanupService = cleanupService @@ -81,12 +78,8 @@ public class PairingClient: PairingRegisterer, PairingInteracting { return try await appPairService.create() } - public func activate(pairingTopic: String) { - appPairActivateService.activate(for: pairingTopic) - } - - public func updateMetadata(_ topic: String, metadata: AppMetadata) { - appUpdateMetadataService.updatePairingMetadata(topic: topic, metadata: metadata) + public func activate(pairingTopic: String, peerMetadata: AppMetadata?) { + appPairActivateService.activate(for: pairingTopic, peerMetadata: peerMetadata) } public func getPairings() -> [Pairing] { diff --git a/Sources/WalletConnectPairing/PairingClientFactory.swift b/Sources/WalletConnectPairing/PairingClientFactory.swift index 573ba8fe0..4efb67249 100644 --- a/Sources/WalletConnectPairing/PairingClientFactory.swift +++ b/Sources/WalletConnectPairing/PairingClientFactory.swift @@ -20,7 +20,6 @@ public struct PairingClientFactory { let deletePairingService = DeletePairingService(networkingInteractor: networkingClient, kms: kms, pairingStorage: pairingStore, logger: logger) let pingService = PairingPingService(pairingStorage: pairingStore, networkingInteractor: networkingClient, logger: logger) let appPairActivateService = AppPairActivationService(pairingStorage: pairingStore, logger: logger) - let appUpdateMetadataService = AppUpdateMetadataService(pairingStore: pairingStore) let expirationService = ExpirationService(pairingStorage: pairingStore, networkInteractor: networkingClient, kms: kms) let resubscribeService = ResubscribeService(networkInteractor: networkingClient, pairingStorage: pairingStore) @@ -34,7 +33,6 @@ public struct PairingClientFactory { expirationService: expirationService, pairingRequestsSubscriber: pairingRequestsSubscriber, appPairActivateService: appPairActivateService, - appUpdateMetadataService: appUpdateMetadataService, cleanupService: cleanupService, pingService: pingService, socketConnectionStatusPublisher: networkingClient.socketConnectionStatusPublisher, diff --git a/Sources/WalletConnectPairing/PairingClientProtocol.swift b/Sources/WalletConnectPairing/PairingClientProtocol.swift new file mode 100644 index 000000000..680cb3090 --- /dev/null +++ b/Sources/WalletConnectPairing/PairingClientProtocol.swift @@ -0,0 +1,3 @@ +public protocol PairingClientProtocol { + func pair(uri: WalletConnectURI) async throws +} diff --git a/Sources/WalletConnectPairing/PairingInteracting.swift b/Sources/WalletConnectPairing/PairingInteracting.swift index 91a5d2923..b0eb2cd38 100644 --- a/Sources/WalletConnectPairing/PairingInteracting.swift +++ b/Sources/WalletConnectPairing/PairingInteracting.swift @@ -12,7 +12,7 @@ public protocol PairingInteracting { func ping(topic: String) async throws func disconnect(topic: String) async throws - + #if DEBUG func cleanup() throws #endif diff --git a/Sources/WalletConnectPairing/PairingRegisterer.swift b/Sources/WalletConnectPairing/PairingRegisterer.swift index 7d3250d66..6aa017038 100644 --- a/Sources/WalletConnectPairing/PairingRegisterer.swift +++ b/Sources/WalletConnectPairing/PairingRegisterer.swift @@ -6,7 +6,6 @@ public protocol PairingRegisterer { method: ProtocolMethod ) -> AnyPublisher, Never> - func activate(pairingTopic: String) + func activate(pairingTopic: String, peerMetadata: AppMetadata?) func validatePairingExistance(_ topic: String) throws - func updateMetadata(_ topic: String, metadata: AppMetadata) } diff --git a/Sources/WalletConnectPairing/Services/App/AppPairActivationService.swift b/Sources/WalletConnectPairing/Services/App/AppPairActivationService.swift index 43d310dc6..3ce060f22 100644 --- a/Sources/WalletConnectPairing/Services/App/AppPairActivationService.swift +++ b/Sources/WalletConnectPairing/Services/App/AppPairActivationService.swift @@ -2,23 +2,30 @@ import Foundation import Combine final class AppPairActivationService { - private let pairingStorage: PairingStorage + enum Errors: Error { + case pairingNotFound + } + + private let pairingStorage: WCPairingStorage private let logger: ConsoleLogging - init(pairingStorage: PairingStorage, logger: ConsoleLogging) { + init(pairingStorage: WCPairingStorage, logger: ConsoleLogging) { self.pairingStorage = pairingStorage self.logger = logger } - func activate(for topic: String) { + func activate(for topic: String, peerMetadata: AppMetadata?) { guard var pairing = pairingStorage.getPairing(forTopic: topic) else { return logger.error("Pairing not found for topic: \(topic)") } + if !pairing.active { pairing.activate() } else { try? pairing.updateExpiry() } + + pairing.updatePeerMetadata(peerMetadata) pairingStorage.setPairing(pairing) } } diff --git a/Sources/WalletConnectPairing/Services/App/AppUpdateMetadataService.swift b/Sources/WalletConnectPairing/Services/App/AppUpdateMetadataService.swift deleted file mode 100644 index 4901e567a..000000000 --- a/Sources/WalletConnectPairing/Services/App/AppUpdateMetadataService.swift +++ /dev/null @@ -1,16 +0,0 @@ -import Foundation - -final class AppUpdateMetadataService { - - private let pairingStore: WCPairingStorage - - init(pairingStore: WCPairingStorage) { - self.pairingStore = pairingStore - } - - func updatePairingMetadata(topic: String, metadata: AppMetadata) { - guard var pairing = pairingStore.getPairing(forTopic: topic) else { return } - pairing.peerMetadata = metadata - pairingStore.setPairing(pairing) - } -} diff --git a/Sources/WalletConnectPairing/Services/Common/Ping/PairingPingService.swift b/Sources/WalletConnectPairing/Services/Common/Ping/PairingPingService.swift index c0e7c723e..e1250bd5a 100644 --- a/Sources/WalletConnectPairing/Services/Common/Ping/PairingPingService.swift +++ b/Sources/WalletConnectPairing/Services/Common/Ping/PairingPingService.swift @@ -6,7 +6,7 @@ public class PairingPingService { private let pingResponder: PingResponder private let pingResponseSubscriber: PingResponseSubscriber - public var onResponse: ((String)->Void)? { + public var onResponse: ((String) -> Void)? { get { return pingResponseSubscriber.onResponse } diff --git a/Sources/WalletConnectPairing/Services/Common/Ping/PingResponseSubscriber.swift b/Sources/WalletConnectPairing/Services/Common/Ping/PingResponseSubscriber.swift index 79c57a8f2..77aa18c7b 100644 --- a/Sources/WalletConnectPairing/Services/Common/Ping/PingResponseSubscriber.swift +++ b/Sources/WalletConnectPairing/Services/Common/Ping/PingResponseSubscriber.swift @@ -6,7 +6,7 @@ public class PingResponseSubscriber { private let logger: ConsoleLogging private var publishers = [AnyCancellable]() - public var onResponse: ((String)->Void)? + public var onResponse: ((String) -> Void)? public init(networkingInteractor: NetworkInteracting, method: ProtocolMethod, diff --git a/Sources/WalletConnectPairing/Types/PairError.swift b/Sources/WalletConnectPairing/Types/PairError.swift index 5c27a00ac..79e28f4a0 100644 --- a/Sources/WalletConnectPairing/Types/PairError.swift +++ b/Sources/WalletConnectPairing/Types/PairError.swift @@ -1,4 +1,3 @@ - public enum PairError: Codable, Equatable, Error, Reason { case methodUnsupported diff --git a/Sources/WalletConnectPairing/Types/WCPairing.swift b/Sources/WalletConnectPairing/Types/WCPairing.swift index a18a7dac5..6ceb323cd 100644 --- a/Sources/WalletConnectPairing/Types/WCPairing.swift +++ b/Sources/WalletConnectPairing/Types/WCPairing.swift @@ -7,8 +7,8 @@ public struct WCPairing: SequenceObject { public let topic: String public let relay: RelayProtocolOptions - public var peerMetadata: AppMetadata? + public private (set) var peerMetadata: AppMetadata? public private (set) var expiryDate: Date public private (set) var active: Bool @@ -53,6 +53,10 @@ public struct WCPairing: SequenceObject { try? updateExpiry() } + public mutating func updatePeerMetadata(_ metadata: AppMetadata?) { + peerMetadata = metadata + } + public mutating func updateExpiry(_ ttl: TimeInterval = WCPairing.timeToLiveActive) throws { let now = Self.dateInitializer() let newExpiryDate = now.advanced(by: ttl) diff --git a/Sources/WalletConnectPush/Client/Common/DeletePushSubscriptionService.swift b/Sources/WalletConnectPush/Client/Common/DeletePushSubscriptionService.swift new file mode 100644 index 000000000..14c3bba6b --- /dev/null +++ b/Sources/WalletConnectPush/Client/Common/DeletePushSubscriptionService.swift @@ -0,0 +1,38 @@ +import Foundation +import WalletConnectKMS +import WalletConnectUtils + +class DeletePushSubscriptionService { + enum Errors: Error { + case pushSubscriptionNotFound + } + private let networkingInteractor: NetworkInteracting + private let kms: KeyManagementServiceProtocol + private let logger: ConsoleLogging + private let pushSubscriptionStore: CodableStore + + init(networkingInteractor: NetworkInteracting, + kms: KeyManagementServiceProtocol, + logger: ConsoleLogging, + pushSubscriptionStore: CodableStore) { + self.networkingInteractor = networkingInteractor + self.kms = kms + self.logger = logger + self.pushSubscriptionStore = pushSubscriptionStore + } + + func delete(topic: String) async throws { + guard let _ = try? pushSubscriptionStore.get(key: topic) + else { throw Errors.pushSubscriptionNotFound} + let protocolMethod = PushDeleteProtocolMethod() + let params = PushDeleteParams.userDisconnected + logger.debug("Will delete push subscription for reason: message: \(params.message) code: \(params.code)") + let request = RPCRequest(method: protocolMethod.method, params: params) + try await networkingInteractor.request(request, topic: topic, protocolMethod: protocolMethod) + + networkingInteractor.unsubscribe(topic: topic) + pushSubscriptionStore.delete(forKey: topic) + + kms.deleteSymmetricKey(for: topic) + } +} diff --git a/Sources/WalletConnectPush/Client/Common/DeletePushSubscriptionSubscriber.swift b/Sources/WalletConnectPush/Client/Common/DeletePushSubscriptionSubscriber.swift new file mode 100644 index 000000000..6d20b3dc8 --- /dev/null +++ b/Sources/WalletConnectPush/Client/Common/DeletePushSubscriptionSubscriber.swift @@ -0,0 +1,39 @@ +import Foundation +import Combine +import WalletConnectKMS +import WalletConnectPairing + +class DeletePushSubscriptionSubscriber { + private let networkingInteractor: NetworkInteracting + private let kms: KeyManagementServiceProtocol + private let logger: ConsoleLogging + private var publishers = [AnyCancellable]() + private let pushSubscriptionStore: CodableStore + + var onDelete: ((String) -> Void)? + + init(networkingInteractor: NetworkInteracting, + kms: KeyManagementServiceProtocol, + logger: ConsoleLogging, + pushSubscriptionStore: CodableStore + ) { + self.networkingInteractor = networkingInteractor + self.kms = kms + self.logger = logger + self.pushSubscriptionStore = pushSubscriptionStore + subscribeForDeleteSubscription() + } + + private func subscribeForDeleteSubscription() { + let protocolMethod = PushDeleteProtocolMethod() + networkingInteractor.requestSubscription(on: protocolMethod) + .sink { [unowned self] (payload: RequestSubscriptionPayload) in + logger.debug("Peer deleted subscription") + let topic = payload.topic + networkingInteractor.unsubscribe(topic: topic) + pushSubscriptionStore.delete(forKey: topic) + kms.deleteSymmetricKey(for: topic) + onDelete?(payload.topic) + }.store(in: &publishers) + } +} diff --git a/Sources/WalletConnectPush/Client/Common/SubscriptionsProvider.swift b/Sources/WalletConnectPush/Client/Common/SubscriptionsProvider.swift new file mode 100644 index 000000000..7db605eaf --- /dev/null +++ b/Sources/WalletConnectPush/Client/Common/SubscriptionsProvider.swift @@ -0,0 +1,13 @@ +import Foundation + +class SubscriptionsProvider { + let store: CodableStore + + init(store: CodableStore) { + self.store = store + } + + public func getActiveSubscriptions() -> [PushSubscription] { + store.getAll() + } +} diff --git a/Sources/WalletConnectPush/Client/Dapp/DappPushClient.swift b/Sources/WalletConnectPush/Client/Dapp/DappPushClient.swift new file mode 100644 index 000000000..819b27120 --- /dev/null +++ b/Sources/WalletConnectPush/Client/Dapp/DappPushClient.swift @@ -0,0 +1,74 @@ +import Foundation +import Combine +import WalletConnectUtils + +public class DappPushClient { + + private let responsePublisherSubject = PassthroughSubject<(id: RPCID, result: Result), Never>() + + public var responsePublisher: AnyPublisher<(id: RPCID, result: Result), Never> { + responsePublisherSubject.eraseToAnyPublisher() + } + + private let deleteSubscriptionPublisherSubject = PassthroughSubject() + + public var deleteSubscriptionPublisher: AnyPublisher { + deleteSubscriptionPublisherSubject.eraseToAnyPublisher() + } + + public let logger: ConsoleLogging + + private let pushProposer: PushProposer + private let pushMessageSender: PushMessageSender + private let proposalResponseSubscriber: ProposalResponseSubscriber + private let subscriptionsProvider: SubscriptionsProvider + private let deletePushSubscriptionService: DeletePushSubscriptionService + private let deletePushSubscriptionSubscriber: DeletePushSubscriptionSubscriber + + init(logger: ConsoleLogging, + kms: KeyManagementServiceProtocol, + pushProposer: PushProposer, + proposalResponseSubscriber: ProposalResponseSubscriber, + pushMessageSender: PushMessageSender, + subscriptionsProvider: SubscriptionsProvider, + deletePushSubscriptionService: DeletePushSubscriptionService, + deletePushSubscriptionSubscriber: DeletePushSubscriptionSubscriber) { + self.logger = logger + self.pushProposer = pushProposer + self.proposalResponseSubscriber = proposalResponseSubscriber + self.pushMessageSender = pushMessageSender + self.subscriptionsProvider = subscriptionsProvider + self.deletePushSubscriptionService = deletePushSubscriptionService + self.deletePushSubscriptionSubscriber = deletePushSubscriptionSubscriber + setupSubscriptions() + } + + public func request(account: Account, topic: String) async throws { + try await pushProposer.request(topic: topic, account: account) + } + + public func notify(topic: String, message: PushMessage) async throws { + try await pushMessageSender.request(topic: topic, message: message) + } + + public func getActiveSubscriptions() -> [PushSubscription] { + subscriptionsProvider.getActiveSubscriptions() + } + + public func delete(topic: String) async throws { + try await deletePushSubscriptionService.delete(topic: topic) + } + +} + +private extension DappPushClient { + + func setupSubscriptions() { + proposalResponseSubscriber.onResponse = {[unowned self] (id, result) in + responsePublisherSubject.send((id, result)) + } + deletePushSubscriptionSubscriber.onDelete = {[unowned self] topic in + deleteSubscriptionPublisherSubject.send(topic) + } + } +} diff --git a/Sources/WalletConnectPush/Client/Dapp/DappPushClientFactory.swift b/Sources/WalletConnectPush/Client/Dapp/DappPushClientFactory.swift new file mode 100644 index 000000000..37a029646 --- /dev/null +++ b/Sources/WalletConnectPush/Client/Dapp/DappPushClientFactory.swift @@ -0,0 +1,39 @@ +import Foundation +import WalletConnectPairing + +public struct DappPushClientFactory { + + public static func create(metadata: AppMetadata, networkInteractor: NetworkInteracting) -> DappPushClient { + let logger = ConsoleLogger(loggingLevel: .off) + let keyValueStorage = UserDefaults.standard + let keychainStorage = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk") + return DappPushClientFactory.create( + metadata: metadata, + logger: logger, + keyValueStorage: keyValueStorage, + keychainStorage: keychainStorage, + networkInteractor: networkInteractor + ) + } + + static func create(metadata: AppMetadata, logger: ConsoleLogging, keyValueStorage: KeyValueStorage, keychainStorage: KeychainStorageProtocol, networkInteractor: NetworkInteracting) -> DappPushClient { + let kms = KeyManagementService(keychain: keychainStorage) + let pushProposer = PushProposer(networkingInteractor: networkInteractor, kms: kms, appMetadata: metadata, logger: logger) + let subscriptionStore = CodableStore(defaults: keyValueStorage, identifier: PushStorageIdntifiers.pushSubscription) + let proposalResponseSubscriber = ProposalResponseSubscriber(networkingInteractor: networkInteractor, kms: kms, logger: logger, metadata: metadata, relay: RelayProtocolOptions(protocol: "irn", data: nil), subscriptionsStore: subscriptionStore) + let pushMessageSender = PushMessageSender(networkingInteractor: networkInteractor, kms: kms, logger: logger) + let subscriptionProvider = SubscriptionsProvider(store: subscriptionStore) + let deletePushSubscriptionService = DeletePushSubscriptionService(networkingInteractor: networkInteractor, kms: kms, logger: logger, pushSubscriptionStore: subscriptionStore) + let deletePushSubscriptionSubscriber = DeletePushSubscriptionSubscriber(networkingInteractor: networkInteractor, kms: kms, logger: logger, pushSubscriptionStore: subscriptionStore) + return DappPushClient( + logger: logger, + kms: kms, + pushProposer: pushProposer, + proposalResponseSubscriber: proposalResponseSubscriber, + pushMessageSender: pushMessageSender, + subscriptionsProvider: subscriptionProvider, + deletePushSubscriptionService: deletePushSubscriptionService, + deletePushSubscriptionSubscriber: deletePushSubscriptionSubscriber + ) + } +} diff --git a/Sources/WalletConnectPush/Client/Dapp/ProposalResponseSubscriber.swift b/Sources/WalletConnectPush/Client/Dapp/ProposalResponseSubscriber.swift new file mode 100644 index 000000000..b1a97d158 --- /dev/null +++ b/Sources/WalletConnectPush/Client/Dapp/ProposalResponseSubscriber.swift @@ -0,0 +1,71 @@ +import Foundation +import Combine +import WalletConnectKMS +import WalletConnectNetworking + +class ProposalResponseSubscriber { + private let networkingInteractor: NetworkInteracting + private let kms: KeyManagementServiceProtocol + private let logger: ConsoleLogging + private var publishers = [AnyCancellable]() + private let metadata: AppMetadata + private let relay: RelayProtocolOptions + var onResponse: ((_ id: RPCID, _ result: Result) -> Void)? + private let subscriptionsStore: CodableStore + + init(networkingInteractor: NetworkInteracting, + kms: KeyManagementServiceProtocol, + logger: ConsoleLogging, + metadata: AppMetadata, + relay: RelayProtocolOptions, + subscriptionsStore: CodableStore) { + self.networkingInteractor = networkingInteractor + self.kms = kms + self.logger = logger + self.metadata = metadata + self.relay = relay + self.subscriptionsStore = subscriptionsStore + subscribeForProposalErrors() + subscribeForProposalResponse() + } + + private func subscribeForProposalResponse() { + let protocolMethod = PushRequestProtocolMethod() + networkingInteractor.responseSubscription(on: protocolMethod) + .sink { [unowned self] (payload: ResponseSubscriptionPayload) in + logger.debug("Received Push Proposal response") + Task(priority: .userInitiated) { + let pushSubscription = try await handleResponse(payload: payload) + onResponse?(payload.id, .success(pushSubscription)) + } + }.store(in: &publishers) + } + + private func handleResponse(payload: ResponseSubscriptionPayload) async throws -> PushSubscription { + let peerPublicKeyHex = payload.response.publicKey + let selfpublicKeyHex = payload.request.publicKey + let (topic, _) = try generateAgreementKeys(peerPublicKeyHex: peerPublicKeyHex, selfpublicKeyHex: selfpublicKeyHex) + + let pushSubscription = PushSubscription(topic: topic, relay: relay, metadata: metadata) + subscriptionsStore.set(pushSubscription, forKey: topic) + try await networkingInteractor.subscribe(topic: topic) + return pushSubscription + } + + private func generateAgreementKeys(peerPublicKeyHex: String, selfpublicKeyHex: String) throws -> (topic: String, keys: AgreementKeys) { + let selfPublicKey = try AgreementPublicKey(hex: selfpublicKeyHex) + let keys = try kms.performKeyAgreement(selfPublicKey: selfPublicKey, peerPublicKey: peerPublicKeyHex) + let topic = keys.derivedTopic() + try kms.setAgreementSecret(keys, topic: topic) + return (topic: topic, keys: keys) + } + + private func subscribeForProposalErrors() { + let protocolMethod = PushRequestProtocolMethod() + networkingInteractor.responseErrorSubscription(on: protocolMethod) + .sink { [unowned self] (payload: ResponseSubscriptionErrorPayload) in + guard let error = PushError(code: payload.error.code) else { return } + onResponse?(payload.id, .failure(error)) + }.store(in: &publishers) + } +} diff --git a/Sources/WalletConnectPush/PushProposer.swift b/Sources/WalletConnectPush/Client/Dapp/PushMessageSender.swift similarity index 58% rename from Sources/WalletConnectPush/PushProposer.swift rename to Sources/WalletConnectPush/Client/Dapp/PushMessageSender.swift index a3360e1b2..a0c134cae 100644 --- a/Sources/WalletConnectPush/PushProposer.swift +++ b/Sources/WalletConnectPush/Client/Dapp/PushMessageSender.swift @@ -1,7 +1,6 @@ import Foundation -import Combine -class PushProposer { +class PushMessageSender { private let networkingInteractor: NetworkInteracting private let kms: KeyManagementServiceProtocol private let logger: ConsoleLogging @@ -14,10 +13,10 @@ class PushProposer { self.logger = logger } - func request(topic: String, params: PushRequestParams) async throws { - logger.debug("Sending Push Proposal") - let protocolMethod = PushProposeProtocolMethod() - let request = RPCRequest(method: protocolMethod.method, params: params) - try await networkingInteractor.requestNetworkAck(request, topic: topic, protocolMethod: protocolMethod) + func request(topic: String, message: PushMessage) async throws { + logger.debug("PushMessageSender: Sending Push Message") + let protocolMethod = PushMessageProtocolMethod() + let request = RPCRequest(method: protocolMethod.method, params: message) + try await networkingInteractor.request(request, topic: topic, protocolMethod: protocolMethod) } } diff --git a/Sources/WalletConnectPush/Client/Dapp/PushProposer.swift b/Sources/WalletConnectPush/Client/Dapp/PushProposer.swift new file mode 100644 index 000000000..973e29edf --- /dev/null +++ b/Sources/WalletConnectPush/Client/Dapp/PushProposer.swift @@ -0,0 +1,29 @@ +import Foundation +import Combine +import WalletConnectPairing + +class PushProposer { + private let networkingInteractor: NetworkInteracting + private let kms: KeyManagementServiceProtocol + private let appMetadata: AppMetadata + private let logger: ConsoleLogging + + init(networkingInteractor: NetworkInteracting, + kms: KeyManagementServiceProtocol, + appMetadata: AppMetadata, + logger: ConsoleLogging) { + self.networkingInteractor = networkingInteractor + self.kms = kms + self.logger = logger + self.appMetadata = appMetadata + } + + func request(topic: String, account: Account) async throws { + logger.debug("PushProposer: Sending Push Proposal") + let protocolMethod = PushRequestProtocolMethod() + let pubKey = try kms.createX25519KeyPair() + let params = PushRequestParams(publicKey: pubKey.hexRepresentation, metadata: appMetadata, account: account) + let request = RPCRequest(method: protocolMethod.method, params: params) + try await networkingInteractor.request(request, topic: topic, protocolMethod: protocolMethod) + } +} diff --git a/Sources/WalletConnectPush/Client/Wallet/PushMessageSubscriber.swift b/Sources/WalletConnectPush/Client/Wallet/PushMessageSubscriber.swift new file mode 100644 index 000000000..8c61a0efd --- /dev/null +++ b/Sources/WalletConnectPush/Client/Wallet/PushMessageSubscriber.swift @@ -0,0 +1,32 @@ +import Foundation +import Combine +import WalletConnectKMS +import WalletConnectPairing + +class PushMessageSubscriber { + private let networkingInteractor: NetworkInteracting + private let kms: KeyManagementServiceProtocol + private let logger: ConsoleLogging + private var publishers = [AnyCancellable]() + var onPushMessage: ((_ message: PushMessage) -> Void)? + + init(networkingInteractor: NetworkInteracting, + kms: KeyManagementServiceProtocol, + logger: ConsoleLogging) { + self.networkingInteractor = networkingInteractor + self.kms = kms + self.logger = logger + subscribeForPushMessages() + } + + private func subscribeForPushMessages() { + let protocolMethod = PushMessageProtocolMethod() + networkingInteractor.requestSubscription(on: protocolMethod) + .sink { [unowned self] (payload: RequestSubscriptionPayload) in + logger.debug("Received Push Message") + onPushMessage?(payload.request) + + }.store(in: &publishers) + + } +} diff --git a/Sources/WalletConnectPush/Client/Wallet/PushRequestResponder.swift b/Sources/WalletConnectPush/Client/Wallet/PushRequestResponder.swift new file mode 100644 index 000000000..397366995 --- /dev/null +++ b/Sources/WalletConnectPush/Client/Wallet/PushRequestResponder.swift @@ -0,0 +1,80 @@ +import WalletConnectNetworking +import Foundation + +class PushRequestResponder { + enum Errors: Error { + case recordForIdNotFound + case malformedRequestParams + } + private let networkingInteractor: NetworkInteracting + private let kms: KeyManagementService + private let rpcHistory: RPCHistory + private let logger: ConsoleLogging + private let subscriptionsStore: CodableStore + + + init(networkingInteractor: NetworkInteracting, + logger: ConsoleLogging, + kms: KeyManagementService, + rpcHistory: RPCHistory, + subscriptionsStore: CodableStore + ) { + self.networkingInteractor = networkingInteractor + self.logger = logger + self.kms = kms + self.rpcHistory = rpcHistory + self.subscriptionsStore = subscriptionsStore + } + + func respond(requestId: RPCID) async throws { + logger.debug("Approving Push Proposal") + + let requestRecord = try getRecord(requestId: requestId) + let peerPublicKey = try getPeerPublicKey(for: requestRecord) + let pairingTopic = requestRecord.topic + + let keys = try generateAgreementKeys(peerPublicKey: peerPublicKey) + let pushTopic = keys.derivedTopic() + + try kms.setAgreementSecret(keys, topic: pushTopic) + try await networkingInteractor.subscribe(topic: pushTopic) + + let responseParams = PushResponseParams(publicKey: keys.publicKey.hexRepresentation) + + let response = RPCResponse(id: requestId, result: responseParams) + + let requestParams = try requestRecord.request.params!.get(PushRequestParams.self) + let pushSubscription = PushSubscription(topic: pushTopic, relay: RelayProtocolOptions(protocol: "irn", data: nil), metadata: requestParams.metadata) + subscriptionsStore.set(pushSubscription, forKey: pushTopic) + + try await networkingInteractor.respond(topic: pairingTopic, response: response, protocolMethod: PushRequestProtocolMethod()) + } + + func respondError(requestId: RPCID) async throws { + logger.debug("PushRequestResponder - rejecting rush request") + let requestRecord = try getRecord(requestId: requestId) + let pairingTopic = requestRecord.topic + + try await networkingInteractor.respondError(topic: pairingTopic, requestId: requestId, protocolMethod: PushRequestProtocolMethod(), reason: PushError.rejected) + } + + private func getRecord(requestId: RPCID) throws -> RPCHistory.Record { + guard let record = rpcHistory.get(recordId: requestId) + else { throw Errors.recordForIdNotFound } + return record + } + + private func getPeerPublicKey(for record: RPCHistory.Record) throws -> AgreementPublicKey { + guard let params = try record.request.params?.get(PushResponseParams.self) + else { throw Errors.malformedRequestParams } + + let peerPublicKey = try AgreementPublicKey(hex: params.publicKey) + return peerPublicKey + } + + private func generateAgreementKeys(peerPublicKey: AgreementPublicKey) throws -> AgreementKeys { + let selfPubKey = try kms.createX25519KeyPair() + let keys = try kms.performKeyAgreement(selfPublicKey: selfPubKey, peerPublicKey: peerPublicKey.hexRepresentation) + return keys + } +} diff --git a/Sources/WalletConnectPush/Client/Wallet/WalletPushClient.swift b/Sources/WalletConnectPush/Client/Wallet/WalletPushClient.swift new file mode 100644 index 000000000..7caa7b680 --- /dev/null +++ b/Sources/WalletConnectPush/Client/Wallet/WalletPushClient.swift @@ -0,0 +1,102 @@ +import Foundation +import Combine +import WalletConnectNetworking +import WalletConnectPairing +import WalletConnectEcho + +public class WalletPushClient { + + private var publishers = Set() + + private let requestPublisherSubject = PassthroughSubject<(id: RPCID, metadata: AppMetadata), Never>() + + public var requestPublisher: AnyPublisher<(id: RPCID, metadata: AppMetadata), Never> { + requestPublisherSubject.eraseToAnyPublisher() + } + + private let pushMessagePublisherSubject = PassthroughSubject() + + public var pushMessagePublisher: AnyPublisher { + pushMessagePublisherSubject.eraseToAnyPublisher() + } + + private let deleteSubscriptionPublisherSubject = PassthroughSubject() + + public var deleteSubscriptionPublisher: AnyPublisher { + deleteSubscriptionPublisherSubject.eraseToAnyPublisher() + } + + private let deletePushSubscriptionService: DeletePushSubscriptionService + private let deletePushSubscriptionSubscriber: DeletePushSubscriptionSubscriber + + public let logger: ConsoleLogging + + private let pairingRegisterer: PairingRegisterer + private let echoClient: EchoClient + private let proposeResponder: PushRequestResponder + private let pushMessageSubscriber: PushMessageSubscriber + private let subscriptionsProvider: SubscriptionsProvider + + init(logger: ConsoleLogging, + kms: KeyManagementServiceProtocol, + echoClient: EchoClient, + pairingRegisterer: PairingRegisterer, + proposeResponder: PushRequestResponder, + pushMessageSubscriber: PushMessageSubscriber, + subscriptionsProvider: SubscriptionsProvider, + deletePushSubscriptionService: DeletePushSubscriptionService, + deletePushSubscriptionSubscriber: DeletePushSubscriptionSubscriber) { + self.logger = logger + self.pairingRegisterer = pairingRegisterer + self.proposeResponder = proposeResponder + self.echoClient = echoClient + self.pushMessageSubscriber = pushMessageSubscriber + self.subscriptionsProvider = subscriptionsProvider + self.deletePushSubscriptionService = deletePushSubscriptionService + self.deletePushSubscriptionSubscriber = deletePushSubscriptionSubscriber + setupSubscriptions() + } + + public func approve(id: RPCID) async throws { + try await proposeResponder.respond(requestId: id) + } + + public func reject(id: RPCID) async throws { + try await proposeResponder.respondError(requestId: id) + } + + public func getActiveSubscriptions() -> [PushSubscription] { + subscriptionsProvider.getActiveSubscriptions() + } + + public func delete(topic: String) async throws { + try await deletePushSubscriptionService.delete(topic: topic) + } + + public func decryptMessage(topic: String, ciphertext: String) throws -> String { + try echoClient.decryptMessage(topic: topic, ciphertext: ciphertext) + } + + public func register(deviceToken: Data) async throws { + try await echoClient.register(deviceToken: deviceToken) + } +} + +private extension WalletPushClient { + + func setupSubscriptions() { + let protocolMethod = PushRequestProtocolMethod() + + pairingRegisterer.register(method: protocolMethod) + .sink { [unowned self] (payload: RequestSubscriptionPayload) in + requestPublisherSubject.send((id: payload.id, metadata: payload.request.metadata)) + }.store(in: &publishers) + + pushMessageSubscriber.onPushMessage = { [unowned self] pushMessage in + pushMessagePublisherSubject.send(pushMessage) + } + deletePushSubscriptionSubscriber.onDelete = {[unowned self] topic in + deleteSubscriptionPublisherSubject.send(topic) + } + } +} diff --git a/Sources/WalletConnectPush/Client/Wallet/WalletPushClientFactory.swift b/Sources/WalletConnectPush/Client/Wallet/WalletPushClientFactory.swift new file mode 100644 index 000000000..06bea0555 --- /dev/null +++ b/Sources/WalletConnectPush/Client/Wallet/WalletPushClientFactory.swift @@ -0,0 +1,47 @@ +import Foundation +import WalletConnectUtils +import WalletConnectEcho + +public struct WalletPushClientFactory { + + public static func create(networkInteractor: NetworkInteracting, pairingRegisterer: PairingRegisterer, echoClient: EchoClient) -> WalletPushClient { + let logger = ConsoleLogger(loggingLevel: .off) + let keyValueStorage = UserDefaults.standard + let keychainStorage = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk") + return WalletPushClientFactory.create( + logger: logger, + keyValueStorage: keyValueStorage, + keychainStorage: keychainStorage, + networkInteractor: networkInteractor, + pairingRegisterer: pairingRegisterer, + echoClient: echoClient + ) + } + + static func create(logger: ConsoleLogging, keyValueStorage: KeyValueStorage, keychainStorage: KeychainStorageProtocol, networkInteractor: NetworkInteracting, pairingRegisterer: PairingRegisterer, echoClient: EchoClient) -> WalletPushClient { + let kms = KeyManagementService(keychain: keychainStorage) + + let history = RPCHistoryFactory.createForNetwork(keyValueStorage: keyValueStorage) + + let subscriptionStore = CodableStore(defaults: keyValueStorage, identifier: PushStorageIdntifiers.pushSubscription) + + let proposeResponder = PushRequestResponder(networkingInteractor: networkInteractor, logger: logger, kms: kms, rpcHistory: history, subscriptionsStore: subscriptionStore) + + let pushMessageSubscriber = PushMessageSubscriber(networkingInteractor: networkInteractor, kms: kms, logger: logger) + let subscriptionProvider = SubscriptionsProvider(store: subscriptionStore) + let deletePushSubscriptionService = DeletePushSubscriptionService(networkingInteractor: networkInteractor, kms: kms, logger: logger, pushSubscriptionStore: subscriptionStore) + let deletePushSubscriptionSubscriber = DeletePushSubscriptionSubscriber(networkingInteractor: networkInteractor, kms: kms, logger: logger, pushSubscriptionStore: subscriptionStore) + + return WalletPushClient( + logger: logger, + kms: kms, + echoClient: echoClient, + pairingRegisterer: pairingRegisterer, + proposeResponder: proposeResponder, + pushMessageSubscriber: pushMessageSubscriber, + subscriptionsProvider: subscriptionProvider, + deletePushSubscriptionService: deletePushSubscriptionService, + deletePushSubscriptionSubscriber: deletePushSubscriptionSubscriber + ) + } +} diff --git a/Sources/WalletConnectPush/ProposalResponseSubscriber.swift b/Sources/WalletConnectPush/ProposalResponseSubscriber.swift deleted file mode 100644 index e6bdcff51..000000000 --- a/Sources/WalletConnectPush/ProposalResponseSubscriber.swift +++ /dev/null @@ -1,28 +0,0 @@ -import Foundation -import Combine - -class ProposalResponseSubscriber { - private let networkingInteractor: NetworkInteracting - private let kms: KeyManagementServiceProtocol - private let logger: ConsoleLogging - private var publishers = [AnyCancellable]() - var onResponse: ((_ id: RPCID, _ result: Result) -> Void)? - - init(networkingInteractor: NetworkInteracting, - kms: KeyManagementServiceProtocol, - logger: ConsoleLogging) { - self.networkingInteractor = networkingInteractor - self.kms = kms - self.logger = logger - subscribeForProposalErrors() - } - - private func subscribeForProposalErrors() { - let protocolMethod = PushProposeProtocolMethod() - networkingInteractor.responseErrorSubscription(on: protocolMethod) - .sink { [unowned self] (payload: ResponseSubscriptionErrorPayload) in - guard let error = PairError(code: payload.error.code) else { return } - onResponse?(payload.id, .failure(error)) - }.store(in: &publishers) - } -} diff --git a/Sources/WalletConnectPush/ProtocolMethods/PushDeleteProtocolMethod.swift b/Sources/WalletConnectPush/ProtocolMethods/PushDeleteProtocolMethod.swift new file mode 100644 index 000000000..1aee08d1f --- /dev/null +++ b/Sources/WalletConnectPush/ProtocolMethods/PushDeleteProtocolMethod.swift @@ -0,0 +1,10 @@ +import Foundation +import WalletConnectPairing + +struct PushDeleteProtocolMethod: ProtocolMethod { + let method: String = "wc_pushDelete" + + let requestConfig = RelayConfig(tag: 4004, prompt: false, ttl: 86400) + + let responseConfig = RelayConfig(tag: 4005, prompt: false, ttl: 86400) +} diff --git a/Sources/WalletConnectPush/ProtocolMethods/PushMessageProtocolMethod.swift b/Sources/WalletConnectPush/ProtocolMethods/PushMessageProtocolMethod.swift new file mode 100644 index 000000000..287b2a3bb --- /dev/null +++ b/Sources/WalletConnectPush/ProtocolMethods/PushMessageProtocolMethod.swift @@ -0,0 +1,10 @@ +import Foundation +import WalletConnectPairing + +struct PushMessageProtocolMethod: ProtocolMethod { + let method: String = "wc_pushMessage" + + let requestConfig: RelayConfig = RelayConfig(tag: 4002, prompt: true, ttl: 86400) + + let responseConfig: RelayConfig = RelayConfig(tag: 4003, prompt: true, ttl: 86400) +} diff --git a/Sources/WalletConnectPush/ProtocolMethods/PushRequestProtocolMethod.swift b/Sources/WalletConnectPush/ProtocolMethods/PushRequestProtocolMethod.swift new file mode 100644 index 000000000..6c04631e1 --- /dev/null +++ b/Sources/WalletConnectPush/ProtocolMethods/PushRequestProtocolMethod.swift @@ -0,0 +1,10 @@ +import Foundation +import WalletConnectPairing + +struct PushRequestProtocolMethod: ProtocolMethod { + let method: String = "wc_pushRequest" + + let requestConfig: RelayConfig = RelayConfig(tag: 4000, prompt: true, ttl: 86400) + + let responseConfig: RelayConfig = RelayConfig(tag: 4001, prompt: true, ttl: 86400) +} diff --git a/Sources/WalletConnectPush/Push.swift b/Sources/WalletConnectPush/Push.swift new file mode 100644 index 000000000..4d2b4fcf7 --- /dev/null +++ b/Sources/WalletConnectPush/Push.swift @@ -0,0 +1,24 @@ +import Foundation +import WalletConnectNetworking +import WalletConnectPairing +import WalletConnectEcho + +public class Push { + + public static var dapp: DappPushClient = { + return DappPushClientFactory.create( + metadata: Pair.metadata, + networkInteractor: Networking.interactor + ) + }() + + public static var wallet: WalletPushClient = { + return WalletPushClientFactory.create( + networkInteractor: Networking.interactor, + pairingRegisterer: Pair.registerer, + echoClient: Echo.instance + ) + }() + + private init() { } +} diff --git a/Sources/WalletConnectPush/PushClient.swift b/Sources/WalletConnectPush/PushClient.swift deleted file mode 100644 index 78ef14e21..000000000 --- a/Sources/WalletConnectPush/PushClient.swift +++ /dev/null @@ -1,58 +0,0 @@ -import Foundation -import Combine - -public class PushClient { - - private var publishers = Set() - - private let requestPublisherSubject = PassthroughSubject<(topic: String, params: PushRequestParams), Never>() - private let responsePublisherSubject = PassthroughSubject<(id: RPCID, result: Result), Never>() - - public var proposalPublisher: AnyPublisher<(topic: String, params: PushRequestParams), Never> { - requestPublisherSubject.eraseToAnyPublisher() - } - public var responsePublisher: AnyPublisher<(id: RPCID, result: Result), Never> { - responsePublisherSubject.eraseToAnyPublisher() - } - - public let logger: ConsoleLogging - - private let pushProposer: PushProposer - private let networkInteractor: NetworkInteracting - private let pairingRegisterer: PairingRegisterer - private let proposalResponseSubscriber: ProposalResponseSubscriber - - init(networkInteractor: NetworkInteracting, - logger: ConsoleLogging, - kms: KeyManagementServiceProtocol, - pushProposer: PushProposer, - proposalResponseSubscriber: ProposalResponseSubscriber, - pairingRegisterer: PairingRegisterer) { - self.networkInteractor = networkInteractor - self.logger = logger - self.pushProposer = pushProposer - self.pairingRegisterer = pairingRegisterer - self.proposalResponseSubscriber = proposalResponseSubscriber - setupSubscriptions() - } - - public func propose(topic: String) async throws { - try await pushProposer.request(topic: topic, params: PushRequestParams()) - } -} - -private extension PushClient { - - func setupSubscriptions() { - let protocolMethod = PushProposeProtocolMethod() - - pairingRegisterer.register(method: protocolMethod) - .sink { [unowned self] (payload: RequestSubscriptionPayload) in - requestPublisherSubject.send((topic: payload.topic, params: payload.request)) - }.store(in: &publishers) - - proposalResponseSubscriber.onResponse = {[unowned self] (id, result) in - responsePublisherSubject.send((id, result)) - } - } -} diff --git a/Sources/WalletConnectPush/PushClientFactory.swift b/Sources/WalletConnectPush/PushClientFactory.swift deleted file mode 100644 index 7fec0861d..000000000 --- a/Sources/WalletConnectPush/PushClientFactory.swift +++ /dev/null @@ -1,18 +0,0 @@ -import Foundation - -public struct PushClientFactory { - - static public func create(logger: ConsoleLogging, keyValueStorage: KeyValueStorage, keychainStorage: KeychainStorageProtocol, networkingClient: NetworkingInteractor, pairingClient: PairingClient) -> PushClient { - let kms = KeyManagementService(keychain: keychainStorage) - let pushProposer = PushProposer(networkingInteractor: networkingClient, kms: kms, logger: logger) - let proposalResponseSubscriber = ProposalResponseSubscriber(networkingInteractor: networkingClient, kms: kms, logger: logger) - - return PushClient( - networkInteractor: networkingClient, - logger: logger, - kms: kms, - pushProposer: pushProposer, proposalResponseSubscriber: proposalResponseSubscriber, - pairingRegisterer: pairingClient - ) - } -} diff --git a/Sources/WalletConnectPush/PushProposeProtocolMethod.swift b/Sources/WalletConnectPush/PushProposeProtocolMethod.swift deleted file mode 100644 index d522ea5c8..000000000 --- a/Sources/WalletConnectPush/PushProposeProtocolMethod.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Foundation - -struct PushProposeProtocolMethod: ProtocolMethod { - let method: String = "wc_pushPropose" - - let requestConfig: RelayConfig = RelayConfig(tag: 111, prompt: true, ttl: 300) - - let responseConfig: RelayConfig = RelayConfig(tag: 112, prompt: true, ttl: 300) -} - -public struct PushRequestParams: Codable {} - -public struct PushResponseParams: Codable, Equatable {} diff --git a/Sources/WalletConnectPush/PushStorageIdntifiers.swift b/Sources/WalletConnectPush/PushStorageIdntifiers.swift new file mode 100644 index 000000000..8139ff15a --- /dev/null +++ b/Sources/WalletConnectPush/PushStorageIdntifiers.swift @@ -0,0 +1,5 @@ +import Foundation + +enum PushStorageIdntifiers { + static let pushSubscription = "com.walletconnect.sdk.pushSbscription" +} diff --git a/Sources/WalletConnectPush/RPCRequests/PushDeleteParams.swift b/Sources/WalletConnectPush/RPCRequests/PushDeleteParams.swift new file mode 100644 index 000000000..cc8829319 --- /dev/null +++ b/Sources/WalletConnectPush/RPCRequests/PushDeleteParams.swift @@ -0,0 +1,10 @@ +import Foundation + +public struct PushDeleteParams: Codable { + let code: Int + let message: String + + static var userDisconnected: PushDeleteParams { + return PushDeleteParams(code: 6000, message: "User Disconnected") + } +} diff --git a/Sources/WalletConnectPush/RPCRequests/PushRequestParams.swift b/Sources/WalletConnectPush/RPCRequests/PushRequestParams.swift new file mode 100644 index 000000000..d3b4abe9f --- /dev/null +++ b/Sources/WalletConnectPush/RPCRequests/PushRequestParams.swift @@ -0,0 +1,7 @@ +import Foundation + +public struct PushRequestParams: Codable { + let publicKey: String + let metadata: AppMetadata + let account: Account +} diff --git a/Sources/WalletConnectPush/RPCRequests/PushResponseParams.swift b/Sources/WalletConnectPush/RPCRequests/PushResponseParams.swift new file mode 100644 index 000000000..04fafb7e2 --- /dev/null +++ b/Sources/WalletConnectPush/RPCRequests/PushResponseParams.swift @@ -0,0 +1,5 @@ +import Foundation + +public struct PushResponseParams: Codable, Equatable { + let publicKey: String +} diff --git a/Sources/WalletConnectPush/Types/PushError.swift b/Sources/WalletConnectPush/Types/PushError.swift new file mode 100644 index 000000000..0da8d1eae --- /dev/null +++ b/Sources/WalletConnectPush/Types/PushError.swift @@ -0,0 +1,38 @@ +import Foundation + +public enum PushError: Codable, Equatable, Error { + case rejected + case methodUnsupported +} + +extension PushError: Reason { + + init?(code: Int) { + switch code { + case Self.rejected.code: + self = .rejected + case Self.methodUnsupported.code: + self = .methodUnsupported + default: + return nil + } + } + public var code: Int { + switch self { + case .methodUnsupported: + return 10001 + case .rejected: + return 15000 + } + } + + public var message: String { + switch self { + case .rejected: + return "Push request rejected" + case .methodUnsupported: + return "Method Unsupported" + } + } + +} diff --git a/Sources/WalletConnectPush/Types/PushMessage.swift b/Sources/WalletConnectPush/Types/PushMessage.swift new file mode 100644 index 000000000..a5823401b --- /dev/null +++ b/Sources/WalletConnectPush/Types/PushMessage.swift @@ -0,0 +1,15 @@ +import Foundation + +public struct PushMessage: Codable, Equatable { + let title: String + let body: String + let icon: String + let url: String + + public init(title: String, body: String, icon: String, url: String) { + self.title = title + self.body = body + self.icon = icon + self.url = url + } +} diff --git a/Sources/WalletConnectPush/Types/PushSubscription.swift b/Sources/WalletConnectPush/Types/PushSubscription.swift new file mode 100644 index 000000000..0dc61526b --- /dev/null +++ b/Sources/WalletConnectPush/Types/PushSubscription.swift @@ -0,0 +1,9 @@ +import Foundation +import WalletConnectUtils +import WalletConnectPairing + +public struct PushSubscription: Codable, Equatable { + let topic: String + let relay: RelayProtocolOptions + let metadata: AppMetadata +} diff --git a/Sources/WalletConnectRelay/BundleFinder.swift b/Sources/WalletConnectRelay/BundleFinder.swift index 8cf44cbb6..90abe6696 100644 --- a/Sources/WalletConnectRelay/BundleFinder.swift +++ b/Sources/WalletConnectRelay/BundleFinder.swift @@ -15,7 +15,7 @@ extension Foundation.Bundle { Bundle(for: BundleFinder.self).resourceURL, // For command-line tools. - Bundle.main.bundleURL, + Bundle.main.bundleURL ] for candidate in candidates { diff --git a/Sources/WalletConnectRelay/PackageConfig.json b/Sources/WalletConnectRelay/PackageConfig.json index 48f893bf2..18631a482 100644 --- a/Sources/WalletConnectRelay/PackageConfig.json +++ b/Sources/WalletConnectRelay/PackageConfig.json @@ -1 +1 @@ -{"version": "1.1.0"} +{"version": "1.2.0"} diff --git a/Sources/WalletConnectRelay/RPC/RelayRPC.swift b/Sources/WalletConnectRelay/RPC/RelayRPC.swift index 1bd41bcbc..c27bf8aae 100644 --- a/Sources/WalletConnectRelay/RPC/RelayRPC.swift +++ b/Sources/WalletConnectRelay/RPC/RelayRPC.swift @@ -1,4 +1,3 @@ - protocol RelayRPC: RPCMethod {} extension RelayRPC where Parameters: Codable { diff --git a/Sources/WalletConnectRelay/RelayClient.swift b/Sources/WalletConnectRelay/RelayClient.swift index a55c1c941..1a618bda3 100644 --- a/Sources/WalletConnectRelay/RelayClient.swift +++ b/Sources/WalletConnectRelay/RelayClient.swift @@ -194,9 +194,31 @@ public final class RelayClient { subscribe(topic: topic) { error in if let error = error { continuation.resume(throwing: error) - return + } else { + continuation.resume(returning: ()) + } + } + } + } + + public func unsubscribe(topic: String) async throws { + return try await withCheckedThrowingContinuation { continuation in + unsubscribe(topic: topic) { error in + if let error = error { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: ()) + } + } + } + } + + public func batchUnsubscribe(topics: [String]) async throws { + await withThrowingTaskGroup(of: Void.self) { group in + for topic in topics { + group.addTask { + try await self.unsubscribe(topic: topic) } - continuation.resume(returning: ()) } } } diff --git a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift index 8ac916277..bab3b0f4b 100644 --- a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift @@ -56,6 +56,7 @@ final class ApproveEngine { } func approveProposal(proposerPubKey: String, validating sessionNamespaces: [String: SessionNamespace]) async throws { + guard let payload = try proposalPayloadsStore.get(key: proposerPubKey) else { throw Errors.wrongRequestParams } @@ -76,15 +77,12 @@ final class ApproveEngine { let sessionTopic = agreementKey.derivedTopic() try kms.setAgreementSecret(agreementKey, topic: sessionTopic) + sessionToPairingTopic.set(payload.topic, forKey: sessionTopic) guard let relay = proposal.relays.first else { throw Errors.relayNotFound } - guard var pairing = pairingStore.getPairing(forTopic: payload.topic) else { - throw Errors.pairingNotFound - } - let result = SessionType.ProposeResponse(relay: relay, responderPublicKey: selfPublicKey.hexRepresentation) let response = RPCResponse(id: payload.id, result: result) @@ -92,10 +90,12 @@ final class ApproveEngine { async let settleRequest: () = settle(topic: sessionTopic, proposal: proposal, namespaces: sessionNamespaces) - let _ = try await [proposeResponse, settleRequest] + _ = try await [proposeResponse, settleRequest] - try pairing.updateExpiry() - pairingStore.setPairing(pairing) + pairingRegisterer.activate( + pairingTopic: payload.topic, + peerMetadata: payload.request.proposer.metadata + ) } func reject(proposerPubKey: String, reason: SignReasonCode) async throws { @@ -149,7 +149,7 @@ final class ApproveEngine { async let subscription: () = networkingInteractor.subscribe(topic: topic) async let settleRequest: () = networkingInteractor.request(request, topic: topic, protocolMethod: protocolMethod) - let _ = try await [settleRequest, subscription] + _ = try await [settleRequest, subscription] onSessionSettle?(session.publicRepresentation()) } @@ -209,21 +209,6 @@ private extension ApproveEngine { // TODO: Move to Non-Controller SettleEngine func handleSessionProposeResponse(payload: ResponseSubscriptionPayload) { do { - let pairingTopic = payload.topic - - guard var pairing = pairingStore.getPairing(forTopic: pairingTopic) else { - throw Errors.pairingNotFound - } - - // Activate the pairing - if !pairing.active { - pairing.activate() - } else { - try pairing.updateExpiry() - } - - pairingStore.setPairing(pairing) - let selfPublicKey = try AgreementPublicKey(hex: payload.request.proposer.publicKey) let agreementKeys = try kms.performKeyAgreement(selfPublicKey: selfPublicKey, peerPublicKey: payload.response.responderPublicKey) @@ -231,7 +216,7 @@ private extension ApproveEngine { logger.debug("Received Session Proposal response") try kms.setAgreementSecret(agreementKeys, topic: sessionTopic) - sessionToPairingTopic.set(pairingTopic, forKey: sessionTopic) + sessionToPairingTopic.set(payload.topic, forKey: sessionTopic) settlingProposal = payload.request @@ -329,8 +314,12 @@ private extension ApproveEngine { publicKey: agreementKeys.publicKey.hexRepresentation, metadata: metadata ) + if let pairingTopic = try? sessionToPairingTopic.get(key: topic) { - pairingRegisterer.updateMetadata(pairingTopic, metadata: params.controller.metadata) + pairingRegisterer.activate( + pairingTopic: pairingTopic, + peerMetadata: params.controller.metadata + ) } let session = WCSession( @@ -343,6 +332,7 @@ private extension ApproveEngine { acknowledged: true ) sessionStore.setSession(session) + Task(priority: .high) { try await networkingInteractor.respondSuccess(topic: payload.topic, requestId: payload.id, protocolMethod: protocolMethod) } diff --git a/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift index 52c5fcb9f..7f552ca32 100644 --- a/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift @@ -6,6 +6,7 @@ final class SessionEngine { case sessionNotFound(topic: String) } + var onSessionsUpdate: (([Session]) -> Void)? var onSessionRequest: ((Request) -> Void)? var onSessionResponse: ((Response) -> Void)? var onSessionRejected: ((String, SessionType.Reason) -> Void)? @@ -32,6 +33,7 @@ final class SessionEngine { setupConnectionSubscriptions() setupRequestSubscriptions() setupResponseSubscriptions() + setupUpdateSubscriptions() setupExpirationSubscriptions() } @@ -150,6 +152,13 @@ private extension SessionEngine { } } + func setupUpdateSubscriptions() { + sessionStore.onSessionsUpdate = { [weak self] in + guard let self else { return } + self.onSessionsUpdate?(self.getSessions()) + } + } + func respondError(payload: SubscriptionPayload, reason: SignReasonCode, protocolMethod: ProtocolMethod) { Task(priority: .high) { do { diff --git a/Sources/WalletConnectSign/Namespace.swift b/Sources/WalletConnectSign/Namespace.swift index 8398072f5..931ae2d4b 100644 --- a/Sources/WalletConnectSign/Namespace.swift +++ b/Sources/WalletConnectSign/Namespace.swift @@ -1,4 +1,3 @@ - public struct ProposalNamespace: Equatable, Codable { public let chains: Set diff --git a/Sources/WalletConnectSign/Services/SessionPingService.swift b/Sources/WalletConnectSign/Services/SessionPingService.swift index 642f2c6aa..d9e5ad3dc 100644 --- a/Sources/WalletConnectSign/Services/SessionPingService.swift +++ b/Sources/WalletConnectSign/Services/SessionPingService.swift @@ -6,7 +6,7 @@ class SessionPingService { private let pingResponder: PingResponder private let pingResponseSubscriber: PingResponseSubscriber - var onResponse: ((String)->Void)? { + var onResponse: ((String) -> Void)? { get { return pingResponseSubscriber.onResponse } diff --git a/Sources/WalletConnectSign/Services/SignCleanupService.swift b/Sources/WalletConnectSign/Services/SignCleanupService.swift index 9142e0183..1da2aee3e 100644 --- a/Sources/WalletConnectSign/Services/SignCleanupService.swift +++ b/Sources/WalletConnectSign/Services/SignCleanupService.swift @@ -6,15 +6,36 @@ final class SignCleanupService { private let sessionStore: WCSessionStorage private let kms: KeyManagementServiceProtocol private let sessionToPairingTopic: CodableStore + private let networkInteractor: NetworkInteracting - init(pairingStore: WCPairingStorage, sessionStore: WCSessionStorage, kms: KeyManagementServiceProtocol, sessionToPairingTopic: CodableStore) { + init(pairingStore: WCPairingStorage, sessionStore: WCSessionStorage, kms: KeyManagementServiceProtocol, sessionToPairingTopic: CodableStore, networkInteractor: NetworkInteracting) { self.pairingStore = pairingStore self.sessionStore = sessionStore self.sessionToPairingTopic = sessionToPairingTopic + self.networkInteractor = networkInteractor self.kms = kms } + func cleanup() async throws { + try await unsubscribe() + try cleanupStorages() + } + func cleanup() throws { + try cleanupStorages() + } +} + +private extension SignCleanupService { + + func unsubscribe() async throws { + let pairing = pairingStore.getAll().map { $0.topic } + let session = sessionStore.getAll().map { $0.topic } + + try await networkInteractor.batchUnsubscribe(topics: pairing + session) + } + + func cleanupStorages() throws { pairingStore.deleteAll() sessionStore.deleteAll() sessionToPairingTopic.deleteAll() diff --git a/Sources/WalletConnectSign/Sign/SignClient.swift b/Sources/WalletConnectSign/Sign/SignClient.swift index bc0ba1ffe..6d015414e 100644 --- a/Sources/WalletConnectSign/Sign/SignClient.swift +++ b/Sources/WalletConnectSign/Sign/SignClient.swift @@ -6,7 +6,7 @@ import Combine /// Cannot be instantiated outside of the SDK /// /// Access via `Sign.instance` -public final class SignClient { +public final class SignClient: SignClientProtocol { enum Errors: Error { case sessionForTopicNotFound } @@ -88,6 +88,13 @@ public final class SignClient { pingResponsePublisherSubject.eraseToAnyPublisher() } + /// Publisher that sends sessions on every sessions update + /// + /// Event will be emited on controller and non-controller clients. + public var sessionsPublisher: AnyPublisher<[Session], Never> { + sessionsPublisherSubject.eraseToAnyPublisher() + } + /// An object that loggs SDK's errors and info messages public let logger: ConsoleLogging @@ -117,6 +124,7 @@ public final class SignClient { private let sessionEventPublisherSubject = PassthroughSubject<(event: Session.Event, sessionTopic: String, chainId: Blockchain?), Never>() private let sessionExtendPublisherSubject = PassthroughSubject<(sessionTopic: String, date: Date), Never>() private let pingResponsePublisherSubject = PassthroughSubject() + private let sessionsPublisherSubject = PassthroughSubject<[Session], Never>() private var publishers = Set() @@ -338,6 +346,13 @@ public final class SignClient { return Request(id: record.id, topic: record.topic, method: record.request.method, params: request, chainId: request.chainId) } + /// Delete all stored data such as: pairings, sessions, keys + /// + /// - Note: Will unsubscribe from all topics + public func cleanup() async throws { + try await cleanupService.cleanup() + } + #if DEBUG /// Delete all stored data such as: pairings, sessions, keys /// @@ -389,6 +404,9 @@ public final class SignClient { sessionPingService.onResponse = { [unowned self] topic in pingResponsePublisherSubject.send(topic) } + sessionEngine.onSessionsUpdate = { [unowned self] sessions in + sessionsPublisherSubject.send(sessions) + } } private func setUpConnectionObserving() { diff --git a/Sources/WalletConnectSign/Sign/SignClientFactory.swift b/Sources/WalletConnectSign/Sign/SignClientFactory.swift index 9fd5c17f4..ce27c1e4c 100644 --- a/Sources/WalletConnectSign/Sign/SignClientFactory.swift +++ b/Sources/WalletConnectSign/Sign/SignClientFactory.swift @@ -29,7 +29,7 @@ public struct SignClientFactory { let nonControllerSessionStateMachine = NonControllerSessionStateMachine(networkingInteractor: networkingClient, kms: kms, sessionStore: sessionStore, logger: logger) let controllerSessionStateMachine = ControllerSessionStateMachine(networkingInteractor: networkingClient, kms: kms, sessionStore: sessionStore, logger: logger) let approveEngine = ApproveEngine(networkingInteractor: networkingClient, proposalPayloadsStore: proposalPayloadsStore, sessionToPairingTopic: sessionToPairingTopic, pairingRegisterer: pairingClient, metadata: metadata, kms: kms, logger: logger, pairingStore: pairingStore, sessionStore: sessionStore) - let cleanupService = SignCleanupService(pairingStore: pairingStore, sessionStore: sessionStore, kms: kms, sessionToPairingTopic: sessionToPairingTopic) + let cleanupService = SignCleanupService(pairingStore: pairingStore, sessionStore: sessionStore, kms: kms, sessionToPairingTopic: sessionToPairingTopic, networkInteractor: networkingClient) 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) diff --git a/Sources/WalletConnectSign/Sign/SignClientProtocol.swift b/Sources/WalletConnectSign/Sign/SignClientProtocol.swift new file mode 100644 index 000000000..933d476ba --- /dev/null +++ b/Sources/WalletConnectSign/Sign/SignClientProtocol.swift @@ -0,0 +1,20 @@ +import Foundation +import Combine + +public protocol SignClientProtocol { + var sessionProposalPublisher: AnyPublisher { get } + var sessionRequestPublisher: AnyPublisher { get } + + func approve(proposalId: String, namespaces: [String: SessionNamespace]) async throws + func reject(proposalId: String, reason: RejectionReason) async throws + func update(topic: String, namespaces: [String: SessionNamespace]) async throws + func extend(topic: String) async throws + func respond(topic: String, requestId: RPCID, response: RPCResult) async throws + func emit(topic: String, event: Session.Event, chainId: Blockchain) async throws + func pair(uri: WalletConnectURI) async throws + func disconnect(topic: String) async throws + func getSessions() -> [Session] + + func getPendingRequests(topic: String?) -> [Request] + func getSessionRequestRecord(id: RPCID) -> Request? +} diff --git a/Sources/WalletConnectSign/Storage/SessionStorage.swift b/Sources/WalletConnectSign/Storage/SessionStorage.swift index 2339052ed..60f7d4dad 100644 --- a/Sources/WalletConnectSign/Storage/SessionStorage.swift +++ b/Sources/WalletConnectSign/Storage/SessionStorage.swift @@ -1,6 +1,7 @@ import Foundation protocol WCSessionStorage: AnyObject { + var onSessionsUpdate: (() -> Void)? { get set } var onSessionExpiration: ((WCSession) -> Void)? { get set } @discardableResult func setSessionIfNewer(_ session: WCSession) -> Bool @@ -14,12 +15,16 @@ protocol WCSessionStorage: AnyObject { final class SessionStorage: WCSessionStorage { + var onSessionsUpdate: (() -> Void)? var onSessionExpiration: ((WCSession) -> Void)? private let storage: SequenceStore init(storage: SequenceStore) { self.storage = storage + storage.onSequenceUpdate = { [unowned self] in + onSessionsUpdate?() + } storage.onSequenceExpiration = { [unowned self] session in onSessionExpiration?(session) } diff --git a/Sources/WalletConnectSign/Types/Session/SessionType.swift b/Sources/WalletConnectSign/Types/Session/SessionType.swift index f682f2df8..d6c7c87d5 100644 --- a/Sources/WalletConnectSign/Types/Session/SessionType.swift +++ b/Sources/WalletConnectSign/Types/Session/SessionType.swift @@ -1,6 +1,6 @@ import Foundation -fileprivate typealias NetworkingReason = Reason +private typealias NetworkingReason = Reason // Internal namespace for session payloads. internal enum SessionType { diff --git a/Sources/WalletConnectSign/Types/SignReasonCode.swift b/Sources/WalletConnectSign/Types/SignReasonCode.swift index 2032ce537..522cbfb4f 100644 --- a/Sources/WalletConnectSign/Types/SignReasonCode.swift +++ b/Sources/WalletConnectSign/Types/SignReasonCode.swift @@ -1,4 +1,3 @@ - enum SignReasonCode: Reason, Codable, Equatable { enum Context: String, Codable { diff --git a/Sources/WalletConnectUtils/Logger.swift b/Sources/WalletConnectUtils/Logger.swift index f88360312..ee6b0aad2 100644 --- a/Sources/WalletConnectUtils/Logger.swift +++ b/Sources/WalletConnectUtils/Logger.swift @@ -33,7 +33,7 @@ public class ConsoleLogger: ConsoleLogging { public func debug(_ items: Any...) { if loggingLevel >= .debug { items.forEach { - Swift.print("\(suffix) \($0)") + Swift.print("\(suffix) \($0) - \(logFormattedDate(Date()))") } } } @@ -70,3 +70,11 @@ public enum LoggingLevel: Comparable { case info case debug } + + +fileprivate func logFormattedDate(_ date: Date) -> String { + let dateFormatter = DateFormatter() + dateFormatter.locale = NSLocale.current + dateFormatter.dateFormat = "HH:mm:ss.SSSS" + return dateFormatter.string(from: date) +} diff --git a/Sources/WalletConnectUtils/RPCHistory/RPCHistory.swift b/Sources/WalletConnectUtils/RPCHistory/RPCHistory.swift index 45129dd0c..8e130c106 100644 --- a/Sources/WalletConnectUtils/RPCHistory/RPCHistory.swift +++ b/Sources/WalletConnectUtils/RPCHistory/RPCHistory.swift @@ -1,4 +1,3 @@ - public final class RPCHistory { public struct Record: Codable { @@ -58,14 +57,18 @@ public final class RPCHistory { return record } - public func deleteAll(forTopic topic: String) { + public func deleteAll(forTopics topics: [String]){ storage.getAll().forEach { record in - if record.topic == topic { + if topics.contains(record.topic) { storage.delete(forKey: "\(record.id)") } } } + public func deleteAll(forTopic topic: String) { + deleteAll(forTopics: [topic]) + } + public func getPending() -> [Record] { storage.getAll().filter {$0.response == nil} } diff --git a/Sources/WalletConnectUtils/SequenceStore.swift b/Sources/WalletConnectUtils/SequenceStore.swift index 738831b74..618ba411e 100644 --- a/Sources/WalletConnectUtils/SequenceStore.swift +++ b/Sources/WalletConnectUtils/SequenceStore.swift @@ -7,6 +7,7 @@ public protocol SequenceObject: Codable { public final class SequenceStore where T: SequenceObject { + public var onSequenceUpdate: (() -> Void)? public var onSequenceExpiration: ((_ sequence: T) -> Void)? private let store: CodableStore @@ -23,6 +24,7 @@ public final class SequenceStore where T: SequenceObject { public func setSequence(_ sequence: T) { store.set(sequence, forKey: sequence.topic) + onSequenceUpdate?() } public func getSequence(forTopic topic: String) throws -> T? { @@ -37,10 +39,12 @@ public final class SequenceStore where T: SequenceObject { public func delete(topic: String) { store.delete(forKey: topic) + onSequenceUpdate?() } public func deleteAll() { store.deleteAll() + onSequenceUpdate?() } } @@ -51,7 +55,7 @@ private extension SequenceStore { func verifyExpiry(on sequence: T) -> T? { let now = dateInitializer() if now >= sequence.expiryDate { - store.delete(forKey: sequence.topic) + delete(topic: sequence.topic) onSequenceExpiration?(sequence) return nil } diff --git a/Sources/WalletConnectVerify/AppAttestationRegistrer.swift b/Sources/WalletConnectVerify/AppAttestationRegistrer.swift index 04e7cd819..8f5b918c5 100644 --- a/Sources/WalletConnectVerify/AppAttestationRegistrer.swift +++ b/Sources/WalletConnectVerify/AppAttestationRegistrer.swift @@ -3,7 +3,6 @@ import DeviceCheck import WalletConnectUtils import CryptoKit - @available(iOS 14.0, *) @available(macOS 11.0, *) class AppAttestationRegistrer { diff --git a/Sources/WalletConnectVerify/AssertionRegistrer.swift b/Sources/WalletConnectVerify/AssertionRegistrer.swift index 2cf2289ad..67640c1fa 100644 --- a/Sources/WalletConnectVerify/AssertionRegistrer.swift +++ b/Sources/WalletConnectVerify/AssertionRegistrer.swift @@ -1,4 +1,3 @@ - import Foundation class AssertionRegistrer { diff --git a/Sources/WalletConnectVerify/AttestKeyGenerator.swift b/Sources/WalletConnectVerify/AttestKeyGenerator.swift index 166d84a01..1826526e0 100644 --- a/Sources/WalletConnectVerify/AttestKeyGenerator.swift +++ b/Sources/WalletConnectVerify/AttestKeyGenerator.swift @@ -13,7 +13,6 @@ class AttestKeyGenerator: AttestKeyGenerating { private let logger: ConsoleLogging private let keyIdStorage: CodableStore - init(logger: ConsoleLogging, keyIdStorage: CodableStore ) { diff --git a/Sources/WalletConnectVerify/KeyAttestationService.swift b/Sources/WalletConnectVerify/KeyAttestationService.swift index ce0833d81..54109ef01 100644 --- a/Sources/WalletConnectVerify/KeyAttestationService.swift +++ b/Sources/WalletConnectVerify/KeyAttestationService.swift @@ -11,12 +11,12 @@ class KeyAttestationService: KeyAttesting { private let service = DCAppAttestService.shared // If the method, which accesses a remote Apple server, returns the serverUnavailable error, // try attestation again later with the same key. For any other error, - //discard the key identifier and create a new key when you want to try again. - //Otherwise, send the completion handler’s attestation object and the keyId to your server for processing. + // discard the key identifier and create a new key when you want to try again. + // Otherwise, send the completion handler’s attestation object and the keyId to your server for processing. func attestKey(keyId: String, clientDataHash: Data) async throws { try await service.attestKey(keyId, clientDataHash: clientDataHash) - //TODO - Send the attestation object to your server for verification. handle errors + // TODO - Send the attestation object to your server for verification. handle errors } diff --git a/Sources/WalletConnectVerify/OriginVerifier.swift b/Sources/WalletConnectVerify/OriginVerifier.swift index de33c5fff..7ad8c2d33 100644 --- a/Sources/WalletConnectVerify/OriginVerifier.swift +++ b/Sources/WalletConnectVerify/OriginVerifier.swift @@ -1,7 +1,5 @@ - import Foundation class OriginVerifier { func verifyOrigin() async throws {} } - diff --git a/Sources/WalletConnectVerify/VerifyClient.swift b/Sources/WalletConnectVerify/VerifyClient.swift index 0936d9da0..aa6919af4 100644 --- a/Sources/WalletConnectVerify/VerifyClient.swift +++ b/Sources/WalletConnectVerify/VerifyClient.swift @@ -34,5 +34,5 @@ public actor VerifyClient { public func registerAssertion() async throws { try await assertionRegistrer.registerAssertion() } - + } diff --git a/Sources/Web3Wallet/Web3Wallet.swift b/Sources/Web3Wallet/Web3Wallet.swift new file mode 100644 index 000000000..38eabb617 --- /dev/null +++ b/Sources/Web3Wallet/Web3Wallet.swift @@ -0,0 +1,44 @@ +import Foundation +import Combine + +/// Web3Wallet instance wrapper +/// +/// ```Swift +/// let metadata = AppMetadata( +/// name: "Swift wallet", +/// description: "wallet", +/// url: "wallet.connect", +/// icons: ["https://my_icon.com/1"] +/// ) +/// Web3Wallet.configure(metadata: metadata, account: account) +/// Web3Wallet.instance.getSessions() +/// ``` +public class Web3Wallet { + /// Web3Wallett client instance + public static var instance: Web3WalletClient = { + guard let config = Web3Wallet.config else { + fatalError("Error - you must call Wallet.configure(_:) before accessing the shared instance.") + } + + return Web3WalletClientFactory.create( + authClient: Auth.instance, + signClient: Sign.instance, + pairingClient: Pair.instance as! PairingClient + ) + }() + + private static var config: Config? + + private init() { } + + /// Wallet instance wallet config method. + /// - Parameters: + /// - metadata: App metadata + /// - signerFactory: Auth signers factory + static public func configure(metadata: AppMetadata, signerFactory: SignerFactory) { + Pair.configure(metadata: metadata) + Auth.configure(signerFactory: signerFactory) + + Web3Wallet.config = Web3Wallet.Config(signerFactory: signerFactory) + } +} diff --git a/Sources/Web3Wallet/Web3WalletClient.swift b/Sources/Web3Wallet/Web3WalletClient.swift new file mode 100644 index 000000000..196a0a471 --- /dev/null +++ b/Sources/Web3Wallet/Web3WalletClient.swift @@ -0,0 +1,163 @@ +import Foundation +import Combine + +/// Web3 Wallet Client +/// +/// Cannot be instantiated outside of the SDK +/// +/// Access via `Web3Wallet.instance` +public class Web3WalletClient { + // MARK: - Public Properties + + /// Publisher that sends session proposal + /// + /// event is emited on responder client only + public var sessionProposalPublisher: AnyPublisher { + signClient.sessionProposalPublisher.eraseToAnyPublisher() + } + + /// Publisher that sends session request + /// + /// In most cases event will be emited on wallet + public var sessionRequestPublisher: AnyPublisher { + signClient.sessionRequestPublisher.eraseToAnyPublisher() + } + + /// Publisher that sends authentication requests + /// + /// Wallet should subscribe on events in order to receive auth requests. + public var authRequestPublisher: AnyPublisher { + authClient.authRequestPublisher.eraseToAnyPublisher() + } + + // MARK: - Private Properties + private let authClient: AuthClientProtocol + private let signClient: SignClientProtocol + private let pairingClient: PairingClientProtocol + + private var account: Account? + + init( + authClient: AuthClientProtocol, + signClient: SignClientProtocol, + pairingClient: PairingClientProtocol + ) { + self.authClient = authClient + self.signClient = signClient + self.pairingClient = pairingClient + } + + /// For a wallet to approve a session proposal. + /// - Parameters: + /// - proposalId: Session Proposal id + /// - namespaces: namespaces for given session, needs to contain at least required namespaces proposed by dApp. + public func approve(proposalId: String, namespaces: [String: SessionNamespace]) async throws { + try await signClient.approve(proposalId: proposalId, namespaces: namespaces) + } + + /// For the wallet to reject a session proposal. + /// - Parameters: + /// - proposalId: Session Proposal id + /// - reason: Reason why the session proposal has been rejected. Conforms to CAIP25. + public func reject(proposalId: String, reason: RejectionReason) async throws { + try await signClient.reject(proposalId: proposalId, reason: reason) + } + + /// For the wallet to update session namespaces + /// - Parameters: + /// - topic: Topic of the session that is intended to be updated. + /// - namespaces: Dictionary of namespaces that will replace existing ones. + public func update(topic: String, namespaces: [String: SessionNamespace]) async throws { + try await signClient.update(topic: topic, namespaces: namespaces) + } + + /// For wallet to extend a session to 7 days + /// - Parameters: + /// - topic: Topic of the session that is intended to be extended. + public func extend(topic: String) async throws { + try await signClient.extend(topic: topic) + } + + /// For the wallet to respond on pending dApp's JSON-RPC request + /// - Parameters: + /// - topic: Topic of the session for which the request was received. + /// - requestId: RPC request ID + /// - response: Your JSON RPC response or an error. + public func respond(topic: String, requestId: RPCID, response: RPCResult) async throws { + try await signClient.respond(topic: topic, requestId: requestId, response: response) + } + + /// For the wallet to emit an event to a dApp + /// + /// When a client wants to emit an event to its peer client (eg. chain changed or tx replaced) + /// + /// Should Error: + /// - When the session topic is not found + /// - When the event params are invalid + /// - Parameters: + /// - topic: Session topic + /// - event: session event + /// - chainId: CAIP-2 chain + public func emit(topic: String, event: Session.Event, chainId: Blockchain) async throws { + try await signClient.emit(topic: topic, event: event, chainId: chainId) + } + + /// For wallet to receive a session proposal from a dApp + /// Responder should call this function in order to accept peer's pairing 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 has invalid format or missing params + /// - When topic is already in use + public func pair(uri: WalletConnectURI) async throws { + try await pairingClient.pair(uri: uri) + } + + /// For a wallet and a dApp to terminate a session + /// + /// Should Error: + /// - When the session topic is not found + /// - Parameters: + /// - topic: Session topic that you want to delete + public func disconnect(topic: String) async throws { + try await signClient.disconnect(topic: topic) + } + + + /// Query sessions + /// - Returns: All sessions + public func getSessions() -> [Session] { + signClient.getSessions() + } + + public func formatMessage(payload: AuthPayload, address: String) throws -> String { + try authClient.formatMessage(payload: payload, address: address) + } + + /// For a wallet to respond on authentication request + /// - Parameters: + /// - requestId: authentication request id + /// - signature: CACAO signature of requested message + public func respond(requestId: RPCID, signature: CacaoSignature, from account: Account) async throws { + try await authClient.respond(requestId: requestId, signature: signature, from: account) + } + + /// Query pending requests + /// - Returns: Pending requests received from peer with `wc_sessionRequest` protocol method + /// - 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] { + signClient.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: RPCID) -> Request? { + signClient.getSessionRequestRecord(id: id) + } + + /// Query pending authentication requests + /// - Returns: Pending authentication requests + public func getPendingRequests(account: Account) throws -> [AuthRequest] { + try authClient.getPendingRequests(account: account) + } +} diff --git a/Sources/Web3Wallet/Web3WalletClientFactory.swift b/Sources/Web3Wallet/Web3WalletClientFactory.swift new file mode 100644 index 000000000..6a91213a7 --- /dev/null +++ b/Sources/Web3Wallet/Web3WalletClientFactory.swift @@ -0,0 +1,15 @@ +import Foundation + +public struct Web3WalletClientFactory { + public static func create( + authClient: AuthClientProtocol, + signClient: SignClientProtocol, + pairingClient: PairingClientProtocol + ) -> Web3WalletClient { + return Web3WalletClient( + authClient: authClient, + signClient: signClient, + pairingClient: pairingClient + ) + } +} diff --git a/Sources/Web3Wallet/Web3WalletConfig.swift b/Sources/Web3Wallet/Web3WalletConfig.swift new file mode 100644 index 000000000..166be6ca3 --- /dev/null +++ b/Sources/Web3Wallet/Web3WalletConfig.swift @@ -0,0 +1,7 @@ +import Foundation + +extension Web3Wallet { + struct Config { + let signerFactory: SignerFactory + } +} diff --git a/Sources/Web3Wallet/Web3WalletImports.swift b/Sources/Web3Wallet/Web3WalletImports.swift new file mode 100644 index 000000000..cbc2a3e73 --- /dev/null +++ b/Sources/Web3Wallet/Web3WalletImports.swift @@ -0,0 +1,5 @@ +#if !CocoaPods +@_exported import Auth +@_exported import WalletConnectSign +@_exported import WalletConnectPairing +#endif diff --git a/Tests/TestingUtils/Mocks/PairingRegistererMock.swift b/Tests/TestingUtils/Mocks/PairingRegistererMock.swift index 9e48d13da..ee0cfec9b 100644 --- a/Tests/TestingUtils/Mocks/PairingRegistererMock.swift +++ b/Tests/TestingUtils/Mocks/PairingRegistererMock.swift @@ -13,15 +13,11 @@ public class PairingRegistererMock: PairingRegisterer where Reque subject.eraseToAnyPublisher() as! AnyPublisher, Never> } - public func activate(pairingTopic: String) { + public func activate(pairingTopic: String, peerMetadata: WalletConnectPairing.AppMetadata?) { isActivateCalled = true } public func validatePairingExistance(_ topic: String) throws { } - - public func updateMetadata(_ topic: String, metadata: AppMetadata) { - - } } diff --git a/Tests/TestingUtils/NetworkingInteractorMock.swift b/Tests/TestingUtils/NetworkingInteractorMock.swift index 36bee9173..323c52279 100644 --- a/Tests/TestingUtils/NetworkingInteractorMock.swift +++ b/Tests/TestingUtils/NetworkingInteractorMock.swift @@ -99,6 +99,12 @@ public class NetworkingInteractorMock: NetworkInteracting { didCallUnsubscribe = true } + public func batchUnsubscribe(topics: [String]) async throws { + for topic in topics { + unsubscribe(topic: topic) + } + } + public func request(_ request: RPCRequest, topic: String, protocolMethod: ProtocolMethod, envelopeType: Envelope.EnvelopeType) async throws { requestCallCount += 1 requests.append((topic, request)) @@ -121,4 +127,8 @@ public class NetworkingInteractorMock: NetworkInteracting { requestCallCount += 1 requests.append((topic, request)) } + + public func getClientId() throws -> String { + return "" + } } diff --git a/Tests/VerifyTests/AppAttestationRegistrerTests.swift b/Tests/VerifyTests/AppAttestationRegistrerTests.swift index e1e81bc40..ceab96b01 100644 --- a/Tests/VerifyTests/AppAttestationRegistrerTests.swift +++ b/Tests/VerifyTests/AppAttestationRegistrerTests.swift @@ -1,4 +1,3 @@ - import Foundation import XCTest import WalletConnectUtils diff --git a/Tests/VerifyTests/Mocks/AttestChallengeProvidingMock.swift b/Tests/VerifyTests/Mocks/AttestChallengeProvidingMock.swift index 1bc958055..ac6737e0e 100644 --- a/Tests/VerifyTests/Mocks/AttestChallengeProvidingMock.swift +++ b/Tests/VerifyTests/Mocks/AttestChallengeProvidingMock.swift @@ -1,4 +1,3 @@ - import Foundation @testable import WalletConnectVerify diff --git a/Tests/VerifyTests/Mocks/AttestKeyGeneratingMock.swift b/Tests/VerifyTests/Mocks/AttestKeyGeneratingMock.swift index a44916d56..fc89a1da6 100644 --- a/Tests/VerifyTests/Mocks/AttestKeyGeneratingMock.swift +++ b/Tests/VerifyTests/Mocks/AttestKeyGeneratingMock.swift @@ -1,8 +1,6 @@ - import Foundation @testable import WalletConnectVerify - class AttestKeyGeneratingMock: AttestKeyGenerating { var keysGenerated = false func generateKeys() async throws -> String { diff --git a/Tests/WalletConnectPairingTests/AppPairActivationServiceTests.swift b/Tests/WalletConnectPairingTests/AppPairActivationServiceTests.swift new file mode 100644 index 000000000..9cfd39b60 --- /dev/null +++ b/Tests/WalletConnectPairingTests/AppPairActivationServiceTests.swift @@ -0,0 +1,42 @@ +import XCTest +@testable import WalletConnectPairing +@testable import TestingUtils +import WalletConnectUtils + +final class AppPairActivationServiceTests: XCTestCase { + + var service: AppPairActivationService! + var storageMock: WCPairingStorage! + var logger: ConsoleLogger! + + override func setUp() { + storageMock = WCPairingStorageMock() + logger = ConsoleLogger() + service = AppPairActivationService(pairingStorage: storageMock, logger: logger) + } + + override func tearDown() { + storageMock = nil + logger = nil + service = nil + } + + func testActivate() { + let topic = "topic" + let pairing = WCPairing(topic: topic) + let date = pairing.expiryDate + + storageMock.setPairing(pairing) + + XCTAssertFalse(pairing.active) + XCTAssertNil(pairing.peerMetadata) + + service.activate(for: topic, peerMetadata: .stub()) + + let activated = storageMock.getPairing(forTopic: topic)! + + XCTAssertTrue(activated.active) + XCTAssertNotNil(activated.peerMetadata) + XCTAssertTrue(activated.expiryDate > date) + } +} diff --git a/Tests/WalletConnectSignTests/AppProposalServiceTests.swift b/Tests/WalletConnectSignTests/AppProposalServiceTests.swift index de37d08cf..f733358ea 100644 --- a/Tests/WalletConnectSignTests/AppProposalServiceTests.swift +++ b/Tests/WalletConnectSignTests/AppProposalServiceTests.swift @@ -124,7 +124,6 @@ final class AppProposalServiceTests: XCTestCase { let sessionTopic = networkingInteractor.subscriptions.last! XCTAssertTrue(networkingInteractor.didCallSubscribe) - XCTAssert(storedPairing.active) XCTAssertEqual(topicB, sessionTopic, "Responder engine calls back with session topic") } diff --git a/Tests/WalletConnectSignTests/ApproveEngineTests.swift b/Tests/WalletConnectSignTests/ApproveEngineTests.swift index 9730deefe..dfbf0523e 100644 --- a/Tests/WalletConnectSignTests/ApproveEngineTests.swift +++ b/Tests/WalletConnectSignTests/ApproveEngineTests.swift @@ -64,11 +64,10 @@ final class ApproveEngineTests: XCTestCase { let topicB = networkingInteractor.subscriptions.last! - let extendedPairing = pairingStorageMock.getPairing(forTopic: topicA)! 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") - XCTAssertEqual(extendedPairing.expiryDate.timeIntervalSince1970, Date(timeIntervalSinceNow: 2_592_000).timeIntervalSince1970, accuracy: 1, "pairing expiry has been extended by 30 days") + XCTAssertTrue(pairingRegisterer.isActivateCalled) } func testReceiveProposal() { diff --git a/Tests/WalletConnectSignTests/Mocks/WCSessionStorageMock.swift b/Tests/WalletConnectSignTests/Mocks/WCSessionStorageMock.swift index 2d4acabc6..90b0cf2de 100644 --- a/Tests/WalletConnectSignTests/Mocks/WCSessionStorageMock.swift +++ b/Tests/WalletConnectSignTests/Mocks/WCSessionStorageMock.swift @@ -3,6 +3,7 @@ import Foundation final class WCSessionStorageMock: WCSessionStorage { + var onSessionsUpdate: (() -> Void)? var onSessionExpiration: ((WCSession) -> Void)? private(set) var sessions: [String: WCSession] = [:] diff --git a/Tests/WalletConnectSignTests/SequenceStoreTests.swift b/Tests/WalletConnectSignTests/SequenceStoreTests.swift index fff7a0b2c..03bdbf46c 100644 --- a/Tests/WalletConnectSignTests/SequenceStoreTests.swift +++ b/Tests/WalletConnectSignTests/SequenceStoreTests.swift @@ -107,6 +107,22 @@ final class SequenceStoreTests: XCTestCase { XCTAssertFalse(sut2.getAll().isEmpty) } + func testUpdateHandler() { + let expectation = expectation(description: "TestUpdateHandler") + expectation.expectedFulfillmentCount = 3 + let sequence = stubSequence() + + sut.onSequenceUpdate = { + expectation.fulfill() + } + + sut.setSequence(sequence) + sut.delete(topic: sequence.topic) + sut.deleteAll() + + wait(for: [expectation], timeout: 1.0) + } + // MARK: - Expiration Tests func testHasSequenceExpiration() { diff --git a/Tests/Web3WalletTests/Mocks/AuthClientMock.swift b/Tests/Web3WalletTests/Mocks/AuthClientMock.swift new file mode 100644 index 000000000..2b5a407f7 --- /dev/null +++ b/Tests/Web3WalletTests/Mocks/AuthClientMock.swift @@ -0,0 +1,43 @@ +import Foundation +import Combine + +@testable import Auth + +final class AuthClientMock: AuthClientProtocol { + var respondCalled = false + + private var authRequest: AuthRequest { + let requestParams = RequestParams( + domain: "", + chainId: "", + nonce: "", + aud: "", + nbf: "", + exp: "", + statement: "", + requestId: "", + resources: nil + ) + + return AuthRequest( + id: .left(""), + payload: AuthPayload(requestParams: requestParams, iat: "") + ) + } + + var authRequestPublisher: AnyPublisher { + return Result.Publisher(authRequest).eraseToAnyPublisher() + } + + func formatMessage(payload: AuthPayload, address: String) throws -> String { + return "formatted_message" + } + + func respond(requestId: JSONRPC.RPCID, signature: CacaoSignature, from account: WalletConnectUtils.Account) async throws { + respondCalled = true + } + + func getPendingRequests(account: WalletConnectUtils.Account) throws -> [AuthRequest] { + return [authRequest] + } +} diff --git a/Tests/Web3WalletTests/Mocks/PairingClientMock.swift b/Tests/Web3WalletTests/Mocks/PairingClientMock.swift new file mode 100644 index 000000000..6c03d2226 --- /dev/null +++ b/Tests/Web3WalletTests/Mocks/PairingClientMock.swift @@ -0,0 +1,12 @@ +import Foundation +import Combine + +@testable import WalletConnectPairing + +final class PairingClientMock: PairingClientProtocol { + var pairCalled = false + + func pair(uri: WalletConnectUtils.WalletConnectURI) async throws { + pairCalled = true + } +} diff --git a/Tests/Web3WalletTests/Mocks/SignClientMock.swift b/Tests/Web3WalletTests/Mocks/SignClientMock.swift new file mode 100644 index 000000000..dacd19109 --- /dev/null +++ b/Tests/Web3WalletTests/Mocks/SignClientMock.swift @@ -0,0 +1,78 @@ +import Foundation +import Combine + +@testable import WalletConnectSign + +final class SignClientMock: SignClientProtocol { + var approveCalled = false + var rejectCalled = false + var updateCalled = false + var extendCalled = false + var respondCalled = false + var emitCalled = false + var pairCalled = false + var disconnectCalled = false + + private let metadata = AppMetadata(name: "", description: "", url: "", icons: []) + private let request = WalletConnectSign.Request(id: .left(""), topic: "", method: "", params: "", chainId: Blockchain("eip155:1")!) + + var sessionProposalPublisher: AnyPublisher { + let proposer = Participant(publicKey: "", metadata: metadata) + let sessionProposal = WalletConnectSign.SessionProposal( + relays: [], + proposer: proposer, + requiredNamespaces: [:] + ) + .publicRepresentation() + + return Result.Publisher(sessionProposal).eraseToAnyPublisher() + } + + var sessionRequestPublisher: AnyPublisher { + return Result.Publisher(request).eraseToAnyPublisher() + } + + func approve(proposalId: String, namespaces: [String : WalletConnectSign.SessionNamespace]) async throws { + approveCalled = true + } + + func reject(proposalId: String, reason: WalletConnectSign.RejectionReason) async throws { + rejectCalled = true + } + + func update(topic: String, namespaces: [String : WalletConnectSign.SessionNamespace]) async throws { + updateCalled = true + } + + func extend(topic: String) async throws { + extendCalled = true + } + + func respond(topic: String, requestId: JSONRPC.RPCID, response: JSONRPC.RPCResult) async throws { + respondCalled = true + } + + func emit(topic: String, event: WalletConnectSign.Session.Event, chainId: WalletConnectUtils.Blockchain) async throws { + emitCalled = true + } + + func pair(uri: WalletConnectUtils.WalletConnectURI) async throws { + pairCalled = true + } + + func disconnect(topic: String) async throws { + disconnectCalled = true + } + + func getSessions() -> [WalletConnectSign.Session] { + return [WalletConnectSign.Session(topic: "", peer: metadata, namespaces: [:], expiryDate: Date())] + } + + func getPendingRequests(topic: String?) -> [WalletConnectSign.Request] { + return [request] + } + + func getSessionRequestRecord(id: JSONRPC.RPCID) -> WalletConnectSign.Request? { + return request + } +} diff --git a/Tests/Web3WalletTests/Web3WalletTests.swift b/Tests/Web3WalletTests/Web3WalletTests.swift new file mode 100644 index 000000000..5aff74851 --- /dev/null +++ b/Tests/Web3WalletTests/Web3WalletTests.swift @@ -0,0 +1,174 @@ +import XCTest +import Combine + +@testable import Auth +@testable import Web3Wallet + +final class Web3WalletTests: XCTestCase { + var web3WalletClient: Web3WalletClient! + var authClient: AuthClientMock! + var signClient: SignClientMock! + var pairingClient: PairingClientMock! + + private var disposeBag = Set() + + override func setUp() { + authClient = AuthClientMock() + signClient = SignClientMock() + pairingClient = PairingClientMock() + + web3WalletClient = Web3WalletClientFactory.create( + authClient: authClient, + signClient: signClient, + pairingClient: pairingClient + ) + } + + func testSessionRequestCalled() { + var success = false + web3WalletClient.sessionRequestPublisher.sink { value in + success = true + XCTAssertTrue(true) + } + .store(in: &disposeBag) + + let expectation = expectation(description: "Fail after 0.1s timeout") + let result = XCTWaiter.wait(for: [expectation], timeout: 0.1) + if result == XCTWaiter.Result.timedOut && success == false { + XCTFail() + } + } + + func testAuthRequestCalled() { + var success = false + web3WalletClient.authRequestPublisher.sink { value in + success = true + XCTAssertTrue(true) + } + .store(in: &disposeBag) + + let expectation = expectation(description: "Fail after 0.1s timeout") + let result = XCTWaiter.wait(for: [expectation], timeout: 0.1) + if result == XCTWaiter.Result.timedOut && success == false { + XCTFail() + } + } + + func testSessionProposalCalled() { + var success = false + web3WalletClient.sessionProposalPublisher.sink { value in + success = true + XCTAssertTrue(true) + } + .store(in: &disposeBag) + + let expectation = expectation(description: "Fail after 0.1s timeout") + let result = XCTWaiter.wait(for: [expectation], timeout: 0.1) + if result == XCTWaiter.Result.timedOut && success == false { + XCTFail() + } + } + + func testApproveCalled() async { + try! await web3WalletClient.approve(proposalId: "", namespaces: [:]) + XCTAssertTrue(signClient.approveCalled) + } + + func testRejectCalled() async { + try! await web3WalletClient.reject(proposalId: "", reason: .userRejected) + XCTAssertTrue(signClient.rejectCalled) + } + + func testUpdateCalled() async { + try! await web3WalletClient.update(topic: "", namespaces: [:]) + XCTAssertTrue(signClient.updateCalled) + } + + func testExtendCalled() async { + try! await web3WalletClient.extend(topic: "") + XCTAssertTrue(signClient.extendCalled) + } + + func testSignRespondCalled() async { + try! await web3WalletClient.respond( + topic: "", + requestId: .left(""), + response: RPCResult.response(AnyCodable(true)) + ) + XCTAssertTrue(signClient.respondCalled) + } + + func testPairCalled() async { + try! await web3WalletClient.pair(uri: WalletConnectURI( + topic: "topic", + symKey: "symKey", + relay: RelayProtocolOptions(protocol: "", data: "") + )) + XCTAssertTrue(pairingClient.pairCalled) + } + + func testDisconnectCalled() async { + try! await web3WalletClient.disconnect(topic: "") + XCTAssertTrue(signClient.disconnectCalled) + } + + func testGetSessionsCalledAndNotEmpty() { + let sessions = web3WalletClient.getSessions() + XCTAssertEqual(1, sessions.count) + } + + func testFormatMessageCalled() { + let authPayload = AuthPayload( + requestParams: RequestParams( + domain: "service.invalid", + chainId: "eip155:1", + nonce: "32891756", + aud: "https://service.invalid/login", + nbf: nil, + exp: nil, + statement: "I accept the ServiceOrg Terms of Service: https://service.invalid/tos", + requestId: nil, + resources: [ + "ipfs://bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq/", + "https://example.com/my-web2-claim.json" + ] + ), + iat: "2021-09-30T16:25:24Z" + ) + + let formattedMessage = try! web3WalletClient.formatMessage( + payload: authPayload, + address: "" + ) + XCTAssertEqual("formatted_message", formattedMessage) + } + + func testAuthRespondCalled() async { + let signature = CacaoSignature(t: .eip191, s: "0x438effc459956b57fcd9f3dac6c675f9cee88abf21acab7305e8e32aa0303a883b06dcbd956279a7a2ca21ffa882ff55cc22e8ab8ec0f3fe90ab45f306938cfa1b") + let account = Account("eip155:56:0xe5EeF1368781911d265fDB6946613dA61915a501")! + + try! await web3WalletClient.respond( + requestId: .left(""), + signature: signature, + from: account + ) + XCTAssertTrue(authClient.respondCalled) + } + + func testSignPendingRequestsCalledAndNotEmpty() async { + let pendingRequests = web3WalletClient.getPendingRequests(topic: "") + XCTAssertEqual(1, pendingRequests.count) + } + + func testSessionRequestRecordCalledAndNotNil() async { + let sessionRequestRecord = web3WalletClient.getSessionRequestRecord(id: .left("")) + XCTAssertNotNil(sessionRequestRecord) + } + + func testAuthPendingRequestsCalledAndNotEmpty() async { + let account = Account("eip155:56:0xe5EeF1368781911d265fDB6946613dA61915a501")! + let pendingRequests = try! web3WalletClient.getPendingRequests(account: account) + XCTAssertEqual(1, pendingRequests.count) + } +} +