diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index 7e1880db3..44f9f0988 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -8,6 +8,9 @@ inputs: description: 'The endpoint of the relay e.g. relay.walletconnect.com' required: false default: 'relay.walletconnect.com' + project-id: + description: 'WalletConnect project id' + required: true runs: using: "composite" @@ -29,12 +32,14 @@ runs: shell: bash env: RELAY_ENDPOINT: ${{ inputs.relay-endpoint }} + PROJECT_ID: ${{ inputs.project-id }} run: "xcodebuild \ -project Example/ExampleApp.xcodeproj \ -scheme IntegrationTests \ -clonedSourcePackagesDirPath SourcePackagesCache \ -destination 'platform=iOS Simulator,name=iPhone 13' \ RELAY_HOST='$RELAY_ENDPOINT' \ + PROJECT_ID='$PROJECT_ID' \ test" # Wallet build diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bcbcf09cf..9b697507a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,12 +28,6 @@ jobs: - name: Setup Xcode Version uses: maxim-lobanov/setup-xcode@v1 - - name: Resolve Dependencies - shell: bash - run: " - xcodebuild -resolvePackageDependencies -project Example/ExampleApp.xcodeproj -scheme DApp -clonedSourcePackagesDirPath SourcePackagesCache; \ - xcodebuild -resolvePackageDependencies -project Example/ExampleApp.xcodeproj -scheme WalletConnect -clonedSourcePackagesDirPath SourcePackagesCache" - - uses: actions/cache@v2 with: path: | @@ -43,9 +37,16 @@ jobs: restore-keys: | ${{ runner.os }}-spm- + - name: Resolve Dependencies + shell: bash + run: " + xcodebuild -resolvePackageDependencies -project Example/ExampleApp.xcodeproj -scheme DApp -clonedSourcePackagesDirPath SourcePackagesCache; \ + xcodebuild -resolvePackageDependencies -project Example/ExampleApp.xcodeproj -scheme WalletConnect -clonedSourcePackagesDirPath SourcePackagesCache" + - uses: ./.github/actions/ci with: type: ${{ matrix.test-type }} + project-id: ${{ secrets.PROJECT_ID }} test-ui: if: github.ref == 'refs/heads/main' diff --git a/.gitignore b/.gitignore index 36f794f62..dad29299f 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,6 @@ Package.resolved */fastlane/test_output */fastlane/README.md +# Configuration +Configuration.xcconfig + diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnect.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnect.xcscheme index 75284ea37..ff9119893 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnect.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnect.xcscheme @@ -272,6 +272,16 @@ ReferencedContainer = "container:"> + + + + () @Published var state: SigningState = .none - @Published var uri: String? + @Published var uriString: String? var qrImage: UIImage? { - return uri.map { QRCodeGenerator.generateQRCode(from: $0) } + return uriString.map { QRCodeGenerator.generateQRCode(from: $0) } } init() { @@ -26,12 +27,14 @@ final class AuthViewModel: ObservableObject { @MainActor func setupInitialState() async throws { state = .none - uri = nil - uri = try await Auth.instance.request(.stub()).absoluteString + uriString = nil + let uri = try! await Pair.instance.create() + uriString = uri.absoluteString + try await Auth.instance.request(.stub(), topic: uri.topic) } func copyDidPressed() { - UIPasteboard.general.string = uri + UIPasteboard.general.string = uriString } func walletDidPressed() { @@ -39,7 +42,7 @@ final class AuthViewModel: ObservableObject { } func deeplinkPressed() { - guard let uri = uri else { return } + guard let uri = uriString else { return } UIApplication.shared.open(URL(string: "showcase://wc?uri=\(uri)")!) } } diff --git a/Example/DApp/Common/InputConfig.swift b/Example/DApp/Common/InputConfig.swift new file mode 100644 index 000000000..53931721a --- /dev/null +++ b/Example/DApp/Common/InputConfig.swift @@ -0,0 +1,12 @@ +import Foundation + +struct InputConfig { + + static var projectId: String { + return config(for: "PROJECT_ID")! + } + + private static func config(for key: String) -> String? { + return Bundle.main.object(forInfoDictionaryKey: key) as? String + } +} diff --git a/Example/DApp/Info.plist b/Example/DApp/Info.plist index 7c5f0be1e..07ba0e705 100644 --- a/Example/DApp/Info.plist +++ b/Example/DApp/Info.plist @@ -2,6 +2,8 @@ + PROJECT_ID + $(PROJECT_ID) ITSAppUsesNonExemptEncryption UIApplicationSceneManifest diff --git a/Example/DApp/SceneDelegate.swift b/Example/DApp/SceneDelegate.swift index d130770b8..afeca0dce 100644 --- a/Example/DApp/SceneDelegate.swift +++ b/Example/DApp/SceneDelegate.swift @@ -1,6 +1,7 @@ import UIKit import Starscream import WalletConnectRelay +import WalletConnectNetworking extension WebSocket: WebSocketConnecting { } @@ -18,7 +19,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { private let authCoordinator = AuthCoordinator() func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { - Relay.configure(projectId: "3ca2919724fbfa5456a25194e369a8b4", socketFactory: SocketFactory()) + Networking.configure(projectId: InputConfig.projectId, socketFactory: SocketFactory()) setupWindow(scene: scene) } diff --git a/Example/DApp/Sign/Connect/ConnectViewController.swift b/Example/DApp/Sign/Connect/ConnectViewController.swift index adebac5c3..15e50a9fd 100644 --- a/Example/DApp/Sign/Connect/ConnectViewController.swift +++ b/Example/DApp/Sign/Connect/ConnectViewController.swift @@ -1,10 +1,11 @@ import Foundation import UIKit import WalletConnectSign +import WalletConnectPairing class ConnectViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { let uri: WalletConnectURI - let activePairings: [Pairing] = Sign.instance.getPairings() + let activePairings: [Pairing] = Pair.instance.getPairings() let segmentedControl = UISegmentedControl(items: ["Pairings", "New Pairing"]) init(uri: WalletConnectURI) { diff --git a/Example/DApp/Sign/ResponseViewController.swift b/Example/DApp/Sign/ResponseViewController.swift index 4f8d1c0b3..eec81065f 100644 --- a/Example/DApp/Sign/ResponseViewController.swift +++ b/Example/DApp/Sign/ResponseViewController.swift @@ -23,14 +23,14 @@ class ResponseViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - let record = Sign.instance.getSessionRequestRecord(id: response.result.id)! + let record = Sign.instance.getSessionRequestRecord(id: response.id)! switch response.result { case .response(let response): - responseView.nameLabel.text = "Received Response\n\(record.request.method)" - responseView.descriptionLabel.text = try! response.result.get(String.self).description + responseView.nameLabel.text = "Received Response\n\(record.method)" + responseView.descriptionLabel.text = try! response.get(String.self).description case .error(let error): - responseView.nameLabel.text = "Received Error\n\(record.request.method)" - responseView.descriptionLabel.text = error.error.message + responseView.nameLabel.text = "Received Error\n\(record.method)" + responseView.descriptionLabel.text = error.message } responseView.dismissButton.addTarget(self, action: #selector(dismissSelf), for: .touchUpInside) } diff --git a/Example/DApp/Sign/SelectChain/SelectChainViewController.swift b/Example/DApp/Sign/SelectChain/SelectChainViewController.swift index 74210f740..7abd2b7d4 100644 --- a/Example/DApp/Sign/SelectChain/SelectChainViewController.swift +++ b/Example/DApp/Sign/SelectChain/SelectChainViewController.swift @@ -1,5 +1,6 @@ import Foundation import WalletConnectSign +import WalletConnectPairing import UIKit import Combine @@ -15,16 +16,12 @@ class SelectChainViewController: UIViewController, UITableViewDataSource { private var publishers = [AnyCancellable]() let chains = [Chain(name: "Ethereum", id: "eip155:1"), Chain(name: "Polygon", id: "eip155:137")] - var onSessionSettled: ((Session) -> Void)? override func viewDidLoad() { super.viewDidLoad() navigationItem.title = "Available Chains" selectChainView.tableView.dataSource = self selectChainView.connectButton.addTarget(self, action: #selector(connect), for: .touchUpInside) selectChainView.openWallet.addTarget(self, action: #selector(openWallet), for: .touchUpInside) - Sign.instance.sessionSettlePublisher.sink {[unowned self] session in - onSessionSettled?(session) - }.store(in: &publishers) } override func loadView() { @@ -38,8 +35,9 @@ class SelectChainViewController: UIViewController, UITableViewDataSource { let blockchains: Set = [Blockchain("eip155:1")!, Blockchain("eip155:137")!] let namespaces: [String: ProposalNamespace] = ["eip155": ProposalNamespace(chains: blockchains, methods: methods, events: [], extensions: nil)] Task { - let uri = try await Sign.instance.connect(requiredNamespaces: namespaces) - showConnectScreen(uri: uri!) + let uri = try await Pair.instance.create() + try await Sign.instance.connect(requiredNamespaces: namespaces, topic: uri.topic) + showConnectScreen(uri: uri) } } diff --git a/Example/DApp/Sign/SignCoordinator.swift b/Example/DApp/Sign/SignCoordinator.swift index 1373e4c30..51e70078e 100644 --- a/Example/DApp/Sign/SignCoordinator.swift +++ b/Example/DApp/Sign/SignCoordinator.swift @@ -2,6 +2,7 @@ import UIKit import Combine import WalletConnectSign import WalletConnectRelay +import WalletConnectPairing final class SignCoordinator { @@ -25,7 +26,7 @@ final class SignCoordinator { url: "wallet.connect", icons: ["https://avatars.githubusercontent.com/u/37784886"]) - Sign.configure(metadata: metadata) + Pair.configure(metadata: metadata) #if DEBUG if CommandLine.arguments.contains("-cleanInstall") { try? Sign.instance.cleanup() @@ -44,6 +45,12 @@ final class SignCoordinator { presentResponse(for: response) }.store(in: &publishers) + Sign.instance.sessionSettlePublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] session in + showAccountsScreen(session) + }.store(in: &publishers) + if let session = Sign.instance.getSessions().first { showAccountsScreen(session) } else { @@ -53,9 +60,6 @@ final class SignCoordinator { private func showSelectChainScreen() { let controller = SelectChainViewController() - controller.onSessionSettled = { [unowned self] session in - showAccountsScreen(session) - } navigationController.viewControllers = [controller] } @@ -64,6 +68,7 @@ final class SignCoordinator { controller.onDisconnect = { [unowned self] in showSelectChainScreen() } + navigationController.presentedViewController?.dismiss(animated: false) navigationController.viewControllers = [controller] } diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index e23637099..f0ef7ebf7 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -30,6 +30,7 @@ 8448F1D427E4726F0000B866 /* WalletConnect in Frameworks */ = {isa = PBXBuildFile; productRef = 8448F1D327E4726F0000B866 /* WalletConnect */; }; 84494388278D9C1B00CC26BB /* UIAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84494387278D9C1B00CC26BB /* UIAlertController.swift */; }; 8460DCFC274F98A10081F94C /* RequestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8460DCFB274F98A10081F94C /* RequestViewController.swift */; }; + 847CF3AF28E3141700F1D760 /* WalletConnectPush in Frameworks */ = {isa = PBXBuildFile; productRef = 847CF3AE28E3141700F1D760 /* WalletConnectPush */; settings = {ATTRIBUTES = (Required, ); }; }; 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 */; }; @@ -44,14 +45,18 @@ 84CE644E279ED2FF00142511 /* SelectChainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CE644D279ED2FF00142511 /* SelectChainView.swift */; }; 84CE6452279ED42B00142511 /* ConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CE6451279ED42B00142511 /* ConnectView.swift */; }; 84CE645527A29D4D00142511 /* ResponseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CE645427A29D4C00142511 /* ResponseViewController.swift */; }; + 84CEC64628D89D6B00D081A8 /* PairingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CEC64528D89D6B00D081A8 /* PairingTests.swift */; }; 84D2A66628A4F51E0088AE09 /* AuthTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D2A66528A4F51E0088AE09 /* AuthTests.swift */; }; 84DDB4ED28ABB663003D66ED /* WalletConnectAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 84DDB4EC28ABB663003D66ED /* WalletConnectAuth */; }; 84F568C2279582D200D0A289 /* Signer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F568C1279582D200D0A289 /* Signer.swift */; }; 84F568C42795832A00D0A289 /* EthereumTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F568C32795832A00D0A289 /* EthereumTransaction.swift */; }; 84FE684628ACDB4700C893FF /* RequestParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FE684528ACDB4700C893FF /* RequestParams.swift */; }; - A501AC2728C8E59800CEAA42 /* URLConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A501AC2628C8E59800CEAA42 /* URLConfig.swift */; }; A50C036528AAD32200FE72D3 /* ClientDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50C036428AAD32200FE72D3 /* ClientDelegate.swift */; }; A50F3946288005B200064555 /* Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50F3945288005B200064555 /* Types.swift */; }; + A518B31428E33A6500A2CE93 /* InputConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A518B31328E33A6500A2CE93 /* InputConfig.swift */; }; + A51AC0D928E436A3001BACF9 /* InputConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51AC0D828E436A3001BACF9 /* InputConfig.swift */; }; + A51AC0DD28E43727001BACF9 /* InputConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51AC0DB28E436E6001BACF9 /* InputConfig.swift */; }; + A51AC0DF28E4379F001BACF9 /* InputConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51AC0DE28E4379F001BACF9 /* InputConfig.swift */; }; A55CAAB028B92AFF00844382 /* ScanModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55CAAAB28B92AFF00844382 /* ScanModule.swift */; }; A55CAAB128B92AFF00844382 /* ScanPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55CAAAC28B92AFF00844382 /* ScanPresenter.swift */; }; A55CAAB228B92AFF00844382 /* ScanRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55CAAAD28B92AFF00844382 /* ScanRouter.swift */; }; @@ -235,13 +240,17 @@ 84CE6451279ED42B00142511 /* ConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectView.swift; sourceTree = ""; }; 84CE6453279FFE1100142511 /* Wallet.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Wallet.entitlements; sourceTree = ""; }; 84CE645427A29D4C00142511 /* ResponseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseViewController.swift; sourceTree = ""; }; + 84CEC64528D89D6B00D081A8 /* PairingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PairingTests.swift; sourceTree = ""; }; 84D2A66528A4F51E0088AE09 /* AuthTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthTests.swift; sourceTree = ""; }; 84F568C1279582D200D0A289 /* Signer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Signer.swift; sourceTree = ""; }; 84F568C32795832A00D0A289 /* EthereumTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthereumTransaction.swift; sourceTree = ""; }; 84FE684528ACDB4700C893FF /* RequestParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestParams.swift; sourceTree = ""; }; - A501AC2628C8E59800CEAA42 /* URLConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLConfig.swift; sourceTree = ""; }; A50C036428AAD32200FE72D3 /* ClientDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClientDelegate.swift; sourceTree = ""; }; A50F3945288005B200064555 /* Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Types.swift; sourceTree = ""; }; + A518B31328E33A6500A2CE93 /* InputConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputConfig.swift; sourceTree = ""; }; + A51AC0D828E436A3001BACF9 /* InputConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputConfig.swift; sourceTree = ""; }; + A51AC0DB28E436E6001BACF9 /* InputConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputConfig.swift; sourceTree = ""; }; + A51AC0DE28E4379F001BACF9 /* InputConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputConfig.swift; sourceTree = ""; }; A55CAAAB28B92AFF00844382 /* ScanModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanModule.swift; sourceTree = ""; }; A55CAAAC28B92AFF00844382 /* ScanPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanPresenter.swift; sourceTree = ""; }; A55CAAAD28B92AFF00844382 /* ScanRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanRouter.swift; sourceTree = ""; }; @@ -354,6 +363,7 @@ A5E22D212840C8D300E36487 /* WalletEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletEngine.swift; sourceTree = ""; }; A5E22D232840C8DB00E36487 /* SafariEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariEngine.swift; sourceTree = ""; }; A5E22D2B2840EAC300E36487 /* XCUIElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCUIElement.swift; sourceTree = ""; }; + A5F48A0528E43D3F0034CBFB /* Configuration.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Configuration.xcconfig; path = ../Configuration.xcconfig; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -402,6 +412,7 @@ files = ( A5E03DFF2864662500888481 /* WalletConnect in Frameworks */, A5E03DF52864651200888481 /* Starscream in Frameworks */, + 847CF3AF28E3141700F1D760 /* WalletConnectPush in Frameworks */, 84DDB4ED28ABB663003D66ED /* WalletConnectAuth in Frameworks */, A5E03E01286466EA00888481 /* WalletConnectChat in Frameworks */, ); @@ -450,6 +461,7 @@ 764E1D3326F8D3FC00A1FB15 = { isa = PBXGroup; children = ( + A5F48A0528E43D3F0034CBFB /* Configuration.xcconfig */, 84CE6453279FFE1100142511 /* Wallet.entitlements */, 764E1D3E26F8D3FC00A1FB15 /* ExampleApp */, 84CE641D27981DED00142511 /* DApp */, @@ -479,6 +491,7 @@ children = ( 764E1D3F26F8D3FC00A1FB15 /* AppDelegate.swift */, 764E1D4126F8D3FC00A1FB15 /* SceneDelegate.swift */, + A51AC0DA28E436DE001BACF9 /* Common */, 761248152819F9A800CB6D48 /* Wallet */, 761C64A426FCB08B004239D1 /* SessionProposal */, 8460DCFE2750D6DF0081F94C /* SessionDetails */, @@ -581,6 +594,14 @@ path = Connect; sourceTree = ""; }; + 84CEC64728D8A98900D081A8 /* Pairing */ = { + isa = PBXGroup; + children = ( + 84CEC64528D89D6B00D081A8 /* PairingTests.swift */, + ); + path = Pairing; + sourceTree = ""; + }; 84D2A66728A4F5260088AE09 /* Auth */ = { isa = PBXGroup; children = ( @@ -597,6 +618,14 @@ path = Types; sourceTree = ""; }; + A51AC0DA28E436DE001BACF9 /* Common */ = { + isa = PBXGroup; + children = ( + A51AC0DB28E436E6001BACF9 /* InputConfig.swift */, + ); + path = Common; + sourceTree = ""; + }; A55CAAAA28B92AF200844382 /* Scan */ = { isa = PBXGroup; children = ( @@ -752,6 +781,7 @@ A58E7CFD2872A0F80082D443 /* Common */ = { isa = PBXGroup; children = ( + A51AC0DE28E4379F001BACF9 /* InputConfig.swift */, A50F3944288005A700064555 /* Types */, A5C2021F287EA5AF007E3188 /* Components */, A578FA332873049400AA7720 /* Style */, @@ -969,6 +999,7 @@ A5BB7FAB28B6AA7100707FC6 /* Common */ = { isa = PBXGroup; children = ( + A51AC0D828E436A3001BACF9 /* InputConfig.swift */, A5BB7FAC28B6AA7D00707FC6 /* QRCodeGenerator.swift */, ); path = Common; @@ -1019,6 +1050,7 @@ A5E03DEE286464DB00888481 /* IntegrationTests */ = { isa = PBXGroup; children = ( + 84CEC64728D8A98900D081A8 /* Pairing */, A5E03E0B28646AA500888481 /* Relay */, A5E03E0A28646A8A00888481 /* Stubs */, A5E03E0928646A8100888481 /* Sign */, @@ -1049,11 +1081,11 @@ A5E03E0A28646A8A00888481 /* Stubs */ = { isa = PBXGroup; children = ( + A518B31328E33A6500A2CE93 /* InputConfig.swift */, A5E03E1028646F8000888481 /* KeychainStorageMock.swift */, A5E03E0E28646D8A00888481 /* WebSocketFactory.swift */, A5E03DFC286465D100888481 /* Stubs.swift */, 84FE684528ACDB4700C893FF /* RequestParams.swift */, - A501AC2628C8E59800CEAA42 /* URLConfig.swift */, ); path = Stubs; sourceTree = ""; @@ -1190,6 +1222,7 @@ A5E03DFE2864662500888481 /* WalletConnect */, A5E03E00286466EA00888481 /* WalletConnectChat */, 84DDB4EC28ABB663003D66ED /* WalletConnectAuth */, + 847CF3AE28E3141700F1D760 /* WalletConnectPush */, ); productName = IntegrationTests; productReference = A5E03DED286464DB00888481 /* IntegrationTests.xctest */; @@ -1296,6 +1329,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + A51AC0DD28E43727001BACF9 /* InputConfig.swift in Sources */, 76235E892820198B004ED0AA /* UIKit+Previews.swift in Sources */, 76B149F02821C03B00F05F91 /* Proposal.swift in Sources */, 765056272821989600F9AE79 /* Color+Extension.swift in Sources */, @@ -1333,6 +1367,7 @@ 84CE645527A29D4D00142511 /* ResponseViewController.swift in Sources */, 84CE641F27981DED00142511 /* AppDelegate.swift in Sources */, A5BB7FAD28B6AA7D00707FC6 /* QRCodeGenerator.swift in Sources */, + A51AC0D928E436A3001BACF9 /* InputConfig.swift in Sources */, A5BB7FA928B6A5FD00707FC6 /* AuthViewModel.swift in Sources */, 84CE6452279ED42B00142511 /* ConnectView.swift in Sources */, 84CE6448279AE68600142511 /* AccountRequestViewController.swift in Sources */, @@ -1352,6 +1387,7 @@ files = ( A58E7D3B2872D55F0082D443 /* ChatInteractor.swift in Sources */, A58E7D1F2872A57B0082D443 /* ApplicationConfigurator.swift in Sources */, + A51AC0DF28E4379F001BACF9 /* InputConfig.swift in Sources */, A58E7D452872EE570082D443 /* ContentMessageView.swift in Sources */, A5C20223287EA7E2007E3188 /* BrandButton.swift in Sources */, A5629ADF2876CC6E00094373 /* InviteListPresenter.swift in Sources */, @@ -1455,14 +1491,15 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 84CEC64628D89D6B00D081A8 /* PairingTests.swift in Sources */, 767DC83528997F8E00080FA9 /* EthSendTransaction.swift in Sources */, + A518B31428E33A6500A2CE93 /* InputConfig.swift in Sources */, A5E03E03286466F400888481 /* ChatTests.swift in Sources */, 84D2A66628A4F51E0088AE09 /* AuthTests.swift in Sources */, 84FE684628ACDB4700C893FF /* RequestParams.swift in Sources */, 7694A5262874296A0001257E /* RegistryTests.swift in Sources */, A50C036528AAD32200FE72D3 /* ClientDelegate.swift in Sources */, A5E03E0D28646AD200888481 /* RelayClientEndToEndTests.swift in Sources */, - A501AC2728C8E59800CEAA42 /* URLConfig.swift in Sources */, A5E03DFA286465C700888481 /* SignClientTests.swift in Sources */, A5E03E0F28646D8A00888481 /* WebSocketFactory.swift in Sources */, 84AA01DB28CF0CD7005D48D8 /* XCTest.swift in Sources */, @@ -1516,6 +1553,7 @@ /* Begin XCBuildConfiguration section */ 764E1D4E26F8D3FE00A1FB15 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = A5F48A0528E43D3F0034CBFB /* Configuration.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -1577,6 +1615,7 @@ }; 764E1D4F26F8D3FE00A1FB15 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = A5F48A0528E43D3F0034CBFB /* Configuration.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -1864,7 +1903,6 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.walletconnect.IntegrationTests; PRODUCT_NAME = "$(TARGET_NAME)"; - RELAY_HOST = relay.walletconnect.com; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -1883,7 +1921,6 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.walletconnect.IntegrationTests; PRODUCT_NAME = "$(TARGET_NAME)"; - RELAY_HOST = relay.walletconnect.com; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -1952,10 +1989,10 @@ /* Begin XCRemoteSwiftPackageReference section */ A5AE354528A1A2AC0059AE8A /* XCRemoteSwiftPackageReference "Web3" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/flypaper0/Web3.swift"; + repositoryURL = "https://github.com/WalletConnect/Web3.swift"; requirement = { - branch = "feature/eip-155"; - kind = branch; + kind = exactVersion; + version = 1.0.0; }; }; A5D85224286333D500DAF5C3 /* XCRemoteSwiftPackageReference "Starscream" */ = { @@ -1977,6 +2014,10 @@ isa = XCSwiftPackageProductDependency; productName = WalletConnect; }; + 847CF3AE28E3141700F1D760 /* WalletConnectPush */ = { + isa = XCSwiftPackageProductDependency; + productName = WalletConnectPush; + }; 84DDB4EC28ABB663003D66ED /* WalletConnectAuth */ = { isa = XCSwiftPackageProductDependency; productName = WalletConnectAuth; diff --git a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 1b5f80e67..01652b58f 100644 --- a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -15,8 +15,8 @@ "repositoryURL": "https://github.com/krzyzanowskim/CryptoSwift.git", "state": { "branch": null, - "revision": "039f56c5d7960f277087a0be51f5eb04ed0ec073", - "version": "1.5.1" + "revision": "19b3c3ceed117c5cc883517c4e658548315ba70b", + "version": "1.6.0" } }, { @@ -24,8 +24,8 @@ "repositoryURL": "https://github.com/mxcl/PromiseKit.git", "state": { "branch": null, - "revision": "ed3192004c0b00b4b3d7baa9578ee655c66faae3", - "version": "6.18.0" + "revision": "43772616c46a44a9977e41924ae01d0e55f2f9ca", + "version": "6.18.1" } }, { @@ -57,11 +57,11 @@ }, { "package": "Web3", - "repositoryURL": "https://github.com/flypaper0/Web3.swift", + "repositoryURL": "https://github.com/WalletConnect/Web3.swift", "state": { - "branch": "feature/eip-155", - "revision": "92a43a8c279b9df25fe23dd6f8311e6fb0ea06ed", - "version": null + "branch": null, + "revision": "bdaaed96eee3a9bf7f341165f89a94288961d14c", + "version": "1.0.0" } } ] diff --git a/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/IntegrationTests.xcscheme b/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/IntegrationTests.xcscheme index 54745ba8d..6c42dec52 100644 --- a/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/IntegrationTests.xcscheme +++ b/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/IntegrationTests.xcscheme @@ -26,6 +26,11 @@ value = "$(RELAY_HOST)" isEnabled = "YES"> + + String? { + return Bundle.main.object(forInfoDictionaryKey: key) as? String + } +} diff --git a/Example/ExampleApp/Info.plist b/Example/ExampleApp/Info.plist index fd00d23da..4d76b146a 100644 --- a/Example/ExampleApp/Info.plist +++ b/Example/ExampleApp/Info.plist @@ -2,6 +2,8 @@ + PROJECT_ID + $(PROJECT_ID) CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName diff --git a/Example/ExampleApp/SceneDelegate.swift b/Example/ExampleApp/SceneDelegate.swift index ba57b9ff4..0037fb05b 100644 --- a/Example/ExampleApp/SceneDelegate.swift +++ b/Example/ExampleApp/SceneDelegate.swift @@ -1,6 +1,10 @@ import UIKit +import Foundation +import Combine import WalletConnectSign +import WalletConnectNetworking import WalletConnectRelay +import WalletConnectPairing import Starscream extension WebSocket: WebSocketConnecting { } @@ -23,8 +27,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { url: "example.wallet", icons: ["https://avatars.githubusercontent.com/u/37784886"]) - Relay.configure(projectId: "3ca2919724fbfa5456a25194e369a8b4", socketFactory: SocketFactory()) - Sign.configure(metadata: metadata) + Networking.configure(projectId: InputConfig.projectId, socketFactory: SocketFactory()) + Pair.configure(metadata: metadata) #if DEBUG if CommandLine.arguments.contains("-cleanInstall") { try? Sign.instance.cleanup() @@ -35,24 +39,25 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { window = UIWindow(windowScene: windowScene) window?.rootViewController = UITabBarController.createExampleApp() window?.makeKeyAndVisible() + + if let userActivity = connectionOptions.userActivities.first { + handle(userActivity: userActivity) + } } func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { - guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, - let incomingURL = userActivity.webpageURL else { - return - } - let wcUri = incomingURL.absoluteString.deletingPrefix("https://walletconnect.com/wc?uri=") - let vc = ((window!.rootViewController as! UINavigationController).viewControllers[0] as! WalletViewController) - Task(priority: .high) {try? await Sign.instance.pair(uri: WalletConnectURI(string: wcUri)!)} - vc.onClientConnected = { - Task(priority: .high) { - do { - try await Sign.instance.pair(uri: WalletConnectURI(string: wcUri)!) - } catch { - print(error) - } - } + handle(userActivity: userActivity) + } + + private func handle(userActivity: NSUserActivity) { + guard + let url = userActivity.webpageURL, + userActivity.activityType == NSUserActivityTypeBrowsingWeb + else { return } + + let wcUri = url.absoluteString.deletingPrefix("https://walletconnect.com/wc?uri=") + Task(priority: .high) { + try! await Pair.instance.pair(uri: WalletConnectURI(string: wcUri)!) } } } diff --git a/Example/ExampleApp/SessionDetails/SessionDetailViewController.swift b/Example/ExampleApp/SessionDetails/SessionDetailViewController.swift index 4d7d6b39e..f1b2a05bc 100644 --- a/Example/ExampleApp/SessionDetails/SessionDetailViewController.swift +++ b/Example/ExampleApp/SessionDetails/SessionDetailViewController.swift @@ -24,8 +24,7 @@ final class SessionDetailViewController: UIHostingController let viewController = RequestViewController(request) viewController.onSign = { [unowned self] in let result = Signer.signEth(request: request) - let response = JSONRPCResponse(id: request.id, result: result) - respondOnSign(request: request, response: response) + respondOnSign(request: request, response: result) reload() } viewController.onReject = { [unowned self] in @@ -35,11 +34,11 @@ final class SessionDetailViewController: UIHostingController present(viewController, animated: true) } - private func respondOnSign(request: Request, response: JSONRPCResponse) { + private func respondOnSign(request: Request, response: AnyCodable) { print("[WALLET] Respond on Sign") Task { do { - try await Sign.instance.respond(topic: request.topic, response: .response(response)) + try await Sign.instance.respond(topic: request.topic, requestId: request.id, response: .response(response)) } catch { print("[DAPP] Respond Error: \(error.localizedDescription)") } @@ -52,10 +51,8 @@ final class SessionDetailViewController: UIHostingController do { try await Sign.instance.respond( topic: request.topic, - response: .error(JSONRPCErrorResponse( - id: request.id, - error: JSONRPCErrorResponse.Error(code: 0, message: "")) - ) + requestId: request.id, + response: .error(.init(code: 0, message: "")) ) } catch { print("[DAPP] Respond Error: \(error.localizedDescription)") diff --git a/Example/ExampleApp/SessionDetails/SessionDetailViewModel.swift b/Example/ExampleApp/SessionDetails/SessionDetailViewModel.swift index f29dea255..d43a5ce3d 100644 --- a/Example/ExampleApp/SessionDetails/SessionDetailViewModel.swift +++ b/Example/ExampleApp/SessionDetails/SessionDetailViewModel.swift @@ -7,6 +7,8 @@ final class SessionDetailViewModel: ObservableObject { private let session: Session private let client: SignClient + private var publishers = Set() + enum Fields { case accounts case methods @@ -22,6 +24,8 @@ final class SessionDetailViewModel: ObservableObject { self.session = session self.client = client self.namespaces = session.namespaces + + setupSubscriptions() } var peerName: String { @@ -81,13 +85,8 @@ final class SessionDetailViewModel: ObservableObject { } func ping() { - client.ping(topic: session.topic) { result in - switch result { - case .success: - self.pingSuccess = true - case .failure: - self.pingFailed = true - } + Task(priority: .userInitiated) { + try await client.ping(topic: session.topic) } } @@ -98,6 +97,15 @@ final class SessionDetailViewModel: ObservableObject { private extension SessionDetailViewModel { + func setupSubscriptions() { + client.pingResponsePublisher + .receive(on: DispatchQueue.main) + .sink { _ in + self.pingSuccess = true + } + .store(in: &publishers) + } + func addTestAccount(for chain: String) { guard let viewModel = namespace(for: chain) else { return } diff --git a/Example/ExampleApp/SessionDetails/SessionDetailsViewController.swift b/Example/ExampleApp/SessionDetails/SessionDetailsViewController.swift index f53ecbdef..bb6d21be6 100644 --- a/Example/ExampleApp/SessionDetails/SessionDetailsViewController.swift +++ b/Example/ExampleApp/SessionDetails/SessionDetailsViewController.swift @@ -49,11 +49,11 @@ final class SessionDetailsViewController: UIViewController, UITableViewDelegate, @objc private func ping() { - Sign.instance.ping(topic: session.topic) { result in - switch result { - case .success: + Task(priority: .userInitiated) { @MainActor in + do { + try await Sign.instance.ping(topic: session.topic) print("received ping response") - case .failure(let error): + } catch { print(error) } } diff --git a/Example/ExampleApp/Wallet/WalletViewController.swift b/Example/ExampleApp/Wallet/WalletViewController.swift index 9d574ea34..a888df7bd 100644 --- a/Example/ExampleApp/Wallet/WalletViewController.swift +++ b/Example/ExampleApp/Wallet/WalletViewController.swift @@ -1,6 +1,7 @@ import UIKit import WalletConnectSign import WalletConnectUtils +import WalletConnectPairing import WalletConnectRouter import Web3 import CryptoSwift @@ -10,9 +11,10 @@ final class WalletViewController: UIViewController { lazy var account = Signer.privateKey.address.hex(eip55: true) var sessionItems: [ActiveSessionItem] = [] var currentProposal: Session.Proposal? - var onClientConnected: (() -> Void)? private var publishers = [AnyCancellable]() + var onClientConnected: (() -> Void)? + private let walletView: WalletView = { WalletView() }() @@ -71,8 +73,7 @@ final class WalletViewController: UIViewController { let requestVC = RequestViewController(request) requestVC.onSign = { [unowned self] in let result = Signer.signEth(request: request) - let response = JSONRPCResponse(id: request.id, result: result) - respondOnSign(request: request, response: response) + respondOnSign(request: request, response: result) reloadSessionDetailsIfNeeded() } requestVC.onReject = { [unowned self] in @@ -90,11 +91,11 @@ final class WalletViewController: UIViewController { } @MainActor - private func respondOnSign(request: Request, response: JSONRPCResponse) { + private func respondOnSign(request: Request, response: AnyCodable) { print("[WALLET] Respond on Sign") Task { do { - try await Sign.instance.respond(topic: request.topic, response: .response(response)) + try await Sign.instance.respond(topic: request.topic, requestId: request.id, response: .response(response)) } catch { print("[DAPP] Respond Error: \(error.localizedDescription)") } @@ -108,10 +109,8 @@ final class WalletViewController: UIViewController { do { try await Sign.instance.respond( topic: request.topic, - response: .error(JSONRPCErrorResponse( - id: request.id, - error: JSONRPCErrorResponse.Error(code: 0, message: "")) - ) + requestId: request.id, + response: .error(.init(code: 0, message: "")) ) } catch { print("[DAPP] Respond Error: \(error.localizedDescription)") @@ -124,7 +123,7 @@ final class WalletViewController: UIViewController { print("[WALLET] Pairing to: \(uri)") Task { do { - try await Sign.instance.pair(uri: uri) + try await Pair.instance.pair(uri: uri) } catch { print("[DAPP] Pairing connect error: \(error)") } @@ -241,8 +240,8 @@ extension WalletViewController { .receive(on: DispatchQueue.main) .sink { [weak self] status in if status == .connected { - self?.onClientConnected?() print("Client connected") + self?.onClientConnected?() } }.store(in: &publishers) diff --git a/Example/IntegrationTests/Auth/AuthTests.swift b/Example/IntegrationTests/Auth/AuthTests.swift index 6abe13983..ea165a98f 100644 --- a/Example/IntegrationTests/Auth/AuthTests.swift +++ b/Example/IntegrationTests/Auth/AuthTests.swift @@ -5,131 +5,196 @@ import WalletConnectUtils import WalletConnectRelay import Combine @testable import Auth +import WalletConnectPairing +import WalletConnectNetworking final class AuthTests: XCTestCase { - var app: AuthClient! - var wallet: AuthClient! + var appPairingClient: PairingClient! + var walletPairingClient: PairingClient! + + var appAuthClient: AuthClient! + var walletAuthClient: AuthClient! let prvKey = Data(hex: "462c1dad6832d7d96ccf87bd6a686a4110e114aaaebd5512e552c0e3a87b480f") + let eip1271Signature = "0xc1505719b2504095116db01baaf276361efd3a73c28cf8cc28dabefa945b8d536011289ac0a3b048600c1e692ff173ca944246cf7ceb319ac2262d27b395c82b1c" private var publishers = [AnyCancellable]() override func setUp() { - app = makeClient(prefix: "👻 App") - let walletAccount = Account(chainIdentifier: "eip155:1", address: "0x724d0D2DaD3fbB0C168f947B87Fa5DBe36F1A8bf")! - wallet = makeClient(prefix: "🤑 Wallet", account: walletAccount) - - let expectation = expectation(description: "Wait Clients Connected") - expectation.expectedFulfillmentCount = 2 - - app.socketConnectionStatusPublisher.sink { status in - if status == .connected { - expectation.fulfill() - } - }.store(in: &publishers) - - wallet.socketConnectionStatusPublisher.sink { status in - if status == .connected { - expectation.fulfill() - } - }.store(in: &publishers) + setupClients() + } - wait(for: [expectation], timeout: 5) + private func setupClients(address: String = "0x724d0D2DaD3fbB0C168f947B87Fa5DBe36F1A8bf", iatProvider: IATProvider = DefaultIATProvider()) { + let walletAccount = Account(chainIdentifier: "eip155:1", address: address)! + (appPairingClient, appAuthClient) = makeClients(prefix: "🤖 App", iatProvider: iatProvider) + (walletPairingClient, walletAuthClient) = makeClients(prefix: "🐶 Wallet", account: walletAccount, iatProvider: iatProvider) } - func makeClient(prefix: String, account: Account? = nil) -> AuthClient { + func makeClients(prefix: String, account: Account? = nil, iatProvider: IATProvider) -> (PairingClient, AuthClient) { let logger = ConsoleLogger(suffix: prefix, loggingLevel: .debug) - let projectId = "3ca2919724fbfa5456a25194e369a8b4" let keychain = KeychainStorageMock() - let relayClient = RelayClient(relayHost: URLConfig.relayHost, projectId: projectId, keychainStorage: keychain, socketFactory: SocketFactory(), logger: logger) + let relayClient = RelayClient(relayHost: InputConfig.relayHost, projectId: InputConfig.projectId, keychainStorage: keychain, socketFactory: SocketFactory(), logger: logger) + let keyValueStorage = RuntimeKeyValueStorage() + + let networkingClient = NetworkingClientFactory.create( + relayClient: relayClient, + logger: logger, + keychainStorage: keychain, + keyValueStorage: keyValueStorage) - return AuthClientFactory.create( + let pairingClient = PairingClientFactory.create( + logger: logger, + keyValueStorage: keyValueStorage, + keychainStorage: keychain, + networkingClient: networkingClient) + + let authClient = AuthClientFactory.create( metadata: AppMetadata(name: name, description: "", url: "", icons: [""]), account: account, + projectId: InputConfig.projectId, logger: logger, - keyValueStorage: RuntimeKeyValueStorage(), + keyValueStorage: keyValueStorage, keychainStorage: keychain, - relayClient: relayClient) + networkingClient: networkingClient, + pairingRegisterer: pairingClient, + iatProvider: iatProvider) + + return (pairingClient, authClient) } func testRequest() async { let requestExpectation = expectation(description: "request delivered to wallet") - let uri = try! await app.request(RequestParams.stub()) - try! await wallet.pair(uri: uri) - wallet.authRequestPublisher.sink { _ in + let uri = try! await appPairingClient.create() + try! await appAuthClient.request(RequestParams.stub(), topic: uri.topic) + + try! await walletPairingClient.pair(uri: uri) + walletAuthClient.authRequestPublisher.sink { _ in requestExpectation.fulfill() }.store(in: &publishers) - wait(for: [requestExpectation], timeout: 2) + wait(for: [requestExpectation], timeout: InputConfig.defaultTimeout) + } + + func testEIP191RespondSuccess() async { + let responseExpectation = expectation(description: "successful response delivered") + let uri = try! await appPairingClient.create() + try! await appAuthClient.request(RequestParams.stub(), topic: uri.topic) + + try! await walletPairingClient.pair(uri: uri) + walletAuthClient.authRequestPublisher.sink { [unowned self] request in + Task(priority: .high) { + let signer = MessageSignerFactory.create(projectId: InputConfig.projectId) + let signature = try! signer.sign(message: request.message, privateKey: prvKey, type: .eip191) + try! await walletAuthClient.respond(requestId: request.id, signature: signature) + } + } + .store(in: &publishers) + appAuthClient.authResponsePublisher.sink { (_, result) in + guard case .success = result else { XCTFail(); return } + responseExpectation.fulfill() + } + .store(in: &publishers) + wait(for: [responseExpectation], timeout: InputConfig.defaultTimeout) } - func testRespondSuccess() async { + func testEIP1271RespondSuccess() async { + setupClients(address: "0x2faf83c542b68f1b4cdc0e770e8cb9f567b08f71", iatProvider: IATProviderMock()) + let responseExpectation = expectation(description: "successful response delivered") - let uri = try! await app.request(RequestParams.stub()) - try! await wallet.pair(uri: uri) - wallet.authRequestPublisher.sink { [unowned self] request in + let uri = try! await appPairingClient.create() + try! await appAuthClient.request(RequestParams( + domain: "localhost", + chainId: "eip155:1", + nonce: "1665443015700", + aud: "http://localhost:3000/", + nbf: nil, + exp: "2022-10-11T23:03:35.700Z", + statement: nil, + requestId: nil, + resources: nil + ), topic: uri.topic) + + try! await walletPairingClient.pair(uri: uri) + walletAuthClient.authRequestPublisher.sink { [unowned self] request in Task(priority: .high) { - let signature = try! MessageSigner().sign(message: request.message, privateKey: prvKey) - try! await wallet.respond(requestId: request.id, signature: signature) + let signature = CacaoSignature(t: .eip1271, s: eip1271Signature) + try! await walletAuthClient.respond(requestId: request.id, signature: signature) } } .store(in: &publishers) - app.authResponsePublisher.sink { (_, result) in + appAuthClient.authResponsePublisher.sink { (_, result) in guard case .success = result else { XCTFail(); return } responseExpectation.fulfill() } .store(in: &publishers) - wait(for: [responseExpectation], timeout: 5) + wait(for: [responseExpectation], timeout: InputConfig.defaultTimeout) + } + + func testEIP191RespondError() async { + let responseExpectation = expectation(description: "error response delivered") + let uri = try! await appPairingClient.create() + try! await appAuthClient.request(RequestParams.stub(), topic: uri.topic) + + try! await walletPairingClient.pair(uri: uri) + walletAuthClient.authRequestPublisher.sink { [unowned self] request in + Task(priority: .high) { + let signature = CacaoSignature(t: .eip1271, s: eip1271Signature) + try! await walletAuthClient.respond(requestId: request.id, signature: signature) + } + } + .store(in: &publishers) + appAuthClient.authResponsePublisher.sink { (_, result) in + guard case let .failure(error) = result, error == .signatureVerificationFailed else { XCTFail(); return } + responseExpectation.fulfill() + } + .store(in: &publishers) + wait(for: [responseExpectation], timeout: InputConfig.defaultTimeout) } func testUserRespondError() async { let responseExpectation = expectation(description: "error response delivered") - let uri = try! await app.request(RequestParams.stub()) - try! await wallet.pair(uri: uri) - wallet.authRequestPublisher.sink { [unowned self] request in + let uri = try! await appPairingClient.create() + try! await appAuthClient.request(RequestParams.stub(), topic: uri.topic) + + try! await walletPairingClient.pair(uri: uri) + walletAuthClient.authRequestPublisher.sink { [unowned self] request in Task(priority: .high) { - try! await wallet.reject(requestId: request.id) + try! await walletAuthClient.reject(requestId: request.id) } } .store(in: &publishers) - app.authResponsePublisher.sink { (_, result) in + appAuthClient.authResponsePublisher.sink { (_, result) in guard case .failure(let error) = result else { XCTFail(); return } XCTAssertEqual(error, .userRejeted) responseExpectation.fulfill() } .store(in: &publishers) - wait(for: [responseExpectation], timeout: 5) + wait(for: [responseExpectation], timeout: InputConfig.defaultTimeout) } func testRespondSignatureVerificationFailed() async { let responseExpectation = expectation(description: "invalid signature response delivered") - let uri = try! await app.request(RequestParams.stub()) - try! await wallet.pair(uri: uri) - wallet.authRequestPublisher.sink { [unowned self] request in + let uri = try! await appPairingClient.create() + try! await appAuthClient.request(RequestParams.stub(), topic: uri.topic) + + try! await walletPairingClient.pair(uri: uri) + walletAuthClient.authRequestPublisher.sink { [unowned self] request in Task(priority: .high) { let invalidSignature = "438effc459956b57fcd9f3dac6c675f9cee88abf21acab7305e8e32aa0303a883b06dcbd956279a7a2ca21ffa882ff55cc22e8ab8ec0f3fe90ab45f306938cfa1b" - let cacaoSignature = CacaoSignature(t: "eip191", s: invalidSignature) - try! await wallet.respond(requestId: request.id, signature: cacaoSignature) + let cacaoSignature = CacaoSignature(t: .eip191, s: invalidSignature) + try! await walletAuthClient.respond(requestId: request.id, signature: cacaoSignature) } } .store(in: &publishers) - app.authResponsePublisher.sink { (_, result) in + appAuthClient.authResponsePublisher.sink { (_, result) in guard case .failure(let error) = result else { XCTFail(); return } XCTAssertEqual(error, .signatureVerificationFailed) responseExpectation.fulfill() } .store(in: &publishers) - wait(for: [responseExpectation], timeout: 2) + wait(for: [responseExpectation], timeout: InputConfig.defaultTimeout) } +} - func testPing() async { - let pingExpectation = expectation(description: "expects ping response") - let uri = try! await app.request(RequestParams.stub()) - try! await wallet.pair(uri: uri) - try! await wallet.ping(topic: uri.topic) - wallet.pingResponsePublisher - .sink { topic in - XCTAssertEqual(topic, uri.topic) - pingExpectation.fulfill() - } - .store(in: &publishers) - wait(for: [pingExpectation], timeout: 5) +private struct IATProviderMock: IATProvider { + var iat: String { + return "2022-10-10T23:03:35.700Z" } } diff --git a/Example/IntegrationTests/Chat/ChatTests.swift b/Example/IntegrationTests/Chat/ChatTests.swift index 191ea398f..177bda36f 100644 --- a/Example/IntegrationTests/Chat/ChatTests.swift +++ b/Example/IntegrationTests/Chat/ChatTests.swift @@ -16,34 +16,12 @@ final class ChatTests: XCTestCase { registry = KeyValueRegistry() invitee = makeClient(prefix: "🦖 Registered") inviter = makeClient(prefix: "🍄 Inviter") - - waitClientsConnected() - } - - private func waitClientsConnected() { - let expectation = expectation(description: "Wait Clients Connected") - expectation.expectedFulfillmentCount = 2 - - invitee.socketConnectionStatusPublisher.sink { status in - if status == .connected { - expectation.fulfill() - } - }.store(in: &publishers) - - inviter.socketConnectionStatusPublisher.sink { status in - if status == .connected { - expectation.fulfill() - } - }.store(in: &publishers) - - wait(for: [expectation], timeout: 5) } func makeClient(prefix: String) -> ChatClient { let logger = ConsoleLogger(suffix: prefix, loggingLevel: .debug) - let projectId = "3ca2919724fbfa5456a25194e369a8b4" let keychain = KeychainStorageMock() - let relayClient = RelayClient(relayHost: URLConfig.relayHost, projectId: projectId, keychainStorage: keychain, socketFactory: SocketFactory(), logger: logger) + let relayClient = RelayClient(relayHost: InputConfig.relayHost, projectId: InputConfig.projectId, keychainStorage: keychain, socketFactory: SocketFactory(), logger: logger) return ChatClientFactory.create(registry: registry, relayClient: relayClient, kms: KeyManagementService(keychain: keychain), logger: logger, keyValueStorage: RuntimeKeyValueStorage()) } @@ -56,7 +34,7 @@ final class ChatTests: XCTestCase { invitee.invitePublisher.sink { _ in inviteExpectation.fulfill() }.store(in: &publishers) - wait(for: [inviteExpectation], timeout: 4) + wait(for: [inviteExpectation], timeout: InputConfig.defaultTimeout) } func testAcceptAndCreateNewThread() { @@ -83,7 +61,7 @@ final class ChatTests: XCTestCase { newThreadInviterExpectation.fulfill() }.store(in: &publishers) - wait(for: [newThreadinviteeExpectation, newThreadInviterExpectation], timeout: 10) + wait(for: [newThreadinviteeExpectation, newThreadInviterExpectation], timeout: InputConfig.defaultTimeout) } func testMessage() { @@ -118,6 +96,6 @@ final class ChatTests: XCTestCase { messageExpectation.fulfill() }.store(in: &publishers) - wait(for: [messageExpectation], timeout: 10) + wait(for: [messageExpectation], timeout: InputConfig.defaultTimeout) } } diff --git a/Example/IntegrationTests/Chat/RegistryTests.swift b/Example/IntegrationTests/Chat/RegistryTests.swift index 7a6fb6a1c..eb98d22f2 100644 --- a/Example/IntegrationTests/Chat/RegistryTests.swift +++ b/Example/IntegrationTests/Chat/RegistryTests.swift @@ -1,5 +1,5 @@ import XCTest -import WalletConnectRelay +import WalletConnectNetworking import WalletConnectKMS import WalletConnectUtils @testable import Chat @@ -7,7 +7,7 @@ import WalletConnectUtils final class RegistryTests: XCTestCase { func testRegistry() async throws { - let client = HTTPClient(host: "keys.walletconnect.com") + let client = HTTPNetworkClient(host: "keys.walletconnect.com") let registry = KeyserverRegistryProvider(client: client) let account = Account("eip155:1:" + Data.randomBytes(count: 16).toHexString())! let pubKey = SigningPrivateKey().publicKey.hexRepresentation diff --git a/Example/IntegrationTests/Pairing/PairingTests.swift b/Example/IntegrationTests/Pairing/PairingTests.swift new file mode 100644 index 000000000..dce08c134 --- /dev/null +++ b/Example/IntegrationTests/Pairing/PairingTests.swift @@ -0,0 +1,149 @@ +import Foundation +import XCTest +import WalletConnectUtils +@testable import WalletConnectKMS +import WalletConnectRelay +import Combine +import WalletConnectNetworking +import WalletConnectPush +@testable import WalletConnectPairing + +final class PairingTests: XCTestCase { + + var appPairingClient: PairingClient! + var walletPairingClient: PairingClient! + + var appPushClient: PushClient! + var walletPushClient: PushClient! + + var pairingStorage: PairingStorage! + + private var publishers = [AnyCancellable]() + + func makeClients(prefix: String) -> (PairingClient, PushClient) { + 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( + relayHost: InputConfig.relayHost, + projectId: InputConfig.projectId, + keyValueStorage: RuntimeKeyValueStorage(), + keychainStorage: keychain, + socketFactory: SocketFactory(), + 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) + + let pushClient = PushClientFactory.create( + logger: pushLogger, + keyValueStorage: keyValueStorage, + keychainStorage: keychain, + networkingClient: networkingClient, + pairingClient: pairingClient) + + return (pairingClient, pushClient) + } + + 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: SocketFactory(), + logger: logger) + + let networkingClient = NetworkingClientFactory.create( + relayClient: relayClient, + logger: logger, + keychainStorage: keychain, + keyValueStorage: keyValueStorage) + + let pairingClient = PairingClientFactory.create( + logger: logger, + keyValueStorage: keyValueStorage, + keychainStorage: keychain, + networkingClient: networkingClient) + + return pairingClient + } + + func testProposePushOnPairing() async { + let expectation = expectation(description: "propose push on pairing") + + (appPairingClient, appPushClient) = makeClients(prefix: "🤖 App") + (walletPairingClient, walletPushClient) = makeClients(prefix: "🐶 Wallet") + + walletPushClient.proposalPublisher.sink { _ in + expectation.fulfill() + }.store(in: &publishers) + + let uri = try! await appPairingClient.create() + + try! await walletPairingClient.pair(uri: uri) + + try! await appPushClient.propose(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") + + let uri = try! await appPairingClient.create() + try! await walletPairingClient.pair(uri: uri) + try! await walletPairingClient.ping(topic: uri.topic) + walletPairingClient.pingResponsePublisher + .sink { topic in + XCTAssertEqual(topic, uri.topic) + expectation.fulfill() + }.store(in: &publishers) + wait(for: [expectation], timeout: InputConfig.defaultTimeout) + } + + func testResponseErrorForMethodUnregistered() async { + (appPairingClient, appPushClient) = makeClients(prefix: "🤖 App") + walletPairingClient = makePairingClient(prefix: "🐶 Wallet") + + let expectation = expectation(description: "wallet responds unsupported method for unregistered method") + + appPushClient.responsePublisher.sink { (_, response) in + XCTAssertEqual(response, .failure(WalletConnectPairing.PairError(code: 10001)!)) + expectation.fulfill() + }.store(in: &publishers) + + let uri = try! await appPairingClient.create() + + try! await walletPairingClient.pair(uri: uri) + + try! await appPushClient.propose(topic: uri.topic) + + wait(for: [expectation], timeout: InputConfig.defaultTimeout) + + } + + func testDisconnect() { + // TODO + } +} diff --git a/Example/IntegrationTests/Relay/RelayClientEndToEndTests.swift b/Example/IntegrationTests/Relay/RelayClientEndToEndTests.swift index d456d69dc..60ab35252 100644 --- a/Example/IntegrationTests/Relay/RelayClientEndToEndTests.swift +++ b/Example/IntegrationTests/Relay/RelayClientEndToEndTests.swift @@ -7,24 +7,22 @@ import Starscream final class RelayClientEndToEndTests: XCTestCase { - let defaultTimeout: TimeInterval = 10 - - let projectId = "3ca2919724fbfa5456a25194e369a8b4" private var publishers = Set() func makeRelayClient() -> RelayClient { - let clientIdStorage = ClientIdStorage(keychain: KeychainStorageMock()) + let didKeyFactory = ED25519DIDKeyFactory() + let clientIdStorage = ClientIdStorage(keychain: KeychainStorageMock(), didKeyFactory: didKeyFactory) let socketAuthenticator = SocketAuthenticator( clientIdStorage: clientIdStorage, - didKeyFactory: ED25519DIDKeyFactory(), - relayHost: URLConfig.relayHost + didKeyFactory: didKeyFactory, + relayHost: InputConfig.relayHost ) let urlFactory = RelayUrlFactory(socketAuthenticator: socketAuthenticator) - let socket = WebSocket(url: urlFactory.create(host: URLConfig.relayHost, projectId: projectId)) + let socket = WebSocket(url: urlFactory.create(host: InputConfig.relayHost, projectId: InputConfig.projectId)) let logger = ConsoleLogger() let dispatcher = Dispatcher(socket: socket, socketConnectionHandler: ManualSocketConnectionHandler(socket: socket), logger: logger) - return RelayClient(dispatcher: dispatcher, logger: logger, keyValueStorage: RuntimeKeyValueStorage()) + return RelayClient(dispatcher: dispatcher, logger: logger, keyValueStorage: RuntimeKeyValueStorage(), clientIdStorage: clientIdStorage) } func testSubscribe() { @@ -42,7 +40,7 @@ final class RelayClientEndToEndTests: XCTestCase { } }.store(in: &publishers) - wait(for: [subscribeExpectation], timeout: defaultTimeout) + wait(for: [subscribeExpectation], timeout: InputConfig.defaultTimeout) } func testEndToEndPayload() { @@ -77,7 +75,7 @@ final class RelayClientEndToEndTests: XCTestCase { }.store(in: &publishers) relayA.socketConnectionStatusPublisher.sink { _ in - relayA.publish(topic: randomTopic, payload: payloadA, tag: 0, onNetworkAcknowledge: { error in + relayA.publish(topic: randomTopic, payload: payloadA, tag: 0, prompt: false, ttl: 60, onNetworkAcknowledge: { error in XCTAssertNil(error) }) relayA.subscribe(topic: randomTopic) { error in @@ -85,7 +83,7 @@ final class RelayClientEndToEndTests: XCTestCase { } }.store(in: &publishers) relayB.socketConnectionStatusPublisher.sink { _ in - relayB.publish(topic: randomTopic, payload: payloadB, tag: 0, onNetworkAcknowledge: { error in + relayB.publish(topic: randomTopic, payload: payloadB, tag: 0, prompt: false, ttl: 60, onNetworkAcknowledge: { error in XCTAssertNil(error) }) relayB.subscribe(topic: randomTopic) { error in @@ -93,7 +91,7 @@ final class RelayClientEndToEndTests: XCTestCase { } }.store(in: &publishers) - wait(for: [expectationA, expectationB], timeout: defaultTimeout) + wait(for: [expectationA, expectationB], timeout: InputConfig.defaultTimeout) XCTAssertEqual(subscriptionATopic, randomTopic) XCTAssertEqual(subscriptionBTopic, randomTopic) diff --git a/Example/IntegrationTests/Sign/Helpers/ClientDelegate.swift b/Example/IntegrationTests/Sign/Helpers/ClientDelegate.swift index 71ac99ab3..e299d0316 100644 --- a/Example/IntegrationTests/Sign/Helpers/ClientDelegate.swift +++ b/Example/IntegrationTests/Sign/Helpers/ClientDelegate.swift @@ -14,6 +14,7 @@ class ClientDelegate { var onSessionDelete: (() -> Void)? var onSessionUpdateNamespaces: ((String, [String: SessionNamespace]) -> Void)? var onSessionExtend: ((String, Date) -> Void)? + var onPing: ((String) -> Void)? var onEventReceived: ((Session.Event, String) -> Void)? private var publishers = Set() @@ -63,5 +64,9 @@ class ClientDelegate { client.sessionExtendPublisher.sink { (topic, date) in self.onSessionExtend?(topic, date) }.store(in: &publishers) + + client.pingResponsePublisher.sink { topic in + self.onPing?(topic) + }.store(in: &publishers) } } diff --git a/Example/IntegrationTests/Sign/SignClientTests.swift b/Example/IntegrationTests/Sign/SignClientTests.swift index e00529b0b..acbfbb728 100644 --- a/Example/IntegrationTests/Sign/SignClientTests.swift +++ b/Example/IntegrationTests/Sign/SignClientTests.swift @@ -1,59 +1,56 @@ import XCTest import WalletConnectUtils +import JSONRPC @testable import WalletConnectKMS @testable import WalletConnectSign @testable import WalletConnectRelay +import WalletConnectPairing +import WalletConnectNetworking final class SignClientTests: XCTestCase { - - let defaultTimeout: TimeInterval = 8 - var dapp: ClientDelegate! var wallet: ClientDelegate! - static private func makeClientDelegate( - name: String, - projectId: String = "3ca2919724fbfa5456a25194e369a8b4" - ) -> ClientDelegate { + static private func makeClientDelegate(name: String) -> ClientDelegate { let logger = ConsoleLogger(suffix: name, loggingLevel: .debug) let keychain = KeychainStorageMock() let relayClient = RelayClient( - relayHost: URLConfig.relayHost, - projectId: projectId, + relayHost: InputConfig.relayHost, + projectId: InputConfig.projectId, keyValueStorage: RuntimeKeyValueStorage(), keychainStorage: keychain, socketFactory: SocketFactory(), socketConnectionType: .automatic, logger: logger ) + let keyValueStorage = RuntimeKeyValueStorage() + + let networkingClient = NetworkingClientFactory.create( + relayClient: relayClient, + logger: logger, + keychainStorage: keychain, + keyValueStorage: keyValueStorage + ) + let pairingClient = PairingClientFactory.create( + logger: logger, + keyValueStorage: keyValueStorage, + keychainStorage: keychain, + networkingClient: networkingClient + ) let client = SignClientFactory.create( metadata: AppMetadata(name: name, description: "", url: "", icons: [""]), logger: logger, - keyValueStorage: RuntimeKeyValueStorage(), + keyValueStorage: keyValueStorage, keychainStorage: keychain, - relayClient: relayClient + pairingClient: pairingClient, + networkingClient: networkingClient ) return ClientDelegate(client: client) } - private func listenForConnection() async { - let group = DispatchGroup() - group.enter() - dapp.onConnected = { - group.leave() - } - group.enter() - wallet.onConnected = { - group.leave() - } - group.wait() - return - } - override func setUp() async throws { dapp = Self.makeClientDelegate(name: "🍏P") wallet = Self.makeClientDelegate(name: "🍎R") - await listenForConnection() } override func tearDown() { @@ -85,7 +82,7 @@ final class SignClientTests: XCTestCase { let uri = try await dapp.client.connect(requiredNamespaces: requiredNamespaces) try await wallet.client.pair(uri: uri!) - wait(for: [dappSettlementExpectation, walletSettlementExpectation], timeout: defaultTimeout) + wait(for: [dappSettlementExpectation, walletSettlementExpectation], timeout: InputConfig.defaultTimeout) } func testSessionReject() async throws { @@ -109,7 +106,7 @@ final class SignClientTests: XCTestCase { XCTAssertEqual(store.rejectedProposal, proposal) sessionRejectExpectation.fulfill() // TODO: Assert reason code } - wait(for: [sessionRejectExpectation], timeout: defaultTimeout) + wait(for: [sessionRejectExpectation], timeout: InputConfig.defaultTimeout) } func testSessionDelete() async throws { @@ -133,21 +130,37 @@ final class SignClientTests: XCTestCase { let uri = try await dapp.client.connect(requiredNamespaces: requiredNamespaces) try await wallet.client.pair(uri: uri!) - wait(for: [sessionDeleteExpectation], timeout: defaultTimeout) + wait(for: [sessionDeleteExpectation], timeout: InputConfig.defaultTimeout) } - func testNewPairingPing() async throws { - let pongResponseExpectation = expectation(description: "Ping sender receives a pong response") + func testSessionPing() async throws { + let expectation = expectation(description: "Proposer receives ping response") - let uri = try await dapp.client.connect(requiredNamespaces: ProposalNamespace.stubRequired())! - try await wallet.client.pair(uri: uri) + let requiredNamespaces = ProposalNamespace.stubRequired() + let sessionNamespaces = SessionNamespace.make(toRespond: requiredNamespaces) + + wallet.onSessionProposal = { proposal in + Task(priority: .high) { + try! await self.wallet.client.approve(proposalId: proposal.id, namespaces: sessionNamespaces) + } + } + + dapp.onSessionSettled = { sessionSettled in + Task(priority: .high) { + try! await self.dapp.client.ping(topic: sessionSettled.topic) + } + } - let pairing = wallet.client.getPairings().first! - wallet.client.ping(topic: pairing.topic) { result in - if case .failure = result { XCTFail() } - pongResponseExpectation.fulfill() + dapp.onPing = { topic in + let session = self.wallet.client.getSessions().first! + XCTAssertEqual(topic, session.topic) + expectation.fulfill() } - wait(for: [pongResponseExpectation], timeout: defaultTimeout) + + let uri = try await dapp.client.connect(requiredNamespaces: requiredNamespaces)! + try await wallet.client.pair(uri: uri) + + wait(for: [expectation], timeout: .infinity) } func testSessionRequest() async throws { @@ -171,7 +184,7 @@ final class SignClientTests: XCTestCase { } dapp.onSessionSettled = { [unowned self] settledSession in Task(priority: .high) { - let request = Request(id: 0, topic: settledSession.topic, method: requestMethod, params: requestParams, chainId: chain) + let request = Request(id: RPCID(0), topic: settledSession.topic, method: requestMethod, params: requestParams, chainId: chain) try await dapp.client.request(params: request) } } @@ -181,14 +194,13 @@ final class SignClientTests: XCTestCase { XCTAssertEqual(sessionRequest.method, requestMethod) requestExpectation.fulfill() Task(priority: .high) { - let jsonrpcResponse = JSONRPCResponse(id: sessionRequest.id, result: AnyCodable(responseParams)) - try await wallet.client.respond(topic: sessionRequest.topic, response: .response(jsonrpcResponse)) + try await wallet.client.respond(topic: sessionRequest.topic, requestId: sessionRequest.id, response: .response(AnyCodable(responseParams))) } } dapp.onSessionResponse = { response in switch response.result { case .response(let response): - XCTAssertEqual(try! response.result.get(String.self), responseParams) + XCTAssertEqual(try! response.get(String.self), responseParams) case .error: XCTFail() } @@ -197,7 +209,7 @@ final class SignClientTests: XCTestCase { let uri = try await dapp.client.connect(requiredNamespaces: requiredNamespaces) try await wallet.client.pair(uri: uri!) - wait(for: [requestExpectation, responseExpectation], timeout: defaultTimeout) + wait(for: [requestExpectation, responseExpectation], timeout: InputConfig.defaultTimeout) } func testSessionRequestFailureResponse() async throws { @@ -207,7 +219,8 @@ final class SignClientTests: XCTestCase { let requestMethod = "eth_sendTransaction" let requestParams = [EthSendTransaction.stub()] - let error = JSONRPCErrorResponse.Error(code: 0, message: "error") + let error = JSONRPCError(code: 0, message: "error") + let chain = Blockchain("eip155:1")! wallet.onSessionProposal = { [unowned self] proposal in @@ -217,32 +230,30 @@ final class SignClientTests: XCTestCase { } dapp.onSessionSettled = { [unowned self] settledSession in Task(priority: .high) { - let request = Request(id: 0, topic: settledSession.topic, method: requestMethod, params: requestParams, chainId: chain) + let request = Request(id: RPCID(0), topic: settledSession.topic, method: requestMethod, params: requestParams, chainId: chain) try await dapp.client.request(params: request) } } wallet.onSessionRequest = { [unowned self] sessionRequest in Task(priority: .high) { - let response = JSONRPCErrorResponse(id: sessionRequest.id, error: error) - try await wallet.client.respond(topic: sessionRequest.topic, response: .error(response)) + try await wallet.client.respond(topic: sessionRequest.topic, requestId: sessionRequest.id, response: .error(error)) } } dapp.onSessionResponse = { response in switch response.result { case .response: XCTFail() - case .error(let errorResponse): - XCTAssertEqual(error, errorResponse.error) + case .error(let receivedError): + XCTAssertEqual(error, receivedError) } expectation.fulfill() } let uri = try await dapp.client.connect(requiredNamespaces: requiredNamespaces) try await wallet.client.pair(uri: uri!) - wait(for: [expectation], timeout: defaultTimeout) + wait(for: [expectation], timeout: InputConfig.defaultTimeout) } - func testNewSessionOnExistingPairing() async { let dappSettlementExpectation = expectation(description: "Dapp settles session") dappSettlementExpectation.expectedFulfillmentCount = 2 @@ -266,7 +277,7 @@ final class SignClientTests: XCTestCase { let pairingTopic = dapp.client.getPairings().first!.topic if !initiatedSecondSession { Task(priority: .high) { - let _ = try! await dapp.client.connect(requiredNamespaces: requiredNamespaces, topic: pairingTopic) + _ = try! await dapp.client.connect(requiredNamespaces: requiredNamespaces, topic: pairingTopic) } initiatedSecondSession = true } @@ -277,31 +288,9 @@ final class SignClientTests: XCTestCase { let uri = try! await dapp.client.connect(requiredNamespaces: requiredNamespaces) try! await wallet.client.pair(uri: uri!) - wait(for: [dappSettlementExpectation, walletSettlementExpectation], timeout: defaultTimeout) - + wait(for: [dappSettlementExpectation, walletSettlementExpectation], timeout: InputConfig.defaultTimeout) } - func testSessionPing() async { - let expectation = expectation(description: "Dapp receives ping response") - let requiredNamespaces = ProposalNamespace.stubRequired() - let sessionNamespaces = SessionNamespace.make(toRespond: requiredNamespaces) - - wallet.onSessionProposal = { [unowned self] proposal in - Task(priority: .high) { - try await wallet.client.approve(proposalId: proposal.id, namespaces: sessionNamespaces) - } - } - dapp.onSessionSettled = { [unowned self] settledSession in - dapp.client.ping(topic: settledSession.topic) {_ in - expectation.fulfill() - } - } - let uri = try! await dapp.client.connect(requiredNamespaces: requiredNamespaces) - try! await wallet.client.pair(uri: uri!) - wait(for: [expectation], timeout: defaultTimeout) - } - - func testSuccessfulSessionUpdateNamespaces() async { let expectation = expectation(description: "Dapp updates namespaces") let requiredNamespaces = ProposalNamespace.stubRequired() @@ -322,10 +311,9 @@ final class SignClientTests: XCTestCase { } let uri = try! await dapp.client.connect(requiredNamespaces: requiredNamespaces) try! await wallet.client.pair(uri: uri!) - wait(for: [expectation], timeout: defaultTimeout) + wait(for: [expectation], timeout: InputConfig.defaultTimeout) } - func testSuccessfulSessionExtend() async { let expectation = expectation(description: "Dapp extends session") @@ -351,7 +339,7 @@ final class SignClientTests: XCTestCase { let uri = try! await dapp.client.connect(requiredNamespaces: requiredNamespaces) try! await wallet.client.pair(uri: uri!) - wait(for: [expectation], timeout: defaultTimeout) + wait(for: [expectation], timeout: InputConfig.defaultTimeout) } func testSessionEventSucceeds() async { @@ -381,7 +369,7 @@ final class SignClientTests: XCTestCase { let uri = try! await dapp.client.connect(requiredNamespaces: requiredNamespaces) try! await wallet.client.pair(uri: uri!) - wait(for: [expectation], timeout: defaultTimeout) + wait(for: [expectation], timeout: InputConfig.defaultTimeout) } func testSessionEventFails() async { @@ -408,6 +396,6 @@ final class SignClientTests: XCTestCase { let uri = try! await dapp.client.connect(requiredNamespaces: requiredNamespaces) try! await wallet.client.pair(uri: uri!) - wait(for: [expectation], timeout: defaultTimeout) + wait(for: [expectation], timeout: InputConfig.defaultTimeout) } } diff --git a/Example/IntegrationTests/Stubs/InputConfig.swift b/Example/IntegrationTests/Stubs/InputConfig.swift new file mode 100644 index 000000000..0e0c2efb7 --- /dev/null +++ b/Example/IntegrationTests/Stubs/InputConfig.swift @@ -0,0 +1,20 @@ +import Foundation + +struct InputConfig { + + static var relayHost: String { + return config(for: "RELAY_HOST")! + } + + static var projectId: String { + return config(for: "PROJECT_ID")! + } + + static var defaultTimeout: TimeInterval { + return 30 + } + + private static func config(for key: String) -> String? { + return ProcessInfo.processInfo.environment[key] + } +} diff --git a/Example/IntegrationTests/Stubs/URLConfig.swift b/Example/IntegrationTests/Stubs/URLConfig.swift deleted file mode 100644 index 8fed455dc..000000000 --- a/Example/IntegrationTests/Stubs/URLConfig.swift +++ /dev/null @@ -1,8 +0,0 @@ -import Foundation - -struct URLConfig { - - static var relayHost: String { - return ProcessInfo.processInfo.environment["RELAY_HOST"]! - } -} diff --git a/Example/Showcase/Classes/ApplicationLayer/Configurator/ThirdPartyConfigurator.swift b/Example/Showcase/Classes/ApplicationLayer/Configurator/ThirdPartyConfigurator.swift index 8aa2f8457..a8936517a 100644 --- a/Example/Showcase/Classes/ApplicationLayer/Configurator/ThirdPartyConfigurator.swift +++ b/Example/Showcase/Classes/ApplicationLayer/Configurator/ThirdPartyConfigurator.swift @@ -1,19 +1,20 @@ -import WalletConnectRelay +import WalletConnectNetworking import WalletConnectPairing import Auth struct ThirdPartyConfigurator: Configurator { func configure() { - Relay.configure(projectId: "relay.walletconnect.com", socketFactory: SocketFactory()) - - Auth.configure( + Networking.configure(projectId: InputConfig.projectId, socketFactory: SocketFactory()) + Pair.configure( metadata: AppMetadata( name: "Showcase App", description: "Showcase description", url: "example.wallet", icons: ["https://avatars.githubusercontent.com/u/37784886"] - ), + )) + + Auth.configure( account: Account("eip155:1:0xe5EeF1368781911d265fDB6946613dA61915a501")! ) } diff --git a/Example/Showcase/Classes/ApplicationLayer/SceneDelegate.swift b/Example/Showcase/Classes/ApplicationLayer/SceneDelegate.swift index 5956c494e..7681f528a 100644 --- a/Example/Showcase/Classes/ApplicationLayer/SceneDelegate.swift +++ b/Example/Showcase/Classes/ApplicationLayer/SceneDelegate.swift @@ -1,5 +1,6 @@ import UIKit import Auth +import WalletConnectPairing class SceneDelegate: UIResponder, UIWindowSceneDelegate { @@ -30,7 +31,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { let uri = context.url.absoluteString.replacingOccurrences(of: "showcase://wc?uri=", with: "") Task { - try await Auth.instance.pair(uri: WalletConnectURI(string: uri)!) + try await Pair.instance.pair(uri: WalletConnectURI(string: uri)!) } } } diff --git a/Example/Showcase/Classes/DomainLayer/Chat/ChatFactory.swift b/Example/Showcase/Classes/DomainLayer/Chat/ChatFactory.swift index b47b23b17..8b4e0c597 100644 --- a/Example/Showcase/Classes/DomainLayer/Chat/ChatFactory.swift +++ b/Example/Showcase/Classes/DomainLayer/Chat/ChatFactory.swift @@ -1,14 +1,15 @@ import Foundation import Chat -import WalletConnectKMS +import WalletConnectNetworking import WalletConnectRelay +import WalletConnectKMS import WalletConnectUtils class ChatFactory { static func create() -> ChatClient { let keychain = KeychainStorage(serviceIdentifier: "com.walletconnect.showcase") - let client = HTTPClient(host: "keys.walletconnect.com") + let client = HTTPNetworkClient(host: "keys.walletconnect.com") let registry = KeyserverRegistryProvider(client: client) return ChatClientFactory.create( registry: registry, diff --git a/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestInteractor.swift b/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestInteractor.swift index ed3702e8f..c0c3a8bd3 100644 --- a/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestInteractor.swift +++ b/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestInteractor.swift @@ -6,8 +6,8 @@ final class AuthRequestInteractor { func approve(request: AuthRequest) async throws { let privateKey = Data(hex: "e56da0e170b5e09a8bb8f1b693392c7d56c3739a9c75740fbc558a2877868540") - let signer = MessageSigner() - let signature = try signer.sign(message: request.message, privateKey: privateKey) + let signer = MessageSignerFactory.create() + let signature = try signer.sign(message: request.message, privateKey: privateKey, type: .eip191) try await Auth.instance.respond(requestId: request.id, signature: signature) } diff --git a/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletInteractor.swift b/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletInteractor.swift index 051ba266e..7379f18c5 100644 --- a/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletInteractor.swift +++ b/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletInteractor.swift @@ -1,10 +1,11 @@ import Combine import Auth +import WalletConnectPairing final class WalletInteractor { func pair(uri: WalletConnectURI) async throws { - try await Auth.instance.pair(uri: uri) + try await Pair.instance.pair(uri: uri) } var requestPublisher: AnyPublisher { diff --git a/Example/Showcase/Common/InputConfig.swift b/Example/Showcase/Common/InputConfig.swift new file mode 100644 index 000000000..53931721a --- /dev/null +++ b/Example/Showcase/Common/InputConfig.swift @@ -0,0 +1,12 @@ +import Foundation + +struct InputConfig { + + static var projectId: String { + return config(for: "PROJECT_ID")! + } + + private static func config(for key: String) -> String? { + return Bundle.main.object(forInfoDictionaryKey: key) as? String + } +} diff --git a/Example/Showcase/Other/Info.plist b/Example/Showcase/Other/Info.plist index 95d2824b0..2b842100f 100644 --- a/Example/Showcase/Other/Info.plist +++ b/Example/Showcase/Other/Info.plist @@ -2,6 +2,8 @@ + PROJECT_ID + $(PROJECT_ID) CFBundleIconName AppIcon CFBundleURLTypes diff --git a/Example/UITests/Engine/WalletEngine.swift b/Example/UITests/Engine/WalletEngine.swift index 536e45443..2255d87b8 100644 --- a/Example/UITests/Engine/WalletEngine.swift +++ b/Example/UITests/Engine/WalletEngine.swift @@ -45,7 +45,15 @@ struct WalletEngine { instance.buttons["Ping"] } + var okButton: XCUIElement { + instance.buttons["OK"] + } + var pingAlert: XCUIElement { instance.alerts.element.staticTexts["Received ping response"] } + + func swipeDismiss() { + instance.swipeDown(velocity: .fast) + } } diff --git a/Example/UITests/Regression/RegressionTests.swift b/Example/UITests/Regression/RegressionTests.swift index 716ba8702..6b5047e9c 100644 --- a/Example/UITests/Regression/RegressionTests.swift +++ b/Example/UITests/Regression/RegressionTests.swift @@ -4,13 +4,10 @@ class PairingTests: XCTestCase { private let engine: Engine = Engine() - private static var cleanLaunch: Bool = true - - override func setUp() { - engine.routing.launch(app: .dapp, clean: PairingTests.cleanLaunch) - engine.routing.launch(app: .wallet, clean: PairingTests.cleanLaunch) - - PairingTests.cleanLaunch = false + override class func setUp() { + let engine: Engine = Engine() + engine.routing.launch(app: .dapp, clean: true) + engine.routing.launch(app: .wallet, clean: true) } /// Check pairing proposal approval via QR code or uri @@ -45,6 +42,9 @@ class PairingTests: XCTestCase { engine.wallet.pingButton.waitTap() XCTAssertTrue(engine.wallet.pingAlert.waitExists()) + + engine.wallet.okButton.waitTap() + engine.wallet.swipeDismiss() } /// Approve session on existing pairing diff --git a/Package.swift b/Package.swift index 7640db6ad..bc8515e34 100644 --- a/Package.swift +++ b/Package.swift @@ -19,6 +19,12 @@ let package = Package( .library( name: "WalletConnectAuth", targets: ["Auth"]), + .library( + name: "WalletConnectPairing", + targets: ["WalletConnectPairing"]), + .library( + name: "WalletConnectPush", + targets: ["WalletConnectPush"]), .library( name: "WalletConnectRouter", targets: ["WalletConnectRouter"]), @@ -27,12 +33,12 @@ let package = Package( targets: ["WalletConnectNetworking"]) ], dependencies: [ - .package(url: "https://github.com/flypaper0/Web3.swift", .branch("feature/eip-155")) + .package(url: "https://github.com/WalletConnect/Web3.swift", .exact("1.0.0")) ], targets: [ .target( name: "WalletConnectSign", - dependencies: ["WalletConnectRelay", "WalletConnectUtils", "WalletConnectKMS", "WalletConnectPairing"], + dependencies: ["WalletConnectNetworking", "WalletConnectPairing"], path: "Sources/WalletConnectSign"), .target( name: "Chat", @@ -42,6 +48,10 @@ let package = Package( name: "Auth", dependencies: ["WalletConnectPairing", "WalletConnectNetworking", .product(name: "Web3", package: "Web3.swift")], path: "Sources/Auth"), + .target( + name: "WalletConnectPush", + dependencies: ["WalletConnectPairing", "WalletConnectNetworking"], + path: "Sources/WalletConnectPush"), .target( name: "WalletConnectRelay", dependencies: ["WalletConnectUtils", "WalletConnectKMS"], @@ -52,7 +62,7 @@ let package = Package( path: "Sources/WalletConnectKMS"), .target( name: "WalletConnectPairing", - dependencies: ["WalletConnectUtils", "WalletConnectNetworking", "JSONRPC"]), + dependencies: ["WalletConnectNetworking"]), .target( name: "WalletConnectUtils", dependencies: ["Commons", "JSONRPC"]), @@ -71,6 +81,9 @@ let package = Package( .testTarget( name: "WalletConnectSignTests", dependencies: ["WalletConnectSign", "TestingUtils"]), + .testTarget( + name: "WalletConnectPairingTests", + dependencies: ["WalletConnectPairing", "TestingUtils"]), .testTarget( name: "ChatTests", dependencies: ["Chat", "WalletConnectUtils", "TestingUtils"]), diff --git a/README.md b/README.md index d723213a6..32b7736dd 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,12 @@ dependencies: [ .package(url: "https://github.com/WalletConnect/WalletConnectSwiftV2", .branch("main")), ], ``` +## Setting Project ID +Follow instructions from *Configuration.xcconfig* and configure PROJECT_ID with your ID from WalletConnect Dashboard +``` +// Uncomment next line and paste your project id. Get this on: https://cloud.walletconnect.com/sign-in +// PROJECT_ID = YOUR_PROJECT_ID +``` ## Example App open `Example/ExampleApp.xcodeproj` diff --git a/Sources/Auth/Auth.swift b/Sources/Auth/Auth.swift index b209104bd..8687e05d9 100644 --- a/Sources/Auth/Auth.swift +++ b/Sources/Auth/Auth.swift @@ -1,5 +1,6 @@ import Foundation -import WalletConnectRelay +import WalletConnectNetworking +import WalletConnectPairing import Combine /// Auth instatnce wrapper @@ -17,26 +18,23 @@ public class Auth { /// Auth client instance public static var instance: AuthClient = { - guard let config = Auth.config else { - fatalError("Error - you must call Auth.configure(_:) before accessing the shared instance.") - } return AuthClientFactory.create( - metadata: config.metadata, - account: config.account, - relayClient: Relay.instance) + metadata: Pair.metadata, + account: config?.account, + projectId: Networking.projectId, + networkingClient: Networking.interactor, + pairingRegisterer: Pair.registerer + ) }() private static var config: Config? private init() { } - /// Auth instance config method + /// Auth instance wallet config method /// - Parameters: - /// - metadata: App metadata - /// - account: account that wallet will be authenticating with. Should be nil for non wallet clients. - static public func configure(metadata: AppMetadata, account: Account?) { - Auth.config = Auth.Config( - metadata: metadata, - account: account) + /// - account: account that wallet will be authenticating with. + static public func configure(account: Account) { + Auth.config = Auth.Config(account: account) } } diff --git a/Sources/Auth/AuthClient.swift b/Sources/Auth/AuthClient.swift index c75a2895e..f4080be27 100644 --- a/Sources/Auth/AuthClient.swift +++ b/Sources/Auth/AuthClient.swift @@ -11,9 +11,7 @@ import WalletConnectRelay /// Access via `Auth.instance` public class AuthClient { enum Errors: Error { - case pairingUriWrongApiParam case unknownWalletAddress - case noPairingMatchingTopic } // MARK: - Public Properties @@ -34,10 +32,6 @@ public class AuthClient { authResponsePublisherSubject.eraseToAnyPublisher() } - public var pingResponsePublisher: AnyPublisher<(String), Never> { - pingResponsePublisherSubject.eraseToAnyPublisher() - } - /// Publisher that sends web socket connection status public let socketConnectionStatusPublisher: AnyPublisher @@ -46,90 +40,45 @@ public class AuthClient { // MARK: - Private Properties + private let pairingRegisterer: PairingRegisterer + private var authResponsePublisherSubject = PassthroughSubject<(id: RPCID, result: Result), Never>() private var authRequestPublisherSubject = PassthroughSubject() - private var pingResponsePublisherSubject = PassthroughSubject() - private let appPairService: AppPairService private let appRequestService: AppRequestService - private let deletePairingService: DeletePairingService private let appRespondSubscriber: AppRespondSubscriber - private let walletPairService: WalletPairService private let walletRequestSubscriber: WalletRequestSubscriber private let walletRespondService: WalletRespondService - private let cleanupService: CleanupService - private let pairingStorage: WCPairingStorage private let pendingRequestsProvider: PendingRequestsProvider - private let pingService: PairingPingService - private let pairingsProvider: PairingsProvider private var account: Account? - init(appPairService: AppPairService, - appRequestService: AppRequestService, + init(appRequestService: AppRequestService, appRespondSubscriber: AppRespondSubscriber, - walletPairService: WalletPairService, walletRequestSubscriber: WalletRequestSubscriber, walletRespondService: WalletRespondService, - deletePairingService: DeletePairingService, account: Account?, pendingRequestsProvider: PendingRequestsProvider, - cleanupService: CleanupService, logger: ConsoleLogging, - pairingStorage: WCPairingStorage, socketConnectionStatusPublisher: AnyPublisher, - pingService: PairingPingService, - pairingsProvider: PairingsProvider + pairingRegisterer: PairingRegisterer ) { - self.appPairService = appPairService self.appRequestService = appRequestService - self.walletPairService = walletPairService self.walletRequestSubscriber = walletRequestSubscriber self.walletRespondService = walletRespondService self.appRespondSubscriber = appRespondSubscriber self.account = account self.pendingRequestsProvider = pendingRequestsProvider - self.cleanupService = cleanupService self.logger = logger - self.pairingStorage = pairingStorage self.socketConnectionStatusPublisher = socketConnectionStatusPublisher - self.deletePairingService = deletePairingService - self.pingService = pingService - self.pairingsProvider = pairingsProvider + self.pairingRegisterer = pairingRegisterer setUpPublishers() } - /// For wallet to establish a pairing and receive an authentication request - /// Wallet should call this function in order to accept peer's pairing proposal and be able to subscribe for future authentication request. - /// - Parameter uri: Pairing URI that is commonly presented as a QR code by a dapp or delivered with universal linking. - /// - /// Throws Error: - /// - When URI is invalid format or missing params - /// - When topic is already in use - public func pair(uri: WalletConnectURI) async throws { - guard uri.api == .auth else { - throw Errors.pairingUriWrongApiParam - } - try await walletPairService.pair(uri) - } - - /// For a dapp to send an authentication request to a wallet - /// - Parameter params: Set of parameters required to request authentication - /// - /// - Returns: Pairing URI that should be shared with wallet out of bound. Common way is to present it as a QR code. - public func request(_ params: RequestParams) async throws -> WalletConnectURI { - logger.debug("Requesting Authentication") - let uri = try await appPairService.create() - try await appRequestService.request(params: params, topic: uri.topic) - return uri - } - /// For a dapp to send an authentication request to a wallet /// - Parameter params: Set of parameters required to request authentication /// - Parameter topic: Pairing topic that wallet already subscribes for public func request(_ params: RequestParams, topic: String) async throws { logger.debug("Requesting Authentication on existing pairing") - guard pairingStorage.hasPairing(forTopic: topic) else { - throw Errors.noPairingMatchingTopic - } + try pairingRegisterer.validatePairingExistance(topic) try await appRequestService.request(params: params, topic: topic) } @@ -148,18 +97,6 @@ public class AuthClient { try await walletRespondService.respondError(requestId: requestId) } - public func disconnect(topic: String) async throws { - try await deletePairingService.delete(topic: topic) - } - - public func ping(topic: String) async throws { - try await pingService.ping(topic: topic) - } - - public func getPairings() -> [Pairing] { - pairingsProvider.getPairings() - } - /// Query pending authentication requests /// - Returns: Pending authentication requests public func getPendingRequests() throws -> [AuthRequest] { @@ -167,15 +104,6 @@ public class AuthClient { return try pendingRequestsProvider.getPendingRequests(account: account) } -#if DEBUG - /// Delete all stored data such as: pairings, keys - /// - /// - Note: Doesn't unsubscribe from topics - public func cleanup() throws { - try cleanupService.cleanup() - } -#endif - private func setUpPublishers() { appRespondSubscriber.onResponse = { [unowned self] (id, result) in authResponsePublisherSubject.send((id, result)) @@ -184,9 +112,5 @@ public class AuthClient { walletRequestSubscriber.onRequest = { [unowned self] request in authRequestPublisherSubject.send(request) } - - pingService.onResponse = { [unowned self] topic in - pingResponsePublisherSubject.send(topic) - } } } diff --git a/Sources/Auth/AuthClientFactory.swift b/Sources/Auth/AuthClientFactory.swift index e0fe3f586..d2cb35859 100644 --- a/Sources/Auth/AuthClientFactory.swift +++ b/Sources/Auth/AuthClientFactory.swift @@ -7,48 +7,33 @@ import WalletConnectNetworking public struct AuthClientFactory { - public static func create(metadata: AppMetadata, account: Account?, relayClient: RelayClient) -> AuthClient { + public static func create(metadata: AppMetadata, account: Account?, projectId: String, networkingClient: NetworkingInteractor, pairingRegisterer: PairingRegisterer) -> AuthClient { let logger = ConsoleLogger(loggingLevel: .off) let keyValueStorage = UserDefaults.standard let keychainStorage = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk") - return AuthClientFactory.create(metadata: metadata, account: account, logger: logger, keyValueStorage: keyValueStorage, keychainStorage: keychainStorage, relayClient: relayClient) + return AuthClientFactory.create(metadata: metadata, account: account, projectId: projectId, logger: logger, keyValueStorage: keyValueStorage, keychainStorage: keychainStorage, networkingClient: networkingClient, pairingRegisterer: pairingRegisterer, iatProvider: DefaultIATProvider()) } - static func create(metadata: AppMetadata, account: Account?, logger: ConsoleLogging, keyValueStorage: KeyValueStorage, keychainStorage: KeychainStorageProtocol, relayClient: RelayClient) -> AuthClient { - let historyStorage = CodableStore(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.jsonRpcHistory.rawValue) - let pairingStore = PairingStorage(storage: SequenceStore(store: .init(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.pairings.rawValue))) + static func create(metadata: AppMetadata, account: Account?, projectId: String, logger: ConsoleLogging, keyValueStorage: KeyValueStorage, keychainStorage: KeychainStorageProtocol, networkingClient: NetworkingInteractor, pairingRegisterer: PairingRegisterer, iatProvider: IATProvider) -> AuthClient { let kms = KeyManagementService(keychain: keychainStorage) - let serializer = Serializer(kms: kms) - let history = RPCHistory(keyValueStore: historyStorage) - let networkingInteractor = NetworkingInteractor(relayClient: relayClient, serializer: serializer, logger: logger, rpcHistory: history) + let history = RPCHistoryFactory.createForNetwork(keyValueStorage: keyValueStorage) let messageFormatter = SIWEMessageFormatter() - let appPairService = AppPairService(networkingInteractor: networkingInteractor, kms: kms, pairingStorage: pairingStore) - let appRequestService = AppRequestService(networkingInteractor: networkingInteractor, kms: kms, appMetadata: metadata, logger: logger) - let messageSigner = MessageSigner(signer: Signer()) - let appRespondSubscriber = AppRespondSubscriber(networkingInteractor: networkingInteractor, logger: logger, rpcHistory: history, signatureVerifier: messageSigner, messageFormatter: messageFormatter, pairingStorage: pairingStore) - let walletPairService = WalletPairService(networkingInteractor: networkingInteractor, kms: kms, pairingStorage: pairingStore) - let walletErrorResponder = WalletErrorResponder(networkingInteractor: networkingInteractor, logger: logger, kms: kms, rpcHistory: history) - let walletRequestSubscriber = WalletRequestSubscriber(networkingInteractor: networkingInteractor, logger: logger, kms: kms, messageFormatter: messageFormatter, address: account?.address, walletErrorResponder: walletErrorResponder) - let walletRespondService = WalletRespondService(networkingInteractor: networkingInteractor, logger: logger, kms: kms, rpcHistory: history, walletErrorResponder: walletErrorResponder) + let appRequestService = AppRequestService(networkingInteractor: networkingClient, kms: kms, appMetadata: metadata, logger: logger, iatProvader: iatProvider) + let messageSigner = MessageSignerFactory.create(projectId: projectId) + let appRespondSubscriber = AppRespondSubscriber(networkingInteractor: networkingClient, logger: logger, rpcHistory: history, signatureVerifier: messageSigner, pairingRegisterer: pairingRegisterer, messageFormatter: messageFormatter) + let walletErrorResponder = WalletErrorResponder(networkingInteractor: networkingClient, logger: logger, kms: kms, rpcHistory: history) + let walletRequestSubscriber = WalletRequestSubscriber(networkingInteractor: networkingClient, logger: logger, kms: kms, messageFormatter: messageFormatter, address: account?.address, walletErrorResponder: walletErrorResponder, pairingRegisterer: pairingRegisterer) + let walletRespondService = WalletRespondService(networkingInteractor: networkingClient, logger: logger, kms: kms, rpcHistory: history, walletErrorResponder: walletErrorResponder) let pendingRequestsProvider = PendingRequestsProvider(rpcHistory: history) - let cleanupService = CleanupService(pairingStore: pairingStore, kms: kms) - let deletePairingService = DeletePairingService(networkingInteractor: networkingInteractor, kms: kms, pairingStorage: pairingStore, logger: logger) - let pingService = PairingPingService(pairingStorage: pairingStore, networkingInteractor: networkingInteractor, logger: logger) - let pairingsProvider = PairingsProvider(pairingStorage: pairingStore) - return AuthClient(appPairService: appPairService, - appRequestService: appRequestService, + return AuthClient(appRequestService: appRequestService, appRespondSubscriber: appRespondSubscriber, - walletPairService: walletPairService, walletRequestSubscriber: walletRequestSubscriber, - walletRespondService: walletRespondService, deletePairingService: deletePairingService, + walletRespondService: walletRespondService, account: account, pendingRequestsProvider: pendingRequestsProvider, - cleanupService: cleanupService, logger: logger, - pairingStorage: pairingStore, - socketConnectionStatusPublisher: relayClient.socketConnectionStatusPublisher, - pingService: pingService, - pairingsProvider: pairingsProvider) + socketConnectionStatusPublisher: networkingClient.socketConnectionStatusPublisher, + pairingRegisterer: pairingRegisterer) } } diff --git a/Sources/Auth/AuthConfig.swift b/Sources/Auth/AuthConfig.swift index b364ec507..00a98faed 100644 --- a/Sources/Auth/AuthConfig.swift +++ b/Sources/Auth/AuthConfig.swift @@ -2,7 +2,6 @@ import Foundation extension Auth { struct Config { - let metadata: AppMetadata let account: Account? } } diff --git a/Sources/Auth/AuthProtocolMethod.swift b/Sources/Auth/AuthProtocolMethod.swift index c3df72dae..85bab9cff 100644 --- a/Sources/Auth/AuthProtocolMethod.swift +++ b/Sources/Auth/AuthProtocolMethod.swift @@ -1,34 +1,26 @@ import Foundation import WalletConnectNetworking -enum AuthProtocolMethod: String, ProtocolMethod { - case authRequest = "wc_authRequest" - case pairingDelete = "wc_pairingDelete" - case pairingPing = "wc_pairingPing" - - var method: String { - return self.rawValue - } - - var requestTag: Int { - switch self { - case .authRequest: - return 3000 - case .pairingDelete: - return 1000 - case .pairingPing: - return 1002 - } - } - - var responseTag: Int { - switch self { - case .authRequest: - return 3001 - case .pairingDelete: - return 1001 - case .pairingPing: - return 1003 - } - } +struct AuthRequestProtocolMethod: ProtocolMethod { + let method: String = "wc_authRequest" + + let requestConfig = RelayConfig(tag: 3000, prompt: true, ttl: 86400) + + let responseConfig = RelayConfig(tag: 3001, prompt: false, ttl: 86400) +} + +struct PairingPingProtocolMethod: ProtocolMethod { + let method: String = "wc_pairingPing" + + let requestConfig = RelayConfig(tag: 1002, prompt: false, ttl: 30) + + let responseConfig = RelayConfig(tag: 1003, prompt: false, ttl: 30) +} + +struct PairingDeleteProtocolMethod: ProtocolMethod { + let method: String = "wc_pairingDelete" + + let requestConfig = RelayConfig(tag: 1000, prompt: false, ttl: 86400) + + let responseConfig = RelayConfig(tag: 1001, prompt: false, ttl: 86400) } diff --git a/Sources/Auth/Services/App/AppPairService.swift b/Sources/Auth/Services/App/AppPairService.swift index dc05600b0..f790e06a6 100644 --- a/Sources/Auth/Services/App/AppPairService.swift +++ b/Sources/Auth/Services/App/AppPairService.swift @@ -19,7 +19,7 @@ actor AppPairService { try await networkingInteractor.subscribe(topic: topic) let symKey = try! kms.createSymmetricKey(topic) let pairing = WCPairing(topic: topic) - let uri = WalletConnectURI(topic: topic, symKey: symKey.hexRepresentation, relay: pairing.relay, api: .auth) + let uri = WalletConnectURI(topic: topic, symKey: symKey.hexRepresentation, relay: pairing.relay) pairingStorage.setPairing(pairing) return uri } diff --git a/Sources/Auth/Services/App/AppRequestService.swift b/Sources/Auth/Services/App/AppRequestService.swift index da88c993a..6ea6ab339 100644 --- a/Sources/Auth/Services/App/AppRequestService.swift +++ b/Sources/Auth/Services/App/AppRequestService.swift @@ -9,28 +9,30 @@ actor AppRequestService { private let appMetadata: AppMetadata private let kms: KeyManagementService private let logger: ConsoleLogging + private let iatProvader: IATProvider init(networkingInteractor: NetworkInteracting, kms: KeyManagementService, appMetadata: AppMetadata, - logger: ConsoleLogging) { + logger: ConsoleLogging, + iatProvader: IATProvider) { self.networkingInteractor = networkingInteractor self.kms = kms self.appMetadata = appMetadata self.logger = logger + self.iatProvader = iatProvader } func request(params: RequestParams, topic: String) async throws { let pubKey = try kms.createX25519KeyPair() let responseTopic = pubKey.rawRepresentation.sha256().toHexString() let requester = AuthRequestParams.Requester(publicKey: pubKey.hexRepresentation, metadata: appMetadata) - let issueAt = ISO8601DateFormatter().string(from: Date()) - let payload = AuthPayload(requestParams: params, iat: issueAt) + let payload = AuthPayload(requestParams: params, iat: iatProvader.iat) let params = AuthRequestParams(requester: requester, payloadParams: payload) let request = RPCRequest(method: "wc_authRequest", params: params) try kms.setPublicKey(publicKey: pubKey, for: responseTopic) logger.debug("AppRequestService: Subscribibg for response topic: \(responseTopic)") - try await networkingInteractor.requestNetworkAck(request, topic: topic, tag: AuthProtocolMethod.authRequest.responseTag) + try await networkingInteractor.requestNetworkAck(request, topic: topic, protocolMethod: AuthRequestProtocolMethod()) try await networkingInteractor.subscribe(topic: responseTopic) } } diff --git a/Sources/Auth/Services/App/AppRespondSubscriber.swift b/Sources/Auth/Services/App/AppRespondSubscriber.swift index 2b3b353fa..4b5628221 100644 --- a/Sources/Auth/Services/App/AppRespondSubscriber.swift +++ b/Sources/Auth/Services/App/AppRespondSubscriber.swift @@ -7,11 +7,11 @@ import WalletConnectPairing class AppRespondSubscriber { private let networkingInteractor: NetworkInteracting - private let pairingStorage: WCPairingStorage private let logger: ConsoleLogging private let rpcHistory: RPCHistory private let signatureVerifier: MessageSignatureVerifying private let messageFormatter: SIWEMessageFormatting + private let pairingRegisterer: PairingRegisterer private var publishers = [AnyCancellable]() var onResponse: ((_ id: RPCID, _ result: Result) -> Void)? @@ -20,28 +20,28 @@ class AppRespondSubscriber { logger: ConsoleLogging, rpcHistory: RPCHistory, signatureVerifier: MessageSignatureVerifying, - messageFormatter: SIWEMessageFormatting, - pairingStorage: WCPairingStorage) { + pairingRegisterer: PairingRegisterer, + messageFormatter: SIWEMessageFormatting) { self.networkingInteractor = networkingInteractor self.logger = logger self.rpcHistory = rpcHistory self.signatureVerifier = signatureVerifier self.messageFormatter = messageFormatter - self.pairingStorage = pairingStorage + self.pairingRegisterer = pairingRegisterer subscribeForResponse() } private func subscribeForResponse() { - networkingInteractor.responseErrorSubscription(on: AuthProtocolMethod.authRequest) - .sink { [unowned self] payload in + networkingInteractor.responseErrorSubscription(on: AuthRequestProtocolMethod()) + .sink { [unowned self] (payload: ResponseSubscriptionErrorPayload) in guard let error = AuthError(code: payload.error.code) else { return } onResponse?(payload.id, .failure(error)) }.store(in: &publishers) - networkingInteractor.responseSubscription(on: AuthProtocolMethod.authRequest) + networkingInteractor.responseSubscription(on: AuthRequestProtocolMethod()) .sink { [unowned self] (payload: ResponseSubscriptionPayload) in - activatePairingIfNeeded(id: payload.id) + pairingRegisterer.activate(pairingTopic: payload.topic) networkingInteractor.unsubscribe(topic: payload.topic) let requestId = payload.id @@ -56,22 +56,20 @@ class AppRespondSubscriber { guard messageFormatter.formatMessage(from: requestPayload.payloadParams, address: address) == message else { self.onResponse?(requestId, .failure(.messageCompromised)); return } - guard let _ = try? signatureVerifier.verify(signature: cacao.s, message: message, address: address) - else { self.onResponse?(requestId, .failure(.signatureVerificationFailed)); return } - - onResponse?(requestId, .success(cacao)) - + Task(priority: .high) { + do { + try await signatureVerifier.verify( + signature: cacao.s, + message: message, + address: address, + chainId: requestPayload.payloadParams.chainId + ) + onResponse?(requestId, .success(cacao)) + } catch { + logger.error("Signature verification failed with: \(error.localizedDescription)") + onResponse?(requestId, .failure(.signatureVerificationFailed)) + } + } }.store(in: &publishers) } - - private func activatePairingIfNeeded(id: RPCID) { - guard let record = rpcHistory.get(recordId: id) else { return } - let pairingTopic = record.topic - guard var pairing = pairingStorage.getPairing(forTopic: pairingTopic) else { return } - if !pairing.active { - pairing.activate() - } else { - try? pairing.updateExpiry() - } - } } diff --git a/Sources/Auth/Services/Common/DeletePairingService.swift b/Sources/Auth/Services/Common/DeletePairingService.swift deleted file mode 100644 index 721bdc81f..000000000 --- a/Sources/Auth/Services/Common/DeletePairingService.swift +++ /dev/null @@ -1,37 +0,0 @@ -import Foundation -import WalletConnectNetworking -import JSONRPC -import WalletConnectKMS -import WalletConnectUtils -import WalletConnectPairing - -class DeletePairingService { - enum Errors: Error { - case pairingNotFound - } - private let networkingInteractor: NetworkInteracting - private let kms: KeyManagementServiceProtocol - private let pairingStorage: WCPairingStorage - private let logger: ConsoleLogging - - init(networkingInteractor: NetworkInteracting, - kms: KeyManagementServiceProtocol, - pairingStorage: WCPairingStorage, - logger: ConsoleLogging) { - self.networkingInteractor = networkingInteractor - self.kms = kms - self.pairingStorage = pairingStorage - self.logger = logger - } - - func delete(topic: String) async throws { - guard pairingStorage.hasPairing(forTopic: topic) else { throw Errors.pairingNotFound} - let reason = AuthError.userDisconnected - logger.debug("Will delete pairing for reason: message: \(reason.message) code: \(reason.code)") - let request = RPCRequest(method: AuthProtocolMethod.pairingDelete.rawValue, params: reason) - try await networkingInteractor.request(request, topic: topic, tag: AuthProtocolMethod.pairingDelete.requestTag) - pairingStorage.delete(topic: topic) - kms.deleteSymmetricKey(for: topic) - networkingInteractor.unsubscribe(topic: topic) - } -} diff --git a/Sources/Auth/Services/Common/IATProvider.swift b/Sources/Auth/Services/Common/IATProvider.swift new file mode 100644 index 000000000..1d386c11e --- /dev/null +++ b/Sources/Auth/Services/Common/IATProvider.swift @@ -0,0 +1,11 @@ +import Foundation + +protocol IATProvider { + var iat: String { get } +} + +struct DefaultIATProvider: IATProvider { + var iat: String { + return ISO8601DateFormatter().string(from: Date()) + } +} diff --git a/Sources/Auth/Services/Common/PairingsProvider.swift b/Sources/Auth/Services/Common/PairingsProvider.swift deleted file mode 100644 index 081f3fa6d..000000000 --- a/Sources/Auth/Services/Common/PairingsProvider.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation -import WalletConnectPairing - -public class PairingsProvider { - private let pairingStorage: WCPairingStorage - - public init(pairingStorage: WCPairingStorage) { - self.pairingStorage = pairingStorage - } - - func getPairings() -> [Pairing] { - pairingStorage.getAll() - .map {Pairing(topic: $0.topic, peer: $0.peerMetadata, expiryDate: $0.expiryDate)} - } -} diff --git a/Sources/Auth/Services/Signer/EIP1271/EIP1271Verifier.swift b/Sources/Auth/Services/Signer/EIP1271/EIP1271Verifier.swift new file mode 100644 index 000000000..a68dd7742 --- /dev/null +++ b/Sources/Auth/Services/Signer/EIP1271/EIP1271Verifier.swift @@ -0,0 +1,44 @@ +import Foundation +import JSONRPC +import WalletConnectNetworking +import WalletConnectUtils + +actor EIP1271Verifier { + private let projectId: String + private let httpClient: HTTPClient + + init(projectId: String, httpClient: HTTPClient) { + self.projectId = projectId + self.httpClient = httpClient + } + + func verify(signature: Data, message: Data, address: String, chainId: String) async throws { + let encoder = ValidSignatureMethod(signature: signature, messageHash: message.keccak256) + let call = EthCall(to: address, data: encoder.encode()) + let params = AnyCodable([AnyCodable(call), AnyCodable("latest")]) + let request = RPCRequest(method: "eth_call", params: params) + let data = try JSONEncoder().encode(request) + let httpService = RPCService(data: data, projectId: projectId, chainId: chainId) + let response = try await httpClient.request(RPCResponse.self, at: httpService) + try validateResponse(response) + } + + private func validateResponse(_ response: RPCResponse) throws { + guard + let result = try response.result?.get(String.self), + result.starts(with: ValidSignatureMethod.methodHash) + else { throw Errors.invalidSignature } + } +} + +extension EIP1271Verifier { + + enum Errors: Error { + case invalidSignature + } + + struct EthCall: Codable { + let to: String + let data: String + } +} diff --git a/Sources/Auth/Services/Signer/EIP1271/RPCService.swift b/Sources/Auth/Services/Signer/EIP1271/RPCService.swift new file mode 100644 index 000000000..c7c81c995 --- /dev/null +++ b/Sources/Auth/Services/Signer/EIP1271/RPCService.swift @@ -0,0 +1,31 @@ +import Foundation +import WalletConnectNetworking + +struct RPCService: HTTPService { + let data: Data + let projectId: String + let chainId: String + + var path: String { + return "/v1" + } + + var method: HTTPMethod { + return .post + } + + var scheme: String { + return "https" + } + + var body: Data? { + return data + } + + var queryParameters: [String: String]? { + return [ + "chainId": chainId, + "projectId": projectId + ] + } +} diff --git a/Sources/Auth/Services/Signer/EIP1271/ValidSignatureMethod.swift b/Sources/Auth/Services/Signer/EIP1271/ValidSignatureMethod.swift new file mode 100644 index 000000000..134d62427 --- /dev/null +++ b/Sources/Auth/Services/Signer/EIP1271/ValidSignatureMethod.swift @@ -0,0 +1,28 @@ +import Foundation + +struct ValidSignatureMethod { + static let methodHash = "0x1626ba7e" + static let paddingIndex = "0000000000000000000000000000000000000000000000000000000000000040" + static let signatureLength = "0000000000000000000000000000000000000000000000000000000000000041" + static let signaturePadding = "00000000000000000000000000000000000000000000000000000000000000" + + let signature: Data + let messageHash: Data + + func encode() -> String { + return [ + ValidSignatureMethod.methodHash, + leadingZeros(for: messageHash.toHexString(), end: false), + ValidSignatureMethod.paddingIndex, + ValidSignatureMethod.signatureLength, + leadingZeros(for: signature.toHexString(), end: true), + ValidSignatureMethod.signaturePadding + ].joined() + } + + private func leadingZeros(for value: String, end: Bool) -> String { + let count = max(0, value.count % 32 - 2) + let padding = String(repeating: "0", count: count) + return end ? padding + value : value + padding + } +} diff --git a/Sources/Auth/Services/Signer/EIP191/EIP191Verifier.swift b/Sources/Auth/Services/Signer/EIP191/EIP191Verifier.swift new file mode 100644 index 000000000..7622cae14 --- /dev/null +++ b/Sources/Auth/Services/Signer/EIP191/EIP191Verifier.swift @@ -0,0 +1,36 @@ +import Foundation +import Web3 + +actor EIP191Verifier { + + func verify(signature: Data, message: Data, address: String) async throws { + let sig = decompose(signature: signature) + let publicKey = try EthereumPublicKey.init( + message: message.bytes, + v: EthereumQuantity(quantity: BigUInt(sig.v)), + r: EthereumQuantity(sig.r), + s: EthereumQuantity(sig.s) + ) + try verifyPublicKey(publicKey, address: address) + } + + private func decompose(signature: Data) -> Signer.Signature { + let v = signature.bytes[signature.count-1] + let r = signature.bytes[0..<32] + let s = signature.bytes[32..<64] + return (UInt(v), [UInt8](r), [UInt8](s)) + } + + private func verifyPublicKey(_ publicKey: EthereumPublicKey, address: String) throws { + guard publicKey.address.hex(eip55: false) == address.lowercased() else { + throw Errors.invalidSignature + } + } +} + +extension EIP191Verifier { + + enum Errors: Error { + case invalidSignature + } +} diff --git a/Sources/Auth/Services/Signer/MessageSigner.swift b/Sources/Auth/Services/Signer/MessageSigner.swift index 3f6252efd..fd4ffffbb 100644 --- a/Sources/Auth/Services/Signer/MessageSigner.swift +++ b/Sources/Auth/Services/Signer/MessageSigner.swift @@ -1,37 +1,65 @@ import Foundation -protocol MessageSignatureVerifying { - func verify(signature: CacaoSignature, message: String, address: String) throws +public protocol MessageSignatureVerifying { + func verify(signature: CacaoSignature, message: String, address: String, chainId: String) async throws } -protocol MessageSigning { - func sign(message: String, privateKey: Data) throws -> CacaoSignature +public protocol MessageSigning { + func sign(message: String, privateKey: Data, type: CacaoSignatureType) throws -> CacaoSignature } -public struct MessageSigner: MessageSignatureVerifying, MessageSigning { +struct MessageSigner: MessageSignatureVerifying, MessageSigning { enum Errors: Error { - case signatureValidationFailed case utf8EncodingFailed } private let signer: Signer + private let eip191Verifier: EIP191Verifier + private let eip1271Verifier: EIP1271Verifier - public init(signer: Signer = Signer()) { + init(signer: Signer, eip191Verifier: EIP191Verifier, eip1271Verifier: EIP1271Verifier) { self.signer = signer + self.eip191Verifier = eip191Verifier + self.eip1271Verifier = eip1271Verifier } - public func sign(message: String, privateKey: Data) throws -> CacaoSignature { + func sign(message: String, privateKey: Data, type: CacaoSignatureType) throws -> CacaoSignature { guard let messageData = message.data(using: .utf8) else { throw Errors.utf8EncodingFailed } - let signature = try signer.sign(message: messageData, with: privateKey) + let signature = try signer.sign(message: prefixed(messageData), with: privateKey) let prefixedHexSignature = "0x" + signature.toHexString() - return CacaoSignature(t: "eip191", s: prefixedHexSignature) + return CacaoSignature(t: type, s: prefixedHexSignature) } - public func verify(signature: CacaoSignature, message: String, address: String) throws { - guard let messageData = message.data(using: .utf8) else { throw Errors.utf8EncodingFailed } + func verify(signature: CacaoSignature, message: String, address: String, chainId: String) async throws { + guard let messageData = message.data(using: .utf8) else { + throw Errors.utf8EncodingFailed + } + let signatureData = Data(hex: signature.s) - guard try signer.isValid(signature: signatureData, message: messageData, address: address) - else { throw Errors.signatureValidationFailed } + + switch signature.t { + case .eip191: + return try await eip191Verifier.verify( + signature: signatureData, + message: prefixed(messageData), + address: address + ) + case .eip1271: + return try await eip1271Verifier.verify( + signature: signatureData, + message: prefixed(messageData), + address: address, + chainId: chainId + ) + } + } +} + +private extension MessageSigner { + + private func prefixed(_ message: Data) -> Data { + return "\u{19}Ethereum Signed Message:\n\(message.count)" + .data(using: .utf8)! + message } } diff --git a/Sources/Auth/Services/Signer/MessageSignerFactory.swift b/Sources/Auth/Services/Signer/MessageSignerFactory.swift new file mode 100644 index 000000000..81ba54de5 --- /dev/null +++ b/Sources/Auth/Services/Signer/MessageSignerFactory.swift @@ -0,0 +1,20 @@ +import Foundation +import WalletConnectNetworking + +public struct MessageSignerFactory { + + public static func create() -> MessageSigning & MessageSignatureVerifying { + return create(projectId: Networking.projectId) + } + + static func create(projectId: String) -> MessageSigning & MessageSignatureVerifying { + return MessageSigner( + signer: Signer(), + eip191Verifier: EIP191Verifier(), + eip1271Verifier: EIP1271Verifier( + projectId: projectId, + httpClient: HTTPNetworkClient(host: "rpc.walletconnect.com") + ) + ) + } +} diff --git a/Sources/Auth/Services/Signer/Signer.swift b/Sources/Auth/Services/Signer/Signer.swift index 3d5903296..cbd56ca89 100644 --- a/Sources/Auth/Services/Signer/Signer.swift +++ b/Sources/Auth/Services/Signer/Signer.swift @@ -1,44 +1,19 @@ import Foundation import Web3 -public struct Signer { +struct Signer { typealias Signature = (v: UInt, r: [UInt8], s: [UInt8]) - public init() {} + init() {} func sign(message: Data, with key: Data) throws -> Data { - let prefixed = prefixed(message: message) let privateKey = try EthereumPrivateKey(privateKey: key.bytes) - let signature = try privateKey.sign(message: prefixed.bytes) + let signature = try privateKey.sign(message: message.bytes) return serialized(signature: signature) } - func isValid(signature: Data, message: Data, address: String) throws -> Bool { - let sig = decompose(signature: signature) - let prefixed = prefixed(message: message) - let publicKey = try EthereumPublicKey( - message: prefixed.bytes, - v: EthereumQuantity(quantity: BigUInt(sig.v)), - r: EthereumQuantity(sig.r), - s: EthereumQuantity(sig.s) - ) - return publicKey.address.hex(eip55: false) == address.lowercased() - } - - private func decompose(signature: Data) -> Signature { - let v = signature.bytes[signature.count-1] - let r = signature.bytes[0..<32] - let s = signature.bytes[32..<64] - return (UInt(v), [UInt8](r), [UInt8](s)) - } - private func serialized(signature: Signature) -> Data { return Data(signature.r + signature.s + [UInt8(signature.v)]) } - - private func prefixed(message: Data) -> Data { - return "\u{19}Ethereum Signed Message:\n\(message.count)" - .data(using: .utf8)! + message - } } diff --git a/Sources/Auth/Services/Wallet/WalletErrorResponder.swift b/Sources/Auth/Services/Wallet/WalletErrorResponder.swift index 10e318c24..3234b86fd 100644 --- a/Sources/Auth/Services/Wallet/WalletErrorResponder.swift +++ b/Sources/Auth/Services/Wallet/WalletErrorResponder.swift @@ -25,16 +25,14 @@ actor WalletErrorResponder { self.rpcHistory = rpcHistory } - func respondError(_ error: AuthError, requestId: RPCID) async throws { let authRequestParams = try getAuthRequestParams(requestId: requestId) let (topic, keys) = try generateAgreementKeys(requestParams: authRequestParams) try kms.setAgreementSecret(keys, topic: topic) - let tag = AuthProtocolMethod.authRequest.responseTag let envelopeType = Envelope.EnvelopeType.type1(pubKey: keys.publicKey.rawRepresentation) - try await networkingInteractor.respondError(topic: topic, requestId: requestId, tag: tag, reason: error, envelopeType: envelopeType) + try await networkingInteractor.respondError(topic: topic, requestId: requestId, protocolMethod: AuthRequestProtocolMethod(), reason: error, envelopeType: envelopeType) } private func getAuthRequestParams(requestId: RPCID) throws -> AuthRequestParams { @@ -52,7 +50,7 @@ actor WalletErrorResponder { let topic = peerPubKey.rawRepresentation.sha256().toHexString() let selfPubKey = try kms.createX25519KeyPair() let keys = try kms.performKeyAgreement(selfPublicKey: selfPubKey, peerPublicKey: peerPubKey.hexRepresentation) - //TODO - remove keys + // TODO - remove keys return (topic, keys) } } diff --git a/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift index c204cc526..fde464a43 100644 --- a/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift +++ b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift @@ -4,6 +4,7 @@ import JSONRPC import WalletConnectNetworking import WalletConnectUtils import WalletConnectKMS +import WalletConnectPairing class WalletRequestSubscriber { private let networkingInteractor: NetworkInteracting @@ -13,6 +14,7 @@ class WalletRequestSubscriber { private var publishers = [AnyCancellable]() private let messageFormatter: SIWEMessageFormatting private let walletErrorResponder: WalletErrorResponder + private let pairingRegisterer: PairingRegisterer var onRequest: ((AuthRequest) -> Void)? init(networkingInteractor: NetworkInteracting, @@ -20,20 +22,22 @@ class WalletRequestSubscriber { kms: KeyManagementServiceProtocol, messageFormatter: SIWEMessageFormatting, address: String?, - walletErrorResponder: WalletErrorResponder) { + walletErrorResponder: WalletErrorResponder, + pairingRegisterer: PairingRegisterer) { self.networkingInteractor = networkingInteractor self.logger = logger self.kms = kms self.address = address self.messageFormatter = messageFormatter self.walletErrorResponder = walletErrorResponder + self.pairingRegisterer = pairingRegisterer subscribeForRequest() } private func subscribeForRequest() { guard let address = address else { return } - networkingInteractor.requestSubscription(on: AuthProtocolMethod.authRequest) + pairingRegisterer.register(method: AuthRequestProtocolMethod()) .sink { [unowned self] (payload: RequestSubscriptionPayload) in logger.debug("WalletRequestSubscriber: Received request") guard let message = messageFormatter.formatMessage(from: payload.request.payloadParams, address: address) else { @@ -42,8 +46,8 @@ class WalletRequestSubscriber { } return } + pairingRegisterer.activate(pairingTopic: payload.topic) onRequest?(.init(id: payload.id, message: message)) }.store(in: &publishers) } } - diff --git a/Sources/Auth/Services/Wallet/WalletRespondService.swift b/Sources/Auth/Services/Wallet/WalletRespondService.swift index f9a53b10d..32090f956 100644 --- a/Sources/Auth/Services/Wallet/WalletRespondService.swift +++ b/Sources/Auth/Services/Wallet/WalletRespondService.swift @@ -39,7 +39,7 @@ actor WalletRespondService { let responseParams = AuthResponseParams(h: header, p: payload, s: signature) let response = RPCResponse(id: requestId, result: responseParams) - try await networkingInteractor.respond(topic: topic, response: response, tag: AuthProtocolMethod.authRequest.responseTag, envelopeType: .type1(pubKey: keys.publicKey.rawRepresentation)) + try await networkingInteractor.respond(topic: topic, response: response, protocolMethod: AuthRequestProtocolMethod(), envelopeType: .type1(pubKey: keys.publicKey.rawRepresentation)) } func respondError(requestId: RPCID) async throws { diff --git a/Sources/Auth/Types/Cacao/CacaoSignature.swift b/Sources/Auth/Types/Cacao/CacaoSignature.swift index b34ee1a40..7137f9dea 100644 --- a/Sources/Auth/Types/Cacao/CacaoSignature.swift +++ b/Sources/Auth/Types/Cacao/CacaoSignature.swift @@ -1,11 +1,16 @@ import Foundation +public enum CacaoSignatureType: String, Codable { + case eip191 + case eip1271 +} + public struct CacaoSignature: Codable, Equatable { - let t: String + let t: CacaoSignatureType let s: String let m: String? - public init(t: String, s: String, m: String? = nil) { + public init(t: CacaoSignatureType, s: String, m: String? = nil) { self.t = t self.s = s self.m = m diff --git a/Sources/Chat/ChatClientFactory.swift b/Sources/Chat/ChatClientFactory.swift index 9918dbc70..b5af0d7b2 100644 --- a/Sources/Chat/ChatClientFactory.swift +++ b/Sources/Chat/ChatClientFactory.swift @@ -15,7 +15,7 @@ public struct ChatClientFactory { ) -> ChatClient { let topicToRegistryRecordStore = CodableStore(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.topicToInvitationPubKey.rawValue) let serialiser = Serializer(kms: kms) - let rpcHistory = RPCHistory(keyValueStore: CodableStore(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.jsonRpcHistory.rawValue)) + let rpcHistory = RPCHistoryFactory.createForNetwork(keyValueStorage: keyValueStorage) let networkingInteractor = NetworkingInteractor(relayClient: relayClient, serializer: serialiser, logger: logger, rpcHistory: rpcHistory) let invitePayloadStore = CodableStore>(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.invite.rawValue) let registryService = RegistryService(registry: registry, networkingInteractor: networkingInteractor, kms: kms, logger: logger, topicToRegistryRecordStore: topicToRegistryRecordStore) diff --git a/Sources/Chat/HTTPServices/RegisterService.swift b/Sources/Chat/HTTPServices/RegisterService.swift index 0734be114..618c64035 100644 --- a/Sources/Chat/HTTPServices/RegisterService.swift +++ b/Sources/Chat/HTTPServices/RegisterService.swift @@ -1,5 +1,5 @@ import Foundation -import WalletConnectRelay +import WalletConnectNetworking struct RegisterService: HTTPService { diff --git a/Sources/Chat/HTTPServices/ResolveService.swift b/Sources/Chat/HTTPServices/ResolveService.swift index 993833c64..795584000 100644 --- a/Sources/Chat/HTTPServices/ResolveService.swift +++ b/Sources/Chat/HTTPServices/ResolveService.swift @@ -1,5 +1,5 @@ import Foundation -import WalletConnectRelay +import WalletConnectNetworking import WalletConnectUtils struct ResolveService: HTTPService { diff --git a/Sources/Chat/ProtocolServices/Common/MessagingService.swift b/Sources/Chat/ProtocolServices/Common/MessagingService.swift index 20edfb6b5..e4c746cf3 100644 --- a/Sources/Chat/ProtocolServices/Common/MessagingService.swift +++ b/Sources/Chat/ProtocolServices/Common/MessagingService.swift @@ -29,12 +29,13 @@ class MessagingService { func send(topic: String, messageString: String) async throws { // TODO - manage author account + let protocolMethod = ChatMessageProtocolMethod() let thread = await threadStore.first {$0.topic == topic} guard let authorAccount = thread?.selfAccount else { throw Errors.threadDoNotExist} let timestamp = Int64(Date().timeIntervalSince1970 * 1000) let message = Message(topic: topic, message: messageString, authorAccount: authorAccount, timestamp: timestamp) - let request = RPCRequest(method: ChatProtocolMethod.message.method, params: message) - try await networkingInteractor.request(request, topic: topic, tag: ChatProtocolMethod.message.requestTag) + let request = RPCRequest(method: protocolMethod.method, params: message) + try await networkingInteractor.request(request, topic: topic, protocolMethod: protocolMethod) Task(priority: .background) { await messagesStore.add(message) onMessage?(message) @@ -42,14 +43,14 @@ class MessagingService { } private func setUpResponseHandling() { - networkingInteractor.responseSubscription(on: ChatProtocolMethod.message) - .sink { [unowned self] (payload: ResponseSubscriptionPayload) in + networkingInteractor.responseSubscription(on: ChatMessageProtocolMethod()) + .sink { [unowned self] (_: ResponseSubscriptionPayload) in logger.debug("Received Message response") }.store(in: &publishers) } private func setUpRequestHandling() { - networkingInteractor.requestSubscription(on: ChatProtocolMethod.message) + networkingInteractor.requestSubscription(on: ChatMessageProtocolMethod()) .sink { [unowned self] (payload: RequestSubscriptionPayload) in var message = payload.request message.topic = payload.topic @@ -59,7 +60,7 @@ class MessagingService { private func handleMessage(_ message: Message, topic: String, requestId: RPCID) { Task(priority: .background) { - try await networkingInteractor.respondSuccess(topic: topic, requestId: requestId, tag: ChatProtocolMethod.message.responseTag) + try await networkingInteractor.respondSuccess(topic: topic, requestId: requestId, protocolMethod: ChatMessageProtocolMethod()) await messagesStore.add(message) logger.debug("Received message") onMessage?(message) diff --git a/Sources/Chat/ProtocolServices/Common/File.swift b/Sources/Chat/ProtocolServices/Common/ResubscriptionService.swift similarity index 71% rename from Sources/Chat/ProtocolServices/Common/File.swift rename to Sources/Chat/ProtocolServices/Common/ResubscriptionService.swift index e821f03d7..c08738891 100644 --- a/Sources/Chat/ProtocolServices/Common/File.swift +++ b/Sources/Chat/ProtocolServices/Common/ResubscriptionService.swift @@ -22,11 +22,10 @@ class ResubscriptionService { func setUpResubscription() { networkingInteractor.socketConnectionStatusPublisher .sink { [unowned self] status in - if status == .connected { - Task(priority: .background) { - let topics = await threadStore.getAll().map {$0.topic} - topics.forEach { topic in Task(priority: .background) { try? await networkingInteractor.subscribe(topic: topic) } } - } + guard status == .connected else { return } + Task(priority: .background) { + let topics = await threadStore.getAll().map {$0.topic} + topics.forEach { topic in Task(priority: .background) { try? await networkingInteractor.subscribe(topic: topic) } } } }.store(in: &publishers) } diff --git a/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift b/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift index c6dd9aede..e3e30028c 100644 --- a/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift +++ b/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift @@ -39,6 +39,8 @@ class InvitationHandlingService { } func accept(inviteId: String) async throws { + let protocolMethod = ChatInviteProtocolMethod() + guard let payload = try invitePayloadStore.get(key: inviteId) else { throw Error.inviteForIdNotFound } let selfThreadPubKey = try kms.createX25519KeyPair() @@ -47,7 +49,7 @@ class InvitationHandlingService { let response = RPCResponse(id: payload.id, result: inviteResponse) let responseTopic = try getInviteResponseTopic(requestTopic: payload.topic, invite: payload.request) - try await networkingInteractor.respond(topic: responseTopic, response: response, tag: ChatProtocolMethod.invite.responseTag) + try await networkingInteractor.respond(topic: responseTopic, response: response, protocolMethod: protocolMethod) let threadAgreementKeys = try kms.performKeyAgreement(selfPublicKey: selfThreadPubKey, peerPublicKey: payload.request.publicKey) let threadTopic = threadAgreementKeys.derivedTopic() @@ -71,13 +73,13 @@ class InvitationHandlingService { let responseTopic = try getInviteResponseTopic(requestTopic: payload.topic, invite: payload.request) - try await networkingInteractor.respondError(topic: responseTopic, requestId: payload.id, tag: ChatProtocolMethod.invite.responseTag, reason: ChatError.userRejected) + try await networkingInteractor.respondError(topic: responseTopic, requestId: payload.id, protocolMethod: ChatInviteProtocolMethod(), reason: ChatError.userRejected) invitePayloadStore.delete(forKey: inviteId) } private func setUpRequestHandling() { - networkingInteractor.requestSubscription(on: ChatProtocolMethod.invite) + networkingInteractor.requestSubscription(on: ChatInviteProtocolMethod()) .sink { [unowned self] (payload: RequestSubscriptionPayload) in logger.debug("did receive an invite") invitePayloadStore.set(payload, forKey: payload.request.publicKey) diff --git a/Sources/Chat/ProtocolServices/Inviter/InviteService.swift b/Sources/Chat/ProtocolServices/Inviter/InviteService.swift index 514138042..9568bfac4 100644 --- a/Sources/Chat/ProtocolServices/Inviter/InviteService.swift +++ b/Sources/Chat/ProtocolServices/Inviter/InviteService.swift @@ -33,6 +33,7 @@ class InviteService { func invite(peerPubKey: String, peerAccount: Account, openingMessage: String, account: Account) async throws { // TODO ad storage + let protocolMethod = ChatInviteProtocolMethod() self.peerAccount = peerAccount let selfPubKeyY = try kms.createX25519KeyPair() let invite = Invite(message: openingMessage, account: account, publicKey: selfPubKeyY.hexRepresentation) @@ -42,7 +43,7 @@ class InviteService { // overrides on invite toipic try kms.setSymmetricKey(symKeyI.sharedKey, for: inviteTopic) - let request = RPCRequest(method: ChatProtocolMethod.invite.method, params: invite) + let request = RPCRequest(method: protocolMethod.method, params: invite) // 2. Proposer subscribes to topic R which is the hash of the derived symKey let responseTopic = symKeyI.derivedTopic() @@ -50,13 +51,13 @@ class InviteService { try kms.setSymmetricKey(symKeyI.sharedKey, for: responseTopic) try await networkingInteractor.subscribe(topic: responseTopic) - try await networkingInteractor.request(request, topic: inviteTopic, tag: ChatProtocolMethod.invite.requestTag, envelopeType: .type1(pubKey: selfPubKeyY.rawRepresentation)) + try await networkingInteractor.request(request, topic: inviteTopic, protocolMethod: protocolMethod, envelopeType: .type1(pubKey: selfPubKeyY.rawRepresentation)) logger.debug("invite sent on topic: \(inviteTopic)") } private func setUpResponseHandling() { - networkingInteractor.responseSubscription(on: ChatProtocolMethod.invite) + networkingInteractor.responseSubscription(on: ChatInviteProtocolMethod()) .sink { [unowned self] (payload: ResponseSubscriptionPayload) in logger.debug("Invite has been accepted") diff --git a/Sources/Chat/Registry.swift b/Sources/Chat/Registry.swift index e7f31d357..bd8b82a55 100644 --- a/Sources/Chat/Registry.swift +++ b/Sources/Chat/Registry.swift @@ -1,5 +1,5 @@ import Foundation -import WalletConnectRelay +import WalletConnectNetworking import WalletConnectUtils public protocol Registry { diff --git a/Sources/Chat/Types/ChatProtocolMethod.swift b/Sources/Chat/Types/ChatProtocolMethod.swift index a32e5d4bf..f24a5f971 100644 --- a/Sources/Chat/Types/ChatProtocolMethod.swift +++ b/Sources/Chat/Types/ChatProtocolMethod.swift @@ -1,34 +1,20 @@ import Foundation import WalletConnectNetworking -enum ChatProtocolMethod: ProtocolMethod { - case invite - case message - - var requestTag: Int { - switch self { - case .invite: - return 2000 - case .message: - return 2002 - } - } - - var responseTag: Int { - switch self { - case .invite: - return 2001 - case .message: - return 2003 - } - } - - var method: String { - switch self { - case .invite: - return "wc_chatInvite" - case .message: - return "wc_chatMessage" - } - } +struct ChatInviteProtocolMethod: ProtocolMethod { + let method: String = "wc_chatInvite" + + let requestConfig = RelayConfig(tag: 2000, prompt: true, ttl: 86400) + + let responseConfig = RelayConfig(tag: 2001, prompt: false, ttl: 86400) + +} + +struct ChatMessageProtocolMethod: ProtocolMethod { + let method: String = "wc_chatMessage" + + let requestConfig = RelayConfig(tag: 2002, prompt: true, ttl: 86400) + + let responseConfig = RelayConfig(tag: 2003, prompt: false, ttl: 86400) + } diff --git a/Sources/JSONRPC/RPCID.swift b/Sources/JSONRPC/RPCID.swift index ff0ef3c83..21dcfb682 100644 --- a/Sources/JSONRPC/RPCID.swift +++ b/Sources/JSONRPC/RPCID.swift @@ -1,3 +1,4 @@ +import Foundation import Commons import Foundation @@ -15,3 +16,12 @@ struct IntIdentifierGenerator: IdentifierGenerator { return RPCID(timestamp + random) } } + +extension RPCID { + + public var timestamp: Date { + guard let id = self.right else { return .distantPast } + let interval = TimeInterval(id / 1000 / 1000) + return Date(timeIntervalSince1970: interval) + } +} diff --git a/Sources/JSONRPC/RPCResponse.swift b/Sources/JSONRPC/RPCResponse.swift index ad6ba9ca6..d38ef9c49 100644 --- a/Sources/JSONRPC/RPCResponse.swift +++ b/Sources/JSONRPC/RPCResponse.swift @@ -10,65 +10,65 @@ public struct RPCResponse: Equatable { public let id: RPCID? public var result: AnyCodable? { - if case .success(let value) = outcome { return value } + if case .response(let value) = outcome { return value } return nil } public var error: JSONRPCError? { - if case .failure(let error) = outcome { return error } + if case .error(let error) = outcome { return error } return nil } - public let outcome: Result + public let outcome: RPCResult - internal init(id: RPCID?, outcome: Result) { + internal init(id: RPCID?, outcome: RPCResult) { self.jsonrpc = "2.0" self.id = id self.outcome = outcome } public init(matchingRequest: RPCRequest, result: C) where C: Codable { - self.init(id: matchingRequest.id, outcome: .success(AnyCodable(result))) + self.init(id: matchingRequest.id, outcome: .response(AnyCodable(result))) } public init(matchingRequest: RPCRequest, error: JSONRPCError) { - self.init(id: matchingRequest.id, outcome: .failure(error)) + self.init(id: matchingRequest.id, outcome: .error(error)) } public init(id: Int64, result: C) where C: Codable { - self.init(id: RPCID(id), outcome: .success(AnyCodable(result))) + self.init(id: RPCID(id), outcome: .response(AnyCodable(result))) } public init(id: String, result: C) where C: Codable { - self.init(id: RPCID(id), outcome: .success(AnyCodable(result))) + self.init(id: RPCID(id), outcome: .response(AnyCodable(result))) } public init(id: RPCID, result: C) where C: Codable { - self.init(id: id, outcome: .success(AnyCodable(result))) + self.init(id: id, outcome: .response(AnyCodable(result))) } public init(id: RPCID?, error: JSONRPCError) { - self.init(id: id, outcome: .failure(error)) + self.init(id: id, outcome: .error(error)) } public init(id: Int64, error: JSONRPCError) { - self.init(id: RPCID(id), outcome: .failure(error)) + self.init(id: RPCID(id), outcome: .error(error)) } public init(id: String, error: JSONRPCError) { - self.init(id: RPCID(id), outcome: .failure(error)) + self.init(id: RPCID(id), outcome: .error(error)) } public init(id: Int64, errorCode: Int, message: String, associatedData: AnyCodable? = nil) { - self.init(id: RPCID(id), outcome: .failure(JSONRPCError(code: errorCode, message: message, data: associatedData))) + self.init(id: RPCID(id), outcome: .error(JSONRPCError(code: errorCode, message: message, data: associatedData))) } public init(id: String, errorCode: Int, message: String, associatedData: AnyCodable? = nil) { - self.init(id: RPCID(id), outcome: .failure(JSONRPCError(code: errorCode, message: message, data: associatedData))) + self.init(id: RPCID(id), outcome: .error(JSONRPCError(code: errorCode, message: message, data: associatedData))) } public init(errorWithoutID: JSONRPCError) { - self.init(id: nil, outcome: .failure(errorWithoutID)) + self.init(id: nil, outcome: .error(errorWithoutID)) } } @@ -104,9 +104,9 @@ extension RPCResponse: Codable { codingPath: [CodingKeys.result, CodingKeys.id], debugDescription: "A success response must have a valid `id`.")) } - outcome = .success(result) + outcome = .response(result) } else if let error = error { - outcome = .failure(error) + outcome = .error(error) } else { throw DecodingError.dataCorrupted(.init( codingPath: [CodingKeys.result, CodingKeys.error], @@ -119,9 +119,9 @@ extension RPCResponse: Codable { try container.encode(jsonrpc, forKey: .jsonrpc) try container.encode(id, forKey: .id) switch outcome { - case .success(let anyCodable): + case .response(let anyCodable): try container.encode(anyCodable, forKey: .result) - case .failure(let rpcError): + case .error(let rpcError): try container.encode(rpcError, forKey: .error) } } diff --git a/Sources/JSONRPC/RPCResult.swift b/Sources/JSONRPC/RPCResult.swift new file mode 100644 index 000000000..a3b81e094 --- /dev/null +++ b/Sources/JSONRPC/RPCResult.swift @@ -0,0 +1,39 @@ +import Foundation +import Commons + +public enum RPCResult: Codable, Equatable { + enum Errors: Error { + case decoding + } + + case response(AnyCodable) + case error(JSONRPCError) + + public var value: Codable { + switch self { + case .response(let value): + return value + case .error(let value): + return value + } + } + + public init(from decoder: Decoder) throws { + if let value = try? JSONRPCError(from: decoder) { + self = .error(value) + } else if let value = try? AnyCodable(from: decoder) { + self = .response(value) + } else { + throw Errors.decoding + } + } + + public func encode(to encoder: Encoder) throws { + switch self { + case .error(let value): + try value.encode(to: encoder) + case .response(let value): + try value.encode(to: encoder) + } + } +} diff --git a/Sources/WalletConnectNetworking/HTTPClient/HTTPClient.swift b/Sources/WalletConnectNetworking/HTTPClient/HTTPClient.swift new file mode 100644 index 000000000..0fb4c8a68 --- /dev/null +++ b/Sources/WalletConnectNetworking/HTTPClient/HTTPClient.swift @@ -0,0 +1,6 @@ +import Foundation + +public protocol HTTPClient { + func request(_ type: T.Type, at service: HTTPService) async throws -> T + func request(service: HTTPService) async throws +} diff --git a/Sources/WalletConnectRelay/HTTP/HTTPError.swift b/Sources/WalletConnectNetworking/HTTPClient/HTTPError.swift similarity index 100% rename from Sources/WalletConnectRelay/HTTP/HTTPError.swift rename to Sources/WalletConnectNetworking/HTTPClient/HTTPError.swift diff --git a/Sources/WalletConnectRelay/HTTP/HTTPClient.swift b/Sources/WalletConnectNetworking/HTTPClient/HTTPNetworkClient.swift similarity index 85% rename from Sources/WalletConnectRelay/HTTP/HTTPClient.swift rename to Sources/WalletConnectNetworking/HTTPClient/HTTPNetworkClient.swift index 03ec8626d..b87948bd6 100644 --- a/Sources/WalletConnectRelay/HTTP/HTTPClient.swift +++ b/Sources/WalletConnectNetworking/HTTPClient/HTTPNetworkClient.swift @@ -1,6 +1,6 @@ import Foundation -public actor HTTPClient { +public actor HTTPNetworkClient: HTTPClient { let host: String @@ -32,14 +32,14 @@ public actor HTTPClient { } } - func request(_ type: T.Type, at service: HTTPService, completion: @escaping (Result) -> Void) { + private func request(_ type: T.Type, at service: HTTPService, completion: @escaping (Result) -> Void) { guard let request = service.resolve(for: host) else { completion(.failure(HTTPError.malformedURL(service))) return } session.dataTask(with: request) { data, response, error in do { - try HTTPClient.validate(response, error) + try HTTPNetworkClient.validate(response, error) guard let validData = data else { throw HTTPError.responseDataNil } @@ -51,14 +51,14 @@ public actor HTTPClient { }.resume() } - func request(service: HTTPService, completion: @escaping (Result) -> Void) { + private func request(service: HTTPService, completion: @escaping (Result) -> Void) { guard let request = service.resolve(for: host) else { completion(.failure(HTTPError.malformedURL(service))) return } session.dataTask(with: request) { _, response, error in do { - try HTTPClient.validate(response, error) + try HTTPNetworkClient.validate(response, error) completion(.success(())) } catch { completion(.failure(error)) diff --git a/Sources/WalletConnectRelay/HTTP/HTTPService.swift b/Sources/WalletConnectNetworking/HTTPClient/HTTPService.swift similarity index 100% rename from Sources/WalletConnectRelay/HTTP/HTTPService.swift rename to Sources/WalletConnectNetworking/HTTPClient/HTTPService.swift diff --git a/Sources/WalletConnectNetworking/NetworkInteracting.swift b/Sources/WalletConnectNetworking/NetworkInteracting.swift index 8dd02d382..f23d5b66d 100644 --- a/Sources/WalletConnectNetworking/NetworkInteracting.swift +++ b/Sources/WalletConnectNetworking/NetworkInteracting.swift @@ -6,13 +6,14 @@ import WalletConnectRelay public protocol NetworkInteracting { var socketConnectionStatusPublisher: AnyPublisher { get } + var requestPublisher: AnyPublisher<(topic: String, request: RPCRequest), Never> { get } func subscribe(topic: String) async throws func unsubscribe(topic: String) - func request(_ request: RPCRequest, topic: String, tag: Int, envelopeType: Envelope.EnvelopeType) async throws - func requestNetworkAck(_ request: RPCRequest, topic: String, tag: Int) async throws - func respond(topic: String, response: RPCResponse, tag: Int, envelopeType: Envelope.EnvelopeType) async throws - func respondSuccess(topic: String, requestId: RPCID, tag: Int, envelopeType: Envelope.EnvelopeType) async throws - func respondError(topic: String, requestId: RPCID, tag: Int, reason: Reason, envelopeType: Envelope.EnvelopeType) 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 + func respondSuccess(topic: String, requestId: RPCID, protocolMethod: ProtocolMethod, envelopeType: Envelope.EnvelopeType) async throws + func respondError(topic: String, requestId: RPCID, protocolMethod: ProtocolMethod, reason: Reason, envelopeType: Envelope.EnvelopeType) async throws func requestSubscription( on request: ProtocolMethod @@ -22,23 +23,25 @@ public protocol NetworkInteracting { on request: ProtocolMethod ) -> AnyPublisher, Never> - func responseErrorSubscription(on request: ProtocolMethod) -> AnyPublisher + func responseErrorSubscription( + on request: ProtocolMethod + ) -> AnyPublisher, Never> } extension NetworkInteracting { - public func request(_ request: RPCRequest, topic: String, tag: Int) async throws { - try await self.request(request, topic: topic, tag: tag, envelopeType: .type0) + public func request(_ request: RPCRequest, topic: String, protocolMethod: ProtocolMethod) async throws { + try await self.request(request, topic: topic, protocolMethod: protocolMethod, envelopeType: .type0) } - public func respond(topic: String, response: RPCResponse, tag: Int) async throws { - try await self.respond(topic: topic, response: response, tag: tag, envelopeType: .type0) + public func respond(topic: String, response: RPCResponse, protocolMethod: ProtocolMethod) async throws { + try await self.respond(topic: topic, response: response, protocolMethod: protocolMethod, envelopeType: .type0) } - public func respondSuccess(topic: String, requestId: RPCID, tag: Int) async throws { - try await self.respondSuccess(topic: topic, requestId: requestId, tag: tag, envelopeType: .type0) + public func respondSuccess(topic: String, requestId: RPCID, protocolMethod: ProtocolMethod) async throws { + try await self.respondSuccess(topic: topic, requestId: requestId, protocolMethod: protocolMethod, envelopeType: .type0) } - public func respondError(topic: String, requestId: RPCID, tag: Int, reason: Reason) async throws { - try await self.respondError(topic: topic, requestId: requestId, tag: tag, reason: reason, envelopeType: .type0) + public func respondError(topic: String, requestId: RPCID, protocolMethod: ProtocolMethod, reason: Reason) async throws { + try await self.respondError(topic: topic, requestId: requestId, protocolMethod: protocolMethod, reason: reason, envelopeType: .type0) } } diff --git a/Sources/WalletConnectNetworking/Networking.swift b/Sources/WalletConnectNetworking/Networking.swift new file mode 100644 index 000000000..fdad4f621 --- /dev/null +++ b/Sources/WalletConnectNetworking/Networking.swift @@ -0,0 +1,56 @@ +import WalletConnectRelay +import WalletConnectUtils +import WalletConnectKMS +import Foundation + +public class Networking { + + /// Networking client instance + public static var instance: NetworkingClient { + return Networking.interactor + } + + public static var interactor: NetworkingInteractor = { + guard let _ = Networking.config else { + fatalError("Error - you must call Networking.configure(_:) before accessing the shared instance.") + } + + return NetworkingClientFactory.create(relayClient: Relay.instance) + }() + + public static var projectId: String { + guard let projectId = config?.projectId else { + fatalError("Error - you must configure projectId with Networking.configure(_:)") + } + return projectId + } + + private static var config: Config? + + private init() { } + + /// Networking instance config method + /// - Parameters: + /// - relayHost: relay host + /// - projectId: project id + /// - socketFactory: web socket factory + /// - socketConnectionType: socket connection type + static public func configure( + relayHost: String = "relay.walletconnect.com", + projectId: String, + socketFactory: WebSocketFactory, + socketConnectionType: SocketConnectionType = .automatic + ) { + Networking.config = Networking.Config( + relayHost: relayHost, + projectId: projectId, + socketFactory: socketFactory, + socketConnectionType: socketConnectionType + ) + Relay.configure( + relayHost: relayHost, + projectId: projectId, + socketFactory: socketFactory, + socketConnectionType: socketConnectionType) + } +} diff --git a/Sources/WalletConnectNetworking/NetworkingClient.swift b/Sources/WalletConnectNetworking/NetworkingClient.swift new file mode 100644 index 000000000..f39e2790d --- /dev/null +++ b/Sources/WalletConnectNetworking/NetworkingClient.swift @@ -0,0 +1,10 @@ +import Foundation +import Combine +import WalletConnectRelay + +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/NetworkingClientFactory.swift b/Sources/WalletConnectNetworking/NetworkingClientFactory.swift new file mode 100644 index 000000000..91a1cdacf --- /dev/null +++ b/Sources/WalletConnectNetworking/NetworkingClientFactory.swift @@ -0,0 +1,28 @@ +import Foundation +import WalletConnectKMS +import WalletConnectRelay +import WalletConnectUtils + +public struct NetworkingClientFactory { + + public static func create(relayClient: RelayClient) -> NetworkingInteractor { + let logger = ConsoleLogger(loggingLevel: .off) + let keyValueStorage = UserDefaults.standard + let keychainStorage = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk") + return NetworkingClientFactory.create(relayClient: relayClient, logger: logger, keychainStorage: keychainStorage, keyValueStorage: keyValueStorage) + } + + public static func create(relayClient: RelayClient, logger: ConsoleLogging, keychainStorage: KeychainStorageProtocol, keyValueStorage: KeyValueStorage) -> NetworkingInteractor { + let kms = KeyManagementService(keychain: keychainStorage) + + let serializer = Serializer(kms: kms) + + let rpcHistory = RPCHistoryFactory.createForNetwork(keyValueStorage: keyValueStorage) + + return NetworkingInteractor( + relayClient: relayClient, + serializer: serializer, + logger: logger, + rpcHistory: rpcHistory) + } +} diff --git a/Sources/WalletConnectNetworking/NetworkingConfig.swift b/Sources/WalletConnectNetworking/NetworkingConfig.swift new file mode 100644 index 000000000..c569d813b --- /dev/null +++ b/Sources/WalletConnectNetworking/NetworkingConfig.swift @@ -0,0 +1,11 @@ +import Foundation +import WalletConnectRelay + +extension Networking { + struct Config { + let relayHost: String + let projectId: String + let socketFactory: WebSocketFactory + let socketConnectionType: SocketConnectionType + } +} diff --git a/Sources/WalletConnectNetworking/NetworkInteractor.swift b/Sources/WalletConnectNetworking/NetworkingInteractor.swift similarity index 67% rename from Sources/WalletConnectNetworking/NetworkInteractor.swift rename to Sources/WalletConnectNetworking/NetworkingInteractor.swift index 13859ead6..66647efd2 100644 --- a/Sources/WalletConnectNetworking/NetworkInteractor.swift +++ b/Sources/WalletConnectNetworking/NetworkingInteractor.swift @@ -15,7 +15,7 @@ public class NetworkingInteractor: NetworkInteracting { private let requestPublisherSubject = PassthroughSubject<(topic: String, request: RPCRequest), Never>() private let responsePublisherSubject = PassthroughSubject<(topic: String, request: RPCRequest, response: RPCResponse), Never>() - private var requestPublisher: AnyPublisher<(topic: String, request: RPCRequest), Never> { + public var requestPublisher: AnyPublisher<(topic: String, request: RPCRequest), Never> { requestPublisherSubject.eraseToAnyPublisher() } @@ -36,10 +36,14 @@ public class NetworkingInteractor: NetworkInteracting { self.rpcHistory = rpcHistory self.logger = logger self.socketConnectionStatusPublisher = relayClient.socketConnectionStatusPublisher - relayClient.messagePublisher.sink { [unowned self] (topic, message) in - manageSubscription(topic, message) - } - .store(in: &publishers) + setupRelaySubscribtion() + } + + private func setupRelaySubscribtion() { + relayClient.messagePublisher + .sink { [unowned self] (topic, message) in + manageSubscription(topic, message) + }.store(in: &publishers) } public func subscribe(topic: String) async throws { @@ -56,11 +60,13 @@ public class NetworkingInteractor: NetworkInteracting { } } - public func requestSubscription(on request: ProtocolMethod) -> AnyPublisher, Never> { + public func requestSubscription(on request: ProtocolMethod) -> AnyPublisher, Never> { return requestPublisher - .filter { $0.request.method == request.method } + .filter { rpcRequest in + return rpcRequest.request.method == request.method + } .compactMap { topic, rpcRequest in - guard let id = rpcRequest.id, let request = try? rpcRequest.params?.get(Request.self) else { return nil } + guard let id = rpcRequest.id, let request = try? rpcRequest.params?.get(RequestParams.self) else { return nil } return RequestSubscriptionPayload(id: id, topic: topic, request: request) } .eraseToAnyPublisher() @@ -68,7 +74,9 @@ public class NetworkingInteractor: NetworkInteracting { public func responseSubscription(on request: ProtocolMethod) -> AnyPublisher, Never> { return responsePublisher - .filter { $0.request.method == request.method } + .filter { rpcRequest in + return rpcRequest.request.method == request.method + } .compactMap { topic, rpcRequest, rpcResponse in guard let id = rpcRequest.id, @@ -79,31 +87,31 @@ public class NetworkingInteractor: NetworkInteracting { .eraseToAnyPublisher() } - public func responseErrorSubscription(on request: ProtocolMethod) -> AnyPublisher { + public func responseErrorSubscription(on request: ProtocolMethod) -> AnyPublisher, Never> { return responsePublisher .filter { $0.request.method == request.method } - .compactMap { (_, _, rpcResponse) in - guard let id = rpcResponse.id, let error = rpcResponse.error else { return nil } - return ResponseSubscriptionErrorPayload(id: id, error: error) + .compactMap { (topic, rpcRequest, rpcResponse) in + guard let id = rpcResponse.id, let request = try? rpcRequest.params?.get(Request.self), let error = rpcResponse.error else { return nil } + return ResponseSubscriptionErrorPayload(id: id, topic: topic, request: request, error: error) } .eraseToAnyPublisher() } - public func request(_ request: RPCRequest, topic: String, tag: Int, envelopeType: Envelope.EnvelopeType) async throws { + public func request(_ request: RPCRequest, topic: String, protocolMethod: ProtocolMethod, envelopeType: Envelope.EnvelopeType) async throws { try rpcHistory.set(request, forTopic: topic, emmitedBy: .local) let message = try! serializer.serialize(topic: topic, encodable: request, envelopeType: envelopeType) - try await relayClient.publish(topic: topic, payload: message, tag: tag) + try await relayClient.publish(topic: topic, payload: message, tag: protocolMethod.requestConfig.tag, prompt: protocolMethod.requestConfig.prompt, ttl: protocolMethod.requestConfig.ttl) } /// Completes with an acknowledgement from the relay network. /// completes with error if networking client was not able to send a message /// TODO - relay client should provide async function - continualion should be removed from here - public func requestNetworkAck(_ request: RPCRequest, topic: String, tag: Int) async throws { + public func requestNetworkAck(_ request: RPCRequest, topic: String, protocolMethod: ProtocolMethod) async throws { do { try rpcHistory.set(request, forTopic: topic, emmitedBy: .local) let message = try serializer.serialize(topic: topic, encodable: request) return try await withCheckedThrowingContinuation { continuation in - relayClient.publish(topic: topic, payload: message, tag: tag) { error in + relayClient.publish(topic: topic, payload: message, tag: protocolMethod.requestConfig.tag, prompt: protocolMethod.requestConfig.prompt, ttl: protocolMethod.requestConfig.ttl) { error in if let error = error { continuation.resume(throwing: error) } else { @@ -116,21 +124,21 @@ public class NetworkingInteractor: NetworkInteracting { } } - public func respond(topic: String, response: RPCResponse, tag: Int, envelopeType: Envelope.EnvelopeType) async throws { + public func respond(topic: String, response: RPCResponse, protocolMethod: ProtocolMethod, envelopeType: Envelope.EnvelopeType) async throws { try rpcHistory.resolve(response) let message = try! serializer.serialize(topic: topic, encodable: response, envelopeType: envelopeType) - try await relayClient.publish(topic: topic, payload: message, tag: tag) + try await relayClient.publish(topic: topic, payload: message, tag: protocolMethod.responseConfig.tag, prompt: protocolMethod.responseConfig.prompt, ttl: protocolMethod.responseConfig.ttl) } - public func respondSuccess(topic: String, requestId: RPCID, tag: Int, envelopeType: Envelope.EnvelopeType) async throws { + public func respondSuccess(topic: String, requestId: RPCID, protocolMethod: ProtocolMethod, envelopeType: Envelope.EnvelopeType) async throws { let response = RPCResponse(id: requestId, result: true) - try await respond(topic: topic, response: response, tag: tag, envelopeType: envelopeType) + try await respond(topic: topic, response: response, protocolMethod: protocolMethod, envelopeType: envelopeType) } - public func respondError(topic: String, requestId: RPCID, tag: Int, reason: Reason, envelopeType: Envelope.EnvelopeType) async throws { + 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) - try await respond(topic: topic, response: response, tag: tag, envelopeType: envelopeType) + try await respond(topic: topic, response: response, protocolMethod: protocolMethod, envelopeType: envelopeType) } private func manageSubscription(_ topic: String, _ encodedEnvelope: String) { @@ -162,3 +170,17 @@ public class NetworkingInteractor: NetworkInteracting { } } } + +extension NetworkingInteractor: NetworkingClient { + public func connect() throws { + try relayClient.connect() + } + + public func disconnect(closeCode: URLSessionWebSocketTask.CloseCode) throws { + try relayClient.disconnect(closeCode: closeCode) + } + + public func getClientId() throws -> String { + try relayClient.getClientId() + } +} diff --git a/Sources/WalletConnectNetworking/ProtocolMethod.swift b/Sources/WalletConnectNetworking/ProtocolMethod.swift index dea8bd255..624d74aa2 100644 --- a/Sources/WalletConnectNetworking/ProtocolMethod.swift +++ b/Sources/WalletConnectNetworking/ProtocolMethod.swift @@ -2,6 +2,18 @@ import Foundation public protocol ProtocolMethod { var method: String { get } - var requestTag: Int { get } - var responseTag: Int { get } + var requestConfig: RelayConfig { get } + var responseConfig: RelayConfig { get } +} + +public struct RelayConfig { + let tag: Int + let prompt: Bool + let ttl: Int + + public init(tag: Int, prompt: Bool, ttl: Int) { + self.tag = tag + self.prompt = prompt + self.ttl = ttl + } } diff --git a/Sources/WalletConnectNetworking/RequestSubscriptionPayload.swift b/Sources/WalletConnectNetworking/RequestSubscriptionPayload.swift index fa6b0e8db..d8767d692 100644 --- a/Sources/WalletConnectNetworking/RequestSubscriptionPayload.swift +++ b/Sources/WalletConnectNetworking/RequestSubscriptionPayload.swift @@ -1,7 +1,7 @@ import Foundation import JSONRPC -public struct RequestSubscriptionPayload: Codable { +public struct RequestSubscriptionPayload: Codable, SubscriptionPayload { public let id: RPCID public let topic: String public let request: Request diff --git a/Sources/WalletConnectNetworking/ResponseSubscriptionErrorPayload.swift b/Sources/WalletConnectNetworking/ResponseSubscriptionErrorPayload.swift index 8b38df244..589ebbcc2 100644 --- a/Sources/WalletConnectNetworking/ResponseSubscriptionErrorPayload.swift +++ b/Sources/WalletConnectNetworking/ResponseSubscriptionErrorPayload.swift @@ -1,12 +1,16 @@ import Foundation import JSONRPC -public struct ResponseSubscriptionErrorPayload { +public struct ResponseSubscriptionErrorPayload: Codable, SubscriptionPayload { public let id: RPCID + public let topic: String + public let request: Request public let error: JSONRPCError - public init(id: RPCID, error: JSONRPCError) { + public init(id: RPCID, topic: String, request: Request, error: JSONRPCError) { self.id = id + self.topic = topic + self.request = request self.error = error } } diff --git a/Sources/WalletConnectNetworking/ResponseSubscriptionPayload.swift b/Sources/WalletConnectNetworking/ResponseSubscriptionPayload.swift index 21043eb9d..93b21538a 100644 --- a/Sources/WalletConnectNetworking/ResponseSubscriptionPayload.swift +++ b/Sources/WalletConnectNetworking/ResponseSubscriptionPayload.swift @@ -1,7 +1,7 @@ import Foundation import JSONRPC -public struct ResponseSubscriptionPayload { +public struct ResponseSubscriptionPayload: SubscriptionPayload { public let id: RPCID public let topic: String public let request: Request diff --git a/Sources/WalletConnectNetworking/SubscriptionPayload.swift b/Sources/WalletConnectNetworking/SubscriptionPayload.swift new file mode 100644 index 000000000..ad5e60eab --- /dev/null +++ b/Sources/WalletConnectNetworking/SubscriptionPayload.swift @@ -0,0 +1,7 @@ +import Foundation +import JSONRPC + +public protocol SubscriptionPayload { + var id: RPCID { get } + var topic: String { get } +} diff --git a/Sources/WalletConnectPairing/Pair.swift b/Sources/WalletConnectPairing/Pair.swift new file mode 100644 index 000000000..b016ed6e6 --- /dev/null +++ b/Sources/WalletConnectPairing/Pair.swift @@ -0,0 +1,43 @@ +import Foundation +import WalletConnectNetworking +import Combine + +public class Pair { + + /// Pairing client instance + public static var instance: PairingInteracting { + return Pair.client + } + + public static var registerer: PairingRegisterer { + return Pair.client + } + + public static var metadata: AppMetadata { + guard let metadata = config?.metadata else { + fatalError("Error - you must configure metadata with Pair.configure(metadata:)") + } + return metadata + } + + private static var config: Config? + + private init() { } + + /// Pairing instance config method + /// - Parameters: + /// - metadata: App metadata + static public func configure(metadata: AppMetadata) { + Pair.config = Pair.Config(metadata: metadata) + } +} + +private extension Pair { + + static var client: PairingClient = { + guard let config = Pair.config else { + fatalError("Error - you must call Pair.configure(_:) before accessing the shared instance.") + } + return PairingClientFactory.create(networkingClient: Networking.interactor) + }() +} diff --git a/Sources/WalletConnectPairing/PairConfig.swift b/Sources/WalletConnectPairing/PairConfig.swift new file mode 100644 index 000000000..38f7e35ed --- /dev/null +++ b/Sources/WalletConnectPairing/PairConfig.swift @@ -0,0 +1,12 @@ +import Foundation + +extension Pair { + + public struct Config { + public let metadata: AppMetadata + + public init(metadata: AppMetadata) { + self.metadata = metadata + } + } +} diff --git a/Sources/WalletConnectPairing/PairingClient.swift b/Sources/WalletConnectPairing/PairingClient.swift new file mode 100644 index 000000000..2b115c4e5 --- /dev/null +++ b/Sources/WalletConnectPairing/PairingClient.swift @@ -0,0 +1,129 @@ +import Foundation +import WalletConnectUtils +import WalletConnectRelay +import WalletConnectNetworking +import Combine +import JSONRPC + +public class PairingClient: PairingRegisterer, PairingInteracting { + public var pingResponsePublisher: AnyPublisher<(String), Never> { + pingResponsePublisherSubject.eraseToAnyPublisher() + } + public let socketConnectionStatusPublisher: AnyPublisher + + 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 + private let networkingInteractor: NetworkInteracting + private let pairingRequestsSubscriber: PairingRequestsSubscriber + private let pairingsProvider: PairingsProvider + private let deletePairingService: DeletePairingService + private let resubscribeService: ResubscribeService + private let expirationService: ExpirationService + + private let cleanupService: CleanupService + + init(appPairService: AppPairService, + networkingInteractor: NetworkInteracting, + logger: ConsoleLogging, + walletPairService: WalletPairService, + deletePairingService: DeletePairingService, + resubscribeService: ResubscribeService, + expirationService: ExpirationService, + pairingRequestsSubscriber: PairingRequestsSubscriber, + appPairActivateService: AppPairActivationService, + appUpdateMetadataService: AppUpdateMetadataService, + cleanupService: CleanupService, + pingService: PairingPingService, + socketConnectionStatusPublisher: AnyPublisher, + pairingsProvider: PairingsProvider + ) { + self.appPairService = appPairService + self.walletPairService = walletPairService + self.networkingInteractor = networkingInteractor + self.socketConnectionStatusPublisher = socketConnectionStatusPublisher + self.logger = logger + self.deletePairingService = deletePairingService + self.appPairActivateService = appPairActivateService + self.appUpdateMetadataService = appUpdateMetadataService + self.resubscribeService = resubscribeService + self.expirationService = expirationService + self.cleanupService = cleanupService + self.pingService = pingService + self.pairingRequestsSubscriber = pairingRequestsSubscriber + self.pairingsProvider = pairingsProvider + setUpPublishers() + setUpExpiration() + } + + private func setUpPublishers() { + pingService.onResponse = { [unowned self] topic in + pingResponsePublisherSubject.send(topic) + } + } + + private func setUpExpiration() { + expirationService.setupExpirationHandling() + } + + /// For wallet to establish a pairing + /// Wallet should call this function in order to accept peer's pairing proposal and be able to subscribe for future requests. + /// - Parameter uri: Pairing URI that is commonly presented as a QR code by a dapp or delivered with universal linking. + /// + /// Throws Error: + /// - When URI is invalid format or missing params + /// - When topic is already in use + public func pair(uri: WalletConnectURI) async throws { + try await walletPairService.pair(uri) + } + + public func create() async throws -> WalletConnectURI { + 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 getPairings() -> [Pairing] { + pairingsProvider.getPairings() + } + + public func getPairing(for topic: String) throws -> Pairing { + try pairingsProvider.getPairing(for: topic) + } + + public func ping(topic: String) async throws { + try await pingService.ping(topic: topic) + } + + public func disconnect(topic: String) async throws { + try await deletePairingService.delete(topic: topic) + } + + public func validatePairingExistance(_ topic: String) throws { + _ = try pairingsProvider.getPairing(for: topic) + } + + public func register(method: ProtocolMethod) -> AnyPublisher, Never> { + logger.debug("Pairing Client - registering for \(method.method)") + return pairingRequestsSubscriber.subscribeForRequest(method) + } + +#if DEBUG + /// Delete all stored data such as: pairings, keys + /// + /// - Note: Doesn't unsubscribe from topics + public func cleanup() throws { + try cleanupService.cleanup() + } +#endif +} diff --git a/Sources/WalletConnectPairing/PairingClientFactory.swift b/Sources/WalletConnectPairing/PairingClientFactory.swift new file mode 100644 index 000000000..9926292e0 --- /dev/null +++ b/Sources/WalletConnectPairing/PairingClientFactory.swift @@ -0,0 +1,48 @@ +import Foundation +import WalletConnectRelay +import WalletConnectUtils +import WalletConnectKMS +import WalletConnectNetworking + +public struct PairingClientFactory { + + public static func create(networkingClient: NetworkingInteractor) -> PairingClient { + let logger = ConsoleLogger(loggingLevel: .off) + let keyValueStorage = UserDefaults.standard + let keychainStorage = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk") + return PairingClientFactory.create(logger: logger, keyValueStorage: keyValueStorage, keychainStorage: keychainStorage, networkingClient: networkingClient) + } + + public static func create(logger: ConsoleLogging, keyValueStorage: KeyValueStorage, keychainStorage: KeychainStorageProtocol, networkingClient: NetworkingInteractor) -> PairingClient { + let pairingStore = PairingStorage(storage: SequenceStore(store: .init(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.pairings.rawValue))) + let kms = KeyManagementService(keychain: keychainStorage) + let appPairService = AppPairService(networkingInteractor: networkingClient, kms: kms, pairingStorage: pairingStore) + let walletPairService = WalletPairService(networkingInteractor: networkingClient, kms: kms, pairingStorage: pairingStore) + let pairingRequestsSubscriber = PairingRequestsSubscriber(networkingInteractor: networkingClient, pairingStorage: pairingStore, logger: logger) + let pairingsProvider = PairingsProvider(pairingStorage: pairingStore) + let cleanupService = CleanupService(pairingStore: pairingStore, kms: kms) + 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) + + return PairingClient( + appPairService: appPairService, + networkingInteractor: networkingClient, + logger: logger, + walletPairService: walletPairService, + deletePairingService: deletePairingService, + resubscribeService: resubscribeService, + expirationService: expirationService, + pairingRequestsSubscriber: pairingRequestsSubscriber, + appPairActivateService: appPairActivateService, + appUpdateMetadataService: appUpdateMetadataService, + cleanupService: cleanupService, + pingService: pingService, + socketConnectionStatusPublisher: networkingClient.socketConnectionStatusPublisher, + pairingsProvider: pairingsProvider + ) + } +} diff --git a/Sources/WalletConnectPairing/PairingInteracting.swift b/Sources/WalletConnectPairing/PairingInteracting.swift new file mode 100644 index 000000000..90b85bd51 --- /dev/null +++ b/Sources/WalletConnectPairing/PairingInteracting.swift @@ -0,0 +1,18 @@ +import Foundation +import WalletConnectUtils + +public protocol PairingInteracting { + func pair(uri: WalletConnectURI) async throws + + func create() async throws -> WalletConnectURI + + func getPairings() -> [Pairing] + + func getPairing(for topic: String) throws -> Pairing + + func ping(topic: String) async throws + + func disconnect(topic: String) async throws + + func cleanup() throws +} diff --git a/Sources/WalletConnectPairing/PairingProtocolMethod.swift b/Sources/WalletConnectPairing/PairingProtocolMethod.swift deleted file mode 100644 index a6f9d14cf..000000000 --- a/Sources/WalletConnectPairing/PairingProtocolMethod.swift +++ /dev/null @@ -1,18 +0,0 @@ -import Foundation -import WalletConnectNetworking - -enum PairingProtocolMethod: String, ProtocolMethod { - case ping = "wc_pairingPing" - - var method: String { - return self.rawValue - } - - var requestTag: Int { - return 1002 - } - - var responseTag: Int { - return 1003 - } -} diff --git a/Sources/WalletConnectPairing/PairingRegisterer.swift b/Sources/WalletConnectPairing/PairingRegisterer.swift new file mode 100644 index 000000000..f6b136d74 --- /dev/null +++ b/Sources/WalletConnectPairing/PairingRegisterer.swift @@ -0,0 +1,14 @@ +import Foundation +import WalletConnectNetworking +import Combine +import JSONRPC + +public protocol PairingRegisterer { + func register( + method: ProtocolMethod + ) -> AnyPublisher, Never> + + func activate(pairingTopic: String) + func validatePairingExistance(_ topic: String) throws + func updateMetadata(_ topic: String, metadata: AppMetadata) +} diff --git a/Sources/WalletConnectPairing/PairingRequestsSubscriber.swift b/Sources/WalletConnectPairing/PairingRequestsSubscriber.swift new file mode 100644 index 000000000..f10808792 --- /dev/null +++ b/Sources/WalletConnectPairing/PairingRequestsSubscriber.swift @@ -0,0 +1,43 @@ +import Foundation +import Combine +import WalletConnectUtils +import WalletConnectNetworking +import JSONRPC + +public class PairingRequestsSubscriber { + private let networkingInteractor: NetworkInteracting + private let pairingStorage: PairingStorage + private var publishers = Set() + private var registeredProtocolMethods = SetStore(label: "com.walletconnect.sdk.pairing.registered_protocol_methods") + private let pairingProtocolMethods = PairingProtocolMethod.allCases.map { $0.method } + private let logger: ConsoleLogging + + init(networkingInteractor: NetworkInteracting, + pairingStorage: PairingStorage, + logger: ConsoleLogging) { + self.networkingInteractor = networkingInteractor + self.pairingStorage = pairingStorage + self.logger = logger + handleUnregisteredRequests() + } + + func subscribeForRequest(_ protocolMethod: ProtocolMethod) -> AnyPublisher, Never> { + registeredProtocolMethods.insert(protocolMethod.method) + return networkingInteractor.requestSubscription(on: protocolMethod).eraseToAnyPublisher() + } + + func handleUnregisteredRequests() { + networkingInteractor.requestPublisher + .filter { [unowned self] in !pairingProtocolMethods.contains($0.request.method)} + .filter { [unowned self] in pairingStorage.hasPairing(forTopic: $0.topic)} + .filter { [unowned self] in !registeredProtocolMethods.contains($0.request.method)} + .sink { [unowned self] topic, request in + Task(priority: .high) { + let protocolMethod = UnsupportedProtocolMethod(method: request.method) + logger.debug("PairingRequestsSubscriber: responding unregistered request method") + try await networkingInteractor.respondError(topic: topic, requestId: request.id!, protocolMethod: protocolMethod, reason: PairError.methodUnsupported) + } + }.store(in: &publishers) + } + +} diff --git a/Sources/WalletConnectPairing/Services/App/AppPairActivationService.swift b/Sources/WalletConnectPairing/Services/App/AppPairActivationService.swift new file mode 100644 index 000000000..5b96e3908 --- /dev/null +++ b/Sources/WalletConnectPairing/Services/App/AppPairActivationService.swift @@ -0,0 +1,26 @@ +import Foundation +import Combine +import WalletConnectNetworking +import WalletConnectUtils + +final class AppPairActivationService { + private let pairingStorage: PairingStorage + private let logger: ConsoleLogging + + init(pairingStorage: PairingStorage, logger: ConsoleLogging) { + self.pairingStorage = pairingStorage + self.logger = logger + } + + func activate(for topic: String) { + 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() + } + pairingStorage.setPairing(pairing) + } +} diff --git a/Sources/WalletConnectPairing/Services/App/AppPairService.swift b/Sources/WalletConnectPairing/Services/App/AppPairService.swift new file mode 100644 index 000000000..fb51d8e9b --- /dev/null +++ b/Sources/WalletConnectPairing/Services/App/AppPairService.swift @@ -0,0 +1,26 @@ +import Foundation +import WalletConnectKMS +import WalletConnectNetworking +import WalletConnectUtils + +actor AppPairService { + private let networkingInteractor: NetworkInteracting + private let kms: KeyManagementServiceProtocol + private let pairingStorage: WCPairingStorage + + init(networkingInteractor: NetworkInteracting, kms: KeyManagementServiceProtocol, pairingStorage: WCPairingStorage) { + self.networkingInteractor = networkingInteractor + self.kms = kms + self.pairingStorage = pairingStorage + } + + func create() async throws -> WalletConnectURI { + let topic = String.generateTopic() + try await networkingInteractor.subscribe(topic: topic) + let symKey = try! kms.createSymmetricKey(topic) + let pairing = WCPairing(topic: topic) + let uri = WalletConnectURI(topic: topic, symKey: symKey.hexRepresentation, relay: pairing.relay) + pairingStorage.setPairing(pairing) + return uri + } +} diff --git a/Sources/WalletConnectPairing/Services/App/AppUpdateMetadataService.swift b/Sources/WalletConnectPairing/Services/App/AppUpdateMetadataService.swift new file mode 100644 index 000000000..4901e567a --- /dev/null +++ b/Sources/WalletConnectPairing/Services/App/AppUpdateMetadataService.swift @@ -0,0 +1,16 @@ +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/Auth/Services/Common/CleanupService.swift b/Sources/WalletConnectPairing/Services/Common/CleanupService.swift similarity index 94% rename from Sources/Auth/Services/Common/CleanupService.swift rename to Sources/WalletConnectPairing/Services/Common/CleanupService.swift index 6a5a01334..2ee49d54d 100644 --- a/Sources/Auth/Services/Common/CleanupService.swift +++ b/Sources/WalletConnectPairing/Services/Common/CleanupService.swift @@ -1,7 +1,6 @@ import Foundation import WalletConnectKMS import WalletConnectUtils -import WalletConnectPairing final class CleanupService { diff --git a/Sources/WalletConnectSign/Engine/Common/DeletePairingService.swift b/Sources/WalletConnectPairing/Services/Common/DeletePairingService.swift similarity index 73% rename from Sources/WalletConnectSign/Engine/Common/DeletePairingService.swift rename to Sources/WalletConnectPairing/Services/Common/DeletePairingService.swift index a6324481e..05369d56c 100644 --- a/Sources/WalletConnectSign/Engine/Common/DeletePairingService.swift +++ b/Sources/WalletConnectPairing/Services/Common/DeletePairingService.swift @@ -1,7 +1,8 @@ import Foundation +import JSONRPC import WalletConnectKMS import WalletConnectUtils -import WalletConnectPairing +import WalletConnectNetworking class DeletePairingService { private let networkingInteractor: NetworkInteracting @@ -20,10 +21,11 @@ class DeletePairingService { } func delete(topic: String) async throws { - let reasonCode = ReasonCode.userDisconnected - let reason = SessionType.Reason(code: reasonCode.code, message: reasonCode.message) + let reason = ReasonCode.userDisconnected + let protocolMethod = PairingProtocolMethod.delete logger.debug("Will delete pairing for reason: message: \(reason.message) code: \(reason.code)") - try await networkingInteractor.request(.wcSessionDelete(reason), onTopic: topic) + let request = RPCRequest(method: protocolMethod.method, params: reason) + try await networkingInteractor.request(request, topic: topic, protocolMethod: protocolMethod) pairingStorage.delete(topic: topic) kms.deleteSymmetricKey(for: topic) networkingInteractor.unsubscribe(topic: topic) diff --git a/Sources/WalletConnectPairing/Services/Common/ExpirationService.swift b/Sources/WalletConnectPairing/Services/Common/ExpirationService.swift new file mode 100644 index 000000000..d973e093a --- /dev/null +++ b/Sources/WalletConnectPairing/Services/Common/ExpirationService.swift @@ -0,0 +1,22 @@ +import Foundation +import WalletConnectNetworking +import WalletConnectKMS + +final class ExpirationService { + private let pairingStorage: WCPairingStorage + private let networkInteractor: NetworkInteracting + private let kms: KeyManagementServiceProtocol + + init(pairingStorage: WCPairingStorage, networkInteractor: NetworkInteracting, kms: KeyManagementServiceProtocol) { + self.pairingStorage = pairingStorage + self.networkInteractor = networkInteractor + self.kms = kms + } + + func setupExpirationHandling() { + pairingStorage.onPairingExpiration = { [weak self] pairing in + self?.kms.deleteSymmetricKey(for: pairing.topic) + self?.networkInteractor.unsubscribe(topic: pairing.topic) + } + } +} diff --git a/Sources/WalletConnectPairing/Services/Common/PairingsProvider.swift b/Sources/WalletConnectPairing/Services/Common/PairingsProvider.swift new file mode 100644 index 000000000..aa087a3c0 --- /dev/null +++ b/Sources/WalletConnectPairing/Services/Common/PairingsProvider.swift @@ -0,0 +1,24 @@ +import Foundation + +class PairingsProvider { + enum Errors: Error { + case noPairingMatchingTopic + } + private let pairingStorage: WCPairingStorage + + public init(pairingStorage: WCPairingStorage) { + self.pairingStorage = pairingStorage + } + + func getPairings() -> [Pairing] { + pairingStorage.getAll() + .map {Pairing($0)} + } + + func getPairing(for topic: String) throws -> Pairing { + guard let pairing = pairingStorage.getPairing(forTopic: topic) else { + throw Errors.noPairingMatchingTopic + } + return Pairing(pairing) + } +} diff --git a/Sources/WalletConnectPairing/Services/PairingPingService.swift b/Sources/WalletConnectPairing/Services/Common/Ping/PairingPingService.swift similarity index 50% rename from Sources/WalletConnectPairing/Services/PairingPingService.swift rename to Sources/WalletConnectPairing/Services/Common/Ping/PairingPingService.swift index 6baef98e0..8cd852eb9 100644 --- a/Sources/WalletConnectPairing/Services/PairingPingService.swift +++ b/Sources/WalletConnectPairing/Services/Common/Ping/PairingPingService.swift @@ -1,13 +1,14 @@ -import WalletConnectUtils import Foundation +import WalletConnectUtils import WalletConnectNetworking public class PairingPingService { + private let pairingStorage: WCPairingStorage private let pingRequester: PingRequester private let pingResponder: PingResponder private let pingResponseSubscriber: PingResponseSubscriber - public var onResponse: ((String)->())? { + public var onResponse: ((String)->Void)? { get { return pingResponseSubscriber.onResponse } @@ -20,13 +21,15 @@ public class PairingPingService { pairingStorage: WCPairingStorage, networkingInteractor: NetworkInteracting, logger: ConsoleLogging) { - pingRequester = PingRequester(pairingStorage: pairingStorage, networkingInteractor: networkingInteractor) - pingResponder = PingResponder(networkingInteractor: networkingInteractor, logger: logger) - pingResponseSubscriber = PingResponseSubscriber(networkingInteractor: networkingInteractor, logger: logger) + let protocolMethod = PairingProtocolMethod.ping + self.pairingStorage = pairingStorage + self.pingRequester = PingRequester(networkingInteractor: networkingInteractor, method: protocolMethod) + self.pingResponder = PingResponder(networkingInteractor: networkingInteractor, method: protocolMethod, logger: logger) + self.pingResponseSubscriber = PingResponseSubscriber(networkingInteractor: networkingInteractor, method: protocolMethod, logger: logger) } public func ping(topic: String) async throws { + guard pairingStorage.hasPairing(forTopic: topic) else { return } try await pingRequester.ping(topic: topic) } - } diff --git a/Sources/WalletConnectPairing/Services/Common/Ping/PingRequester.swift b/Sources/WalletConnectPairing/Services/Common/Ping/PingRequester.swift new file mode 100644 index 000000000..f5007c9c6 --- /dev/null +++ b/Sources/WalletConnectPairing/Services/Common/Ping/PingRequester.swift @@ -0,0 +1,18 @@ +import Foundation +import JSONRPC +import WalletConnectNetworking + +public class PingRequester { + private let method: ProtocolMethod + private let networkingInteractor: NetworkInteracting + + public init(networkingInteractor: NetworkInteracting, method: ProtocolMethod) { + self.method = method + self.networkingInteractor = networkingInteractor + } + + public func ping(topic: String) async throws { + let request = RPCRequest(method: method.method, params: PairingPingParams()) + try await networkingInteractor.request(request, topic: topic, protocolMethod: method) + } +} diff --git a/Sources/WalletConnectPairing/Services/PingResponder.swift b/Sources/WalletConnectPairing/Services/Common/Ping/PingResponder.swift similarity index 70% rename from Sources/WalletConnectPairing/Services/PingResponder.swift rename to Sources/WalletConnectPairing/Services/Common/Ping/PingResponder.swift index 9ed8c0806..60835c29c 100644 --- a/Sources/WalletConnectPairing/Services/PingResponder.swift +++ b/Sources/WalletConnectPairing/Services/Common/Ping/PingResponder.swift @@ -2,24 +2,27 @@ import Combine import WalletConnectUtils import WalletConnectNetworking -class PingResponder { +public class PingResponder { private let networkingInteractor: NetworkInteracting + private let method: ProtocolMethod private let logger: ConsoleLogging private var publishers = [AnyCancellable]() - init(networkingInteractor: NetworkInteracting, + public init(networkingInteractor: NetworkInteracting, + method: ProtocolMethod, logger: ConsoleLogging) { self.networkingInteractor = networkingInteractor + self.method = method self.logger = logger subscribePingRequests() } private func subscribePingRequests() { - networkingInteractor.requestSubscription(on: PairingProtocolMethod.ping) + networkingInteractor.requestSubscription(on: method) .sink { [unowned self] (payload: RequestSubscriptionPayload) in logger.debug("Responding for pairing ping") Task(priority: .high) { - try? await networkingInteractor.respondSuccess(topic: payload.topic, requestId: payload.id, tag: PairingProtocolMethod.ping.responseTag) + try? await networkingInteractor.respondSuccess(topic: payload.topic, requestId: payload.id, protocolMethod: method) } } .store(in: &publishers) diff --git a/Sources/WalletConnectPairing/Services/PingResponseSubscriber.swift b/Sources/WalletConnectPairing/Services/Common/Ping/PingResponseSubscriber.swift similarity index 51% rename from Sources/WalletConnectPairing/Services/PingResponseSubscriber.swift rename to Sources/WalletConnectPairing/Services/Common/Ping/PingResponseSubscriber.swift index d2ce74a89..7da5c8a56 100644 --- a/Sources/WalletConnectPairing/Services/PingResponseSubscriber.swift +++ b/Sources/WalletConnectPairing/Services/Common/Ping/PingResponseSubscriber.swift @@ -2,24 +2,35 @@ import Combine import WalletConnectUtils import WalletConnectNetworking -class PingResponseSubscriber { +public class PingResponseSubscriber { private let networkingInteractor: NetworkInteracting + private let method: ProtocolMethod private let logger: ConsoleLogging private var publishers = [AnyCancellable]() - var onResponse: ((String)->())? + public var onResponse: ((String)->Void)? - init(networkingInteractor: NetworkInteracting, + public init(networkingInteractor: NetworkInteracting, + method: ProtocolMethod, logger: ConsoleLogging) { self.networkingInteractor = networkingInteractor + self.method = method self.logger = logger subscribePingResponses() } private func subscribePingResponses() { - networkingInteractor.responseSubscription(on: PairingProtocolMethod.ping) + networkingInteractor.responseSubscription(on: method) .sink { [unowned self] (payload: ResponseSubscriptionPayload) in onResponse?(payload.topic) + + Task(priority: .high) { + try await networkingInteractor.respondSuccess( + topic: payload.topic, + requestId: payload.id, + protocolMethod: method + ) + } } .store(in: &publishers) } diff --git a/Sources/WalletConnectPairing/Services/Common/ResubscribeService.swift b/Sources/WalletConnectPairing/Services/Common/ResubscribeService.swift new file mode 100644 index 000000000..0c5c091fd --- /dev/null +++ b/Sources/WalletConnectPairing/Services/Common/ResubscribeService.swift @@ -0,0 +1,29 @@ +import Foundation +import Combine +import WalletConnectNetworking + +final class ResubscribeService { + + private var publishers = Set() + + private let networkInteractor: NetworkInteracting + private let pairingStorage: PairingStorage + + init(networkInteractor: NetworkInteracting, pairingStorage: PairingStorage) { + self.networkInteractor = networkInteractor + self.pairingStorage = pairingStorage + setUpResubscription() + } + + func setUpResubscription() { + networkInteractor.socketConnectionStatusPublisher + .sink { [unowned self] status in + guard status == .connected else { return } + pairingStorage.getAll() + .forEach { pairing in + Task(priority: .high) { try await networkInteractor.subscribe(topic: pairing.topic) } + } + } + .store(in: &publishers) + } +} diff --git a/Sources/WalletConnectPairing/Services/PingRequester.swift b/Sources/WalletConnectPairing/Services/PingRequester.swift deleted file mode 100644 index f936a8c9c..000000000 --- a/Sources/WalletConnectPairing/Services/PingRequester.swift +++ /dev/null @@ -1,19 +0,0 @@ -import Foundation -import WalletConnectNetworking -import JSONRPC - -class PingRequester { - private let pairingStorage: WCPairingStorage - private let networkingInteractor: NetworkInteracting - - init(pairingStorage: WCPairingStorage, networkingInteractor: NetworkInteracting) { - self.pairingStorage = pairingStorage - self.networkingInteractor = networkingInteractor - } - - func ping(topic: String) async throws { - guard pairingStorage.hasPairing(forTopic: topic) else { return } - let request = RPCRequest(method: PairingProtocolMethod.ping.rawValue, params: PairingPingParams()) - try await networkingInteractor.request(request, topic: topic, tag: PairingProtocolMethod.ping.requestTag) - } -} diff --git a/Sources/Auth/Services/Wallet/WalletPairService.swift b/Sources/WalletConnectPairing/Services/Wallet/WalletPairService.swift similarity index 88% rename from Sources/Auth/Services/Wallet/WalletPairService.swift rename to Sources/WalletConnectPairing/Services/Wallet/WalletPairService.swift index 7868c01a4..97b168564 100644 --- a/Sources/Auth/Services/Wallet/WalletPairService.swift +++ b/Sources/WalletConnectPairing/Services/Wallet/WalletPairService.swift @@ -1,15 +1,15 @@ import Foundation import WalletConnectKMS -import WalletConnectPairing import WalletConnectNetworking +import WalletConnectUtils actor WalletPairService { enum Errors: Error { case pairingAlreadyExist } - private let networkingInteractor: NetworkInteracting - private let kms: KeyManagementServiceProtocol + let networkingInteractor: NetworkInteracting + let kms: KeyManagementServiceProtocol private let pairingStorage: WCPairingStorage init(networkingInteractor: NetworkInteracting, diff --git a/Sources/Auth/StorageDomainIdentifiers.swift b/Sources/WalletConnectPairing/StorageDomainIdentifiers.swift similarity index 62% rename from Sources/Auth/StorageDomainIdentifiers.swift rename to Sources/WalletConnectPairing/StorageDomainIdentifiers.swift index ed7156c67..c328999c6 100644 --- a/Sources/Auth/StorageDomainIdentifiers.swift +++ b/Sources/WalletConnectPairing/StorageDomainIdentifiers.swift @@ -1,6 +1,5 @@ import Foundation enum StorageDomainIdentifiers: String { - case jsonRpcHistory = "com.walletconnect.sdk.wc_jsonRpcHistoryRecord" case pairings = "com.walletconnect.sdk.pairingSequences" } diff --git a/Sources/WalletConnectPairing/Types/AppMetadata.swift b/Sources/WalletConnectPairing/Types/AppMetadata.swift index 531bd5fe3..5a039740d 100644 --- a/Sources/WalletConnectPairing/Types/AppMetadata.swift +++ b/Sources/WalletConnectPairing/Types/AppMetadata.swift @@ -11,6 +11,26 @@ import Foundation */ public struct AppMetadata: Codable, Equatable { + public struct Redirect: Codable, Equatable { + /// Native deeplink URL string. + public let native: String? + + /// Universal link URL string. + public let universal: String? + + /** + Creates a new Redirect object with the specified information. + + - parameters: + - native: Native deeplink URL string. + - universal: Universal link URL string. + */ + public init(native: String?, universal: String?) { + self.native = native + self.universal = universal + } + } + /// The name of the app. public let name: String @@ -23,6 +43,9 @@ public struct AppMetadata: Codable, Equatable { /// An array of URL strings pointing to the icon assets on the web. public let icons: [String] + /// Redirect links which could be manually used on wallet side + public let redirect: Redirect? + /** Creates a new metadata object with the specified information. @@ -32,10 +55,11 @@ public struct AppMetadata: Codable, Equatable { - url: The URL string that identifies the official domain of the app. - icons: An array of URL strings pointing to the icon assets on the web. */ - public init(name: String, description: String, url: String, icons: [String]) { + public init(name: String, description: String, url: String, icons: [String], redirect: Redirect? = nil) { self.name = name self.description = description self.url = url self.icons = icons + self.redirect = redirect } } diff --git a/Sources/WalletConnectPairing/Types/PairError.swift b/Sources/WalletConnectPairing/Types/PairError.swift new file mode 100644 index 000000000..419edd9b3 --- /dev/null +++ b/Sources/WalletConnectPairing/Types/PairError.swift @@ -0,0 +1,26 @@ +import WalletConnectNetworking + +public enum PairError: Codable, Equatable, Error, Reason { + case methodUnsupported + + public init?(code: Int) { + switch code { + case Self.methodUnsupported.code: + self = .methodUnsupported + default: + return nil + } + } + + public var code: Int { + switch self { + case .methodUnsupported: + return 10001 + } + } + + public var message: String { + return "Method Unsupported" + } + +} diff --git a/Sources/WalletConnectPairing/Types/Pairing.swift b/Sources/WalletConnectPairing/Types/Pairing.swift index f9886d6a2..03ed01a41 100644 --- a/Sources/WalletConnectPairing/Types/Pairing.swift +++ b/Sources/WalletConnectPairing/Types/Pairing.swift @@ -12,4 +12,10 @@ public struct Pairing { self.peer = peer self.expiryDate = expiryDate } + + init(_ pairing: WCPairing) { + self.topic = pairing.topic + self.peer = pairing.peerMetadata + self.expiryDate = pairing.expiryDate + } } diff --git a/Sources/WalletConnectPairing/Types/PairingProtocolMethod.swift b/Sources/WalletConnectPairing/Types/PairingProtocolMethod.swift new file mode 100644 index 000000000..f121a62a9 --- /dev/null +++ b/Sources/WalletConnectPairing/Types/PairingProtocolMethod.swift @@ -0,0 +1,43 @@ +import Foundation +import WalletConnectNetworking + +enum PairingProtocolMethod: CaseIterable, ProtocolMethod { + case ping + case delete + + var method: String { + switch self { + case .ping: + return "wc_pairingPing" + case .delete: + return "wc_pairingDelete" + } + } + + var requestConfig: RelayConfig { + switch self { + case .ping: + return RelayConfig(tag: 1002, prompt: false, ttl: 30) + case .delete: + return RelayConfig(tag: 1003, prompt: false, ttl: 30) + } + } + + var responseConfig: RelayConfig { + switch self { + case .ping: + return RelayConfig(tag: 1003, prompt: false, ttl: 30) + case .delete: + return RelayConfig(tag: 1001, prompt: false, ttl: 86400) + } + } +} + +struct UnsupportedProtocolMethod: ProtocolMethod { + let method: String + + // TODO - spec tag + let requestConfig = RelayConfig(tag: 0, prompt: false, ttl: 86400) + + let responseConfig = RelayConfig(tag: 0, prompt: false, ttl: 86400) +} diff --git a/Sources/WalletConnectPairing/Types/ReasonCode.swift b/Sources/WalletConnectPairing/Types/ReasonCode.swift new file mode 100644 index 000000000..e5f633835 --- /dev/null +++ b/Sources/WalletConnectPairing/Types/ReasonCode.swift @@ -0,0 +1,14 @@ +import Foundation +import WalletConnectNetworking + +enum ReasonCode: Reason, Codable { + case userDisconnected + + var code: Int { + return 6000 + } + + var message: String { + return "User Disconnected" + } +} diff --git a/Sources/WalletConnectPush/ProposalResponseSubscriber.swift b/Sources/WalletConnectPush/ProposalResponseSubscriber.swift new file mode 100644 index 000000000..8a2e0b750 --- /dev/null +++ b/Sources/WalletConnectPush/ProposalResponseSubscriber.swift @@ -0,0 +1,33 @@ +import Foundation +import Combine +import JSONRPC +import WalletConnectUtils +import WalletConnectKMS +import WalletConnectNetworking +import WalletConnectPairing + +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/PushClient.swift b/Sources/WalletConnectPush/PushClient.swift new file mode 100644 index 000000000..4e4d170bd --- /dev/null +++ b/Sources/WalletConnectPush/PushClient.swift @@ -0,0 +1,63 @@ +import Foundation +import JSONRPC +import Combine +import WalletConnectKMS +import WalletConnectUtils +import WalletConnectNetworking +import WalletConnectPairing + +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 new file mode 100644 index 000000000..f9715f86d --- /dev/null +++ b/Sources/WalletConnectPush/PushClientFactory.swift @@ -0,0 +1,23 @@ +import Foundation +import WalletConnectRelay +import WalletConnectUtils +import WalletConnectKMS +import WalletConnectNetworking +import WalletConnectPairing + +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 new file mode 100644 index 000000000..60d5ee6bf --- /dev/null +++ b/Sources/WalletConnectPush/PushProposeProtocolMethod.swift @@ -0,0 +1,14 @@ +import Foundation +import WalletConnectNetworking + +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/PushProposer.swift b/Sources/WalletConnectPush/PushProposer.swift new file mode 100644 index 000000000..6a08305fe --- /dev/null +++ b/Sources/WalletConnectPush/PushProposer.swift @@ -0,0 +1,27 @@ +import Foundation +import Combine +import JSONRPC +import WalletConnectUtils +import WalletConnectKMS +import WalletConnectNetworking + +class PushProposer { + private let networkingInteractor: NetworkInteracting + private let kms: KeyManagementServiceProtocol + private let logger: ConsoleLogging + + init(networkingInteractor: NetworkInteracting, + kms: KeyManagementServiceProtocol, + logger: ConsoleLogging) { + self.networkingInteractor = networkingInteractor + self.kms = kms + 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) + } +} diff --git a/Sources/WalletConnectRelay/ClientAuth/ClientIdStorage.swift b/Sources/WalletConnectRelay/ClientAuth/ClientIdStorage.swift index 826ae67e6..7d945bcda 100644 --- a/Sources/WalletConnectRelay/ClientAuth/ClientIdStorage.swift +++ b/Sources/WalletConnectRelay/ClientAuth/ClientIdStorage.swift @@ -3,14 +3,18 @@ import WalletConnectKMS protocol ClientIdStoring { func getOrCreateKeyPair() throws -> SigningPrivateKey + func getClientId() throws -> String } struct ClientIdStorage: ClientIdStoring { private let key = "com.walletconnect.iridium.client_id" private let keychain: KeychainStorageProtocol + private let didKeyFactory: DIDKeyFactory - init(keychain: KeychainStorageProtocol) { + init(keychain: KeychainStorageProtocol, + didKeyFactory: DIDKeyFactory) { self.keychain = keychain + self.didKeyFactory = didKeyFactory } func getOrCreateKeyPair() throws -> SigningPrivateKey { @@ -22,4 +26,10 @@ struct ClientIdStorage: ClientIdStoring { return privateKey } } + + func getClientId() throws -> String { + let privateKey: SigningPrivateKey = try keychain.read(key: key) + let pubKey = privateKey.publicKey.rawRepresentation + return didKeyFactory.make(pubKey: pubKey, prefix: true) + } } diff --git a/Sources/WalletConnectRelay/Dispatching.swift b/Sources/WalletConnectRelay/Dispatching.swift index 2d8be9ba5..d08c7e0bf 100644 --- a/Sources/WalletConnectRelay/Dispatching.swift +++ b/Sources/WalletConnectRelay/Dispatching.swift @@ -1,25 +1,33 @@ import Foundation +import Combine import WalletConnectUtils protocol Dispatching { - var onConnect: (() -> Void)? {get set} - var onDisconnect: (() -> Void)? {get set} - var onMessage: ((String) -> Void)? {get set} - func send(_ string: String) async throws + var onMessage: ((String) -> Void)? { get set } + var socketConnectionStatusPublisher: AnyPublisher { get } func send(_ string: String, completion: @escaping (Error?) -> Void) + func protectedSend(_ string: String, completion: @escaping (Error?) -> Void) + func protectedSend(_ string: String) async throws func connect() throws func disconnect(closeCode: URLSessionWebSocketTask.CloseCode) throws } final class Dispatcher: NSObject, Dispatching { - var onConnect: (() -> Void)? - var onDisconnect: (() -> Void)? var onMessage: ((String) -> Void)? - private var textFramesQueue = Queue() - private let logger: ConsoleLogging var socket: WebSocketConnecting var socketConnectionHandler: SocketConnectionHandler + private let logger: ConsoleLogging + private let defaultTimeout: Int = 5 + + private let socketConnectionStatusPublisherSubject = PassthroughSubject() + + var socketConnectionStatusPublisher: AnyPublisher { + socketConnectionStatusPublisherSubject.eraseToAnyPublisher() + } + + private let concurrentQueue = DispatchQueue(label: "com.walletconnect.sdk.dispatcher", attributes: .concurrent) + init(socket: WebSocketConnecting, socketConnectionHandler: SocketConnectionHandler, logger: ConsoleLogging) { @@ -31,28 +39,48 @@ final class Dispatcher: NSObject, Dispatching { setUpSocketConnectionObserving() } - func send(_ string: String) async throws { - return try await withCheckedThrowingContinuation { continuation in - if socket.isConnected { - socket.write(string: string) { - continuation.resume(returning: ()) - } - } else { - continuation.resume(throwing: NetworkError.webSocketNotConnected) - } - } - } - func send(_ string: String, completion: @escaping (Error?) -> Void) { - // TODO - add policy for retry and "single try" if socket.isConnected { self.socket.write(string: string) { completion(nil) } - // TODO - enqueue if fails } else { completion(NetworkError.webSocketNotConnected) -// textFramesQueue.enqueue(string) + } + } + + func protectedSend(_ string: String, completion: @escaping (Error?) -> Void) { + guard !socket.isConnected else { + return send(string, completion: completion) + } + + var cancellable: AnyCancellable? + cancellable = socketConnectionStatusPublisher + .filter { $0 == .connected } + .setFailureType(to: NetworkError.self) + .timeout(.seconds(defaultTimeout), scheduler: concurrentQueue, customError: { .webSocketNotConnected }) + .sink(receiveCompletion: { result in + switch result { + case .failure(let error): + cancellable?.cancel() + completion(error) + case .finished: break + } + }, receiveValue: { [unowned self] _ in + cancellable?.cancel() + send(string, completion: completion) + }) + } + + func protectedSend(_ string: String) async throws { + return try await withCheckedThrowingContinuation { continuation in + protectedSend(string) { error in + if let error = error { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: ()) + } + } } } @@ -72,21 +100,10 @@ final class Dispatcher: NSObject, Dispatching { private func setUpSocketConnectionObserving() { socket.onConnect = { [unowned self] in - self.dequeuePendingTextFrames() - self.onConnect?() + self.socketConnectionStatusPublisherSubject.send(.connected) } socket.onDisconnect = { [unowned self] _ in - self.onDisconnect?() - } - } - - private func dequeuePendingTextFrames() { - while let frame = textFramesQueue.dequeue() { - send(frame) { [unowned self] error in - if let error = error { - self.logger.error(error.localizedDescription) - } - } + self.socketConnectionStatusPublisherSubject.send(.disconnected) } } } diff --git a/Sources/WalletConnectRelay/EnvironmentInfo.swift b/Sources/WalletConnectRelay/EnvironmentInfo.swift index e00872260..7dab63f3b 100644 --- a/Sources/WalletConnectRelay/EnvironmentInfo.swift +++ b/Sources/WalletConnectRelay/EnvironmentInfo.swift @@ -25,8 +25,9 @@ enum EnvironmentInfo { #if os(iOS) return "\(UIDevice.current.systemName)-\(UIDevice.current.systemVersion)" #elseif os(macOS) - let systemVersion = ProcessInfo.processInfo.operatingSystemVersion - return "macOS-\(systemVersion)" + return "macOS-\(ProcessInfo.processInfo.operatingSystemVersion)" +#elseif os(tvOS) + return "tvOS-\(ProcessInfo.processInfo.operatingSystemVersion)" #endif } } diff --git a/Sources/WalletConnectRelay/Misc/Time.swift b/Sources/WalletConnectRelay/Misc/Time.swift deleted file mode 100644 index 5aaaafb87..000000000 --- a/Sources/WalletConnectRelay/Misc/Time.swift +++ /dev/null @@ -1,7 +0,0 @@ -// - -import Foundation - -enum Time { - static let hour = 3600 -} diff --git a/Sources/WalletConnectRelay/RelayClient.swift b/Sources/WalletConnectRelay/RelayClient.swift index 94ab8ceab..0a156c7f5 100644 --- a/Sources/WalletConnectRelay/RelayClient.swift +++ b/Sources/WalletConnectRelay/RelayClient.swift @@ -20,9 +20,6 @@ public final class RelayClient { case subscriptionIdNotFound } - static let historyIdentifier = "com.walletconnect.sdk.relayer_client.subscription_json_rpc_record" - - let defaultTtl = 6*Time.hour var subscriptions: [String: String] = [:] public var messagePublisher: AnyPublisher<(topic: String, message: String), Never> { @@ -30,11 +27,10 @@ public final class RelayClient { } public var socketConnectionStatusPublisher: AnyPublisher { - socketConnectionStatusPublisherSubject.eraseToAnyPublisher() + dispatcher.socketConnectionStatusPublisher } private let messagePublisherSubject = PassthroughSubject<(topic: String, message: String), Never>() - private let socketConnectionStatusPublisherSubject = PassthroughSubject() private let subscriptionResponsePublisherSubject = PassthroughSubject<(RPCID?, String), Never>() private var subscriptionResponsePublisher: AnyPublisher<(RPCID?, String), Never> { @@ -45,6 +41,8 @@ public final class RelayClient { requestAcknowledgePublisherSubject.eraseToAnyPublisher() } + private let clientIdStorage: ClientIdStoring + private var dispatcher: Dispatching private let rpcHistory: RPCHistory private let logger: ConsoleLogging @@ -56,11 +54,13 @@ public final class RelayClient { init( dispatcher: Dispatching, logger: ConsoleLogging, - keyValueStorage: KeyValueStorage + keyValueStorage: KeyValueStorage, + clientIdStorage: ClientIdStoring ) { self.logger = logger self.dispatcher = dispatcher - self.rpcHistory = RPCHistory(keyValueStore: CodableStore(defaults: keyValueStorage, identifier: Self.historyIdentifier)) + self.rpcHistory = RPCHistoryFactory.createForRelay(keyValueStorage: keyValueStorage) + self.clientIdStorage = clientIdStorage setUpBindings() } @@ -68,9 +68,6 @@ public final class RelayClient { dispatcher.onMessage = { [weak self] payload in self?.handlePayloadMessage(payload) } - dispatcher.onConnect = { [unowned self] in - self.socketConnectionStatusPublisherSubject.send(.connected) - } } /// Instantiates Relay Client @@ -89,9 +86,11 @@ public final class RelayClient { socketConnectionType: SocketConnectionType = .automatic, logger: ConsoleLogging = ConsoleLogger(loggingLevel: .off) ) { + let didKeyFactory = ED25519DIDKeyFactory() + let clientIdStorage = ClientIdStorage(keychain: keychainStorage, didKeyFactory: didKeyFactory) let socketAuthenticator = SocketAuthenticator( - clientIdStorage: ClientIdStorage(keychain: keychainStorage), - didKeyFactory: ED25519DIDKeyFactory(), + clientIdStorage: clientIdStorage, + didKeyFactory: didKeyFactory, relayHost: relayHost ) let relayUrlFactory = RelayUrlFactory(socketAuthenticator: socketAuthenticator) @@ -108,7 +107,7 @@ public final class RelayClient { socketConnectionHandler = ManualSocketConnectionHandler(socket: socket) } let dispatcher = Dispatcher(socket: socket, socketConnectionHandler: socketConnectionHandler, logger: logger) - self.init(dispatcher: dispatcher, logger: logger, keyValueStorage: keyValueStorage) + self.init(dispatcher: dispatcher, logger: logger, keyValueStorage: keyValueStorage, clientIdStorage: clientIdStorage) } /// Connects web socket @@ -126,13 +125,13 @@ public final class RelayClient { } /// Completes when networking client sends a request, error if it fails on client side - public func publish(topic: String, payload: String, tag: Int, prompt: Bool = false) async throws { - let request = Publish(params: .init(topic: topic, message: payload, ttl: defaultTtl, prompt: prompt, tag: tag)) + public func publish(topic: String, payload: String, tag: Int, prompt: Bool, ttl: Int) async throws { + let request = Publish(params: .init(topic: topic, message: payload, ttl: ttl, prompt: prompt, tag: tag)) .wrapToIRN() .asRPCRequest() let message = try request.asJSONEncodedString() logger.debug("Publishing payload on topic: \(topic)") - try await dispatcher.send(message) + try await dispatcher.protectedSend(message) } /// Completes with an acknowledgement from the relay network. @@ -140,10 +139,11 @@ public final class RelayClient { topic: String, payload: String, tag: Int, - prompt: Bool = false, + prompt: Bool, + ttl: Int, onNetworkAcknowledge: @escaping ((Error?) -> Void) ) { - let rpc = Publish(params: .init(topic: topic, message: payload, ttl: defaultTtl, prompt: prompt, tag: tag)) + let rpc = Publish(params: .init(topic: topic, message: payload, ttl: ttl, prompt: prompt, tag: tag)) let request = rpc .wrapToIRN() .asRPCRequest() @@ -156,7 +156,7 @@ public final class RelayClient { cancellable?.cancel() onNetworkAcknowledge(nil) } - dispatcher.send(message) { [weak self] error in + dispatcher.protectedSend(message) { [weak self] error in if let error = error { self?.logger.debug("Failed to Publish Payload, error: \(error)") cancellable?.cancel() @@ -183,7 +183,7 @@ public final class RelayClient { } completion(nil) } - dispatcher.send(message) { [weak self] error in + dispatcher.protectedSend(message) { [weak self] error in if let error = error { self?.logger.debug("Failed to subscribe to topic \(error)") cancellable?.cancel() @@ -223,7 +223,7 @@ public final class RelayClient { cancellable?.cancel() completion(nil) } - dispatcher.send(message) { [weak self] error in + dispatcher.protectedSend(message) { [weak self] error in if let error = error { self?.logger.debug("Failed to unsubscribe from topic") cancellable?.cancel() @@ -237,6 +237,10 @@ public final class RelayClient { } } + public func getClientId() throws -> String { + try clientIdStorage.getClientId() + } + // FIXME: Parse data to string once before trying to decode -> respond error on fail private func handlePayloadMessage(_ payload: String) { if let request = tryDecode(RPCRequest.self, from: payload) { @@ -253,13 +257,13 @@ public final class RelayClient { } } else if let response = tryDecode(RPCResponse.self, from: payload) { switch response.outcome { - case .success(let anyCodable): + case .response(let anyCodable): if let _ = try? anyCodable.get(Bool.self) { // TODO: Handle success vs. error requestAcknowledgePublisherSubject.send(response.id) } else if let subscriptionId = try? anyCodable.get(String.self) { subscriptionResponsePublisherSubject.send((response.id, subscriptionId)) } - case .failure(let rpcError): + case .error(let rpcError): logger.error("Received RPC error from relay network: \(rpcError)") } } else { @@ -279,7 +283,7 @@ public final class RelayClient { private func acknowledgeRequest(_ request: RPCRequest) throws { let response = RPCResponse(matchingRequest: request, result: true) let message = try response.asJSONEncodedString() - dispatcher.send(message) { [unowned self] in + dispatcher.protectedSend(message) { [unowned self] in if let error = $0 { logger.debug("Failed to dispatch response: \(response), error: \(error)") } else { diff --git a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift index 229e2e698..9f8fcbcf1 100644 --- a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift @@ -1,8 +1,10 @@ import Foundation import Combine +import JSONRPC import WalletConnectUtils import WalletConnectKMS import WalletConnectPairing +import WalletConnectNetworking final class ApproveEngine { enum Errors: Error { @@ -10,12 +12,12 @@ final class ApproveEngine { case relayNotFound case proposalPayloadsNotFound case pairingNotFound + case sessionNotFound case agreementMissingOrInvalid - case respondError(payload: WCRequestSubscriptionPayload, reason: ReasonCode) } var onSessionProposal: ((Session.Proposal) -> Void)? - var onSessionRejected: ((Session.Proposal, SessionType.Reason) -> Void)? + var onSessionRejected: ((Session.Proposal, Reason) -> Void)? var onSessionSettle: ((Session) -> Void)? var settlingProposal: SessionProposal? @@ -23,8 +25,9 @@ final class ApproveEngine { private let networkingInteractor: NetworkInteracting private let pairingStore: WCPairingStorage private let sessionStore: WCSessionStorage - private let proposalPayloadsStore: CodableStore + private let proposalPayloadsStore: CodableStore> private let sessionToPairingTopic: CodableStore + private let pairingRegisterer: PairingRegisterer private let metadata: AppMetadata private let kms: KeyManagementServiceProtocol private let logger: ConsoleLogging @@ -33,8 +36,9 @@ final class ApproveEngine { init( networkingInteractor: NetworkInteracting, - proposalPayloadsStore: CodableStore, + proposalPayloadsStore: CodableStore>, sessionToPairingTopic: CodableStore, + pairingRegisterer: PairingRegisterer, metadata: AppMetadata, kms: KeyManagementServiceProtocol, logger: ConsoleLogging, @@ -44,22 +48,25 @@ final class ApproveEngine { self.networkingInteractor = networkingInteractor self.proposalPayloadsStore = proposalPayloadsStore self.sessionToPairingTopic = sessionToPairingTopic + self.pairingRegisterer = pairingRegisterer self.metadata = metadata self.kms = kms self.logger = logger self.pairingStore = pairingStore self.sessionStore = sessionStore - setupNetworkingSubscriptions() + setupRequestSubscriptions() + setupResponseSubscriptions() + setupResponseErrorSubscriptions() } func approveProposal(proposerPubKey: String, validating sessionNamespaces: [String: SessionNamespace]) async throws { - let payload = try proposalPayloadsStore.get(key: proposerPubKey) - - guard let payload = payload, case .sessionPropose(let proposal) = payload.wcRequest.params else { + guard let payload = try proposalPayloadsStore.get(key: proposerPubKey) else { throw Errors.wrongRequestParams } + let proposal = payload.request + proposalPayloadsStore.delete(forKey: proposerPubKey) try Namespace.validate(sessionNamespaces) @@ -79,14 +86,13 @@ final class ApproveEngine { throw Errors.relayNotFound } - let proposeResponse = SessionType.ProposeResponse(relay: relay, responderPublicKey: selfPublicKey.hexRepresentation) - let response = JSONRPCResponse(id: payload.wcRequest.id, result: AnyCodable(proposeResponse)) - guard var pairing = pairingStore.getPairing(forTopic: payload.topic) else { throw Errors.pairingNotFound } - try await networkingInteractor.respond(topic: payload.topic, response: .response(response), tag: payload.wcRequest.responseTag) + let result = SessionType.ProposeResponse(relay: relay, responderPublicKey: selfPublicKey.hexRepresentation) + let response = RPCResponse(id: payload.id, result: result) + try await networkingInteractor.respond(topic: payload.topic, response: response, protocolMethod: SessionProposeProtocolMethod()) try pairing.updateExpiry() pairingStore.setPairing(pairing) @@ -99,7 +105,7 @@ final class ApproveEngine { throw Errors.proposalPayloadsNotFound } proposalPayloadsStore.delete(forKey: proposerPubKey) - try await networkingInteractor.respondError(payload: payload, reason: reason) + try await networkingInteractor.respondError(topic: payload.topic, requestId: payload.id, protocolMethod: SessionProposeProtocolMethod(), reason: reason) // TODO: Delete pairing if inactive } @@ -140,7 +146,9 @@ final class ApproveEngine { try await networkingInteractor.subscribe(topic: topic) sessionStore.setSession(session) - try await networkingInteractor.request(.wcSessionSettle(settleParams), onTopic: topic) + let protocolMethod = SessionSettleProtocolMethod() + let request = RPCRequest(method: protocolMethod.method, params: settleParams) + try await networkingInteractor.request(request, topic: topic, protocolMethod: protocolMethod) onSessionSettle?(session.publicRepresentation()) } } @@ -149,81 +157,62 @@ final class ApproveEngine { private extension ApproveEngine { - func setupNetworkingSubscriptions() { - networkingInteractor.responsePublisher - .sink { [unowned self] response in - switch response.requestParams { - case .sessionPropose(let proposal): - handleSessionProposeResponse(response: response, proposal: proposal) - case .sessionSettle: - handleSessionSettleResponse(response: response) - default: - break - } + func setupRequestSubscriptions() { + pairingRegisterer.register(method: SessionProposeProtocolMethod()) + .sink { [unowned self] (payload: RequestSubscriptionPayload) in + handleSessionProposeRequest(payload: payload) + }.store(in: &publishers) + + networkingInteractor.requestSubscription(on: SessionSettleProtocolMethod()) + .sink { [unowned self] (payload: RequestSubscriptionPayload) in + handleSessionSettleRequest(payload: payload) + }.store(in: &publishers) + } + + func setupResponseSubscriptions() { + networkingInteractor.responseSubscription(on: SessionProposeProtocolMethod()) + .sink { [unowned self] (payload: ResponseSubscriptionPayload) in + handleSessionProposeResponse(payload: payload) }.store(in: &publishers) - networkingInteractor.wcRequestPublisher - .sink { [unowned self] subscriptionPayload in - do { - switch subscriptionPayload.wcRequest.params { - case .sessionPropose(let proposal): - try handleSessionProposeRequest(payload: subscriptionPayload, proposal: proposal) - case .sessionSettle(let settleParams): - try handleSessionSettleRequest(payload: subscriptionPayload, settleParams: settleParams) - default: return - } - } catch Errors.respondError(let payload, let reason) { - respondError(payload: payload, reason: reason) - } catch { - logger.error("Unexpected Error: \(error.localizedDescription)") - } + networkingInteractor.responseSubscription(on: SessionSettleProtocolMethod()) + .sink { [unowned self] (payload: ResponseSubscriptionPayload) in + handleSessionSettleResponse(payload: payload) }.store(in: &publishers) } - func respondError(payload: WCRequestSubscriptionPayload, reason: ReasonCode) { - Task { + func setupResponseErrorSubscriptions() { + networkingInteractor.responseErrorSubscription(on: SessionProposeProtocolMethod()) + .sink { [unowned self] (payload: ResponseSubscriptionErrorPayload) in + handleSessionProposeResponseError(payload: payload) + }.store(in: &publishers) + + networkingInteractor.responseErrorSubscription(on: SessionSettleProtocolMethod()) + .sink { [unowned self] (payload: ResponseSubscriptionErrorPayload) in + handleSessionSettleResponseError(payload: payload) + }.store(in: &publishers) + } + + func respondError(payload: SubscriptionPayload, reason: ReasonCode, protocolMethod: ProtocolMethod) { + Task(priority: .high) { do { - try await networkingInteractor.respondError(payload: payload, reason: reason) + try await networkingInteractor.respondError(topic: payload.topic, requestId: payload.id, protocolMethod: protocolMethod, reason: reason) } catch { logger.error("Respond Error failed with: \(error.localizedDescription)") } } } - func updatePairingMetadata(topic: String, metadata: AppMetadata) { - guard var pairing = pairingStore.getPairing(forTopic: topic) else { return } - pairing.peerMetadata = metadata - pairingStore.setPairing(pairing) - } - // MARK: SessionProposeResponse // TODO: Move to Non-Controller SettleEngine - func handleSessionProposeResponse(response: WCResponse, proposal: SessionType.ProposeParams) { + func handleSessionProposeResponse(payload: ResponseSubscriptionPayload) { do { - let sessionTopic = try handleProposeResponse( - pairingTopic: response.topic, - proposal: proposal, - result: response.result - ) - settlingProposal = proposal + let pairingTopic = payload.topic - Task(priority: .high) { - try? await networkingInteractor.subscribe(topic: sessionTopic) + guard var pairing = pairingStore.getPairing(forTopic: pairingTopic) else { + throw Errors.pairingNotFound } - } catch { - guard let error = error as? JSONRPCErrorResponse else { - return logger.debug(error.localizedDescription) - } - onSessionRejected?(proposal.publicRepresentation(), SessionType.Reason(code: error.error.code, message: error.error.message)) - } - } - - func handleProposeResponse(pairingTopic: String, proposal: SessionProposal, result: JsonRpcResult) throws -> String { - guard var pairing = pairingStore.getPairing(forTopic: pairingTopic) - else { throw Errors.pairingNotFound } - switch result { - case .response(let response): // Activate the pairing if !pairing.active { pairing.activate() @@ -233,9 +222,8 @@ private extension ApproveEngine { pairingStore.setPairing(pairing) - let selfPublicKey = try AgreementPublicKey(hex: proposal.proposer.publicKey) - let proposeResponse = try response.result.get(SessionType.ProposeResponse.self) - let agreementKeys = try kms.performKeyAgreement(selfPublicKey: selfPublicKey, peerPublicKey: proposeResponse.responderPublicKey) + let selfPublicKey = try AgreementPublicKey(hex: payload.request.proposer.publicKey) + let agreementKeys = try kms.performKeyAgreement(selfPublicKey: selfPublicKey, peerPublicKey: payload.response.responderPublicKey) let sessionTopic = agreementKeys.derivedTopic() logger.debug("Received Session Proposal response") @@ -243,64 +231,94 @@ private extension ApproveEngine { try kms.setAgreementSecret(agreementKeys, topic: sessionTopic) sessionToPairingTopic.set(pairingTopic, forKey: sessionTopic) - return sessionTopic + settlingProposal = payload.request - case .error(let error): - if !pairing.active { - kms.deleteSymmetricKey(for: pairing.topic) - networkingInteractor.unsubscribe(topic: pairing.topic) - pairingStore.delete(topic: pairingTopic) + Task(priority: .high) { + try await networkingInteractor.subscribe(topic: sessionTopic) } - logger.debug("Session Proposal has been rejected") - kms.deletePrivateKey(for: proposal.proposer.publicKey) - throw error + } catch { + return logger.debug(error.localizedDescription) + } + } + + func handleSessionProposeResponseError(payload: ResponseSubscriptionErrorPayload) { + guard let pairing = pairingStore.getPairing(forTopic: payload.topic) else { + return logger.debug(Errors.pairingNotFound.localizedDescription) } + + if !pairing.active { + kms.deleteSymmetricKey(for: pairing.topic) + networkingInteractor.unsubscribe(topic: pairing.topic) + pairingStore.delete(topic: payload.topic) + } + logger.debug("Session Proposal has been rejected") + kms.deletePrivateKey(for: payload.request.proposer.publicKey) + + onSessionRejected?( + payload.request.publicRepresentation(), + SessionType.Reason(code: payload.error.code, message: payload.error.message) + ) } // MARK: SessionSettleResponse - func handleSessionSettleResponse(response: WCResponse) { - guard let session = sessionStore.getSession(forTopic: response.topic) else { return } - switch response.result { - case .response: - logger.debug("Received session settle response") - guard var session = sessionStore.getSession(forTopic: response.topic) else { return } - session.acknowledge() - sessionStore.setSession(session) - case .error(let error): - logger.error("Error - session rejected, Reason: \(error)") - networkingInteractor.unsubscribe(topic: response.topic) - sessionStore.delete(topic: response.topic) - kms.deleteAgreementSecret(for: response.topic) - kms.deletePrivateKey(for: session.publicKey!) + func handleSessionSettleResponse(payload: ResponseSubscriptionPayload) { + guard var session = sessionStore.getSession(forTopic: payload.topic) else { + return logger.debug(Errors.sessionNotFound.localizedDescription) } + + logger.debug("Received session settle response") + session.acknowledge() + sessionStore.setSession(session) + } + + func handleSessionSettleResponseError(payload: ResponseSubscriptionErrorPayload) { + guard let session = sessionStore.getSession(forTopic: payload.topic) else { + return logger.debug(Errors.sessionNotFound.localizedDescription) + } + + logger.error("Error - session rejected, Reason: \(payload.error)") + networkingInteractor.unsubscribe(topic: payload.topic) + sessionStore.delete(topic: payload.topic) + kms.deleteAgreementSecret(for: payload.topic) + kms.deletePrivateKey(for: session.publicKey!) } // MARK: SessionProposeRequest - func handleSessionProposeRequest(payload: WCRequestSubscriptionPayload, proposal: SessionType.ProposeParams) throws { + func handleSessionProposeRequest(payload: RequestSubscriptionPayload) { logger.debug("Received Session Proposal") - do { try Namespace.validate(proposal.requiredNamespaces) } catch { throw Errors.respondError(payload: payload, reason: .invalidUpdateRequest) } + let proposal = payload.request + do { try Namespace.validate(proposal.requiredNamespaces) } catch { + return respondError(payload: payload, reason: .invalidUpdateRequest, protocolMethod: SessionProposeProtocolMethod()) + } proposalPayloadsStore.set(payload, forKey: proposal.proposer.publicKey) onSessionProposal?(proposal.publicRepresentation()) } // MARK: SessionSettleRequest - func handleSessionSettleRequest(payload: WCRequestSubscriptionPayload, settleParams: SessionType.SettleParams) throws { + + func handleSessionSettleRequest(payload: RequestSubscriptionPayload) { logger.debug("Did receive session settle request") - guard let proposedNamespaces = settlingProposal?.requiredNamespaces - else { throw Errors.respondError(payload: payload, reason: .invalidUpdateRequest) } + let protocolMethod = SessionSettleProtocolMethod() + + guard let proposedNamespaces = settlingProposal?.requiredNamespaces else { + return respondError(payload: payload, reason: .invalidUpdateRequest, protocolMethod: protocolMethod) + } settlingProposal = nil - let sessionNamespaces = settleParams.namespaces + let params = payload.request + let sessionNamespaces = params.namespaces do { try Namespace.validate(sessionNamespaces) try Namespace.validateApproved(sessionNamespaces, against: proposedNamespaces) } catch WalletConnectError.unsupportedNamespace(let reason) { - throw Errors.respondError(payload: payload, reason: reason) + return respondError(payload: payload, reason: reason, protocolMethod: protocolMethod) + } catch { + return respondError(payload: payload, reason: .invalidUpdateRequest, protocolMethod: protocolMethod) } let topic = payload.topic @@ -310,20 +328,22 @@ private extension ApproveEngine { metadata: metadata ) if let pairingTopic = try? sessionToPairingTopic.get(key: topic) { - updatePairingMetadata(topic: pairingTopic, metadata: settleParams.controller.metadata) + pairingRegisterer.updateMetadata(pairingTopic, metadata: params.controller.metadata) } let session = WCSession( topic: topic, timestamp: Date(), selfParticipant: selfParticipant, - peerParticipant: settleParams.controller, - settleParams: settleParams, + peerParticipant: params.controller, + settleParams: params, requiredNamespaces: proposedNamespaces, acknowledged: true ) sessionStore.setSession(session) - networkingInteractor.respondSuccess(for: payload) + Task(priority: .high) { + try await networkingInteractor.respondSuccess(topic: payload.topic, requestId: payload.id, protocolMethod: protocolMethod) + } onSessionSettle?(session.publicRepresentation()) } } diff --git a/Sources/WalletConnectSign/Engine/Common/DeleteSessionService.swift b/Sources/WalletConnectSign/Engine/Common/DeleteSessionService.swift index 561af3507..be2be4c42 100644 --- a/Sources/WalletConnectSign/Engine/Common/DeleteSessionService.swift +++ b/Sources/WalletConnectSign/Engine/Common/DeleteSessionService.swift @@ -1,6 +1,8 @@ import Foundation +import JSONRPC import WalletConnectKMS import WalletConnectUtils +import WalletConnectNetworking class DeleteSessionService { private let networkingInteractor: NetworkInteracting @@ -20,9 +22,11 @@ class DeleteSessionService { func delete(topic: String) async throws { let reasonCode = ReasonCode.userDisconnected + let protocolMethod = SessionDeleteProtocolMethod() let reason = SessionType.Reason(code: reasonCode.code, message: reasonCode.message) logger.debug("Will delete session for reason: message: \(reason.message) code: \(reason.code)") - try await networkingInteractor.request(.wcSessionDelete(reason), onTopic: topic) + let request = RPCRequest(method: protocolMethod.method, params: reason) + try await networkingInteractor.request(request, topic: topic, protocolMethod: protocolMethod) sessionStore.delete(topic: topic) kms.deleteSymmetricKey(for: topic) networkingInteractor.unsubscribe(topic: topic) diff --git a/Sources/WalletConnectSign/Engine/Common/DisconnectService.swift b/Sources/WalletConnectSign/Engine/Common/DisconnectService.swift deleted file mode 100644 index d0b417a7f..000000000 --- a/Sources/WalletConnectSign/Engine/Common/DisconnectService.swift +++ /dev/null @@ -1,33 +0,0 @@ -import Foundation -import WalletConnectPairing - -class DisconnectService { - enum Errors: Error { - case objectForTopicNotFound - } - - private let deletePairingService: DeletePairingService - private let deleteSessionService: DeleteSessionService - private let pairingStorage: WCPairingStorage - private let sessionStorage: WCSessionStorage - - init(deletePairingService: DeletePairingService, - deleteSessionService: DeleteSessionService, - pairingStorage: WCPairingStorage, - sessionStorage: WCSessionStorage) { - self.deletePairingService = deletePairingService - self.deleteSessionService = deleteSessionService - self.pairingStorage = pairingStorage - self.sessionStorage = sessionStorage - } - - func disconnect(topic: String) async throws { - if pairingStorage.hasPairing(forTopic: topic) { - try await deletePairingService.delete(topic: topic) - } else if sessionStorage.hasSession(forTopic: topic) { - try await deleteSessionService.delete(topic: topic) - } else { - throw Errors.objectForTopicNotFound - } - } -} diff --git a/Sources/WalletConnectSign/Engine/Common/PairingEngine.swift b/Sources/WalletConnectSign/Engine/Common/PairingEngine.swift deleted file mode 100644 index d9ec3ac3c..000000000 --- a/Sources/WalletConnectSign/Engine/Common/PairingEngine.swift +++ /dev/null @@ -1,133 +0,0 @@ -import Foundation -import Combine -import WalletConnectPairing -import WalletConnectUtils -import WalletConnectKMS - -final class PairingEngine { - - private let networkingInteractor: NetworkInteracting - private let kms: KeyManagementServiceProtocol - private let pairingStore: WCPairingStorage - private var metadata: AppMetadata - private var publishers = [AnyCancellable]() - private let logger: ConsoleLogging - private let topicInitializer: () -> String - - init( - networkingInteractor: NetworkInteracting, - kms: KeyManagementServiceProtocol, - pairingStore: WCPairingStorage, - metadata: AppMetadata, - logger: ConsoleLogging, - topicGenerator: @escaping () -> String = String.generateTopic - ) { - self.networkingInteractor = networkingInteractor - self.kms = kms - self.metadata = metadata - self.pairingStore = pairingStore - self.logger = logger - self.topicInitializer = topicGenerator - setupNetworkingSubscriptions() - setupExpirationHandling() - } - - func hasPairing(for topic: String) -> Bool { - return pairingStore.hasPairing(forTopic: topic) - } - - func getSettledPairing(for topic: String) -> WCPairing? { - guard let pairing = pairingStore.getPairing(forTopic: topic) else { - return nil - } - return pairing - } - - func getPairings() -> [Pairing] { - pairingStore.getAll() - .map {Pairing(topic: $0.topic, peer: $0.peerMetadata, expiryDate: $0.expiryDate)} - } - - func create() async throws -> WalletConnectURI { - let topic = topicInitializer() - try await networkingInteractor.subscribe(topic: topic) - let symKey = try! kms.createSymmetricKey(topic) - let pairing = WCPairing(topic: topic) - let uri = WalletConnectURI(topic: topic, symKey: symKey.hexRepresentation, relay: pairing.relay) - pairingStore.setPairing(pairing) - return uri - } - - func propose(pairingTopic: String, namespaces: [String: ProposalNamespace], relay: RelayProtocolOptions) async throws { - logger.debug("Propose Session on topic: \(pairingTopic)") - try Namespace.validate(namespaces) - let publicKey = try! kms.createX25519KeyPair() - let proposer = Participant( - publicKey: publicKey.hexRepresentation, - metadata: metadata) - let proposal = SessionProposal( - relays: [relay], - proposer: proposer, - requiredNamespaces: namespaces) - return try await withCheckedThrowingContinuation { continuation in - networkingInteractor.requestNetworkAck(.wcSessionPropose(proposal), onTopic: pairingTopic) { error in - if let error = error { - continuation.resume(throwing: error) - } else { - continuation.resume() - } - } - } - } - - func ping(topic: String, completion: @escaping ((Result) -> Void)) { - guard pairingStore.hasPairing(forTopic: topic) else { - logger.debug("Could not find pairing to ping for topic \(topic)") - return - } - networkingInteractor.requestPeerResponse(.wcPairingPing, onTopic: topic) { [unowned self] result in - switch result { - case .success: - logger.debug("Did receive ping response") - completion(.success(())) - case .failure(let error): - logger.debug("error: \(error)") - } - } - } -} - -// MARK: Private - -private extension PairingEngine { - - func setupNetworkingSubscriptions() { - networkingInteractor.transportConnectionPublisher - .sink { [unowned self] (_) in - let topics = pairingStore.getAll() - .map {$0.topic} - topics.forEach { topic in Task {try? await networkingInteractor.subscribe(topic: topic)}} - }.store(in: &publishers) - - networkingInteractor.wcRequestPublisher - .sink { [unowned self] subscriptionPayload in - switch subscriptionPayload.wcRequest.params { - case .pairingPing: - wcPairingPing(subscriptionPayload) - default: - return - } - }.store(in: &publishers) - } - - func wcPairingPing(_ payload: WCRequestSubscriptionPayload) { - networkingInteractor.respondSuccess(for: payload) - } - - func setupExpirationHandling() { - pairingStore.onPairingExpiration = { [weak self] pairing in - self?.kms.deleteSymmetricKey(for: pairing.topic) - self?.networkingInteractor.unsubscribe(topic: pairing.topic) - } - } -} diff --git a/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift index a1197c8e1..028b4016a 100644 --- a/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift @@ -1,11 +1,12 @@ import Foundation import Combine +import JSONRPC import WalletConnectUtils import WalletConnectKMS +import WalletConnectNetworking final class SessionEngine { enum Errors: Error { - case respondError(payload: WCRequestSubscriptionPayload, reason: ReasonCode) case sessionNotFound(topic: String) } @@ -32,7 +33,9 @@ final class SessionEngine { self.sessionStore = sessionStore self.logger = logger - setupNetworkingSubscriptions() + setupConnectionSubscriptions() + setupRequestSubscriptions() + setupResponseSubscriptions() setupExpirationSubscriptions() } @@ -44,22 +47,6 @@ final class SessionEngine { sessionStore.getAll().map {$0.publicRepresentation()} } - func ping(topic: String, completion: @escaping (Result) -> Void) { - guard sessionStore.hasSession(forTopic: topic) else { - logger.debug("Could not find session to ping for topic \(topic)") - return - } - networkingInteractor.requestPeerResponse(.wcSessionPing, onTopic: topic) { [unowned self] result in - switch result { - case .success: - logger.debug("Did receive ping response") - completion(.success(())) - case .failure(let error): - logger.debug("error: \(error)") - } - } - } - func request(_ request: Request) async throws { logger.debug("will request on session topic: \(request.topic)") guard let session = sessionStore.getSession(forTopic: request.topic), session.acknowledged else { @@ -71,17 +58,21 @@ final class SessionEngine { } let chainRequest = SessionType.RequestParams.Request(method: request.method, params: request.params) let sessionRequestParams = SessionType.RequestParams(request: chainRequest, chainId: request.chainId) - try await networkingInteractor.request(.wcSessionRequest(sessionRequestParams), onTopic: request.topic) + let protocolMethod = SessionRequestProtocolMethod() + let rpcRequest = RPCRequest(method: protocolMethod.method, params: sessionRequestParams) + try await networkingInteractor.request(rpcRequest, topic: request.topic, protocolMethod: SessionRequestProtocolMethod()) } - func respondSessionRequest(topic: String, response: JsonRpcResult) async throws { + func respondSessionRequest(topic: String, requestId: RPCID, response: RPCResult) async throws { guard sessionStore.hasSession(forTopic: topic) else { throw Errors.sessionNotFound(topic: topic) } - try await networkingInteractor.respond(topic: topic, response: response, tag: 1109) // FIXME: Hardcoded tag + let response = RPCResponse(id: requestId, result: response) + try await networkingInteractor.respond(topic: topic, response: response, protocolMethod: SessionRequestProtocolMethod()) } func emit(topic: String, event: SessionType.EventParams.Event, chainId: Blockchain) async throws { + let protocolMethod = SessionEventProtocolMethod() guard let session = sessionStore.getSession(forTopic: topic) else { logger.debug("Could not find session for topic \(topic)") return @@ -89,8 +80,8 @@ final class SessionEngine { guard session.hasPermission(forEvent: event.name, onChain: chainId) else { throw WalletConnectError.invalidEvent } - let params = SessionType.EventParams(event: event, chainId: chainId) - try await networkingInteractor.request(.wcSessionEvent(params), onTopic: topic) + let rpcRequest = RPCRequest(method: protocolMethod.method, params: SessionType.EventParams(event: event, chainId: chainId)) + try await networkingInteractor.request(rpcRequest, topic: topic, protocolMethod: protocolMethod) } } @@ -98,117 +89,125 @@ final class SessionEngine { private extension SessionEngine { - func setupNetworkingSubscriptions() { - networkingInteractor.wcRequestPublisher.sink { [unowned self] subscriptionPayload in - do { - switch subscriptionPayload.wcRequest.params { - case .sessionDelete(let deleteParams): - try onSessionDelete(subscriptionPayload, deleteParams: deleteParams) - case .sessionRequest(let sessionRequestParams): - try onSessionRequest(subscriptionPayload, payloadParams: sessionRequestParams) - case .sessionPing: - onSessionPing(subscriptionPayload) - case .sessionEvent(let eventParams): - try onSessionEvent(subscriptionPayload, eventParams: eventParams) - default: return - } - } catch Errors.respondError(let payload, let reason) { - respondError(payload: payload, reason: reason) - } catch { - logger.error("Unexpected Error: \(error.localizedDescription)") + func setupConnectionSubscriptions() { + networkingInteractor.socketConnectionStatusPublisher + .sink { [unowned self] status in + guard status == .connected else { return } + sessionStore.getAll() + .forEach { session in + Task(priority: .high) { try await networkingInteractor.subscribe(topic: session.topic) } + } } - }.store(in: &publishers) + .store(in: &publishers) + } + + func setupRequestSubscriptions() { + networkingInteractor.requestSubscription(on: SessionDeleteProtocolMethod()) + .sink { [unowned self] (payload: RequestSubscriptionPayload) in + onSessionDelete(payload: payload) + }.store(in: &publishers) + + networkingInteractor.requestSubscription(on: SessionRequestProtocolMethod()) + .sink { [unowned self] (payload: RequestSubscriptionPayload) in + onSessionRequest(payload: payload) + }.store(in: &publishers) - networkingInteractor.transportConnectionPublisher - .sink { [unowned self] (_) in - let topics = sessionStore.getAll().map {$0.topic} - topics.forEach { topic in Task { try? await networkingInteractor.subscribe(topic: topic) } } + networkingInteractor.requestSubscription(on: SessionPingProtocolMethod()) + .sink { [unowned self] (payload: RequestSubscriptionPayload) in + onSessionPing(payload: payload) }.store(in: &publishers) - networkingInteractor.responsePublisher - .sink { [unowned self] response in - self.handleResponse(response) + networkingInteractor.requestSubscription(on: SessionEventProtocolMethod()) + .sink { [unowned self] (payload: RequestSubscriptionPayload) in + onSessionEvent(payload: payload) }.store(in: &publishers) } - func respondError(payload: WCRequestSubscriptionPayload, reason: ReasonCode) { - Task { + func setupResponseSubscriptions() { + networkingInteractor.responseSubscription(on: SessionRequestProtocolMethod()) + .sink { [unowned self] (payload: ResponseSubscriptionPayload) in + onSessionResponse?(Response( + id: payload.id, + topic: payload.topic, + chainId: payload.request.chainId.absoluteString, + result: payload.response + )) + } + .store(in: &publishers) + } + + func setupExpirationSubscriptions() { + sessionStore.onSessionExpiration = { [weak self] session in + self?.kms.deletePrivateKey(for: session.selfParticipant.publicKey) + self?.kms.deleteAgreementSecret(for: session.topic) + } + } + + func respondError(payload: SubscriptionPayload, reason: ReasonCode, protocolMethod: ProtocolMethod) { + Task(priority: .high) { do { - try await networkingInteractor.respondError(payload: payload, reason: reason) + try await networkingInteractor.respondError(topic: payload.topic, requestId: payload.id, protocolMethod: protocolMethod, reason: reason) } catch { logger.error("Respond Error failed with: \(error.localizedDescription)") } } } - func onSessionDelete(_ payload: WCRequestSubscriptionPayload, deleteParams: SessionType.DeleteParams) throws { + func onSessionDelete(payload: RequestSubscriptionPayload) { + let protocolMethod = SessionDeleteProtocolMethod() let topic = payload.topic guard sessionStore.hasSession(forTopic: topic) else { - throw Errors.respondError(payload: payload, reason: .noSessionForTopic) + return respondError(payload: payload, reason: .noSessionForTopic, protocolMethod: protocolMethod) } sessionStore.delete(topic: topic) networkingInteractor.unsubscribe(topic: topic) - networkingInteractor.respondSuccess(for: payload) - onSessionDelete?(topic, deleteParams) + Task(priority: .high) { + try await networkingInteractor.respondSuccess(topic: payload.topic, requestId: payload.id, protocolMethod: protocolMethod) + } + onSessionDelete?(topic, payload.request) } - func onSessionRequest(_ payload: WCRequestSubscriptionPayload, payloadParams: SessionType.RequestParams) throws { + func onSessionRequest(payload: RequestSubscriptionPayload) { + let protocolMethod = SessionRequestProtocolMethod() let topic = payload.topic - let jsonRpcRequest = JSONRPCRequest(id: payload.wcRequest.id, method: payloadParams.request.method, params: payloadParams.request.params) let request = Request( - id: jsonRpcRequest.id, - topic: topic, - method: jsonRpcRequest.method, - params: jsonRpcRequest.params, - chainId: payloadParams.chainId) + id: payload.id, + topic: payload.topic, + method: payload.request.request.method, + params: payload.request.request.params, + chainId: payload.request.chainId) guard let session = sessionStore.getSession(forTopic: topic) else { - throw Errors.respondError(payload: payload, reason: .noSessionForTopic) + return respondError(payload: payload, reason: .noSessionForTopic, protocolMethod: protocolMethod) } - let chain = request.chainId - guard session.hasNamespace(for: chain) else { - throw Errors.respondError(payload: payload, reason: .unauthorizedChain) + guard session.hasNamespace(for: request.chainId) else { + return respondError(payload: payload, reason: .unauthorizedChain, protocolMethod: protocolMethod) } - guard session.hasPermission(forMethod: request.method, onChain: chain) else { - throw Errors.respondError(payload: payload, reason: .unauthorizedMethod(request.method)) + guard session.hasPermission(forMethod: request.method, onChain: request.chainId) else { + return respondError(payload: payload, reason: .unauthorizedMethod(request.method), protocolMethod: protocolMethod) } onSessionRequest?(request) } - func onSessionPing(_ payload: WCRequestSubscriptionPayload) { - networkingInteractor.respondSuccess(for: payload) + func onSessionPing(payload: SubscriptionPayload) { + Task(priority: .high) { + try await networkingInteractor.respondSuccess(topic: payload.topic, requestId: payload.id, protocolMethod: SessionPingProtocolMethod()) + } } - func onSessionEvent(_ payload: WCRequestSubscriptionPayload, eventParams: SessionType.EventParams) throws { - let event = eventParams.event + func onSessionEvent(payload: RequestSubscriptionPayload) { + let protocolMethod = SessionEventProtocolMethod() + let event = payload.request.event let topic = payload.topic guard let session = sessionStore.getSession(forTopic: topic) else { - throw Errors.respondError(payload: payload, reason: .noSessionForTopic) + return respondError(payload: payload, reason: .noSessionForTopic, protocolMethod: protocolMethod) } - guard - session.peerIsController, - session.hasPermission(forEvent: event.name, onChain: eventParams.chainId) - else { - throw Errors.respondError(payload: payload, reason: .unauthorizedEvent(event.name)) + guard session.peerIsController, session.hasPermission(forEvent: event.name, onChain: payload.request.chainId) else { + return respondError(payload: payload, reason: .unauthorizedEvent(event.name), protocolMethod: protocolMethod) } - networkingInteractor.respondSuccess(for: payload) - onEventReceived?(topic, event.publicRepresentation(), eventParams.chainId) - } - - func setupExpirationSubscriptions() { - sessionStore.onSessionExpiration = { [weak self] session in - self?.kms.deletePrivateKey(for: session.selfParticipant.publicKey) - self?.kms.deleteAgreementSecret(for: session.topic) - } - } - - func handleResponse(_ response: WCResponse) { - switch response.requestParams { - case .sessionRequest: - let response = Response(topic: response.topic, chainId: response.chainId, result: response.result) - onSessionResponse?(response) - default: - break + Task(priority: .high) { + try await networkingInteractor.respondSuccess(topic: payload.topic, requestId: payload.id, protocolMethod: protocolMethod) } + onEventReceived?(topic, event.publicRepresentation(), payload.request.chainId) } } diff --git a/Sources/WalletConnectSign/Engine/Controller/ControllerSessionStateMachine.swift b/Sources/WalletConnectSign/Engine/Controller/ControllerSessionStateMachine.swift index b76b63d50..748ae7713 100644 --- a/Sources/WalletConnectSign/Engine/Controller/ControllerSessionStateMachine.swift +++ b/Sources/WalletConnectSign/Engine/Controller/ControllerSessionStateMachine.swift @@ -1,7 +1,9 @@ import Foundation +import Combine +import JSONRPC import WalletConnectUtils import WalletConnectKMS -import Combine +import WalletConnectNetworking final class ControllerSessionStateMachine { @@ -22,48 +24,54 @@ final class ControllerSessionStateMachine { self.kms = kms self.sessionStore = sessionStore self.logger = logger - networkingInteractor.responsePublisher.sink { [unowned self] response in - handleResponse(response) - }.store(in: &publishers) + + setupSubscriptions() } func update(topic: String, namespaces: [String: SessionNamespace]) async throws { let session = try getSession(for: topic) + let protocolMethod = SessionUpdateProtocolMethod() try validateController(session) try Namespace.validate(namespaces) logger.debug("Controller will update methods") sessionStore.setSession(session) - try await networkingInteractor.request(.wcSessionUpdate(SessionType.UpdateParams(namespaces: namespaces)), onTopic: topic) + let request = RPCRequest(method: protocolMethod.method, params: SessionType.UpdateParams(namespaces: namespaces)) + try await networkingInteractor.request(request, topic: topic, protocolMethod: protocolMethod) } func extend(topic: String, by ttl: Int64) async throws { var session = try getSession(for: topic) + let protocolMethod = SessionExtendProtocolMethod() try validateController(session) try session.updateExpiry(by: ttl) let newExpiry = Int64(session.expiryDate.timeIntervalSince1970 ) sessionStore.setSession(session) - try await networkingInteractor.request(.wcSessionExtend(SessionType.UpdateExpiryParams(expiry: newExpiry)), onTopic: topic) + let request = RPCRequest(method: protocolMethod.method, params: SessionType.UpdateExpiryParams(expiry: newExpiry)) + try await networkingInteractor.request(request, topic: topic, protocolMethod: protocolMethod) } // MARK: - Handle Response - private func handleResponse(_ response: WCResponse) { - switch response.requestParams { - case .sessionUpdate(let payload): - handleUpdateResponse(response: response, payload: payload) - case .sessionExtend(let payload): - handleUpdateExpiryResponse(response: response, payload: payload) - default: - break - } + private func setupSubscriptions() { + networkingInteractor.responseSubscription(on: SessionUpdateProtocolMethod()) + .sink { [unowned self] (payload: ResponseSubscriptionPayload) in + handleUpdateResponse(payload: payload) + } + .store(in: &publishers) + + networkingInteractor.responseSubscription(on: SessionExtendProtocolMethod()) + .sink { [unowned self] (payload: ResponseSubscriptionPayload) in + handleUpdateExpiryResponse(payload: payload) + } + .store(in: &publishers) } - private func handleUpdateResponse(response: WCResponse, payload: SessionType.UpdateParams) { - guard var session = sessionStore.getSession(forTopic: response.topic) else { return } - switch response.result { + private func handleUpdateResponse(payload: ResponseSubscriptionPayload) { + guard var session = sessionStore.getSession(forTopic: payload.topic) else { return } + switch payload.response { case .response: do { - try session.updateNamespaces(payload.namespaces, timestamp: response.timestamp) + try session.updateNamespaces(payload.request.namespaces, timestamp: payload.id.timestamp) if sessionStore.setSessionIfNewer(session) { onNamespacesUpdate?(session.topic, session.namespaces) @@ -76,12 +84,12 @@ final class ControllerSessionStateMachine { } } - private func handleUpdateExpiryResponse(response: WCResponse, payload: SessionType.UpdateExpiryParams) { - guard var session = sessionStore.getSession(forTopic: response.topic) else { return } - switch response.result { + private func handleUpdateExpiryResponse(payload: ResponseSubscriptionPayload) { + guard var session = sessionStore.getSession(forTopic: payload.topic) else { return } + switch payload.response { case .response: do { - try session.updateExpiry(to: payload.expiry) + try session.updateExpiry(to: payload.request.expiry) sessionStore.setSession(session) onExtend?(session.topic, session.expiryDate) } catch { diff --git a/Sources/WalletConnectSign/Engine/Controller/PairEngine.swift b/Sources/WalletConnectSign/Engine/Controller/PairEngine.swift deleted file mode 100644 index 9ad8956b7..000000000 --- a/Sources/WalletConnectSign/Engine/Controller/PairEngine.swift +++ /dev/null @@ -1,33 +0,0 @@ -import Foundation -import WalletConnectKMS -import WalletConnectPairing - -actor PairEngine { - private let networkingInteractor: NetworkInteracting - private let kms: KeyManagementServiceProtocol - private let pairingStore: WCPairingStorage - - init(networkingInteractor: NetworkInteracting, - kms: KeyManagementServiceProtocol, - pairingStore: WCPairingStorage) { - self.networkingInteractor = networkingInteractor - self.kms = kms - self.pairingStore = pairingStore - } - - func pair(_ uri: WalletConnectURI) async throws { - guard !hasPairing(for: uri.topic) else { - throw WalletConnectError.pairingAlreadyExist - } - var pairing = WCPairing(uri: uri) - let symKey = try SymmetricKey(hex: uri.symKey) - try kms.setSymmetricKey(symKey, for: pairing.topic) - pairing.activate() - pairingStore.setPairing(pairing) - try await networkingInteractor.subscribe(topic: pairing.topic) - } - - func hasPairing(for topic: String) -> Bool { - return pairingStore.hasPairing(forTopic: topic) - } -} diff --git a/Sources/WalletConnectSign/Engine/NonController/NonControllerSessionStateMachine.swift b/Sources/WalletConnectSign/Engine/NonController/NonControllerSessionStateMachine.swift index 0e4e22f66..dbde21388 100644 --- a/Sources/WalletConnectSign/Engine/NonController/NonControllerSessionStateMachine.swift +++ b/Sources/WalletConnectSign/Engine/NonController/NonControllerSessionStateMachine.swift @@ -1,12 +1,10 @@ import Foundation import WalletConnectUtils import WalletConnectKMS +import WalletConnectNetworking import Combine final class NonControllerSessionStateMachine { - enum Errors: Error { - case respondError(payload: WCRequestSubscriptionPayload, reason: ReasonCode) - } var onNamespacesUpdate: ((String, [String: SessionNamespace]) -> Void)? var onExtend: ((String, Date) -> Void)? @@ -25,32 +23,25 @@ final class NonControllerSessionStateMachine { self.kms = kms self.sessionStore = sessionStore self.logger = logger - setUpWCRequestHandling() + setupSubscriptions() } - private func setUpWCRequestHandling() { - networkingInteractor.wcRequestPublisher - .sink { [unowned self] subscriptionPayload in - do { - switch subscriptionPayload.wcRequest.params { - case .sessionUpdate(let updateParams): - try onSessionUpdateNamespacesRequest(payload: subscriptionPayload, updateParams: updateParams) - case .sessionExtend(let updateExpiryParams): - try onSessionUpdateExpiry(subscriptionPayload, updateExpiryParams: updateExpiryParams) - default: return - } - } catch Errors.respondError(let payload, let reason) { - respondError(payload: payload, reason: reason) - } catch { - logger.error("Unexpected Error: \(error.localizedDescription)") - } + private func setupSubscriptions() { + networkingInteractor.requestSubscription(on: SessionUpdateProtocolMethod()) + .sink { [unowned self] (payload: RequestSubscriptionPayload) in + onSessionUpdateNamespacesRequest(payload: payload, updateParams: payload.request) + }.store(in: &publishers) + + networkingInteractor.requestSubscription(on: SessionExtendProtocolMethod()) + .sink { [unowned self] (payload: RequestSubscriptionPayload) in + onSessionUpdateExpiry(payload: payload, updateExpiryParams: payload.request) }.store(in: &publishers) } - private func respondError(payload: WCRequestSubscriptionPayload, reason: ReasonCode) { - Task { + private func respondError(payload: SubscriptionPayload, reason: ReasonCode, protocolMethod: ProtocolMethod) { + Task(priority: .high) { do { - try await networkingInteractor.respondError(payload: payload, reason: reason) + try await networkingInteractor.respondError(topic: payload.topic, requestId: payload.id, protocolMethod: protocolMethod, reason: reason) } catch { logger.error("Respond Error failed with: \(error.localizedDescription)") } @@ -58,43 +49,53 @@ final class NonControllerSessionStateMachine { } // TODO: Update stored session namespaces - private func onSessionUpdateNamespacesRequest(payload: WCRequestSubscriptionPayload, updateParams: SessionType.UpdateParams) throws { + private func onSessionUpdateNamespacesRequest(payload: SubscriptionPayload, updateParams: SessionType.UpdateParams) { + let protocolMethod = SessionUpdateProtocolMethod() do { try Namespace.validate(updateParams.namespaces) } catch { - throw Errors.respondError(payload: payload, reason: .invalidUpdateRequest) + return respondError(payload: payload, reason: .invalidUpdateRequest, protocolMethod: protocolMethod) } guard var session = sessionStore.getSession(forTopic: payload.topic) else { - throw Errors.respondError(payload: payload, reason: .noSessionForTopic) + return respondError(payload: payload, reason: .noSessionForTopic, protocolMethod: protocolMethod) } guard session.peerIsController else { - throw Errors.respondError(payload: payload, reason: .unauthorizedUpdateRequest) + return respondError(payload: payload, reason: .unauthorizedUpdateRequest, protocolMethod: protocolMethod) } do { - try session.updateNamespaces(updateParams.namespaces, timestamp: payload.timestamp) + try session.updateNamespaces(updateParams.namespaces, timestamp: payload.id.timestamp) } catch { - throw Errors.respondError(payload: payload, reason: .invalidUpdateRequest) + return respondError(payload: payload, reason: .invalidUpdateRequest, protocolMethod: protocolMethod) } sessionStore.setSession(session) - networkingInteractor.respondSuccess(for: payload) + + Task(priority: .high) { + try await networkingInteractor.respondSuccess(topic: payload.topic, requestId: payload.id, protocolMethod: protocolMethod) + } + onNamespacesUpdate?(session.topic, updateParams.namespaces) } - private func onSessionUpdateExpiry(_ payload: WCRequestSubscriptionPayload, updateExpiryParams: SessionType.UpdateExpiryParams) throws { + private func onSessionUpdateExpiry(payload: SubscriptionPayload, updateExpiryParams: SessionType.UpdateExpiryParams) { + let protocolMethod = SessionExtendProtocolMethod() let topic = payload.topic guard var session = sessionStore.getSession(forTopic: topic) else { - throw Errors.respondError(payload: payload, reason: .noSessionForTopic) + return respondError(payload: payload, reason: .noSessionForTopic, protocolMethod: protocolMethod) } guard session.peerIsController else { - throw Errors.respondError(payload: payload, reason: .unauthorizedExtendRequest) + return respondError(payload: payload, reason: .unauthorizedExtendRequest, protocolMethod: protocolMethod) } do { try session.updateExpiry(to: updateExpiryParams.expiry) } catch { - throw Errors.respondError(payload: payload, reason: .invalidExtendRequest) + return respondError(payload: payload, reason: .invalidExtendRequest, protocolMethod: protocolMethod) } sessionStore.setSession(session) - networkingInteractor.respondSuccess(for: payload) + + Task(priority: .high) { + try await networkingInteractor.respondSuccess(topic: payload.topic, requestId: payload.id, protocolMethod: protocolMethod) + } + onExtend?(session.topic, session.expiryDate) } } diff --git a/Sources/WalletConnectSign/JsonRpcHistory/JsonRpcHistory.swift b/Sources/WalletConnectSign/JsonRpcHistory/JsonRpcHistory.swift deleted file mode 100644 index 321bb5a62..000000000 --- a/Sources/WalletConnectSign/JsonRpcHistory/JsonRpcHistory.swift +++ /dev/null @@ -1,63 +0,0 @@ -import Foundation -import WalletConnectUtils - -protocol JsonRpcHistoryRecording { - func get(id: Int64) -> JsonRpcRecord? - func set(topic: String, request: WCRequest, chainId: String?) throws - func delete(topic: String) - func resolve(response: JsonRpcResult) throws -> JsonRpcRecord - func exist(id: Int64) -> Bool -} -// TODO -remove and use jsonrpc history only from utils -class JsonRpcHistory: JsonRpcHistoryRecording { - let storage: CodableStore - let logger: ConsoleLogging - - init(logger: ConsoleLogging, keyValueStore: CodableStore) { - self.logger = logger - self.storage = keyValueStore - } - - func get(id: Int64) -> JsonRpcRecord? { - try? storage.get(key: "\(id)") - } - - func set(topic: String, request: WCRequest, chainId: String? = nil) throws { - guard !exist(id: request.id) else { - throw WalletConnectError.internal(.jsonRpcDuplicateDetected) - } - logger.debug("Setting JSON-RPC request history record - ID: \(request.id)") - let record = JsonRpcRecord(id: request.id, topic: topic, request: JsonRpcRecord.Request(method: request.method, params: request.params), response: nil, chainId: chainId) - storage.set(record, forKey: "\(request.id)") - } - - func delete(topic: String) { - storage.getAll().forEach { record in - if record.topic == topic { - storage.delete(forKey: "\(record.id)") - } - } - } - - func resolve(response: JsonRpcResult) throws -> JsonRpcRecord { - logger.debug("Resolving JSON-RPC response - ID: \(response.id)") - guard var record = try? storage.get(key: "\(response.id)") else { - throw WalletConnectError.internal(.noJsonRpcRequestMatchingResponse) - } - if record.response != nil { - throw WalletConnectError.internal(.jsonRpcDuplicateDetected) - } else { - record.response = response - storage.set(record, forKey: "\(record.id)") - return record - } - } - - func exist(id: Int64) -> Bool { - return (try? storage.get(key: "\(id)")) != nil - } - - public func getPending() -> [JsonRpcRecord] { - storage.getAll().filter {$0.response == nil} - } -} diff --git a/Sources/WalletConnectSign/JsonRpcHistory/JsonRpcRecord.swift b/Sources/WalletConnectSign/JsonRpcHistory/JsonRpcRecord.swift deleted file mode 100644 index edfb14dc8..000000000 --- a/Sources/WalletConnectSign/JsonRpcHistory/JsonRpcRecord.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation -import WalletConnectUtils - -struct JsonRpcRecord: Codable { - let id: Int64 - let topic: String - let request: Request - var response: JsonRpcResult? - let chainId: String? - - struct Request: Codable { - let method: WCRequest.Method - let params: WCRequest.Params - } -} diff --git a/Sources/WalletConnectSign/NetworkInteractor/NetworkInteractor.swift b/Sources/WalletConnectSign/NetworkInteractor/NetworkInteractor.swift deleted file mode 100644 index 3389771b3..000000000 --- a/Sources/WalletConnectSign/NetworkInteractor/NetworkInteractor.swift +++ /dev/null @@ -1,259 +0,0 @@ -import Foundation -import Combine -import WalletConnectUtils -import WalletConnectKMS - -protocol NetworkInteracting: AnyObject { - var transportConnectionPublisher: AnyPublisher {get} - var wcRequestPublisher: AnyPublisher {get} - var responsePublisher: AnyPublisher {get} - /// Completes when request sent from a networking client - func request(_ wcMethod: WCMethod, onTopic topic: String) async throws - /// Completes with an acknowledgement from the relay network - func requestNetworkAck(_ wcMethod: WCMethod, onTopic topic: String, completion: @escaping ((Error?) -> Void)) - /// Completes with a peer response - func requestPeerResponse(_ wcMethod: WCMethod, onTopic topic: String, completion: ((Result, JSONRPCErrorResponse>) -> Void)?) - func respond(topic: String, response: JsonRpcResult, tag: Int) async throws - func respondSuccess(payload: WCRequestSubscriptionPayload) async throws - func respondSuccess(for payload: WCRequestSubscriptionPayload) - func respondError(payload: WCRequestSubscriptionPayload, reason: ReasonCode) async throws - func subscribe(topic: String) async throws - func unsubscribe(topic: String) -} - -extension NetworkInteracting { - func request(_ wcMethod: WCMethod, onTopic topic: String) { - requestPeerResponse(wcMethod, onTopic: topic, completion: nil) - } -} - -class NetworkInteractor: NetworkInteracting { - - private var publishers = Set() - - private var relayClient: NetworkRelaying - private let serializer: Serializing - private let jsonRpcHistory: JsonRpcHistoryRecording - - private let transportConnectionPublisherSubject = PassthroughSubject() - private let responsePublisherSubject = PassthroughSubject() - private let wcRequestPublisherSubject = PassthroughSubject() - - var transportConnectionPublisher: AnyPublisher { - transportConnectionPublisherSubject.eraseToAnyPublisher() - } - var wcRequestPublisher: AnyPublisher { - wcRequestPublisherSubject.eraseToAnyPublisher() - } - var responsePublisher: AnyPublisher { - responsePublisherSubject.eraseToAnyPublisher() - } - - let logger: ConsoleLogging - - init(relayClient: NetworkRelaying, - serializer: Serializing, - logger: ConsoleLogging, - jsonRpcHistory: JsonRpcHistoryRecording) { - self.relayClient = relayClient - self.serializer = serializer - self.logger = logger - self.jsonRpcHistory = jsonRpcHistory - setUpPublishers() - } - - func request(_ wcMethod: WCMethod, onTopic topic: String) async throws { - try await request(topic: topic, payload: wcMethod.asRequest()) - } - - /// Completes when networking client sends a request - func request(topic: String, payload: WCRequest) async throws { - try jsonRpcHistory.set(topic: topic, request: payload, chainId: getChainId(payload)) - let message = try serializer.serialize(topic: topic, encodable: payload) - let prompt = shouldPrompt(payload.method) - try await relayClient.publish(topic: topic, payload: message, tag: payload.tag, prompt: prompt) - } - - func requestPeerResponse(_ wcMethod: WCMethod, onTopic topic: String, completion: ((Result, JSONRPCErrorResponse>) -> Void)?) { - let payload = wcMethod.asRequest() - do { - try jsonRpcHistory.set(topic: topic, request: payload, chainId: getChainId(payload)) - let message = try serializer.serialize(topic: topic, encodable: payload) - let prompt = shouldPrompt(payload.method) - relayClient.publish(topic: topic, payload: message, tag: payload.tag, prompt: prompt) { [weak self] error in - guard let self = self else {return} - if let error = error { - self.logger.error(error) - } else { - var cancellable: AnyCancellable! - cancellable = self.responsePublisher - .filter {$0.result.id == payload.id} - .sink { (response) in - cancellable.cancel() - self.logger.debug("WC Relay - received response on topic: \(topic)") - switch response.result { - case .response(let response): - completion?(.success(response)) - case .error(let error): - self.logger.debug("Request error: \(error)") - completion?(.failure(error)) - } - } - } - } - } catch WalletConnectError.internal(.jsonRpcDuplicateDetected) { - logger.info("Info: Json Rpc Duplicate Detected") - } catch { - logger.error(error) - } - } - - /// Completes with an acknowledgement from the relay network. - /// completes with error if networking client was not able to send a message - func requestNetworkAck(_ wcMethod: WCMethod, onTopic topic: String, completion: @escaping ((Error?) -> Void)) { - do { - let payload = wcMethod.asRequest() - try jsonRpcHistory.set(topic: topic, request: payload, chainId: getChainId(payload)) - let message = try serializer.serialize(topic: topic, encodable: payload) - let prompt = shouldPrompt(payload.method) - relayClient.publish(topic: topic, payload: message, tag: payload.tag, prompt: prompt) { error in - completion(error) - } - } catch WalletConnectError.internal(.jsonRpcDuplicateDetected) { - logger.info("Info: Json Rpc Duplicate Detected") - } catch { - logger.error(error) - } - } - - func respond(topic: String, response: JsonRpcResult, tag: Int) async throws { - _ = try jsonRpcHistory.resolve(response: response) - - let message = try serializer.serialize(topic: topic, encodable: response.value) - logger.debug("Responding....topic: \(topic)") - - do { - try await relayClient.publish(topic: topic, payload: message, tag: tag, prompt: false) - } catch WalletConnectError.internal(.jsonRpcDuplicateDetected) { - logger.info("Info: Json Rpc Duplicate Detected") - } - } - - func respondSuccess(payload: WCRequestSubscriptionPayload) async throws { - let response = JSONRPCResponse(id: payload.wcRequest.id, result: AnyCodable(true)) - try await respond(topic: payload.topic, response: JsonRpcResult.response(response), tag: payload.wcRequest.responseTag) - } - - func respondError(payload: WCRequestSubscriptionPayload, reason: ReasonCode) async throws { - let response = JSONRPCErrorResponse(id: payload.wcRequest.id, error: JSONRPCErrorResponse.Error(code: reason.code, message: reason.message)) - try await respond(topic: payload.topic, response: JsonRpcResult.error(response), tag: payload.wcRequest.responseTag) - } - - // TODO: Move to async - func respondSuccess(for payload: WCRequestSubscriptionPayload) { - Task(priority: .background) { - do { - try await respondSuccess(payload: payload) - } catch { - self.logger.error("Respond Success failed with: \(error.localizedDescription)") - } - } - } - - func subscribe(topic: String) async throws { - try await relayClient.subscribe(topic: topic) - } - - func unsubscribe(topic: String) { - relayClient.unsubscribe(topic: topic) { [weak self] error in - if let error = error { - self?.logger.error(error) - } else { - self?.jsonRpcHistory.delete(topic: topic) - } - } - } - - // MARK: - Private - - private func setUpPublishers() { - relayClient.socketConnectionStatusPublisher.sink { [weak self] status in - if status == .connected { - self?.transportConnectionPublisherSubject.send() - } - }.store(in: &publishers) - - relayClient.messagePublisher.sink { [weak self] (topic, message) in - self?.manageSubscription(topic, message) - } - .store(in: &publishers) - } - - private func manageSubscription(_ topic: String, _ encodedEnvelope: String) { - if let deserializedJsonRpcRequest: WCRequest = serializer.tryDeserialize(topic: topic, encodedEnvelope: encodedEnvelope) { - handleWCRequest(topic: topic, request: deserializedJsonRpcRequest) - } else if let deserializedJsonRpcResponse: JSONRPCResponse = serializer.tryDeserialize(topic: topic, encodedEnvelope: encodedEnvelope) { - handleJsonRpcResponse(response: deserializedJsonRpcResponse) - } else if let deserializedJsonRpcError: JSONRPCErrorResponse = serializer.tryDeserialize(topic: topic, encodedEnvelope: encodedEnvelope) { - handleJsonRpcErrorResponse(response: deserializedJsonRpcError) - } else { - logger.warn("Warning: Networking Interactor - Received unknown object type from networking relay") - } - } - - private func handleWCRequest(topic: String, request: WCRequest) { - do { - try jsonRpcHistory.set(topic: topic, request: request, chainId: getChainId(request)) - let payload = WCRequestSubscriptionPayload(topic: topic, wcRequest: request) - wcRequestPublisherSubject.send(payload) - } catch WalletConnectError.internal(.jsonRpcDuplicateDetected) { - logger.info("Info: Json Rpc Duplicate Detected") - } catch { - logger.error(error) - } - } - - private func handleJsonRpcResponse(response: JSONRPCResponse) { - do { - let record = try jsonRpcHistory.resolve(response: JsonRpcResult.response(response)) - let wcResponse = WCResponse( - topic: record.topic, - chainId: record.chainId, - requestMethod: record.request.method, - requestParams: record.request.params, - result: JsonRpcResult.response(response)) - responsePublisherSubject.send(wcResponse) - } catch { - logger.info("Info: \(error.localizedDescription)") - } - } - - private func handleJsonRpcErrorResponse(response: JSONRPCErrorResponse) { - do { - let record = try jsonRpcHistory.resolve(response: JsonRpcResult.error(response)) - let wcResponse = WCResponse( - topic: record.topic, - chainId: record.chainId, - requestMethod: record.request.method, - requestParams: record.request.params, - result: JsonRpcResult.error(response)) - responsePublisherSubject.send(wcResponse) - } catch { - logger.info("Info: \(error.localizedDescription)") - } - } - - private func shouldPrompt(_ method: WCRequest.Method) -> Bool { - switch method { - case .sessionRequest: - return true - default: - return false - } - } - - func getChainId(_ request: WCRequest) -> String? { - guard case let .sessionRequest(payload) = request.params else {return nil} - return payload.chainId.absoluteString - } -} diff --git a/Sources/WalletConnectSign/NetworkInteractor/NetworkRelaying.swift b/Sources/WalletConnectSign/NetworkInteractor/NetworkRelaying.swift deleted file mode 100644 index 5574cf826..000000000 --- a/Sources/WalletConnectSign/NetworkInteractor/NetworkRelaying.swift +++ /dev/null @@ -1,19 +0,0 @@ -import Foundation -import WalletConnectRelay -import Combine - -extension RelayClient: NetworkRelaying {} - -protocol NetworkRelaying { - var messagePublisher: AnyPublisher<(topic: String, message: String), Never> { get } - var socketConnectionStatusPublisher: AnyPublisher { get } - func connect() throws - func disconnect(closeCode: URLSessionWebSocketTask.CloseCode) throws - func publish(topic: String, payload: String, tag: Int, prompt: Bool) async throws - /// - returns: request id - func publish(topic: String, payload: String, tag: Int, prompt: Bool, onNetworkAcknowledge: @escaping ((Error?) -> Void)) - func subscribe(topic: String, completion: @escaping (Error?) -> Void) - func subscribe(topic: String) async throws - /// - returns: request id - func unsubscribe(topic: String, completion: @escaping ((Error?) -> Void)) -} diff --git a/Sources/WalletConnectSign/Reason.swift b/Sources/WalletConnectSign/Reason.swift deleted file mode 100644 index d49aab825..000000000 --- a/Sources/WalletConnectSign/Reason.swift +++ /dev/null @@ -1,11 +0,0 @@ -// TODO: Refactor into codes. Reference: https://docs.walletconnect.com/2.0/protocol/reason-codes -public struct Reason { - - public let code: Int - public let message: String - - public init(code: Int, message: String) { - self.code = code - self.message = message - } -} diff --git a/Sources/WalletConnectSign/Request.swift b/Sources/WalletConnectSign/Request.swift index da55722b2..5cca66797 100644 --- a/Sources/WalletConnectSign/Request.swift +++ b/Sources/WalletConnectSign/Request.swift @@ -1,14 +1,15 @@ import Foundation +import JSONRPC import WalletConnectUtils public struct Request: Codable, Equatable { - public let id: Int64 + public let id: RPCID public let topic: String public let method: String public let params: AnyCodable public let chainId: Blockchain - internal init(id: Int64, topic: String, method: String, params: AnyCodable, chainId: Blockchain) { + internal init(id: RPCID, topic: String, method: String, params: AnyCodable, chainId: Blockchain) { self.id = id self.topic = topic self.method = method @@ -17,10 +18,10 @@ public struct Request: Codable, Equatable { } public init(topic: String, method: String, params: AnyCodable, chainId: Blockchain) { - self.init(id: JsonRpcID.generate(), topic: topic, method: method, params: params, chainId: chainId) + self.init(id: RPCID(JsonRpcID.generate()), topic: topic, method: method, params: params, chainId: chainId) } - internal init(id: Int64, topic: String, method: String, params: C, chainId: Blockchain) where C: Codable { + internal init(id: RPCID, topic: String, method: String, params: C, chainId: Blockchain) where C: Codable { self.init(id: id, topic: topic, method: method, params: AnyCodable(params), chainId: chainId) } } diff --git a/Sources/WalletConnectSign/Response.swift b/Sources/WalletConnectSign/Response.swift index 8dd4a0f0f..aaaaf02fa 100644 --- a/Sources/WalletConnectSign/Response.swift +++ b/Sources/WalletConnectSign/Response.swift @@ -1,8 +1,10 @@ import Foundation +import JSONRPC import WalletConnectUtils public struct Response: Codable { + public let id: RPCID public let topic: String public let chainId: String? - public let result: JsonRpcResult + public let result: RPCResult } diff --git a/Sources/WalletConnectSign/Services/App/AppProposeService.swift b/Sources/WalletConnectSign/Services/App/AppProposeService.swift new file mode 100644 index 000000000..f24db9b39 --- /dev/null +++ b/Sources/WalletConnectSign/Services/App/AppProposeService.swift @@ -0,0 +1,41 @@ +import Foundation +import JSONRPC +import WalletConnectNetworking +import WalletConnectKMS +import WalletConnectUtils + +final class AppProposeService { + private let metadata: AppMetadata + private let networkingInteractor: NetworkInteracting + private let kms: KeyManagementServiceProtocol + private let logger: ConsoleLogging + + init( + metadata: AppMetadata, + networkingInteractor: NetworkInteracting, + kms: KeyManagementServiceProtocol, + logger: ConsoleLogging + ) { + self.metadata = metadata + self.networkingInteractor = networkingInteractor + self.kms = kms + self.logger = logger + } + + func propose(pairingTopic: String, namespaces: [String: ProposalNamespace], relay: RelayProtocolOptions) async throws { + logger.debug("Propose Session on topic: \(pairingTopic)") + try Namespace.validate(namespaces) + let protocolMethod = SessionProposeProtocolMethod() + let publicKey = try! kms.createX25519KeyPair() + let proposer = Participant( + publicKey: publicKey.hexRepresentation, + metadata: metadata) + let proposal = SessionProposal( + relays: [relay], + proposer: proposer, + requiredNamespaces: namespaces) + + let request = RPCRequest(method: protocolMethod.method, params: proposal) + try await networkingInteractor.requestNetworkAck(request, topic: pairingTopic, protocolMethod: protocolMethod) + } +} diff --git a/Sources/WalletConnectSign/Services/DisconnectService.swift b/Sources/WalletConnectSign/Services/DisconnectService.swift new file mode 100644 index 000000000..a0fab00e8 --- /dev/null +++ b/Sources/WalletConnectSign/Services/DisconnectService.swift @@ -0,0 +1,24 @@ +import Foundation + +class DisconnectService { + enum Errors: Error { + case sessionForTopicNotFound + } + + private let deleteSessionService: DeleteSessionService + private let sessionStorage: WCSessionStorage + + init(deleteSessionService: DeleteSessionService, + sessionStorage: WCSessionStorage) { + self.deleteSessionService = deleteSessionService + self.sessionStorage = sessionStorage + } + + func disconnect(topic: String) async throws { + if sessionStorage.hasSession(forTopic: topic) { + try await deleteSessionService.delete(topic: topic) + } else { + throw Errors.sessionForTopicNotFound + } + } +} diff --git a/Sources/WalletConnectSign/Services/SessionPingService.swift b/Sources/WalletConnectSign/Services/SessionPingService.swift new file mode 100644 index 000000000..8bc03dbad --- /dev/null +++ b/Sources/WalletConnectSign/Services/SessionPingService.swift @@ -0,0 +1,36 @@ +import Foundation +import WalletConnectPairing +import WalletConnectUtils +import WalletConnectNetworking + +class SessionPingService { + private let sessionStorage: WCSessionStorage + private let pingRequester: PingRequester + private let pingResponder: PingResponder + private let pingResponseSubscriber: PingResponseSubscriber + + var onResponse: ((String)->Void)? { + get { + return pingResponseSubscriber.onResponse + } + set { + pingResponseSubscriber.onResponse = newValue + } + } + + init( + sessionStorage: WCSessionStorage, + networkingInteractor: NetworkInteracting, + logger: ConsoleLogging) { + let protocolMethod = SessionPingProtocolMethod() + self.sessionStorage = sessionStorage + self.pingRequester = PingRequester(networkingInteractor: networkingInteractor, method: protocolMethod) + self.pingResponder = PingResponder(networkingInteractor: networkingInteractor, method: protocolMethod, logger: logger) + self.pingResponseSubscriber = PingResponseSubscriber(networkingInteractor: networkingInteractor, method: protocolMethod, logger: logger) + } + + func ping(topic: String) async throws { + guard sessionStorage.hasSession(forTopic: topic) else { return } + try await pingRequester.ping(topic: topic) + } +} diff --git a/Sources/WalletConnectSign/Sign/Sign.swift b/Sources/WalletConnectSign/Sign/Sign.swift index 42685d660..d61e7b0ff 100644 --- a/Sources/WalletConnectSign/Sign/Sign.swift +++ b/Sources/WalletConnectSign/Sign/Sign.swift @@ -1,10 +1,15 @@ import Foundation +import Combine +import JSONRPC import WalletConnectUtils import WalletConnectRelay -import Combine +import WalletConnectNetworking +import WalletConnectPairing public typealias Account = WalletConnectUtils.Account public typealias Blockchain = WalletConnectUtils.Blockchain +public typealias Reason = WalletConnectNetworking.Reason +public typealias RPCID = JSONRPC.RPCID /// Sign instatnce wrapper /// @@ -21,15 +26,14 @@ public class Sign { /// Sign client instance public static var instance: SignClient = { - guard let metadata = Sign.metadata else { - fatalError("Error - you must call Sign.configure(_:) before accessing the shared instance.") - } return SignClientFactory.create( - metadata: metadata, - relayClient: Relay.instance + metadata: Sign.metadata ?? Pair.metadata, + pairingClient: Pair.instance as! PairingClient, + networkingClient: Networking.instance as! NetworkingInteractor ) }() + @available(*, deprecated, message: "Remove after clients migration") private static var metadata: AppMetadata? private init() { } @@ -37,7 +41,9 @@ public class Sign { /// Sign instance config method /// - Parameters: /// - metadata: App metadata + @available(*, deprecated, message: "Use Pair.configure(metadata:) instead") static public func configure(metadata: AppMetadata) { + Pair.configure(metadata: metadata) Sign.metadata = metadata } } diff --git a/Sources/WalletConnectSign/Sign/SignClient.swift b/Sources/WalletConnectSign/Sign/SignClient.swift index befbca947..27a156a63 100644 --- a/Sources/WalletConnectSign/Sign/SignClient.swift +++ b/Sources/WalletConnectSign/Sign/SignClient.swift @@ -1,8 +1,10 @@ import Foundation -import WalletConnectRelay +import Combine +import JSONRPC import WalletConnectUtils import WalletConnectKMS -import Combine +import WalletConnectNetworking +import WalletConnectPairing /// WalletConnect Sign Client /// @@ -10,6 +12,9 @@ import Combine /// /// Access via `Sign.instance` public final class SignClient { + enum Errors: Error { + case sessionForTopicNotFound + } // MARK: - Public Properties @@ -81,20 +86,29 @@ public final class SignClient { sessionExtendPublisherSubject.eraseToAnyPublisher() } + /// Publisher that sends session topic when session ping received + /// + /// Event will be emited on controller and non-controller clients. + public var pingResponsePublisher: AnyPublisher { + pingResponsePublisherSubject.eraseToAnyPublisher() + } + /// An object that loggs SDK's errors and info messages public let logger: ConsoleLogging // MARK: - Private properties - private let relayClient: RelayClient - private let pairingEngine: PairingEngine - private let pairEngine: PairEngine + private let pairingClient: PairingClient + private let networkingClient: NetworkingInteractor private let sessionEngine: SessionEngine private let approveEngine: ApproveEngine private let disconnectService: DisconnectService + private let pairingPingService: PairingPingService + private let sessionPingService: SessionPingService private let nonControllerSessionStateMachine: NonControllerSessionStateMachine private let controllerSessionStateMachine: ControllerSessionStateMachine - private let history: JsonRpcHistory + private let appProposeService: AppProposeService + private let history: RPCHistory private let cleanupService: CleanupService private let sessionProposalPublisherSubject = PassthroughSubject() @@ -107,34 +121,40 @@ public final class SignClient { private let sessionUpdatePublisherSubject = PassthroughSubject<(sessionTopic: String, namespaces: [String: SessionNamespace]), Never>() 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 var publishers = Set() // MARK: - Initialization init(logger: ConsoleLogging, - relayClient: RelayClient, - pairingEngine: PairingEngine, - pairEngine: PairEngine, + networkingClient: NetworkingInteractor, sessionEngine: SessionEngine, approveEngine: ApproveEngine, + pairingPingService: PairingPingService, + sessionPingService: SessionPingService, nonControllerSessionStateMachine: NonControllerSessionStateMachine, controllerSessionStateMachine: ControllerSessionStateMachine, + appProposeService: AppProposeService, disconnectService: DisconnectService, - history: JsonRpcHistory, - cleanupService: CleanupService + history: RPCHistory, + cleanupService: CleanupService, + pairingClient: PairingClient ) { self.logger = logger - self.relayClient = relayClient - self.pairingEngine = pairingEngine - self.pairEngine = pairEngine + self.networkingClient = networkingClient self.sessionEngine = sessionEngine self.approveEngine = approveEngine + self.pairingPingService = pairingPingService + self.sessionPingService = sessionPingService self.nonControllerSessionStateMachine = nonControllerSessionStateMachine self.controllerSessionStateMachine = controllerSessionStateMachine + self.appProposeService = appProposeService self.history = history self.cleanupService = cleanupService self.disconnectService = disconnectService + self.pairingClient = pairingClient + setUpConnectionObserving() setUpEnginesCallbacks() } @@ -147,22 +167,43 @@ public final class SignClient { /// - requiredNamespaces: required namespaces for a session /// - topic: Optional parameter - use it if you already have an established pairing with peer client. /// - Returns: Pairing URI that should be shared with responder out of bound. Common way is to present it as a QR code. Pairing URI will be nil if you are going to establish a session on existing Pairing and `topic` function parameter was provided. + @available(*, deprecated, message: "use Pair.instance.create() and connect(requiredNamespaces: [String: ProposalNamespace]): instead") public func connect(requiredNamespaces: [String: ProposalNamespace], topic: String? = nil) async throws -> WalletConnectURI? { logger.debug("Connecting Application") if let topic = topic { - guard let pairing = pairingEngine.getSettledPairing(for: topic) else { - throw WalletConnectError.noPairingMatchingTopic(topic) - } - logger.debug("Proposing session on existing pairing") - try await pairingEngine.propose(pairingTopic: topic, namespaces: requiredNamespaces, relay: pairing.relay) + try pairingClient.validatePairingExistance(topic) + try await appProposeService.propose( + pairingTopic: topic, + namespaces: requiredNamespaces, + relay: RelayProtocolOptions(protocol: "irn", data: nil) + ) return nil } else { - let pairingURI = try await pairingEngine.create() - try await pairingEngine.propose(pairingTopic: pairingURI.topic, namespaces: requiredNamespaces, relay: pairingURI.relay) + let pairingURI = try await pairingClient.create() + try await appProposeService.propose( + pairingTopic: pairingURI.topic, + namespaces: requiredNamespaces, + relay: RelayProtocolOptions(protocol: "irn", data: nil) + ) return pairingURI } } + /// For a dApp to propose a session to a wallet. + /// Function will propose a session on existing pairing. + /// - Parameters: + /// - requiredNamespaces: required namespaces for a session + /// - topic: pairing topic + public func connect(requiredNamespaces: [String: ProposalNamespace], topic: String) async throws { + logger.debug("Connecting Application") + try pairingClient.validatePairingExistance(topic) + try await appProposeService.propose( + pairingTopic: topic, + namespaces: requiredNamespaces, + relay: RelayProtocolOptions(protocol: "irn", data: nil) + ) + } + /// 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. @@ -170,11 +211,9 @@ public final class SignClient { /// Should Error: /// - When URI has invalid format or missing params /// - When topic is already in use + @available(*, deprecated, message: "use Pair.instance.pair(uri: WalletConnectURI): instead") public func pair(uri: WalletConnectURI) async throws { - guard uri.api == .sign else { - throw WalletConnectError.pairingUriWrongApiParam - } - try await pairEngine.pair(uri) + try await pairingClient.pair(uri: uri) } /// For a wallet to approve a session proposal. @@ -221,30 +260,22 @@ public final class SignClient { /// 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, response: JsonRpcResult) async throws { - try await sessionEngine.respondSessionRequest(topic: topic, response: response) + public func respond(topic: String, requestId: RPCID, response: RPCResult) async throws { + try await sessionEngine.respondSessionRequest(topic: topic, requestId: requestId, response: response) } /// Ping method allows to check if peer client is online and is subscribing for given topic /// /// Should Error: /// - When the session topic is not found - /// - When the response is neither result or error /// /// - Parameters: - /// - topic: Topic of a session or a pairing - /// - completion: Result will be success on response or an error - public func ping(topic: String, completion: @escaping ((Result) -> Void)) { - if pairingEngine.hasPairing(for: topic) { - pairingEngine.ping(topic: topic) { result in - completion(result) - } - } else if sessionEngine.hasSession(for: topic) { - sessionEngine.ping(topic: topic) { result in - completion(result) - } - } + /// - topic: Topic of a session + public func ping(topic: String) async throws { + guard sessionEngine.hasSession(for: topic) else { throw Errors.sessionForTopicNotFound } + try await sessionPingService.ping(topic: topic) } /// For the wallet to emit an event to a dApp @@ -280,8 +311,9 @@ public final class SignClient { /// Query pairings /// - Returns: All pairings + @available(*, deprecated, message: "use Pair.instance.getPairings(uri: WalletConnectURI): instead") public func getPairings() -> [Pairing] { - pairingEngine.getPairings() + pairingClient.getPairings() } /// Query pending requests @@ -289,10 +321,9 @@ public final class SignClient { /// - 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] { let pendingRequests: [Request] = history.getPending() - .filter {$0.request.method == .sessionRequest} .compactMap { - guard case let .sessionRequest(payloadRequest) = $0.request.params else {return nil} - return Request(id: $0.id, topic: $0.topic, method: payloadRequest.request.method, params: payloadRequest.request.params, chainId: payloadRequest.chainId) + guard let request = try? $0.request.params?.get(SessionType.RequestParams.self) else { return nil } + return Request(id: $0.id, topic: $0.topic, method: request.request.method, params: request.request.params, chainId: request.chainId) } if let topic = topic { return pendingRequests.filter {$0.topic == topic} @@ -303,11 +334,13 @@ public final class SignClient { /// - Parameter id: id of a wc_sessionRequest jsonrpc request /// - Returns: json rpc record object for given id or nil if record for give id does not exits - public func getSessionRequestRecord(id: Int64) -> WalletConnectUtils.JsonRpcRecord? { - guard let record = history.get(id: id), - case .sessionRequest(let payload) = record.request.params else {return nil} - let request = WalletConnectUtils.JsonRpcRecord.Request(method: payload.request.method, params: payload.request.params) - return WalletConnectUtils.JsonRpcRecord(id: record.id, topic: record.topic, request: request, response: record.response, chainId: record.chainId) + public func getSessionRequestRecord(id: RPCID) -> Request? { + guard + let record = history.get(recordId: id), + let request = try? record.request.params?.get(SessionType.RequestParams.self) + else { return nil } + + return Request(id: record.id, topic: record.topic, method: record.request.method, params: request, chainId: request.chainId) } #if DEBUG @@ -326,7 +359,7 @@ public final class SignClient { sessionProposalPublisherSubject.send(proposal) } approveEngine.onSessionRejected = { [unowned self] proposal, reason in - sessionRejectionPublisherSubject.send((proposal, reason.publicRepresentation())) + sessionRejectionPublisherSubject.send((proposal, reason)) } approveEngine.onSessionSettle = { [unowned self] settledSession in sessionSettlePublisherSubject.send(settledSession) @@ -335,7 +368,7 @@ public final class SignClient { sessionRequestPublisherSubject.send(sessionRequest) } sessionEngine.onSessionDelete = { [unowned self] topic, reason in - sessionDeletePublisherSubject.send((topic, reason.publicRepresentation())) + sessionDeletePublisherSubject.send((topic, reason)) } controllerSessionStateMachine.onNamespacesUpdate = { [unowned self] topic, namespaces in sessionUpdatePublisherSubject.send((topic, namespaces)) @@ -355,10 +388,16 @@ public final class SignClient { sessionEngine.onSessionResponse = { [unowned self] response in sessionResponsePublisherSubject.send(response) } + pairingPingService.onResponse = { [unowned self] topic in + pingResponsePublisherSubject.send(topic) + } + sessionPingService.onResponse = { [unowned self] topic in + pingResponsePublisherSubject.send(topic) + } } private func setUpConnectionObserving() { - relayClient.socketConnectionStatusPublisher.sink { [weak self] status in + networkingClient.socketConnectionStatusPublisher.sink { [weak self] status in self?.socketConnectionStatusPublisherSubject.send(status) }.store(in: &publishers) } diff --git a/Sources/WalletConnectSign/Sign/SignClientFactory.swift b/Sources/WalletConnectSign/Sign/SignClientFactory.swift index 9a0412ad4..a1c086783 100644 --- a/Sources/WalletConnectSign/Sign/SignClientFactory.swift +++ b/Sources/WalletConnectSign/Sign/SignClientFactory.swift @@ -3,6 +3,7 @@ import WalletConnectRelay import WalletConnectUtils import WalletConnectKMS import WalletConnectPairing +import WalletConnectNetworking public struct SignClientFactory { @@ -15,44 +16,45 @@ public struct SignClientFactory { /// - keyValueStorage: by default WalletConnect SDK will store sequences in UserDefaults /// /// WalletConnect Client is not a singleton but once you create an instance, you should not deinitialize it. Usually only one instance of a client is required in the application. - public static func create(metadata: AppMetadata, relayClient: RelayClient) -> SignClient { + public static func create(metadata: AppMetadata, pairingClient: PairingClient, networkingClient: NetworkingInteractor) -> SignClient { let logger = ConsoleLogger(loggingLevel: .off) let keyValueStorage = UserDefaults.standard let keychainStorage = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk") - return SignClientFactory.create(metadata: metadata, logger: logger, keyValueStorage: keyValueStorage, keychainStorage: keychainStorage, relayClient: relayClient) + return SignClientFactory.create(metadata: metadata, logger: logger, keyValueStorage: keyValueStorage, keychainStorage: keychainStorage, pairingClient: pairingClient, networkingClient: networkingClient) } - static func create(metadata: AppMetadata, logger: ConsoleLogging, keyValueStorage: KeyValueStorage, keychainStorage: KeychainStorageProtocol, relayClient: RelayClient) -> SignClient { + static func create(metadata: AppMetadata, logger: ConsoleLogging, keyValueStorage: KeyValueStorage, keychainStorage: KeychainStorageProtocol, pairingClient: PairingClient, networkingClient: NetworkingInteractor) -> SignClient { let kms = KeyManagementService(keychain: keychainStorage) - let serializer = Serializer(kms: kms) - let history = JsonRpcHistory(logger: logger, keyValueStore: CodableStore(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.jsonRpcHistory.rawValue)) - let networkingInteractor = NetworkInteractor(relayClient: relayClient, serializer: serializer, logger: logger, jsonRpcHistory: history) + let rpcHistory = RPCHistoryFactory.createForNetwork(keyValueStorage: keyValueStorage) let pairingStore = PairingStorage(storage: SequenceStore(store: .init(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.pairings.rawValue))) let sessionStore = SessionStorage(storage: SequenceStore(store: .init(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.sessions.rawValue))) let sessionToPairingTopic = CodableStore(defaults: RuntimeKeyValueStorage(), identifier: StorageDomainIdentifiers.sessionToPairingTopic.rawValue) - let proposalPayloadsStore = CodableStore(defaults: RuntimeKeyValueStorage(), identifier: StorageDomainIdentifiers.proposals.rawValue) - let pairingEngine = PairingEngine(networkingInteractor: networkingInteractor, kms: kms, pairingStore: pairingStore, metadata: metadata, logger: logger) - let sessionEngine = SessionEngine(networkingInteractor: networkingInteractor, kms: kms, sessionStore: sessionStore, logger: logger) - let nonControllerSessionStateMachine = NonControllerSessionStateMachine(networkingInteractor: networkingInteractor, kms: kms, sessionStore: sessionStore, logger: logger) - let controllerSessionStateMachine = ControllerSessionStateMachine(networkingInteractor: networkingInteractor, kms: kms, sessionStore: sessionStore, logger: logger) - let pairEngine = PairEngine(networkingInteractor: networkingInteractor, kms: kms, pairingStore: pairingStore) - let approveEngine = ApproveEngine(networkingInteractor: networkingInteractor, proposalPayloadsStore: proposalPayloadsStore, sessionToPairingTopic: sessionToPairingTopic, metadata: metadata, kms: kms, logger: logger, pairingStore: pairingStore, sessionStore: sessionStore) + let proposalPayloadsStore = CodableStore>(defaults: RuntimeKeyValueStorage(), identifier: StorageDomainIdentifiers.proposals.rawValue) + let sessionEngine = SessionEngine(networkingInteractor: networkingClient, kms: kms, sessionStore: sessionStore, logger: logger) + 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 = CleanupService(pairingStore: pairingStore, sessionStore: sessionStore, kms: kms, sessionToPairingTopic: sessionToPairingTopic) - let deletePairingService = DeletePairingService(networkingInteractor: networkingInteractor, kms: kms, pairingStorage: pairingStore, logger: logger) - let deleteSessionService = DeleteSessionService(networkingInteractor: networkingInteractor, kms: kms, sessionStore: sessionStore, logger: logger) - let disconnectService = DisconnectService(deletePairingService: deletePairingService, deleteSessionService: deleteSessionService, pairingStorage: pairingStore, sessionStorage: sessionStore) + let deleteSessionService = DeleteSessionService(networkingInteractor: networkingClient, kms: kms, sessionStore: sessionStore, logger: logger) + let disconnectService = DisconnectService(deleteSessionService: deleteSessionService, sessionStorage: sessionStore) + let sessionPingService = SessionPingService(sessionStorage: sessionStore, networkingInteractor: networkingClient, logger: logger) + let pairingPingService = PairingPingService(pairingStorage: pairingStore, networkingInteractor: networkingClient, logger: logger) + let appProposerService = AppProposeService(metadata: metadata, networkingInteractor: networkingClient, kms: kms, logger: logger) let client = SignClient( logger: logger, - relayClient: relayClient, - pairingEngine: pairingEngine, - pairEngine: pairEngine, + networkingClient: networkingClient, sessionEngine: sessionEngine, approveEngine: approveEngine, + pairingPingService: pairingPingService, + sessionPingService: sessionPingService, nonControllerSessionStateMachine: nonControllerSessionStateMachine, - controllerSessionStateMachine: controllerSessionStateMachine, disconnectService: disconnectService, - history: history, - cleanupService: cleanupService + controllerSessionStateMachine: controllerSessionStateMachine, + appProposeService: appProposerService, + disconnectService: disconnectService, + history: rpcHistory, + cleanupService: cleanupService, + pairingClient: pairingClient ) return client } diff --git a/Sources/WalletConnectSign/StorageDomainIdentifiers.swift b/Sources/WalletConnectSign/StorageDomainIdentifiers.swift index 1e0c2a799..89a0b52a1 100644 --- a/Sources/WalletConnectSign/StorageDomainIdentifiers.swift +++ b/Sources/WalletConnectSign/StorageDomainIdentifiers.swift @@ -1,7 +1,6 @@ import Foundation enum StorageDomainIdentifiers: String { - case jsonRpcHistory = "com.walletconnect.sdk.wc_jsonRpcHistoryRecord" case pairings = "com.walletconnect.sdk.pairingSequences" case sessions = "com.walletconnect.sdk.sessionSequences" case proposals = "com.walletconnect.sdk.sessionProposals" diff --git a/Sources/WalletConnectSign/Subscription/WCRequestSubscriptionPayload.swift b/Sources/WalletConnectSign/Subscription/WCRequestSubscriptionPayload.swift deleted file mode 100644 index 91efa9654..000000000 --- a/Sources/WalletConnectSign/Subscription/WCRequestSubscriptionPayload.swift +++ /dev/null @@ -1,11 +0,0 @@ -import Foundation -import WalletConnectUtils - -struct WCRequestSubscriptionPayload: Codable { - let topic: String - let wcRequest: WCRequest - - var timestamp: Date { - return JsonRpcID.timestamp(from: wcRequest.id) - } -} diff --git a/Sources/WalletConnectSign/Types/ProtocolMethods/SessionDeleteProtocolMethod.swift b/Sources/WalletConnectSign/Types/ProtocolMethods/SessionDeleteProtocolMethod.swift new file mode 100644 index 000000000..927f100af --- /dev/null +++ b/Sources/WalletConnectSign/Types/ProtocolMethods/SessionDeleteProtocolMethod.swift @@ -0,0 +1,10 @@ +import Foundation +import WalletConnectNetworking + +struct SessionDeleteProtocolMethod: ProtocolMethod { + let method: String = "wc_sessionDelete" + + let requestConfig = RelayConfig(tag: 1112, prompt: false, ttl: 86400) + + let responseConfig = RelayConfig(tag: 1113, prompt: false, ttl: 86400) +} diff --git a/Sources/WalletConnectSign/Types/ProtocolMethods/SessionEventProtocolMethod.swift b/Sources/WalletConnectSign/Types/ProtocolMethods/SessionEventProtocolMethod.swift new file mode 100644 index 000000000..bee208ed4 --- /dev/null +++ b/Sources/WalletConnectSign/Types/ProtocolMethods/SessionEventProtocolMethod.swift @@ -0,0 +1,10 @@ +import Foundation +import WalletConnectNetworking + +struct SessionEventProtocolMethod: ProtocolMethod { + let method: String = "wc_sessionEvent" + + let requestConfig = RelayConfig(tag: 1110, prompt: true, ttl: 300) + + let responseConfig = RelayConfig(tag: 1111, prompt: false, ttl: 300) +} diff --git a/Sources/WalletConnectSign/Types/ProtocolMethods/SessionExtendProtocolMethod.swift b/Sources/WalletConnectSign/Types/ProtocolMethods/SessionExtendProtocolMethod.swift new file mode 100644 index 000000000..b77e5b9ff --- /dev/null +++ b/Sources/WalletConnectSign/Types/ProtocolMethods/SessionExtendProtocolMethod.swift @@ -0,0 +1,10 @@ +import Foundation +import WalletConnectNetworking + +struct SessionExtendProtocolMethod: ProtocolMethod { + let method: String = "wc_sessionExtend" + + let requestConfig = RelayConfig(tag: 1106, prompt: false, ttl: 86400) + + let responseConfig = RelayConfig(tag: 1107, prompt: false, ttl: 86400) +} diff --git a/Sources/WalletConnectSign/Types/ProtocolMethods/SessionPingProtocolMethod.swift b/Sources/WalletConnectSign/Types/ProtocolMethods/SessionPingProtocolMethod.swift new file mode 100644 index 000000000..91ff13035 --- /dev/null +++ b/Sources/WalletConnectSign/Types/ProtocolMethods/SessionPingProtocolMethod.swift @@ -0,0 +1,10 @@ +import Foundation +import WalletConnectNetworking + +struct SessionPingProtocolMethod: ProtocolMethod { + let method: String = "wc_sessionPing" + + let requestConfig = RelayConfig(tag: 1114, prompt: false, ttl: 30) + + let responseConfig = RelayConfig(tag: 1115, prompt: false, ttl: 30) +} diff --git a/Sources/WalletConnectSign/Types/ProtocolMethods/SessionProposeProtocolMethod.swift b/Sources/WalletConnectSign/Types/ProtocolMethods/SessionProposeProtocolMethod.swift new file mode 100644 index 000000000..47191c4a2 --- /dev/null +++ b/Sources/WalletConnectSign/Types/ProtocolMethods/SessionProposeProtocolMethod.swift @@ -0,0 +1,10 @@ +import Foundation +import WalletConnectNetworking + +struct SessionProposeProtocolMethod: ProtocolMethod { + let method: String = "wc_sessionPropose" + + let requestConfig = RelayConfig(tag: 1100, prompt: true, ttl: 300) + + let responseConfig = RelayConfig(tag: 1101, prompt: false, ttl: 300) +} diff --git a/Sources/WalletConnectSign/Types/ProtocolMethods/SessionRequestProtocolMethod.swift b/Sources/WalletConnectSign/Types/ProtocolMethods/SessionRequestProtocolMethod.swift new file mode 100644 index 000000000..e2c0e4234 --- /dev/null +++ b/Sources/WalletConnectSign/Types/ProtocolMethods/SessionRequestProtocolMethod.swift @@ -0,0 +1,10 @@ +import Foundation +import WalletConnectNetworking + +struct SessionRequestProtocolMethod: ProtocolMethod { + let method: String = "wc_sessionRequest" + + let requestConfig = RelayConfig(tag: 1108, prompt: true, ttl: 300) + + let responseConfig = RelayConfig(tag: 1109, prompt: false, ttl: 300) +} diff --git a/Sources/WalletConnectSign/Types/ProtocolMethods/SessionSettleProtocolMethod.swift b/Sources/WalletConnectSign/Types/ProtocolMethods/SessionSettleProtocolMethod.swift new file mode 100644 index 000000000..131560b4c --- /dev/null +++ b/Sources/WalletConnectSign/Types/ProtocolMethods/SessionSettleProtocolMethod.swift @@ -0,0 +1,10 @@ +import Foundation +import WalletConnectNetworking + +struct SessionSettleProtocolMethod: ProtocolMethod { + let method: String = "wc_sessionSettle" + + let requestConfig = RelayConfig(tag: 1102, prompt: false, ttl: 300) + + let responseConfig = RelayConfig(tag: 1103, prompt: false, ttl: 300) +} diff --git a/Sources/WalletConnectSign/Types/ProtocolMethods/SessionUpdateProtocolMethod.swift b/Sources/WalletConnectSign/Types/ProtocolMethods/SessionUpdateProtocolMethod.swift new file mode 100644 index 000000000..eb7936b4b --- /dev/null +++ b/Sources/WalletConnectSign/Types/ProtocolMethods/SessionUpdateProtocolMethod.swift @@ -0,0 +1,10 @@ +import Foundation +import WalletConnectNetworking + +struct SessionUpdateProtocolMethod: ProtocolMethod { + let method: String = "wc_sessionUpdate" + + let requestConfig = RelayConfig(tag: 1104, prompt: false, ttl: 86400) + + let responseConfig = RelayConfig(tag: 1105, prompt: false, ttl: 86400) +} diff --git a/Sources/WalletConnectSign/Types/ReasonCode.swift b/Sources/WalletConnectSign/Types/ReasonCode.swift index cdc4f8db5..8ca23bd4c 100644 --- a/Sources/WalletConnectSign/Types/ReasonCode.swift +++ b/Sources/WalletConnectSign/Types/ReasonCode.swift @@ -1,4 +1,6 @@ -enum ReasonCode: Codable, Equatable { +import WalletConnectNetworking + +enum ReasonCode: Reason, Codable, Equatable { enum Context: String, Codable { case pairing = "pairing" diff --git a/Sources/WalletConnectSign/Types/Session/SessionType.swift b/Sources/WalletConnectSign/Types/Session/SessionType.swift index 054704eae..f450e94aa 100644 --- a/Sources/WalletConnectSign/Types/Session/SessionType.swift +++ b/Sources/WalletConnectSign/Types/Session/SessionType.swift @@ -1,5 +1,6 @@ import Foundation import WalletConnectUtils +import WalletConnectNetworking // Internal namespace for session payloads. internal enum SessionType { @@ -24,7 +25,7 @@ internal enum SessionType { typealias DeleteParams = SessionType.Reason - struct Reason: Codable, Equatable { + struct Reason: Codable, Equatable, WalletConnectNetworking.Reason { let code: Int let message: String @@ -64,15 +65,3 @@ internal enum SessionType { let expiry: Int64 } } - -internal extension Reason { - func internalRepresentation() -> SessionType.Reason { - SessionType.Reason(code: self.code, message: self.message) - } -} - -extension SessionType.Reason { - func publicRepresentation() -> Reason { - Reason(code: self.code, message: self.message) - } -} diff --git a/Sources/WalletConnectSign/Types/WCMethod.swift b/Sources/WalletConnectSign/Types/WCMethod.swift deleted file mode 100644 index e2002188a..000000000 --- a/Sources/WalletConnectSign/Types/WCMethod.swift +++ /dev/null @@ -1,37 +0,0 @@ -import Foundation -import WalletConnectPairing - -enum WCMethod { - case wcPairingPing - case wcSessionPropose(SessionType.ProposeParams) - case wcSessionSettle(SessionType.SettleParams) - case wcSessionUpdate(SessionType.UpdateParams) - case wcSessionExtend(SessionType.UpdateExpiryParams) - case wcSessionDelete(SessionType.DeleteParams) - case wcSessionRequest(SessionType.RequestParams) - case wcSessionPing - case wcSessionEvent(SessionType.EventParams) - - func asRequest() -> WCRequest { - switch self { - case .wcPairingPing: - return WCRequest(method: .pairingPing, params: .pairingPing(PairingType.PingParams())) - case .wcSessionPropose(let proposalParams): - return WCRequest(method: .sessionPropose, params: .sessionPropose(proposalParams)) - case .wcSessionSettle(let settleParams): - return WCRequest(method: .sessionSettle, params: .sessionSettle(settleParams)) - case .wcSessionUpdate(let updateParams): - return WCRequest(method: .sessionUpdate, params: .sessionUpdate(updateParams)) - case .wcSessionExtend(let updateExpiryParams): - return WCRequest(method: .sessionExtend, params: .sessionExtend(updateExpiryParams)) - case .wcSessionDelete(let deleteParams): - return WCRequest(method: .sessionDelete, params: .sessionDelete(deleteParams)) - case .wcSessionRequest(let payloadParams): - return WCRequest(method: .sessionRequest, params: .sessionRequest(payloadParams)) - case .wcSessionPing: - return WCRequest(method: .sessionPing, params: .sessionPing(SessionType.PingParams())) - case .wcSessionEvent(let eventParams): - return WCRequest(method: .sessionEvent, params: .sessionEvent(eventParams)) - } - } -} diff --git a/Sources/WalletConnectSign/Types/WCRequest.swift b/Sources/WalletConnectSign/Types/WCRequest.swift deleted file mode 100644 index ec9edfdf8..000000000 --- a/Sources/WalletConnectSign/Types/WCRequest.swift +++ /dev/null @@ -1,179 +0,0 @@ -import Foundation -import WalletConnectPairing -import WalletConnectUtils - -struct WCRequest: Codable { - let id: Int64 - let jsonrpc: String - let method: Method - let params: Params - - enum CodingKeys: CodingKey { - case id - case jsonrpc - case method - case params - } - - internal init(id: Int64 = JsonRpcID.generate(), jsonrpc: String = "2.0", method: Method, params: Params) { - self.id = id - self.jsonrpc = jsonrpc - self.method = method - self.params = params - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - id = try container.decode(Int64.self, forKey: .id) - jsonrpc = try container.decode(String.self, forKey: .jsonrpc) - method = try container.decode(Method.self, forKey: .method) - switch method { - case .pairingDelete: - let paramsValue = try container.decode(PairingType.DeleteParams.self, forKey: .params) - params = .pairingDelete(paramsValue) - case .pairingPing: - let paramsValue = try container.decode(PairingType.PingParams.self, forKey: .params) - params = .pairingPing(paramsValue) - case .sessionPropose: - let paramsValue = try container.decode(SessionType.ProposeParams.self, forKey: .params) - params = .sessionPropose(paramsValue) - case .sessionSettle: - let paramsValue = try container.decode(SessionType.SettleParams.self, forKey: .params) - params = .sessionSettle(paramsValue) - case .sessionUpdate: - let paramsValue = try container.decode(SessionType.UpdateParams.self, forKey: .params) - params = .sessionUpdate(paramsValue) - case .sessionDelete: - let paramsValue = try container.decode(SessionType.DeleteParams.self, forKey: .params) - params = .sessionDelete(paramsValue) - case .sessionRequest: - let paramsValue = try container.decode(SessionType.RequestParams.self, forKey: .params) - params = .sessionRequest(paramsValue) - case .sessionPing: - let paramsValue = try container.decode(SessionType.PingParams.self, forKey: .params) - params = .sessionPing(paramsValue) - case .sessionExtend: - let paramsValue = try container.decode(SessionType.UpdateExpiryParams.self, forKey: .params) - params = .sessionExtend(paramsValue) - case .sessionEvent: - let paramsValue = try container.decode(SessionType.EventParams.self, forKey: .params) - params = .sessionEvent(paramsValue) - } - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(id, forKey: .id) - try container.encode(jsonrpc, forKey: .jsonrpc) - try container.encode(method.rawValue, forKey: .method) - switch params { - case .pairingDelete(let params): - try container.encode(params, forKey: .params) - case .pairingPing(let params): - try container.encode(params, forKey: .params) - case .sessionPropose(let params): - try container.encode(params, forKey: .params) - case .sessionSettle(let params): - try container.encode(params, forKey: .params) - case .sessionUpdate(let params): - try container.encode(params, forKey: .params) - case .sessionExtend(let params): - try container.encode(params, forKey: .params) - case .sessionDelete(let params): - try container.encode(params, forKey: .params) - case .sessionRequest(let params): - try container.encode(params, forKey: .params) - case .sessionPing(let params): - try container.encode(params, forKey: .params) - case .sessionEvent(let params): - try container.encode(params, forKey: .params) - } - } -} - -extension WCRequest { - enum Method: String, Codable { - case pairingDelete = "wc_pairingDelete" - case pairingPing = "wc_pairingPing" - case sessionPropose = "wc_sessionPropose" - case sessionSettle = "wc_sessionSettle" - case sessionUpdate = "wc_sessionUpdate" - case sessionExtend = "wc_sessionExtend" - case sessionDelete = "wc_sessionDelete" - case sessionRequest = "wc_sessionRequest" - case sessionPing = "wc_sessionPing" - case sessionEvent = "wc_sessionEvent" - } -} - -extension WCRequest { - enum Params: Codable, Equatable { - case pairingDelete(PairingType.DeleteParams) - case pairingPing(PairingType.PingParams) - case sessionPropose(SessionType.ProposeParams) - case sessionSettle(SessionType.SettleParams) - case sessionUpdate(SessionType.UpdateParams) - case sessionExtend(SessionType.UpdateExpiryParams) - case sessionDelete(SessionType.DeleteParams) - case sessionRequest(SessionType.RequestParams) - case sessionPing(SessionType.PingParams) - case sessionEvent(SessionType.EventParams) - - static func == (lhs: Params, rhs: Params) -> Bool { - switch (lhs, rhs) { - case (.pairingDelete(let lhsParam), pairingDelete(let rhsParam)): - return lhsParam == rhsParam - case (.sessionPropose(let lhsParam), sessionPropose(let rhsParam)): - return lhsParam == rhsParam - case (.sessionSettle(let lhsParam), sessionSettle(let rhsParam)): - return lhsParam == rhsParam - case (.sessionUpdate(let lhsParam), sessionUpdate(let rhsParam)): - return lhsParam == rhsParam - case (.sessionExtend(let lhsParam), sessionExtend(let rhsParams)): - return lhsParam == rhsParams - case (.sessionDelete(let lhsParam), sessionDelete(let rhsParam)): - return lhsParam == rhsParam - case (.sessionRequest(let lhsParam), sessionRequest(let rhsParam)): - return lhsParam == rhsParam - case (.sessionPing(let lhsParam), sessionPing(let rhsParam)): - return lhsParam == rhsParam - case (.sessionEvent(let lhsParam), sessionEvent(let rhsParam)): - return lhsParam == rhsParam - default: - return false - } - } - } -} - -extension WCRequest { - - var tag: Int { - switch method { - case .pairingDelete: - return 1000 - case .pairingPing: - return 1002 - case .sessionPropose: - return 1100 - case .sessionSettle: - return 1102 - case .sessionUpdate: - return 1104 - case .sessionExtend: - return 1106 - case .sessionDelete: - return 1112 - case .sessionRequest: - return 1108 - case .sessionPing: - return 1114 - case .sessionEvent: - return 1110 - } - } - - var responseTag: Int { - return tag + 1 - } -} diff --git a/Sources/WalletConnectSign/Types/WCResponse.swift b/Sources/WalletConnectSign/Types/WCResponse.swift deleted file mode 100644 index b457b9ed0..000000000 --- a/Sources/WalletConnectSign/Types/WCResponse.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Foundation -import WalletConnectUtils - -struct WCResponse: Codable { - let topic: String - let chainId: String? - let requestMethod: WCRequest.Method - let requestParams: WCRequest.Params - let result: JsonRpcResult - - var timestamp: Date { - return JsonRpcID.timestamp(from: result.id) - } -} diff --git a/Sources/WalletConnectSign/WalletConnectError.swift b/Sources/WalletConnectSign/WalletConnectError.swift index afd7f73b8..5579b1d6f 100644 --- a/Sources/WalletConnectSign/WalletConnectError.swift +++ b/Sources/WalletConnectSign/WalletConnectError.swift @@ -1,7 +1,6 @@ enum WalletConnectError: Error { case pairingProposalFailed - case pairingUriWrongApiParam case noPairingMatchingTopic(String) case noSessionMatchingTopic(String) case sessionNotAcknowledged(String) @@ -29,8 +28,6 @@ extension WalletConnectError { switch self { case .pairingProposalFailed: return "Pairing proposal failed." - case .pairingUriWrongApiParam: - return "Pairing URI containt wrong API param" case .noPairingMatchingTopic(let topic): return "There is no existing pairing matching the topic: \(topic)." case .noSessionMatchingTopic(let topic): diff --git a/Sources/WalletConnectUtils/JSONRPC/JSONRPCErrorResponse.swift b/Sources/WalletConnectUtils/JSONRPC/JSONRPCErrorResponse.swift deleted file mode 100644 index 15f2c3de1..000000000 --- a/Sources/WalletConnectUtils/JSONRPC/JSONRPCErrorResponse.swift +++ /dev/null @@ -1,27 +0,0 @@ -import Foundation - -public struct JSONRPCErrorResponse: Error, Equatable, Codable { - public let jsonrpc = "2.0" - public let id: Int64 - public let error: JSONRPCErrorResponse.Error - - enum CodingKeys: String, CodingKey { - case jsonrpc - case id - case error - } - - public init(id: Int64, error: JSONRPCErrorResponse.Error) { - self.id = id - self.error = error - } - - public struct Error: Codable, Equatable { - public let code: Int - public let message: String - public init(code: Int, message: String) { - self.code = code - self.message = message - } - } -} diff --git a/Sources/WalletConnectUtils/JSONRPC/JSONRPCRequest.swift b/Sources/WalletConnectUtils/JSONRPC/JSONRPCRequest.swift deleted file mode 100644 index 2c3f57bb9..000000000 --- a/Sources/WalletConnectUtils/JSONRPC/JSONRPCRequest.swift +++ /dev/null @@ -1,23 +0,0 @@ -import Foundation - -public struct JSONRPCRequest: Codable, Equatable { - - public let id: Int64 - public let jsonrpc: String - public let method: String - public let params: T - - public enum CodingKeys: CodingKey { - case id - case jsonrpc - case method - case params - } - - public init(id: Int64 = JsonRpcID.generate(), method: String, params: T) { - self.id = id - self.jsonrpc = "2.0" - self.method = method - self.params = params - } -} diff --git a/Sources/WalletConnectUtils/JSONRPC/JSONRPCResponse.swift b/Sources/WalletConnectUtils/JSONRPC/JSONRPCResponse.swift deleted file mode 100644 index e7dd48923..000000000 --- a/Sources/WalletConnectUtils/JSONRPC/JSONRPCResponse.swift +++ /dev/null @@ -1,18 +0,0 @@ -import Foundation - -public struct JSONRPCResponse: Codable, Equatable { - public let jsonrpc = "2.0" - public let id: Int64 - public let result: T - - enum CodingKeys: String, CodingKey { - case jsonrpc - case id - case result - } - - public init(id: Int64, result: T) { - self.id = id - self.result = result - } -} diff --git a/Sources/WalletConnectUtils/JSONRPC/JsonRpcResult.swift b/Sources/WalletConnectUtils/JSONRPC/JsonRpcResult.swift deleted file mode 100644 index 611712500..000000000 --- a/Sources/WalletConnectUtils/JSONRPC/JsonRpcResult.swift +++ /dev/null @@ -1,24 +0,0 @@ -import Foundation - -public enum JsonRpcResult: Codable { - case error(JSONRPCErrorResponse) - case response(JSONRPCResponse) - - public var id: Int64 { - switch self { - case .error(let value): - return value.id - case .response(let value): - return value.id - } - } - - public var value: Codable { - switch self { - case .error(let value): - return value - case .response(let value): - return value - } - } -} diff --git a/Sources/WalletConnectUtils/JsonRpcHistory.swift b/Sources/WalletConnectUtils/JsonRpcHistory.swift deleted file mode 100644 index 9616b794b..000000000 --- a/Sources/WalletConnectUtils/JsonRpcHistory.swift +++ /dev/null @@ -1,58 +0,0 @@ -import Foundation - -public class JsonRpcHistory where T: Codable&Equatable { - enum RecordingError: Error { - case jsonRpcDuplicateDetected - case noJsonRpcRequestMatchingResponse - } - private let storage: CodableStore - private let logger: ConsoleLogging - - public init(logger: ConsoleLogging, keyValueStore: CodableStore) { - self.logger = logger - self.storage = keyValueStore - } - - public func get(id: Int64) -> JsonRpcRecord? { - try? storage.get(key: "\(id)") - } - - public func set(topic: String, request: JSONRPCRequest, chainId: String? = nil) throws { - guard !exist(id: request.id) else { - throw RecordingError.jsonRpcDuplicateDetected - } - logger.debug("Setting JSON-RPC request history record") - let record = JsonRpcRecord(id: request.id, topic: topic, request: JsonRpcRecord.Request(method: request.method, params: AnyCodable(request.params)), response: nil, chainId: chainId) - storage.set(record, forKey: "\(request.id)") - } - - public func delete(topic: String) { - storage.getAll().forEach { record in - if record.topic == topic { - storage.delete(forKey: "\(record.id)") - } - } - } - - public func resolve(response: JsonRpcResult) throws -> JsonRpcRecord { - logger.debug("Resolving JSON-RPC response - ID: \(response.id)") - guard var record = try? storage.get(key: "\(response.id)") else { - throw RecordingError.noJsonRpcRequestMatchingResponse - } - if record.response != nil { - throw RecordingError.jsonRpcDuplicateDetected - } else { - record.response = response - storage.set(record, forKey: "\(record.id)") - return record - } - } - - public func exist(id: Int64) -> Bool { - return (try? storage.get(key: "\(id)")) != nil - } - - public func getPending() -> [JsonRpcRecord] { - storage.getAll().filter {$0.response == nil} - } -} diff --git a/Sources/WalletConnectUtils/JsonRpcRecord.swift b/Sources/WalletConnectUtils/JsonRpcRecord.swift deleted file mode 100644 index 48013221f..000000000 --- a/Sources/WalletConnectUtils/JsonRpcRecord.swift +++ /dev/null @@ -1,27 +0,0 @@ -import Foundation - -public struct JsonRpcRecord: Codable { - public let id: Int64 - public let topic: String - public let request: Request - public var response: JsonRpcResult? - public let chainId: String? - - public init(id: Int64, topic: String, request: JsonRpcRecord.Request, response: JsonRpcResult? = nil, chainId: String?) { - self.id = id - self.topic = topic - self.request = request - self.response = response - self.chainId = chainId - } - - public struct Request: Codable { - public let method: String - public let params: AnyCodable - - public init(method: String, params: AnyCodable) { - self.method = method - self.params = params - } - } -} diff --git a/Sources/WalletConnectUtils/RPCHistory.swift b/Sources/WalletConnectUtils/RPCHistory/RPCHistory.swift similarity index 97% rename from Sources/WalletConnectUtils/RPCHistory.swift rename to Sources/WalletConnectUtils/RPCHistory/RPCHistory.swift index 255801b24..c7f382c1b 100644 --- a/Sources/WalletConnectUtils/RPCHistory.swift +++ b/Sources/WalletConnectUtils/RPCHistory/RPCHistory.swift @@ -24,7 +24,7 @@ public final class RPCHistory { private let storage: CodableStore - public init(keyValueStore: CodableStore) { + init(keyValueStore: CodableStore) { self.storage = keyValueStore } diff --git a/Sources/WalletConnectUtils/RPCHistory/RPCHistoryFactory.swift b/Sources/WalletConnectUtils/RPCHistory/RPCHistoryFactory.swift new file mode 100644 index 000000000..817ca2d17 --- /dev/null +++ b/Sources/WalletConnectUtils/RPCHistory/RPCHistoryFactory.swift @@ -0,0 +1,20 @@ +import Foundation + +public struct RPCHistoryFactory { + + private static let networkIdentifier = "com.walletconnect.sdk.wc_jsonRpcHistoryRecord" + private static let relayIdentifier = "com.walletconnect.sdk.relayer_client.subscription_json_rpc_record" + + public static func createForNetwork(keyValueStorage: KeyValueStorage) -> RPCHistory { + return RPCHistoryFactory.create(keyValueStorage: keyValueStorage, identifier: RPCHistoryFactory.networkIdentifier) + } + + public static func createForRelay(keyValueStorage: KeyValueStorage) -> RPCHistory { + return RPCHistoryFactory.create(keyValueStorage: keyValueStorage, identifier: RPCHistoryFactory.relayIdentifier) + } + + static func create(keyValueStorage: KeyValueStorage, identifier: String) -> RPCHistory { + let keyValueStore = CodableStore(defaults: keyValueStorage, identifier: identifier) + return RPCHistory(keyValueStore: keyValueStore) + } +} diff --git a/Sources/WalletConnectUtils/SetStore.swift b/Sources/WalletConnectUtils/SetStore.swift new file mode 100644 index 000000000..a39508150 --- /dev/null +++ b/Sources/WalletConnectUtils/SetStore.swift @@ -0,0 +1,36 @@ +import Foundation + +public class SetStore: CustomStringConvertible { + + private let concurrentQueue: DispatchQueue + + private var store: Set = Set() + + public init(label: String) { + self.concurrentQueue = DispatchQueue(label: label, attributes: .concurrent) + } + + public func insert(_ element: T) { + concurrentQueue.async(flags: .barrier) { [weak self] in + self?.store.insert(element) + } + } + + public func remove(_ element: T) { + concurrentQueue.async(flags: .barrier) { [weak self] in + self?.store.remove(element) + } + } + + public func contains(_ element: T) -> Bool { + var contains = false + concurrentQueue.sync { [unowned self] in + contains = store.contains(element) + } + return contains + } + + public var description: String { + return store.description + } +} diff --git a/Sources/WalletConnectUtils/WalletConnectURI.swift b/Sources/WalletConnectUtils/WalletConnectURI.swift index cd4fa03f7..7db772582 100644 --- a/Sources/WalletConnectUtils/WalletConnectURI.swift +++ b/Sources/WalletConnectUtils/WalletConnectURI.swift @@ -2,36 +2,20 @@ import Foundation public struct WalletConnectURI: Equatable { - public enum TargetAPI: String, CaseIterable { - case sign - case chat - case auth - } - public let topic: String public let version: String public let symKey: String public let relay: RelayProtocolOptions - public var api: TargetAPI { - return apiType ?? .sign - } - public var absoluteString: String { - if let api = apiType { - return "wc:\(api.rawValue)-\(topic)@\(version)?symKey=\(symKey)&\(relayQuery)" - } return "wc:\(topic)@\(version)?symKey=\(symKey)&\(relayQuery)" } - private let apiType: TargetAPI? - - public init(topic: String, symKey: String, relay: RelayProtocolOptions, api: TargetAPI? = nil) { + public init(topic: String, symKey: String, relay: RelayProtocolOptions) { self.version = "2" self.topic = topic self.symKey = symKey self.relay = relay - self.apiType = api } public init?(string: String) { @@ -41,21 +25,19 @@ public struct WalletConnectURI: Equatable { let query: [String: String]? = components.queryItems?.reduce(into: [:]) { $0[$1.name] = $1.value } guard - let userString = components.user, + let topic = components.user, let version = components.host, let symKey = query?["symKey"], let relayProtocol = query?["relay-protocol"] else { return nil } - let uriUser = Self.parseURIUser(from: userString) let relayData = query?["relay-data"] self.version = version - self.topic = uriUser.topic + self.topic = topic self.symKey = symKey self.relay = RelayProtocolOptions(protocol: relayProtocol, data: relayData) - self.apiType = uriUser.api } private var relayQuery: String { @@ -73,13 +55,4 @@ public struct WalletConnectURI: Equatable { let urlString = !string.hasPrefix("wc://") ? string.replacingOccurrences(of: "wc:", with: "wc://") : string return URLComponents(string: urlString) } - - private static func parseURIUser(from string: String) -> (topic: String, api: TargetAPI?) { - let splits = string.split(separator: "-") - if splits.count == 2, let apiFromPrefix = TargetAPI(rawValue: String(splits[0])) { - return (String(splits[1]), apiFromPrefix) - } else { - return (string, nil) - } - } } diff --git a/Tests/AuthTests/AppRespondSubscriberTests.swift b/Tests/AuthTests/AppRespondSubscriberTests.swift index 98b8f47dd..07492323e 100644 --- a/Tests/AuthTests/AppRespondSubscriberTests.swift +++ b/Tests/AuthTests/AppRespondSubscriberTests.swift @@ -1,8 +1,8 @@ import Foundation import XCTest @testable import Auth -import WalletConnectUtils -import WalletConnectNetworking +@testable import WalletConnectUtils +@testable import WalletConnectNetworking @testable import WalletConnectKMS @testable import TestingUtils import JSONRPC @@ -15,23 +15,24 @@ class AppRespondSubscriberTests: XCTestCase { let defaultTimeout: TimeInterval = 0.01 let walletAccount = Account(chainIdentifier: "eip155:1", address: "0x724d0D2DaD3fbB0C168f947B87Fa5DBe36F1A8bf")! let prvKey = Data(hex: "462c1dad6832d7d96ccf87bd6a686a4110e114aaaebd5512e552c0e3a87b480f") - var messageSigner: MessageSigner! + var messageSigner: (MessageSigning & MessageSignatureVerifying)! var pairingStorage: WCPairingStorageMock! + var pairingRegisterer: PairingRegistererMock! override func setUp() { networkingInteractor = NetworkingInteractorMock() messageFormatter = SIWEMessageFormatter() - messageSigner = MessageSigner() - let historyStorage = CodableStore(defaults: RuntimeKeyValueStorage(), identifier: StorageDomainIdentifiers.jsonRpcHistory.rawValue) - rpcHistory = RPCHistory(keyValueStore: historyStorage) + messageSigner = MessageSignerFactory.create(projectId: "project-id") + rpcHistory = RPCHistoryFactory.createForNetwork(keyValueStorage: RuntimeKeyValueStorage()) pairingStorage = WCPairingStorageMock() + pairingRegisterer = PairingRegistererMock() sut = AppRespondSubscriber( networkingInteractor: networkingInteractor, logger: ConsoleLoggerMock(), rpcHistory: rpcHistory, signatureVerifier: messageSigner, - messageFormatter: messageFormatter, - pairingStorage: pairingStorage) + pairingRegisterer: pairingRegisterer, + messageFormatter: messageFormatter) } func testMessageCompromisedFailure() { @@ -56,7 +57,7 @@ class AppRespondSubscriberTests: XCTestCase { let payload = CacaoPayload(params: AuthPayload.stub(nonce: "compromised nonce"), didpkh: DIDPKH(account: walletAccount)) let message = try! messageFormatter.formatMessage(from: payload) - let cacaoSignature = try! messageSigner.sign(message: message, privateKey: prvKey) + let cacaoSignature = try! messageSigner.sign(message: message, privateKey: prvKey, type: .eip191) let cacao = Cacao(h: header, p: payload, s: cacaoSignature) @@ -64,6 +65,7 @@ class AppRespondSubscriberTests: XCTestCase { networkingInteractor.responsePublisherSubject.send((topic, request, response)) wait(for: [messageExpectation], timeout: defaultTimeout) + XCTAssertTrue(pairingRegisterer.isActivateCalled) XCTAssertEqual(result, .failure(AuthError.messageCompromised)) XCTAssertEqual(messageId, requestId) } diff --git a/Tests/AuthTests/CacaoSignerTests.swift b/Tests/AuthTests/CacaoSignerTests.swift index 159eebc41..95a01a538 100644 --- a/Tests/AuthTests/CacaoSignerTests.swift +++ b/Tests/AuthTests/CacaoSignerTests.swift @@ -5,6 +5,8 @@ import TestingUtils class CacaoSignerTest: XCTestCase { + let signer = MessageSignerFactory.create(projectId: "project-id") + let privateKey = Data(hex: "305c6cde3846927892cd32762f6120539f3ec74c9e3a16b9b798b1e85351ae2a") let message: String = @@ -24,17 +26,13 @@ class CacaoSignerTest: XCTestCase { - https://example.com/my-web2-claim.json """ - let signature = CacaoSignature(t: "eip191", s: "0x438effc459956b57fcd9f3dac6c675f9cee88abf21acab7305e8e32aa0303a883b06dcbd956279a7a2ca21ffa882ff55cc22e8ab8ec0f3fe90ab45f306938cfa1b") + let signature = CacaoSignature(t: .eip191, s: "0x438effc459956b57fcd9f3dac6c675f9cee88abf21acab7305e8e32aa0303a883b06dcbd956279a7a2ca21ffa882ff55cc22e8ab8ec0f3fe90ab45f306938cfa1b") func testCacaoSign() throws { - let signer = MessageSigner(signer: Signer()) - - XCTAssertEqual(try signer.sign(message: message, privateKey: privateKey), signature) + XCTAssertEqual(try signer.sign(message: message, privateKey: privateKey, type: .eip191), signature) } - func testCacaoVerify() throws { - let signer = MessageSigner(signer: Signer()) - - try signer.verify(signature: signature, message: message, address: "0x15bca56b6e2728aec2532df9d436bd1600e86688") + func testCacaoVerify() async throws { + try await signer.verify(signature: signature, message: message, address: "0x15bca56b6e2728aec2532df9d436bd1600e86688", chainId: "eip155:1") } } diff --git a/Tests/AuthTests/EIP1271VerifierTests.swift b/Tests/AuthTests/EIP1271VerifierTests.swift new file mode 100644 index 000000000..796c146ff --- /dev/null +++ b/Tests/AuthTests/EIP1271VerifierTests.swift @@ -0,0 +1,38 @@ +import Foundation +import XCTest +@testable import Auth +import JSONRPC +import TestingUtils + +class EIP1271VerifierTests: XCTestCase { + + let signature = Data(hex: "c1505719b2504095116db01baaf276361efd3a73c28cf8cc28dabefa945b8d536011289ac0a3b048600c1e692ff173ca944246cf7ceb319ac2262d27b395c82b1c") + let message = Data(hex: "3aaa8393796c7388e4e062861d8238503de7584c977676fe9d8d551c30e11f84") + let address = "0x2faf83c542b68f1b4cdc0e770e8cb9f567b08f71" + let chainId = "eip155:1" + + func testSuccessVerify() async throws { + let response = RPCResponse(id: "1", result: "0x1626ba7e00000000000000000000000000000000000000000000000000000000") + let httpClient = HTTPClientMock(object: response) + let verifier = EIP1271Verifier(projectId: "project-id", httpClient: httpClient) + try await verifier.verify( + signature: signature, + message: message, + address: address, + chainId: chainId + ) + } + + func testFailureVerify() async throws { + let response = RPCResponse(id: "1", error: .internalError) + let httpClient = HTTPClientMock(object: response) + let verifier = EIP1271Verifier(projectId: "project-id", httpClient: httpClient) + + await XCTAssertThrowsErrorAsync(try await verifier.verify( + signature: signature, + message: message, + address: address, + chainId: chainId + )) + } +} diff --git a/Tests/AuthTests/EIP191VerifierTests.swift b/Tests/AuthTests/EIP191VerifierTests.swift new file mode 100644 index 000000000..7b683ab14 --- /dev/null +++ b/Tests/AuthTests/EIP191VerifierTests.swift @@ -0,0 +1,41 @@ +import Foundation +import XCTest +import TestingUtils +@testable import Auth + +class EIP191VerifierTests: XCTestCase { + + private let verifier = EIP191Verifier() + + private let address = "0x15bca56b6e2728aec2532df9d436bd1600e86688" + private let message = "\u{19}Ethereum Signed Message:\n7Message".data(using: .utf8)! + private let signature = Data(hex: "66121e60cccc01fbf7fcba694a1e08ac5db35fb4ec6c045bedba7860445b95c021cad2c595f0bf68ff896964c7c02bb2f3a3e9540e8e4595c98b737ce264cc541b") + + func testVerify() async throws { + try await verifier.verify(signature: signature, message: message, address: address) + } + + func testEtherscanSignature() async throws { + let address = "0x6721591d424c18b7173d55895efa1839aa57d9c2" + let message = "\u{19}Ethereum Signed Message:\n139[Etherscan.io 12/08/2022 09:26:23] I, hereby verify that I am the owner/creator of the address [0x7e77dcb127f99ece88230a64db8d595f31f1b068]".data(using: .utf8)! + let signature = Data(hex: "60eb9cfe362210f1b4855f4865eafc378bd442c406de22354cc9f643fb84cb265b7f6d9d10b13199e450558c328814a9038884d9993d9feb79b727366736853d1b") + + try await verifier.verify(signature: signature, message: message, address: address) + } + + func testInvalidMessage() async throws { + let message = Data(hex: "0xdeadbeaf") + await XCTAssertThrowsErrorAsync(try await verifier.verify(signature: signature, message: message, address: address)) + } + + func testInvalidPubKey() async throws { + let address = "0xBAc675C310721717Cd4A37F6cbeA1F081b1C2a07" + await XCTAssertThrowsErrorAsync(try await verifier.verify(signature: signature, message: message, address: address)) + } + + func testInvalidSignature() async throws { + let signature = Data(hex: "86deb09d045608f2753ef12f46e8da5fc2559e3a9162e580df3e62c875df7c3f64433462a59bc4ff38ce52412bff10527f4b99cc078f63ef2bb4a6f7427080aa01") + + await XCTAssertThrowsErrorAsync(try await verifier.verify(signature: signature, message: message, address: address)) + } +} diff --git a/Tests/AuthTests/SignerTests.swift b/Tests/AuthTests/SignerTests.swift index 5f546bd08..80b19ecfb 100644 --- a/Tests/AuthTests/SignerTests.swift +++ b/Tests/AuthTests/SignerTests.swift @@ -4,12 +4,13 @@ import XCTest import secp256k1 import Web3 import WalletConnectUtils +import WalletConnectRelay class SignerTest: XCTestCase { private let signer = Signer() - private let message = "Message".data(using: .utf8)! + private let message = "\u{19}Ethereum Signed Message:\n7Message".data(using: .utf8)! private let privateKey = Data(hex: "305c6cde3846927892cd32762f6120539f3ec74c9e3a16b9b798b1e85351ae2a") private let signature = Data(hex: "66121e60cccc01fbf7fcba694a1e08ac5db35fb4ec6c045bedba7860445b95c021cad2c595f0bf68ff896964c7c02bb2f3a3e9540e8e4595c98b737ce264cc541b") private var address = "0x15bca56b6e2728aec2532df9d436bd1600e86688" @@ -18,37 +19,11 @@ class SignerTest: XCTestCase { let result = try signer.sign(message: message, with: privateKey) XCTAssertEqual(signature.toHexString(), result.toHexString()) - XCTAssertTrue(try signer.isValid(signature: result, message: message, address: address)) } - func testEtherscanSignature() throws { - let addressFromEtherscan = "0x6721591d424c18b7173d55895efa1839aa57d9c2" - let message = "[Etherscan.io 12/08/2022 09:26:23] I, hereby verify that I am the owner/creator of the address [0x7e77dcb127f99ece88230a64db8d595f31f1b068]" - let signedMessageFromEtherscan = message.data(using: .utf8)! - let signatureHashFromEtherscan = Data(hex: "60eb9cfe362210f1b4855f4865eafc378bd442c406de22354cc9f643fb84cb265b7f6d9d10b13199e450558c328814a9038884d9993d9feb79b727366736853d1b") - XCTAssertTrue(try signer.isValid( - signature: signatureHashFromEtherscan, - message: signedMessageFromEtherscan, - address: addressFromEtherscan - )) - } - - func testInvalidMessage() throws { - let message = "Message One".data(using: .utf8)! - - XCTAssertFalse(try signer.isValid(signature: signature, message: message, address: address)) - } - - func testInvalidPubKey() throws { - let address = "0xBAc675C310721717Cd4A37F6cbeA1F081b1C2a07" - - XCTAssertFalse(try signer.isValid(signature: signature, message: message, address: address)) - } - - func testInvalidSignature() throws { - let signature = Data(hex: "86deb09d045608f2753ef12f46e8da5fc2559e3a9162e580df3e62c875df7c3f64433462a59bc4ff38ce52412bff10527f4b99cc078f63ef2bb4a6f7427080aa01") - - XCTAssertFalse(try signer.isValid(signature: signature, message: message, address: address)) + private func prefixed(_ message: Data) -> Data { + return "\u{19}Ethereum Signed Message:\n\(message.count)" + .data(using: .utf8)! + message } func testSignerAddressFromIss() throws { diff --git a/Tests/AuthTests/WalletRequestSubscriberTests.swift b/Tests/AuthTests/WalletRequestSubscriberTests.swift index 0fb214242..de7dbe28d 100644 --- a/Tests/AuthTests/WalletRequestSubscriberTests.swift +++ b/Tests/AuthTests/WalletRequestSubscriberTests.swift @@ -1,27 +1,31 @@ import Foundation import XCTest import JSONRPC -import WalletConnectUtils -import WalletConnectNetworking +@testable import WalletConnectUtils +@testable import WalletConnectNetworking @testable import Auth @testable import WalletConnectKMS @testable import TestingUtils class WalletRequestSubscriberTests: XCTestCase { - var networkingInteractor: NetworkingInteractorMock! + var pairingRegisterer: PairingRegistererMock! var sut: WalletRequestSubscriber! var messageFormatter: SIWEMessageFormatterMock! + let defaultTimeout: TimeInterval = 0.01 override func setUp() { - networkingInteractor = NetworkingInteractorMock() + let networkingInteractor = NetworkingInteractorMock() + pairingRegisterer = PairingRegistererMock() messageFormatter = SIWEMessageFormatterMock() let walletErrorResponder = WalletErrorResponder(networkingInteractor: networkingInteractor, logger: ConsoleLoggerMock(), kms: KeyManagementServiceMock(), rpcHistory: RPCHistory(keyValueStore: CodableStore(defaults: RuntimeKeyValueStorage(), identifier: ""))) sut = WalletRequestSubscriber(networkingInteractor: networkingInteractor, logger: ConsoleLoggerMock(), kms: KeyManagementServiceMock(), - messageFormatter: messageFormatter, address: "", walletErrorResponder: walletErrorResponder) + messageFormatter: messageFormatter, address: "", + walletErrorResponder: walletErrorResponder, + pairingRegisterer: pairingRegisterer) } func testSubscribeRequest() { @@ -37,10 +41,12 @@ class WalletRequestSubscriberTests: XCTestCase { messageExpectation.fulfill() } - let request = RPCRequest(method: AuthProtocolMethod.authRequest.method, params: AuthRequestParams.stub(id: expectedRequestId), id: expectedRequestId.right!) - networkingInteractor.requestPublisherSubject.send(("123", request)) + let payload = RequestSubscriptionPayload(id: expectedRequestId, topic: "123", request: AuthRequestParams.stub(id: expectedRequestId)) + + pairingRegisterer.subject.send(payload) wait(for: [messageExpectation], timeout: defaultTimeout) + XCTAssertTrue(pairingRegisterer.isActivateCalled) XCTAssertEqual(message, expectedMessage) XCTAssertEqual(messageId, expectedRequestId) } diff --git a/Tests/RelayerTests/AuthTests/ClientIdStorageTests.swift b/Tests/RelayerTests/AuthTests/ClientIdStorageTests.swift index 38e27a073..aa58c9761 100644 --- a/Tests/RelayerTests/AuthTests/ClientIdStorageTests.swift +++ b/Tests/RelayerTests/AuthTests/ClientIdStorageTests.swift @@ -6,16 +6,33 @@ import WalletConnectKMS final class ClientIdStorageTests: XCTestCase { - func testGetOrCreate() throws { - let keychain = KeychainStorageMock() - let storage = ClientIdStorage(keychain: keychain) + var sut: ClientIdStorage! + var keychain: KeychainStorageMock! + var didKeyFactory: ED25519DIDKeyFactoryMock! + + override func setUp() { + keychain = KeychainStorageMock() + didKeyFactory = ED25519DIDKeyFactoryMock() + sut = ClientIdStorage(keychain: keychain, didKeyFactory: didKeyFactory) + } + func testGetOrCreate() throws { XCTAssertThrowsError(try keychain.read(key: "com.walletconnect.iridium.client_id") as SigningPrivateKey) - let saved = try storage.getOrCreateKeyPair() + let saved = try sut.getOrCreateKeyPair() XCTAssertEqual(saved, try keychain.read(key: "com.walletconnect.iridium.client_id")) - let restored = try storage.getOrCreateKeyPair() + let restored = try sut.getOrCreateKeyPair() XCTAssertEqual(saved, restored) } + + func testGetClientId() { + let did = "did:key:z6MkodHZwneVRShtaLf8JKYkxpDGp1vGZnpGmdBpX8M2exxH" + didKeyFactory.did = did + _ = try! sut.getOrCreateKeyPair() + + let clientId = try! sut.getClientId() + XCTAssertEqual(did, clientId) + + } } diff --git a/Tests/RelayerTests/DispatcherTests.swift b/Tests/RelayerTests/DispatcherTests.swift index 78103a30c..9d32b380d 100644 --- a/Tests/RelayerTests/DispatcherTests.swift +++ b/Tests/RelayerTests/DispatcherTests.swift @@ -1,5 +1,6 @@ import Foundation import XCTest +import Combine @testable import WalletConnectRelay import TestingUtils import Combine @@ -29,6 +30,7 @@ class WebSocketMock: WebSocketConnecting { } final class DispatcherTests: XCTestCase { + var publishers = Set() var sut: Dispatcher! var webSocket: WebSocketMock! var networkMonitor: NetworkMonitoringMock! @@ -66,18 +68,21 @@ final class DispatcherTests: XCTestCase { func testOnConnect() { let expectation = expectation(description: "on connect") - sut.onConnect = { + sut.socketConnectionStatusPublisher.sink { status in + guard status == .connected else { return } expectation.fulfill() - } + }.store(in: &publishers) webSocket.onConnect?() waitForExpectations(timeout: 0.001) } - func testOnDisconnect() { + func testOnDisconnect() throws { let expectation = expectation(description: "on disconnect") - sut.onDisconnect = { + try sut.connect() + sut.socketConnectionStatusPublisher.sink { status in + guard status == .disconnected else { return } expectation.fulfill() - } + }.store(in: &publishers) webSocket.onDisconnect?(nil) waitForExpectations(timeout: 0.001) } diff --git a/Tests/RelayerTests/Helpers/Error+Extension.swift b/Tests/RelayerTests/Helpers/Error+Extension.swift index bee887b0d..cf5525e24 100644 --- a/Tests/RelayerTests/Helpers/Error+Extension.swift +++ b/Tests/RelayerTests/Helpers/Error+Extension.swift @@ -1,5 +1,6 @@ import Foundation @testable import WalletConnectRelay +@testable import WalletConnectNetworking extension NSError { diff --git a/Tests/RelayerTests/Mocks/ClientIdStorageMock.swift b/Tests/RelayerTests/Mocks/ClientIdStorageMock.swift index 2453c6174..5d26c6d34 100644 --- a/Tests/RelayerTests/Mocks/ClientIdStorageMock.swift +++ b/Tests/RelayerTests/Mocks/ClientIdStorageMock.swift @@ -3,9 +3,14 @@ import WalletConnectKMS import Foundation class ClientIdStorageMock: ClientIdStoring { + var keyPair: SigningPrivateKey! func getOrCreateKeyPair() throws -> SigningPrivateKey { return keyPair } + + func getClientId() throws -> String { + fatalError() + } } diff --git a/Tests/RelayerTests/Mocks/DispatcherMock.swift b/Tests/RelayerTests/Mocks/DispatcherMock.swift index 97ddac5dc..d5088bf61 100644 --- a/Tests/RelayerTests/Mocks/DispatcherMock.swift +++ b/Tests/RelayerTests/Mocks/DispatcherMock.swift @@ -1,18 +1,34 @@ import Foundation import JSONRPC +import Combine @testable import WalletConnectRelay class DispatcherMock: Dispatching { + private var publishers = Set() + private let socketConnectionStatusPublisherSubject = CurrentValueSubject(.disconnected) + var socketConnectionStatusPublisher: AnyPublisher { + return socketConnectionStatusPublisherSubject.eraseToAnyPublisher() + } - var onConnect: (() -> Void)? - var onDisconnect: (() -> Void)? + var sent = false + var lastMessage: String = "" var onMessage: ((String) -> Void)? - func connect() {} - func disconnect(closeCode: URLSessionWebSocketTask.CloseCode) {} + func protectedSend(_ string: String, completion: @escaping (Error?) -> Void) { + send(string, completion: completion) + } - var sent = false - var lastMessage: String = "" + func protectedSend(_ string: String) async throws { + try await send(string) + } + + func connect() { + socketConnectionStatusPublisherSubject.send(.connected) + } + + func disconnect(closeCode: URLSessionWebSocketTask.CloseCode) { + socketConnectionStatusPublisherSubject.send(.disconnected) + } func send(_ string: String, completion: @escaping (Error?) -> Void) { sent = true diff --git a/Tests/RelayerTests/Mocks/ED25519DIDKeyFactoryMock.swift b/Tests/RelayerTests/Mocks/ED25519DIDKeyFactoryMock.swift index 23afd5c3c..11ef10631 100644 --- a/Tests/RelayerTests/Mocks/ED25519DIDKeyFactoryMock.swift +++ b/Tests/RelayerTests/Mocks/ED25519DIDKeyFactoryMock.swift @@ -2,7 +2,7 @@ import WalletConnectKMS @testable import WalletConnectRelay import Foundation -struct ED25519DIDKeyFactoryMock: DIDKeyFactory { +class ED25519DIDKeyFactoryMock: DIDKeyFactory { var did: String! func make(pubKey: Data, prefix: Bool) -> String { return did diff --git a/Tests/RelayerTests/RelayClientTests.swift b/Tests/RelayerTests/RelayClientTests.swift index f9c9384c2..f37148fca 100644 --- a/Tests/RelayerTests/RelayClientTests.swift +++ b/Tests/RelayerTests/RelayClientTests.swift @@ -9,13 +9,13 @@ final class RelayClientTests: XCTestCase { var sut: RelayClient! var dispatcher: DispatcherMock! - var publishers = Set() override func setUp() { dispatcher = DispatcherMock() let logger = ConsoleLogger() - sut = RelayClient(dispatcher: dispatcher, logger: logger, keyValueStorage: RuntimeKeyValueStorage()) + let clientIdStorage = ClientIdStorageMock() + sut = RelayClient(dispatcher: dispatcher, logger: logger, keyValueStorage: RuntimeKeyValueStorage(), clientIdStorage: clientIdStorage) } override func tearDown() { @@ -55,7 +55,7 @@ final class RelayClientTests: XCTestCase { func testPublishRequestAcknowledge() { let expectation = expectation(description: "Publish must callback on relay server acknowledgement") - sut.publish(topic: "", payload: "{}", tag: 0) { error in + sut.publish(topic: "", payload: "{}", tag: 0, prompt: false, ttl: 60) { error in XCTAssertNil(error) expectation.fulfill() } @@ -93,7 +93,7 @@ final class RelayClientTests: XCTestCase { } func testSendOnPublish() { - sut.publish(topic: "", payload: "", tag: 0, onNetworkAcknowledge: { _ in}) + sut.publish(topic: "", payload: "", tag: 0, prompt: false, ttl: 60, onNetworkAcknowledge: { _ in}) XCTAssertTrue(dispatcher.sent) } diff --git a/Tests/TestingUtils/Mocks/HTTPClientMock.swift b/Tests/TestingUtils/Mocks/HTTPClientMock.swift new file mode 100644 index 000000000..c769c2b68 --- /dev/null +++ b/Tests/TestingUtils/Mocks/HTTPClientMock.swift @@ -0,0 +1,19 @@ +import Foundation +import WalletConnectNetworking + +public final class HTTPClientMock: HTTPClient { + + private let object: T + + public init(object: T) { + self.object = object + } + + public func request(_ type: T.Type, at service: HTTPService) async throws -> T where T: Decodable { + return object as! T + } + + public func request(service: HTTPService) async throws { + + } +} diff --git a/Tests/TestingUtils/Mocks/PairingRegistererMock.swift b/Tests/TestingUtils/Mocks/PairingRegistererMock.swift new file mode 100644 index 000000000..9e48d13da --- /dev/null +++ b/Tests/TestingUtils/Mocks/PairingRegistererMock.swift @@ -0,0 +1,27 @@ +import Foundation +import WalletConnectPairing +import Combine +import WalletConnectNetworking + +public class PairingRegistererMock: PairingRegisterer where RequestParams: Codable { + + public let subject = PassthroughSubject, Never>() + + public var isActivateCalled: Bool = false + + public func register(method: ProtocolMethod) -> AnyPublisher, Never> where RequestParams: Decodable, RequestParams: Encodable { + subject.eraseToAnyPublisher() as! AnyPublisher, Never> + } + + public func activate(pairingTopic: String) { + 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 10f42fc08..36bee9173 100644 --- a/Tests/TestingUtils/NetworkingInteractorMock.swift +++ b/Tests/TestingUtils/NetworkingInteractorMock.swift @@ -8,6 +8,21 @@ import WalletConnectNetworking public class NetworkingInteractorMock: NetworkInteracting { private(set) var subscriptions: [String] = [] + private(set) var unsubscriptions: [String] = [] + + private(set) var requests: [(topic: String, request: RPCRequest)] = [] + + private(set) var didRespondSuccess = false + private(set) var didRespondError = false + private(set) var didCallSubscribe = false + private(set) var didCallUnsubscribe = false + private(set) var didRespondOnTopic: String? + private(set) var lastErrorCode = -1 + + private(set) var requestCallCount = 0 + var didCallRequest: Bool { requestCallCount > 0 } + + var onSubscribeCalled: (() -> Void)? public let socketConnectionStatusPublisherSubject = PassthroughSubject() public var socketConnectionStatusPublisher: AnyPublisher { @@ -17,7 +32,7 @@ public class NetworkingInteractorMock: NetworkInteracting { public let requestPublisherSubject = PassthroughSubject<(topic: String, request: RPCRequest), Never>() public let responsePublisherSubject = PassthroughSubject<(topic: String, request: RPCRequest, response: RPCResponse), Never>() - private var requestPublisher: AnyPublisher<(topic: String, request: RPCRequest), Never> { + public var requestPublisher: AnyPublisher<(topic: String, request: RPCRequest), Never> { requestPublisherSubject.eraseToAnyPublisher() } @@ -25,9 +40,12 @@ public class NetworkingInteractorMock: NetworkInteracting { responsePublisherSubject.eraseToAnyPublisher() } + // TODO: Avoid copy paste from NetworkInteractor public func requestSubscription(on request: ProtocolMethod) -> AnyPublisher, Never> { return requestPublisher - .filter { $0.request.method == request.method } + .filter { rpcRequest in + return rpcRequest.request.method == request.method + } .compactMap { topic, rpcRequest in guard let id = rpcRequest.id, let request = try? rpcRequest.params?.get(Request.self) else { return nil } return RequestSubscriptionPayload(id: id, topic: topic, request: request) @@ -35,9 +53,12 @@ public class NetworkingInteractorMock: NetworkInteracting { .eraseToAnyPublisher() } + // TODO: Avoid copy paste from NetworkInteractor public func responseSubscription(on request: ProtocolMethod) -> AnyPublisher, Never> { return responsePublisher - .filter { $0.request.method == request.method } + .filter { rpcRequest in + return rpcRequest.request.method == request.method + } .compactMap { topic, rpcRequest, rpcResponse in guard let id = rpcRequest.id, @@ -48,45 +69,56 @@ public class NetworkingInteractorMock: NetworkInteracting { .eraseToAnyPublisher() } - public func responseErrorSubscription(on request: ProtocolMethod) -> AnyPublisher { + // TODO: Avoid copy paste from NetworkInteractor + public func responseErrorSubscription(on request: ProtocolMethod) -> AnyPublisher, Never> { return responsePublisher .filter { $0.request.method == request.method } - .compactMap { (_, _, rpcResponse) in - guard let id = rpcResponse.id, let error = rpcResponse.error else { return nil } - return ResponseSubscriptionErrorPayload(id: id, error: error) + .compactMap { (topic, rpcRequest, rpcResponse) in + guard let id = rpcResponse.id, let request = try? rpcRequest.params?.get(Request.self), let error = rpcResponse.error else { return nil } + return ResponseSubscriptionErrorPayload(id: id, topic: topic, request: request, error: error) } .eraseToAnyPublisher() } public func subscribe(topic: String) async throws { + defer { onSubscribeCalled?() } subscriptions.append(topic) + didCallSubscribe = true } func didSubscribe(to topic: String) -> Bool { - subscriptions.contains { $0 == topic } + subscriptions.contains { $0 == topic } } - public func unsubscribe(topic: String) { - + func didUnsubscribe(to topic: String) -> Bool { + unsubscriptions.contains { $0 == topic } } - public func request(_ request: RPCRequest, topic: String, tag: Int, envelopeType: Envelope.EnvelopeType) async throws { - + public func unsubscribe(topic: String) { + unsubscriptions.append(topic) + didCallUnsubscribe = true } - public func respond(topic: String, response: RPCResponse, tag: Int, envelopeType: Envelope.EnvelopeType) async throws { - + public func request(_ request: RPCRequest, topic: String, protocolMethod: ProtocolMethod, envelopeType: Envelope.EnvelopeType) async throws { + requestCallCount += 1 + requests.append((topic, request)) } - public func respondSuccess(topic: String, requestId: RPCID, tag: Int, envelopeType: Envelope.EnvelopeType) async throws { - + public func respond(topic: String, response: RPCResponse, protocolMethod: ProtocolMethod, envelopeType: Envelope.EnvelopeType) async throws { + didRespondOnTopic = topic } - public func respondError(topic: String, requestId: RPCID, tag: Int, reason: Reason, envelopeType: Envelope.EnvelopeType) async throws { - + public func respondSuccess(topic: String, requestId: RPCID, protocolMethod: ProtocolMethod, envelopeType: Envelope.EnvelopeType) async throws { + didRespondSuccess = true } - public func requestNetworkAck(_ request: RPCRequest, topic: String, tag: Int) async throws { + public func respondError(topic: String, requestId: RPCID, protocolMethod: ProtocolMethod, reason: Reason, envelopeType: Envelope.EnvelopeType) async throws { + lastErrorCode = reason.code + didRespondError = true + } + public func requestNetworkAck(_ request: RPCRequest, topic: String, protocolMethod: ProtocolMethod) async throws { + requestCallCount += 1 + requests.append((topic, request)) } } diff --git a/Tests/WalletConnectSignTests/Mocks/AppMetadata.swift b/Tests/TestingUtils/Stubs/AppMetadata+Stub.swift similarity index 100% rename from Tests/WalletConnectSignTests/Mocks/AppMetadata.swift rename to Tests/TestingUtils/Stubs/AppMetadata+Stub.swift diff --git a/Tests/TestingUtils/Stubs/RelayProtocolOptions+Stub.swift b/Tests/TestingUtils/Stubs/RelayProtocolOptions+Stub.swift new file mode 100644 index 000000000..bd3db0839 --- /dev/null +++ b/Tests/TestingUtils/Stubs/RelayProtocolOptions+Stub.swift @@ -0,0 +1,8 @@ +import WalletConnectUtils + +extension RelayProtocolOptions { + + public static func stub() -> RelayProtocolOptions { + RelayProtocolOptions(protocol: "", data: nil) + } +} diff --git a/Tests/WalletConnectSignTests/Stub/WalletConnectURI+Stub.swift b/Tests/TestingUtils/Stubs/WalletConnectURI+Stub.swift similarity index 60% rename from Tests/WalletConnectSignTests/Stub/WalletConnectURI+Stub.swift rename to Tests/TestingUtils/Stubs/WalletConnectURI+Stub.swift index 5d5129228..67ee5dfe9 100644 --- a/Tests/WalletConnectSignTests/Stub/WalletConnectURI+Stub.swift +++ b/Tests/TestingUtils/Stubs/WalletConnectURI+Stub.swift @@ -1,10 +1,9 @@ -@testable import WalletConnectSign -@testable import WalletConnectKMS -import CryptoKit +import WalletConnectKMS +import WalletConnectUtils extension WalletConnectURI { - static func stub(isController: Bool = false) -> WalletConnectURI { + public static func stub(isController: Bool = false) -> WalletConnectURI { WalletConnectURI( topic: String.generateTopic(), symKey: SymmetricKey().hexRepresentation, diff --git a/Tests/WalletConnectPairingTests/AppPairServiceTests.swift b/Tests/WalletConnectPairingTests/AppPairServiceTests.swift new file mode 100644 index 000000000..d0cb8f9b9 --- /dev/null +++ b/Tests/WalletConnectPairingTests/AppPairServiceTests.swift @@ -0,0 +1,35 @@ +import XCTest +@testable import WalletConnectPairing +@testable import TestingUtils +@testable import WalletConnectKMS +import WalletConnectUtils + +final class AppPairServiceTests: XCTestCase { + + var service: AppPairService! + var networkingInteractor: NetworkingInteractorMock! + var storageMock: WCPairingStorageMock! + var cryptoMock: KeyManagementServiceMock! + + override func setUp() { + networkingInteractor = NetworkingInteractorMock() + storageMock = WCPairingStorageMock() + cryptoMock = KeyManagementServiceMock() + service = AppPairService(networkingInteractor: networkingInteractor, kms: cryptoMock, pairingStorage: storageMock) + } + + override func tearDown() { + networkingInteractor = nil + storageMock = nil + cryptoMock = nil + service = nil + } + + func testCreate() async { + let uri = try! await service.create() + XCTAssert(cryptoMock.hasSymmetricKey(for: uri.topic), "Proposer must store the symmetric key matching the URI.") + XCTAssert(storageMock.hasPairing(forTopic: uri.topic), "The engine must store a pairing after creating one") + XCTAssert(networkingInteractor.didSubscribe(to: uri.topic), "Proposer must subscribe to pairing topic.") + XCTAssert(storageMock.getPairing(forTopic: uri.topic)?.active == false, "Recently created pairing must be inactive.") + } +} diff --git a/Tests/WalletConnectPairingTests/ExpirationServiceTests.swift b/Tests/WalletConnectPairingTests/ExpirationServiceTests.swift new file mode 100644 index 000000000..79b8b20d6 --- /dev/null +++ b/Tests/WalletConnectPairingTests/ExpirationServiceTests.swift @@ -0,0 +1,40 @@ +import XCTest +@testable import WalletConnectPairing +@testable import TestingUtils +@testable import WalletConnectKMS +import WalletConnectUtils +import WalletConnectNetworking + +final class ExpirationServiceTestsTests: XCTestCase { + + var service: ExpirationService! + var appPairService: AppPairService! + var networkingInteractor: NetworkingInteractorMock! + var storageMock: WCPairingStorageMock! + var cryptoMock: KeyManagementServiceMock! + + override func setUp() { + networkingInteractor = NetworkingInteractorMock() + storageMock = WCPairingStorageMock() + cryptoMock = KeyManagementServiceMock() + service = ExpirationService( + pairingStorage: storageMock, + networkInteractor: networkingInteractor, + kms: cryptoMock + ) + appPairService = AppPairService( + networkingInteractor: networkingInteractor, + kms: cryptoMock, + pairingStorage: storageMock + ) + } + + func testPairingExpiration() async { + let uri = try! await appPairService.create() + let pairing = storageMock.getPairing(forTopic: uri.topic)! + service.setupExpirationHandling() + storageMock.onPairingExpiration?(pairing) + XCTAssertFalse(cryptoMock.hasSymmetricKey(for: uri.topic)) + XCTAssert(networkingInteractor.didUnsubscribe(to: uri.topic)) + } +} diff --git a/Tests/WalletConnectSignTests/WCPairingTests.swift b/Tests/WalletConnectPairingTests/WCPairingTests.swift similarity index 98% rename from Tests/WalletConnectSignTests/WCPairingTests.swift rename to Tests/WalletConnectPairingTests/WCPairingTests.swift index 6dcad44a0..f180efee2 100644 --- a/Tests/WalletConnectSignTests/WCPairingTests.swift +++ b/Tests/WalletConnectPairingTests/WCPairingTests.swift @@ -1,6 +1,7 @@ import XCTest @testable import WalletConnectPairing -@testable import WalletConnectSign +@testable import WalletConnectUtils +@testable import WalletConnectUtils final class WCPairingTests: XCTestCase { diff --git a/Tests/WalletConnectSignTests/PairEngineTests.swift b/Tests/WalletConnectPairingTests/WalletPairServiceTests.swift similarity index 53% rename from Tests/WalletConnectSignTests/PairEngineTests.swift rename to Tests/WalletConnectPairingTests/WalletPairServiceTests.swift index 9b581672d..7617aee57 100644 --- a/Tests/WalletConnectSignTests/PairEngineTests.swift +++ b/Tests/WalletConnectPairingTests/WalletPairServiceTests.swift @@ -1,50 +1,31 @@ import XCTest -@testable import WalletConnectSign +@testable import WalletConnectPairing @testable import TestingUtils @testable import WalletConnectKMS import WalletConnectUtils +import WalletConnectNetworking -final class PairEngineTests: XCTestCase { - - var engine: PairEngine! +final class WalletPairServiceTestsTests: XCTestCase { + var service: WalletPairService! var networkingInteractor: NetworkingInteractorMock! var storageMock: WCPairingStorageMock! var cryptoMock: KeyManagementServiceMock! - var proposalPayloadsStore: CodableStore! - - var topicGenerator: TopicGenerator! override func setUp() { networkingInteractor = NetworkingInteractorMock() storageMock = WCPairingStorageMock() cryptoMock = KeyManagementServiceMock() - topicGenerator = TopicGenerator() - proposalPayloadsStore = CodableStore(defaults: RuntimeKeyValueStorage(), identifier: "") - setupEngine() - } - - override func tearDown() { - networkingInteractor = nil - storageMock = nil - cryptoMock = nil - engine = nil - } - - func setupEngine() { - engine = PairEngine( - networkingInteractor: networkingInteractor, - kms: cryptoMock, - pairingStore: storageMock) + service = WalletPairService(networkingInteractor: networkingInteractor, kms: cryptoMock, pairingStorage: storageMock) } func testPairMultipleTimesOnSameURIThrows() async { let uri = WalletConnectURI.stub() for i in 1...10 { if i == 1 { - await XCTAssertNoThrowAsync(try await engine.pair(uri)) + await XCTAssertNoThrowAsync(try await service.pair(uri)) } else { - await XCTAssertThrowsErrorAsync(try await engine.pair(uri)) + await XCTAssertThrowsErrorAsync(try await service.pair(uri)) } } } @@ -52,7 +33,7 @@ final class PairEngineTests: XCTestCase { func testPair() async { let uri = WalletConnectURI.stub() let topic = uri.topic - try! await engine.pair(uri) + try! await service.pair(uri) XCTAssert(networkingInteractor.didSubscribe(to: topic), "Responder must subscribe to pairing topic.") XCTAssert(cryptoMock.hasSymmetricKey(for: topic), "Responder must store the symmetric key matching the pairing topic") XCTAssert(storageMock.hasPairing(forTopic: topic), "The engine must store a pairing") diff --git a/Tests/WalletConnectSignTests/PairingEngineTests.swift b/Tests/WalletConnectSignTests/AppProposalServiceTests.swift similarity index 64% rename from Tests/WalletConnectSignTests/PairingEngineTests.swift rename to Tests/WalletConnectSignTests/AppProposalServiceTests.swift index 785be6dc2..de37d08cf 100644 --- a/Tests/WalletConnectSignTests/PairingEngineTests.swift +++ b/Tests/WalletConnectSignTests/AppProposalServiceTests.swift @@ -1,17 +1,22 @@ import XCTest import Combine +import JSONRPC @testable import WalletConnectSign @testable import TestingUtils @testable import WalletConnectKMS +@testable import WalletConnectPairing import WalletConnectUtils func deriveTopic(publicKey: String, privateKey: AgreementPrivateKey) -> String { try! KeyManagementService.generateAgreementKey(from: privateKey, peerPublicKey: publicKey).derivedTopic() } -final class PairingEngineTests: XCTestCase { +final class AppProposalServiceTests: XCTestCase { - var engine: PairingEngine! + var service: AppProposeService! + + var appPairService: AppPairService! + var pairingRegisterer: PairingRegistererMock! var approveEngine: ApproveEngine! var networkingInteractor: NetworkingInteractorMock! @@ -26,7 +31,8 @@ final class PairingEngineTests: XCTestCase { storageMock = WCPairingStorageMock() cryptoMock = KeyManagementServiceMock() topicGenerator = TopicGenerator() - setupEngines() + pairingRegisterer = PairingRegistererMock() + setupServices() } override func tearDown() { @@ -34,25 +40,30 @@ final class PairingEngineTests: XCTestCase { storageMock = nil cryptoMock = nil topicGenerator = nil - engine = nil + pairingRegisterer = nil approveEngine = nil } - func setupEngines() { + func setupServices() { let meta = AppMetadata.stub() let logger = ConsoleLoggerMock() - engine = PairingEngine( + + appPairService = AppPairService( networkingInteractor: networkingInteractor, kms: cryptoMock, - pairingStore: storageMock, - metadata: meta, - logger: logger, - topicGenerator: topicGenerator.getTopic + pairingStorage: storageMock + ) + service = AppProposeService( + metadata: .stub(), + networkingInteractor: networkingInteractor, + kms: cryptoMock, + logger: logger ) approveEngine = ApproveEngine( networkingInteractor: networkingInteractor, proposalPayloadsStore: .init(defaults: RuntimeKeyValueStorage(), identifier: ""), sessionToPairingTopic: CodableStore(defaults: RuntimeKeyValueStorage(), identifier: ""), + pairingRegisterer: pairingRegisterer, metadata: meta, kms: cryptoMock, logger: logger, @@ -61,24 +72,16 @@ final class PairingEngineTests: XCTestCase { ) } - func testCreate() async { - let uri = try! await engine.create() - XCTAssert(cryptoMock.hasSymmetricKey(for: uri.topic), "Proposer must store the symmetric key matching the URI.") - XCTAssert(storageMock.hasPairing(forTopic: uri.topic), "The engine must store a pairing after creating one") - XCTAssert(networkingInteractor.didSubscribe(to: uri.topic), "Proposer must subscribe to pairing topic.") - XCTAssert(storageMock.getPairing(forTopic: uri.topic)?.active == false, "Recently created pairing must be inactive.") - } - func testPropose() async { let pairing = Pairing.stub() let topicA = pairing.topic let relayOptions = RelayProtocolOptions(protocol: "", data: nil) // FIXME: namespace stub - try! await engine.propose(pairingTopic: pairing.topic, namespaces: ProposalNamespace.stubDictionary(), relay: relayOptions) + try! await service.propose(pairingTopic: pairing.topic, namespaces: ProposalNamespace.stubDictionary(), relay: relayOptions) guard let publishTopic = networkingInteractor.requests.first?.topic, - let proposal = networkingInteractor.requests.first?.request.sessionProposal else { + let proposal = try? networkingInteractor.requests.first?.request.params?.get(SessionType.ProposeParams.self) else { XCTFail("Proposer must publish a proposal request."); return } XCTAssert(cryptoMock.hasPrivateKey(for: proposal.proposer.publicKey), "Proposer must store the private key matching the public key sent through the proposal.") @@ -86,17 +89,18 @@ final class PairingEngineTests: XCTestCase { } func testHandleSessionProposeResponse() async { - let uri = try! await engine.create() + let exp = expectation(description: "testHandleSessionProposeResponse") + let uri = try! await appPairService.create() let pairing = storageMock.getPairing(forTopic: uri.topic)! let topicA = pairing.topic let relayOptions = RelayProtocolOptions(protocol: "", data: nil) // Client proposes session // FIXME: namespace stub - try! await engine.propose(pairingTopic: pairing.topic, namespaces: ProposalNamespace.stubDictionary(), relay: relayOptions) + try! await service.propose(pairingTopic: pairing.topic, namespaces: ProposalNamespace.stubDictionary(), relay: relayOptions) guard let request = networkingInteractor.requests.first?.request, - let proposal = networkingInteractor.requests.first?.request.sessionProposal else { + let proposal = try? networkingInteractor.requests.first?.request.params?.get(SessionType.ProposeParams.self) else { XCTFail("Proposer must publish session proposal request"); return } @@ -104,17 +108,19 @@ final class PairingEngineTests: XCTestCase { let responder = Participant.stub() let proposalResponse = SessionType.ProposeResponse(relay: relayOptions, responderPublicKey: responder.publicKey) - let jsonRpcResponse = JSONRPCResponse(id: request.id, result: AnyCodable.decoded(proposalResponse)) - let response = WCResponse(topic: topicA, - chainId: nil, - requestMethod: request.method, - requestParams: request.params, - result: .response(jsonRpcResponse)) + let response = RPCResponse(id: request.id!, result: RPCResult.response(AnyCodable(proposalResponse))) - networkingInteractor.responsePublisherSubject.send(response) + networkingInteractor.onSubscribeCalled = { + exp.fulfill() + } + + networkingInteractor.responsePublisherSubject.send((topicA, request, response)) let privateKey = try! cryptoMock.getPrivateKey(for: proposal.proposer.publicKey)! let topicB = deriveTopic(publicKey: responder.publicKey, privateKey: privateKey) let storedPairing = storageMock.getPairing(forTopic: topicA)! + + wait(for: [exp], timeout: 5) + let sessionTopic = networkingInteractor.subscriptions.last! XCTAssertTrue(networkingInteractor.didCallSubscribe) @@ -123,22 +129,22 @@ final class PairingEngineTests: XCTestCase { } func testSessionProposeError() async { - let uri = try! await engine.create() + let uri = try! await appPairService.create() let pairing = storageMock.getPairing(forTopic: uri.topic)! let topicA = pairing.topic let relayOptions = RelayProtocolOptions(protocol: "", data: nil) // Client propose session // FIXME: namespace stub - try! await engine.propose(pairingTopic: pairing.topic, namespaces: ProposalNamespace.stubDictionary(), relay: relayOptions) + try! await service.propose(pairingTopic: pairing.topic, namespaces: ProposalNamespace.stubDictionary(), relay: relayOptions) guard let request = networkingInteractor.requests.first?.request, - let proposal = networkingInteractor.requests.first?.request.sessionProposal else { + let proposal = try? networkingInteractor.requests.first?.request.params?.get(SessionType.ProposeParams.self) else { XCTFail("Proposer must publish session proposal request"); return } - let response = WCResponse.stubError(forRequest: request, topic: topicA) - networkingInteractor.responsePublisherSubject.send(response) + let response = RPCResponse.stubError(forRequest: request) + networkingInteractor.responsePublisherSubject.send((topicA, request, response)) XCTAssert(networkingInteractor.didUnsubscribe(to: pairing.topic), "Proposer must unsubscribe if pairing is inactive.") XCTAssertFalse(storageMock.hasPairing(forTopic: pairing.topic), "Proposer must delete an inactive pairing.") @@ -147,17 +153,17 @@ final class PairingEngineTests: XCTestCase { } func testSessionProposeErrorOnActivePairing() async { - let uri = try! await engine.create() + let uri = try! await appPairService.create() let pairing = storageMock.getPairing(forTopic: uri.topic)! let topicA = pairing.topic let relayOptions = RelayProtocolOptions(protocol: "", data: nil) // Client propose session // FIXME: namespace stub - try? await engine.propose(pairingTopic: pairing.topic, namespaces: ProposalNamespace.stubDictionary(), relay: relayOptions) + try? await service.propose(pairingTopic: pairing.topic, namespaces: ProposalNamespace.stubDictionary(), relay: relayOptions) guard let request = networkingInteractor.requests.first?.request, - let proposal = networkingInteractor.requests.first?.request.sessionProposal else { + let proposal = try? networkingInteractor.requests.first?.request.params?.get(SessionType.ProposeParams.self) else { XCTFail("Proposer must publish session proposal request"); return } @@ -165,20 +171,12 @@ final class PairingEngineTests: XCTestCase { storedPairing.activate() storageMock.setPairing(storedPairing) - let response = WCResponse.stubError(forRequest: request, topic: topicA) - networkingInteractor.responsePublisherSubject.send(response) + let response = RPCResponse.stubError(forRequest: request) + networkingInteractor.responsePublisherSubject.send((topicA, request, response)) XCTAssertFalse(networkingInteractor.didUnsubscribe(to: pairing.topic), "Proposer must not unsubscribe if pairing is active.") XCTAssert(storageMock.hasPairing(forTopic: pairing.topic), "Proposer must not delete an active pairing.") XCTAssert(cryptoMock.hasSymmetricKey(for: pairing.topic), "Proposer must not delete symmetric key if pairing is active.") XCTAssertFalse(cryptoMock.hasPrivateKey(for: proposal.proposer.publicKey), "Proposer must remove private key for rejected session") } - - func testPairingExpiration() async { - let uri = try! await engine.create() - let pairing = storageMock.getPairing(forTopic: uri.topic)! - storageMock.onPairingExpiration?(pairing) - XCTAssertFalse(cryptoMock.hasSymmetricKey(for: uri.topic)) - XCTAssert(networkingInteractor.didUnsubscribe(to: uri.topic)) - } } diff --git a/Tests/WalletConnectSignTests/ApproveEngineTests.swift b/Tests/WalletConnectSignTests/ApproveEngineTests.swift index 88e7a541c..9730deefe 100644 --- a/Tests/WalletConnectSignTests/ApproveEngineTests.swift +++ b/Tests/WalletConnectSignTests/ApproveEngineTests.swift @@ -1,7 +1,9 @@ import XCTest import Combine +import JSONRPC import WalletConnectUtils import WalletConnectPairing +import WalletConnectNetworking @testable import WalletConnectSign @testable import TestingUtils @testable import WalletConnectKMS @@ -14,7 +16,8 @@ final class ApproveEngineTests: XCTestCase { var cryptoMock: KeyManagementServiceMock! var pairingStorageMock: WCPairingStorageMock! var sessionStorageMock: WCSessionStorageMock! - var proposalPayloadsStore: CodableStore! + var pairingRegisterer: PairingRegistererMock! + var proposalPayloadsStore: CodableStore>! var publishers = Set() @@ -24,11 +27,13 @@ final class ApproveEngineTests: XCTestCase { cryptoMock = KeyManagementServiceMock() pairingStorageMock = WCPairingStorageMock() sessionStorageMock = WCSessionStorageMock() - proposalPayloadsStore = CodableStore(defaults: RuntimeKeyValueStorage(), identifier: "") + pairingRegisterer = PairingRegistererMock() + proposalPayloadsStore = CodableStore>(defaults: RuntimeKeyValueStorage(), identifier: "") engine = ApproveEngine( networkingInteractor: networkingInteractor, proposalPayloadsStore: proposalPayloadsStore, sessionToPairingTopic: CodableStore(defaults: RuntimeKeyValueStorage(), identifier: ""), + pairingRegisterer: pairingRegisterer, metadata: metadata, kms: cryptoMock, logger: ConsoleLoggerMock(), @@ -41,6 +46,7 @@ final class ApproveEngineTests: XCTestCase { networkingInteractor = nil metadata = nil cryptoMock = nil + pairingRegisterer = nil pairingStorageMock = nil engine = nil } @@ -52,9 +58,7 @@ final class ApproveEngineTests: XCTestCase { pairingStorageMock.setPairing(pairing) let proposerPubKey = AgreementPrivateKey().publicKey.hexRepresentation let proposal = SessionProposal.stub(proposerPubKey: proposerPubKey) - let request = WCRequest(method: .sessionPropose, params: .sessionPropose(proposal)) - let payload = WCRequestSubscriptionPayload(topic: topicA, wcRequest: request) - networkingInteractor.wcRequestPublisherSubject.send(payload) + pairingRegisterer.subject.send(RequestSubscriptionPayload(id: RPCID("id"), topic: topicA, request: proposal)) try await engine.approveProposal(proposerPubKey: proposal.proposer.publicKey, validating: SessionNamespace.stubDictionary()) @@ -74,14 +78,12 @@ final class ApproveEngineTests: XCTestCase { var sessionProposed = false let proposerPubKey = AgreementPrivateKey().publicKey.hexRepresentation let proposal = SessionProposal.stub(proposerPubKey: proposerPubKey) - let request = WCRequest(method: .sessionPropose, params: .sessionPropose(proposal)) - let payload = WCRequestSubscriptionPayload(topic: topicA, wcRequest: request) engine.onSessionProposal = { _ in sessionProposed = true } - networkingInteractor.wcRequestPublisherSubject.send(payload) + pairingRegisterer.subject.send(RequestSubscriptionPayload(id: RPCID("id"), topic: topicA, request: proposal)) XCTAssertNotNil(try! proposalPayloadsStore.get(key: proposal.proposer.publicKey), "Proposer must store proposal payload") XCTAssertTrue(sessionProposed) } @@ -106,7 +108,9 @@ final class ApproveEngineTests: XCTestCase { } engine.settlingProposal = SessionProposal.stub() - networkingInteractor.wcRequestPublisherSubject.send(WCRequestSubscriptionPayload.stubSettle(topic: sessionTopic)) + networkingInteractor.requestPublisherSubject.send((sessionTopic, RPCRequest.stubSettle())) + + usleep(100) XCTAssertTrue(sessionStorageMock.getSession(forTopic: sessionTopic)!.acknowledged, "Proposer must store acknowledged session on topic B") XCTAssertTrue(networkingInteractor.didRespondSuccess, "Proposer must send acknowledge on settle request") @@ -117,14 +121,10 @@ final class ApproveEngineTests: XCTestCase { let session = WCSession.stub(isSelfController: true, acknowledged: false) sessionStorageMock.setSession(session) - let settleResponse = JSONRPCResponse(id: 1, result: AnyCodable(true)) - let response = WCResponse( - topic: session.topic, - chainId: nil, - requestMethod: .sessionSettle, - requestParams: .sessionSettle(SessionType.SettleParams.stub()), - result: .response(settleResponse)) - networkingInteractor.responsePublisherSubject.send(response) + let request = RPCRequest(method: SessionSettleProtocolMethod().method, params: SessionType.SettleParams.stub()) + let response = RPCResponse(matchingRequest: request, result: RPCResult.response(AnyCodable(true))) + + networkingInteractor.responsePublisherSubject.send((session.topic, request, response)) XCTAssertTrue(sessionStorageMock.getSession(forTopic: session.topic)!.acknowledged, "Responder must acknowledged session") } @@ -136,13 +136,10 @@ final class ApproveEngineTests: XCTestCase { cryptoMock.setAgreementSecret(AgreementKeys.stub(), topic: session.topic) try! cryptoMock.setPrivateKey(privateKey) - let response = WCResponse( - topic: session.topic, - chainId: nil, - requestMethod: .sessionSettle, - requestParams: .sessionSettle(SessionType.SettleParams.stub()), - result: .error(JSONRPCErrorResponse(id: 1, error: JSONRPCErrorResponse.Error(code: 0, message: "")))) - networkingInteractor.responsePublisherSubject.send(response) + let request = RPCRequest(method: SessionSettleProtocolMethod().method, params: SessionType.SettleParams.stub()) + let response = RPCResponse.stubError(forRequest: request) + + networkingInteractor.responsePublisherSubject.send((session.topic, request, response)) XCTAssertNil(sessionStorageMock.getSession(forTopic: session.topic), "Responder must remove session") XCTAssertTrue(networkingInteractor.didUnsubscribe(to: session.topic), "Responder must unsubscribe topic B") diff --git a/Tests/WalletConnectSignTests/Helpers/WCRequest+Extension.swift b/Tests/WalletConnectSignTests/Helpers/WCRequest+Extension.swift deleted file mode 100644 index d3616f365..000000000 --- a/Tests/WalletConnectSignTests/Helpers/WCRequest+Extension.swift +++ /dev/null @@ -1,9 +0,0 @@ -@testable import WalletConnectSign - -extension WCRequest { - - var sessionProposal: SessionProposal? { - guard case .sessionPropose(let proposal) = self.params else { return nil } - return proposal - } -} diff --git a/Tests/WalletConnectSignTests/JsonRpcHistoryTests.swift b/Tests/WalletConnectSignTests/JsonRpcHistoryTests.swift deleted file mode 100644 index dd98115f7..000000000 --- a/Tests/WalletConnectSignTests/JsonRpcHistoryTests.swift +++ /dev/null @@ -1,84 +0,0 @@ -import Foundation -import XCTest -import TestingUtils -import WalletConnectUtils -import WalletConnectPairing -@testable import WalletConnectSign - -final class JsonRpcHistoryTests: XCTestCase { - - var sut: WalletConnectSign.JsonRpcHistory! - - override func setUp() { - sut = JsonRpcHistory(logger: ConsoleLoggerMock(), keyValueStore: CodableStore(defaults: RuntimeKeyValueStorage(), identifier: "")) - } - - override func tearDown() { - sut = nil - } - - func testSetRecord() { - let recordinput = getTestJsonRpcRecordInput() - XCTAssertFalse(sut.exist(id: recordinput.request.id)) - try! sut.set(topic: recordinput.topic, request: recordinput.request) - XCTAssertTrue(sut.exist(id: recordinput.request.id)) - } - - func testGetRecord() { - let recordinput = getTestJsonRpcRecordInput() - XCTAssertNil(sut.get(id: recordinput.request.id)) - try! sut.set(topic: recordinput.topic, request: recordinput.request) - XCTAssertNotNil(sut.get(id: recordinput.request.id)) - } - - func testResolve() { - let recordinput = getTestJsonRpcRecordInput() - try! sut.set(topic: recordinput.topic, request: recordinput.request) - XCTAssertNil(sut.get(id: recordinput.request.id)?.response) - let jsonRpcResponse = JSONRPCResponse(id: recordinput.request.id, result: AnyCodable("")) - let response = JsonRpcResult.response(jsonRpcResponse) - _ = try! sut.resolve(response: response) - XCTAssertNotNil(sut.get(id: jsonRpcResponse.id)?.response) - } - - func testThrowsOnResolveDuplicate() { - let recordinput = getTestJsonRpcRecordInput() - try! sut.set(topic: recordinput.topic, request: recordinput.request) - let jsonRpcResponse = JSONRPCResponse(id: recordinput.request.id, result: AnyCodable("")) - let response = JsonRpcResult.response(jsonRpcResponse) - _ = try! sut.resolve(response: response) - XCTAssertThrowsError(try sut.resolve(response: response)) - } - - func testThrowsOnSetDuplicate() { - let recordinput = getTestJsonRpcRecordInput() - try! sut.set(topic: recordinput.topic, request: recordinput.request) - XCTAssertThrowsError(try sut.set(topic: recordinput.topic, request: recordinput.request)) - } - - func testDelete() { - let recordinput = getTestJsonRpcRecordInput() - try! sut.set(topic: recordinput.topic, request: recordinput.request) - XCTAssertNotNil(sut.get(id: recordinput.request.id)) - sut.delete(topic: testTopic) - XCTAssertNil(sut.get(id: recordinput.request.id)) - } - - func testGetPending() { - let recordinput1 = getTestJsonRpcRecordInput(id: 1) - let recordinput2 = getTestJsonRpcRecordInput(id: 2) - try! sut.set(topic: recordinput1.topic, request: recordinput1.request) - try! sut.set(topic: recordinput2.topic, request: recordinput2.request) - XCTAssertEqual(sut.getPending().count, 2) - let jsonRpcResponse = JSONRPCResponse(id: recordinput1.request.id, result: AnyCodable("")) - let response = JsonRpcResult.response(jsonRpcResponse) - _ = try! sut.resolve(response: response) - XCTAssertEqual(sut.getPending().count, 1) - } -} - -private let testTopic = "test_topic" -private func getTestJsonRpcRecordInput(id: Int64 = 0) -> (topic: String, request: WCRequest) { - let request = WCRequest(id: id, jsonrpc: "2.0", method: .pairingPing, params: WCRequest.Params.pairingPing(PairingType.PingParams())) - return (topic: testTopic, request: request) -} diff --git a/Tests/WalletConnectSignTests/Mocks/MockedRelayClient.swift b/Tests/WalletConnectSignTests/Mocks/MockedRelayClient.swift index eac4dacb5..4044c4b2e 100644 --- a/Tests/WalletConnectSignTests/Mocks/MockedRelayClient.swift +++ b/Tests/WalletConnectSignTests/Mocks/MockedRelayClient.swift @@ -3,42 +3,42 @@ import Foundation @testable import WalletConnectRelay @testable import WalletConnectSign -class MockedRelayClient: NetworkRelaying { - - var messagePublisherSubject = PassthroughSubject<(topic: String, message: String), Never>() - var messagePublisher: AnyPublisher<(topic: String, message: String), Never> { - messagePublisherSubject.eraseToAnyPublisher() - } - - var socketConnectionStatusPublisherSubject = PassthroughSubject() - var socketConnectionStatusPublisher: AnyPublisher { - socketConnectionStatusPublisherSubject.eraseToAnyPublisher() - } - - var error: Error? - var prompt = false - - func publish(topic: String, payload: String, tag: Int, prompt: Bool) async throws { - self.prompt = prompt - } - - func publish(topic: String, payload: String, tag: Int, prompt: Bool, onNetworkAcknowledge: @escaping ((Error?) -> Void)) { - self.prompt = prompt - onNetworkAcknowledge(error) - } - - func subscribe(topic: String) async throws {} - - func subscribe(topic: String, completion: @escaping (Error?) -> Void) { - } - - func unsubscribe(topic: String, completion: @escaping ((Error?) -> Void)) { - } - - func connect() { - } - - func disconnect(closeCode: URLSessionWebSocketTask.CloseCode) { - } - -} +// class MockedRelayClient: NetworkRelaying { +// +// var messagePublisherSubject = PassthroughSubject<(topic: String, message: String), Never>() +// var messagePublisher: AnyPublisher<(topic: String, message: String), Never> { +// messagePublisherSubject.eraseToAnyPublisher() +// } +// +// var socketConnectionStatusPublisherSubject = PassthroughSubject() +// var socketConnectionStatusPublisher: AnyPublisher { +// socketConnectionStatusPublisherSubject.eraseToAnyPublisher() +// } +// +// var error: Error? +// var prompt = false +// +// func publish(topic: String, payload: String, tag: Int, prompt: Bool) async throws { +// self.prompt = prompt +// } +// +// func publish(topic: String, payload: String, tag: Int, prompt: Bool, onNetworkAcknowledge: @escaping ((Error?) -> Void)) { +// self.prompt = prompt +// onNetworkAcknowledge(error) +// } +// +// func subscribe(topic: String) async throws {} +// +// func subscribe(topic: String, completion: @escaping (Error?) -> Void) { +// } +// +// func unsubscribe(topic: String, completion: @escaping ((Error?) -> Void)) { +// } +// +// func connect() { +// } +// +// func disconnect(closeCode: URLSessionWebSocketTask.CloseCode) { +// } +// +// } diff --git a/Tests/WalletConnectSignTests/Mocks/NetworkingInteractorMock.swift b/Tests/WalletConnectSignTests/Mocks/NetworkingInteractorMock.swift deleted file mode 100644 index a76d6ef91..000000000 --- a/Tests/WalletConnectSignTests/Mocks/NetworkingInteractorMock.swift +++ /dev/null @@ -1,102 +0,0 @@ -import Foundation -import Combine -import WalletConnectUtils -import WalletConnectPairing -@testable import WalletConnectSign -@testable import TestingUtils - -class NetworkingInteractorMock: NetworkInteracting { - - private(set) var subscriptions: [String] = [] - private(set) var unsubscriptions: [String] = [] - - let transportConnectionPublisherSubject = PassthroughSubject() - let responsePublisherSubject = PassthroughSubject() - let wcRequestPublisherSubject = PassthroughSubject() - - var transportConnectionPublisher: AnyPublisher { - transportConnectionPublisherSubject.eraseToAnyPublisher() - } - var wcRequestPublisher: AnyPublisher { - wcRequestPublisherSubject.eraseToAnyPublisher() - } - var responsePublisher: AnyPublisher { - responsePublisherSubject.eraseToAnyPublisher() - } - - var didCallSubscribe = false - var didRespondOnTopic: String? - var didCallUnsubscribe = false - var didRespondSuccess = false - var didRespondError = false - var lastErrorCode = -1 - var error: Error? - - private(set) var requestCallCount = 0 - var didCallRequest: Bool { requestCallCount > 0 } - - private(set) var requests: [(topic: String, request: WCRequest)] = [] - - func request(topic: String, payload: WCRequest) async throws { - requestCallCount += 1 - requests.append((topic, payload)) - } - - func requestNetworkAck(_ wcMethod: WCMethod, onTopic topic: String, completion: @escaping ((Error?) -> Void)) { - requestCallCount += 1 - requests.append((topic, wcMethod.asRequest())) - completion(nil) - } - - func requestPeerResponse(_ wcMethod: WCMethod, onTopic topic: String, completion: ((Result, JSONRPCErrorResponse>) -> Void)?) { - requestCallCount += 1 - requests.append((topic, wcMethod.asRequest())) - } - - func respond(topic: String, response: JsonRpcResult, completion: @escaping ((Error?) -> Void)) { - didRespondOnTopic = topic - completion(error) - } - - func respond(topic: String, response: JsonRpcResult, tag: Int) async throws { - didRespondOnTopic = topic - } - - func respondSuccess(payload: WCRequestSubscriptionPayload) async throws { - respondSuccess(for: payload) - } - - func respondError(payload: WCRequestSubscriptionPayload, reason: ReasonCode) async throws { - lastErrorCode = reason.code - didRespondError = true - } - - func respondSuccess(for payload: WCRequestSubscriptionPayload) { - didRespondSuccess = true - } - - func subscribe(topic: String) { - subscriptions.append(topic) - didCallSubscribe = true - } - - func unsubscribe(topic: String) { - unsubscriptions.append(topic) - didCallUnsubscribe = true - } - - func sendSubscriptionPayloadOn(topic: String) { - let payload = WCRequestSubscriptionPayload(topic: topic, wcRequest: pingRequest) - wcRequestPublisherSubject.send(payload) - } - - func didSubscribe(to topic: String) -> Bool { - subscriptions.contains { $0 == topic } - } - - func didUnsubscribe(to topic: String) -> Bool { - unsubscriptions.contains { $0 == topic } - } -} - -private let pingRequest = WCRequest(id: 1, jsonrpc: "2.0", method: .pairingPing, params: WCRequest.Params.pairingPing(PairingType.PingParams())) diff --git a/Tests/WalletConnectSignTests/Mocks/SerializerMock.swift b/Tests/WalletConnectSignTests/Mocks/SerializerMock.swift deleted file mode 100644 index 339da3ff8..000000000 --- a/Tests/WalletConnectSignTests/Mocks/SerializerMock.swift +++ /dev/null @@ -1,34 +0,0 @@ -// - -import Foundation -import WalletConnectUtils -@testable import WalletConnectKMS -@testable import WalletConnectSign - -class SerializerMock: Serializing { - var deserialized: Any! - var serialized: String = "" - - func serialize(topic: String, encodable: Encodable, envelopeType: Envelope.EnvelopeType) throws -> String { - try serialize(json: try encodable.json(), agreementKeys: AgreementKeys.stub()) - } - func deserialize(topic: String, encodedEnvelope: String) throws -> T { - return try deserialize(message: encodedEnvelope, symmetricKey: Data()) - } - func deserializeJsonRpc(topic: String, message: String) throws -> Result, JSONRPCErrorResponse> { - .success(try deserialize(message: message, symmetricKey: Data())) - } - - func deserialize(message: String, symmetricKey: Data) throws -> T where T: Codable { - if let deserializedModel = deserialized as? T { - return deserializedModel - } else { - throw NSError.mock() - } - } - - func serialize(json: String, agreementKeys: AgreementKeys) throws -> String { - return serialized - } - -} diff --git a/Tests/WalletConnectSignTests/NonControllerSessionStateMachineTests.swift b/Tests/WalletConnectSignTests/NonControllerSessionStateMachineTests.swift index 47937bd42..8107cfd0d 100644 --- a/Tests/WalletConnectSignTests/NonControllerSessionStateMachineTests.swift +++ b/Tests/WalletConnectSignTests/NonControllerSessionStateMachineTests.swift @@ -2,6 +2,7 @@ import XCTest import WalletConnectUtils @testable import TestingUtils import WalletConnectKMS +import JSONRPC @testable import WalletConnectSign class NonControllerSessionStateMachineTests: XCTestCase { @@ -34,8 +35,9 @@ class NonControllerSessionStateMachineTests: XCTestCase { didCallbackUpdatMethods = true XCTAssertEqual(topic, session.topic) } - networkingInteractor.wcRequestPublisherSubject.send(WCRequestSubscriptionPayload.stubUpdateNamespaces(topic: session.topic)) + networkingInteractor.requestPublisherSubject.send((session.topic, RPCRequest.stubUpdateNamespaces())) XCTAssertTrue(didCallbackUpdatMethods) + usleep(100) XCTAssertTrue(networkingInteractor.didRespondSuccess) } @@ -49,7 +51,7 @@ class NonControllerSessionStateMachineTests: XCTestCase { // } func testUpdateMethodPeerErrorSessionNotFound() { - networkingInteractor.wcRequestPublisherSubject.send(WCRequestSubscriptionPayload.stubUpdateNamespaces(topic: "")) + networkingInteractor.requestPublisherSubject.send(("", RPCRequest.stubUpdateNamespaces())) usleep(100) XCTAssertFalse(networkingInteractor.didRespondSuccess) XCTAssertEqual(networkingInteractor.lastErrorCode, 7001) @@ -58,7 +60,7 @@ class NonControllerSessionStateMachineTests: XCTestCase { func testUpdateMethodPeerErrorUnauthorized() { let session = WCSession.stub(isSelfController: true) // Peer is not a controller storageMock.setSession(session) - networkingInteractor.wcRequestPublisherSubject.send(WCRequestSubscriptionPayload.stubUpdateNamespaces(topic: session.topic)) + networkingInteractor.requestPublisherSubject.send((session.topic, RPCRequest.stubUpdateNamespaces())) usleep(100) XCTAssertFalse(networkingInteractor.didRespondSuccess) XCTAssertEqual(networkingInteractor.lastErrorCode, 3003) @@ -72,7 +74,7 @@ class NonControllerSessionStateMachineTests: XCTestCase { storageMock.setSession(session) let twoDaysFromNowTimestamp = Int64(TimeTraveler.dateByAdding(days: 2).timeIntervalSince1970) - networkingInteractor.wcRequestPublisherSubject.send(WCRequestSubscriptionPayload.stubUpdateExpiry(topic: session.topic, expiry: twoDaysFromNowTimestamp)) + networkingInteractor.requestPublisherSubject.send((session.topic, RPCRequest.stubUpdateExpiry(expiry: twoDaysFromNowTimestamp))) let extendedSession = storageMock.getAll().first {$0.topic == session.topic}! print(extendedSession.expiryDate) @@ -85,7 +87,7 @@ class NonControllerSessionStateMachineTests: XCTestCase { storageMock.setSession(session) let twoDaysFromNowTimestamp = Int64(TimeTraveler.dateByAdding(days: 2).timeIntervalSince1970) - networkingInteractor.wcRequestPublisherSubject.send(WCRequestSubscriptionPayload.stubUpdateExpiry(topic: session.topic, expiry: twoDaysFromNowTimestamp)) + networkingInteractor.requestPublisherSubject.send((session.topic, RPCRequest.stubUpdateExpiry(expiry: twoDaysFromNowTimestamp))) let potentiallyExtendedSession = storageMock.getAll().first {$0.topic == session.topic}! XCTAssertEqual(potentiallyExtendedSession.expiryDate.timeIntervalSinceReferenceDate, tomorrow.timeIntervalSinceReferenceDate, accuracy: 1, "expiry date has been extended for peer non controller request ") @@ -96,7 +98,7 @@ class NonControllerSessionStateMachineTests: XCTestCase { let session = WCSession.stub(isSelfController: false, expiryDate: tomorrow) storageMock.setSession(session) let tenDaysFromNowTimestamp = Int64(TimeTraveler.dateByAdding(days: 10).timeIntervalSince1970) - networkingInteractor.wcRequestPublisherSubject.send(WCRequestSubscriptionPayload.stubUpdateExpiry(topic: session.topic, expiry: tenDaysFromNowTimestamp)) + networkingInteractor.requestPublisherSubject.send((session.topic, RPCRequest.stubUpdateExpiry(expiry: tenDaysFromNowTimestamp))) let potentaillyExtendedSession = storageMock.getAll().first {$0.topic == session.topic}! XCTAssertEqual(potentaillyExtendedSession.expiryDate.timeIntervalSinceReferenceDate, tomorrow.timeIntervalSinceReferenceDate, accuracy: 1, "expiry date has been extended despite ttl to high") @@ -108,7 +110,7 @@ class NonControllerSessionStateMachineTests: XCTestCase { storageMock.setSession(session) let oneDayFromNowTimestamp = Int64(TimeTraveler.dateByAdding(days: 10).timeIntervalSince1970) - networkingInteractor.wcRequestPublisherSubject.send(WCRequestSubscriptionPayload.stubUpdateExpiry(topic: session.topic, expiry: oneDayFromNowTimestamp)) + networkingInteractor.requestPublisherSubject.send((session.topic, RPCRequest.stubUpdateExpiry(expiry: oneDayFromNowTimestamp))) let potentaillyExtendedSession = storageMock.getAll().first {$0.topic == session.topic}! XCTAssertEqual(potentaillyExtendedSession.expiryDate.timeIntervalSinceReferenceDate, tomorrow.timeIntervalSinceReferenceDate, accuracy: 1, "expiry date has been extended despite ttl to low") } diff --git a/Tests/WalletConnectSignTests/Stub/Stubs.swift b/Tests/WalletConnectSignTests/Stub/Stubs.swift index a17ad98a8..3789cdb3e 100644 --- a/Tests/WalletConnectSignTests/Stub/Stubs.swift +++ b/Tests/WalletConnectSignTests/Stub/Stubs.swift @@ -1,5 +1,6 @@ @testable import WalletConnectSign import Foundation +import JSONRPC import WalletConnectKMS import WalletConnectUtils import TestingUtils @@ -41,12 +42,6 @@ extension SessionNamespace { } } -extension RelayProtocolOptions { - static func stub() -> RelayProtocolOptions { - RelayProtocolOptions(protocol: "", data: nil) - } -} - extension Participant { static func stub(publicKey: String = AgreementPrivateKey().publicKey.hexRepresentation) -> Participant { Participant(publicKey: publicKey, metadata: AppMetadata.stub()) @@ -59,29 +54,25 @@ extension AgreementPeer { } } -extension WCRequestSubscriptionPayload { +extension RPCRequest { - static func stubUpdateNamespaces(topic: String, namespaces: [String: SessionNamespace] = SessionNamespace.stubDictionary()) -> WCRequestSubscriptionPayload { - let updateMethod = WCMethod.wcSessionUpdate(SessionType.UpdateParams(namespaces: namespaces)).asRequest() - return WCRequestSubscriptionPayload(topic: topic, wcRequest: updateMethod) + static func stubUpdateNamespaces(namespaces: [String: SessionNamespace] = SessionNamespace.stubDictionary()) -> RPCRequest { + return RPCRequest(method: SessionUpdateProtocolMethod().method, params: SessionType.UpdateParams(namespaces: namespaces)) } - static func stubUpdateExpiry(topic: String, expiry: Int64) -> WCRequestSubscriptionPayload { - let updateExpiryMethod = WCMethod.wcSessionExtend(SessionType.UpdateExpiryParams(expiry: expiry)).asRequest() - return WCRequestSubscriptionPayload(topic: topic, wcRequest: updateExpiryMethod) + static func stubUpdateExpiry(expiry: Int64) -> RPCRequest { + return RPCRequest(method: SessionExtendProtocolMethod().method, params: SessionType.UpdateExpiryParams(expiry: expiry)) } - static func stubSettle(topic: String) -> WCRequestSubscriptionPayload { - let method = WCMethod.wcSessionSettle(SessionType.SettleParams.stub()) - return WCRequestSubscriptionPayload(topic: topic, wcRequest: method.asRequest()) + static func stubSettle() -> RPCRequest { + return RPCRequest(method: SessionSettleProtocolMethod().method, params: SessionType.SettleParams.stub()) } - static func stubRequest(topic: String, method: String, chainId: Blockchain) -> WCRequestSubscriptionPayload { + static func stubRequest(method: String, chainId: Blockchain) -> RPCRequest { let params = SessionType.RequestParams( request: SessionType.RequestParams.Request(method: method, params: AnyCodable(EmptyCodable())), chainId: chainId) - let request = WCRequest(method: .sessionRequest, params: .sessionRequest(params)) - return WCRequestSubscriptionPayload(topic: topic, wcRequest: request) + return RPCRequest(method: SessionRequestProtocolMethod().method, params: params) } } @@ -95,15 +86,8 @@ extension SessionProposal { } } -extension WCResponse { - static func stubError(forRequest request: WCRequest, topic: String) -> WCResponse { - let errorResponse = JSONRPCErrorResponse(id: request.id, error: JSONRPCErrorResponse.Error(code: 0, message: "")) - return WCResponse( - topic: topic, - chainId: nil, - requestMethod: request.method, - requestParams: request.params, - result: .error(errorResponse) - ) +extension RPCResponse { + static func stubError(forRequest request: RPCRequest) -> RPCResponse { + return RPCResponse(matchingRequest: request, error: JSONRPCError(code: 0, message: "")) } } diff --git a/Tests/WalletConnectSignTests/WCRelayTests.swift b/Tests/WalletConnectSignTests/WCRelayTests.swift deleted file mode 100644 index a42fbd328..000000000 --- a/Tests/WalletConnectSignTests/WCRelayTests.swift +++ /dev/null @@ -1,72 +0,0 @@ -import Foundation -import Combine -import XCTest -import WalletConnectUtils -import WalletConnectPairing -@testable import TestingUtils -@testable import WalletConnectSign - -class NetworkingInteractorTests: XCTestCase { - var networkingInteractor: NetworkInteractor! - var relayClient: MockedRelayClient! - var serializer: SerializerMock! - - private var publishers = [AnyCancellable]() - - override func setUp() { - let logger = ConsoleLoggerMock() - serializer = SerializerMock() - relayClient = MockedRelayClient() - networkingInteractor = NetworkInteractor(relayClient: relayClient, serializer: serializer, logger: logger, jsonRpcHistory: JsonRpcHistory(logger: logger, keyValueStore: CodableStore(defaults: RuntimeKeyValueStorage(), identifier: ""))) - } - - override func tearDown() { - networkingInteractor = nil - relayClient = nil - serializer = nil - } - - func testNotifiesOnEncryptedWCJsonRpcRequest() { - let requestExpectation = expectation(description: "notifies with request") - let topic = "fefc3dc39cacbc562ed58f92b296e2d65a6b07ef08992b93db5b3cb86280635a" - networkingInteractor.wcRequestPublisher.sink { (_) in - requestExpectation.fulfill() - }.store(in: &publishers) - serializer.deserialized = request - relayClient.messagePublisherSubject.send((topic, testPayload)) - waitForExpectations(timeout: 1.001, handler: nil) - } - - func testPromptOnSessionRequest() async { - let topic = "fefc3dc39cacbc562ed58f92b296e2d65a6b07ef08992b93db5b3cb86280635a" - let method = getWCSessionMethod() - relayClient.prompt = false - try! await networkingInteractor.request(topic: topic, payload: method.asRequest()) - XCTAssertTrue(relayClient.prompt) - } -} - -extension NetworkingInteractorTests { - func getWCSessionMethod() -> WCMethod { - let sessionRequestParams = SessionType.RequestParams(request: SessionType.RequestParams.Request(method: "method", params: AnyCodable("params")), chainId: Blockchain("eip155:1")!) - return .wcSessionRequest(sessionRequestParams) - } -} - -private let testPayload = -""" -{ - "id":1630300527198334, - "jsonrpc":"2.0", - "method":"irn_subscription", - "params":{ - "id":"0847f4e1dd19cf03a43dc7525f39896b630e9da33e4683c8efbc92ea671b5e07", - "data":{ - "topic":"fefc3dc39cacbc562ed58f92b296e2d65a6b07ef08992b93db5b3cb86280635a", - "message":"7b226964223a313633303330303532383030302c226a736f6e727063223a22322e30222c22726573756c74223a747275657d" - } - } -} -""" -// TODO - change for different request -private let request = WCRequest(id: 1, jsonrpc: "2.0", method: .pairingPing, params: WCRequest.Params.pairingPing(PairingType.PingParams())) diff --git a/Tests/WalletConnectSignTests/WCResponseTests.swift b/Tests/WalletConnectSignTests/WCResponseTests.swift index 24729924b..b4d814d9e 100644 --- a/Tests/WalletConnectSignTests/WCResponseTests.swift +++ b/Tests/WalletConnectSignTests/WCResponseTests.swift @@ -1,17 +1,15 @@ import XCTest +import JSONRPC @testable import WalletConnectSign -final class WCResponseTests: XCTestCase { +final class RPCIDTests: XCTestCase { func testTimestamp() { - let request = WCRequest( - method: .pairingPing, - params: .pairingPing(.init()) - ) - let response = WCResponse.stubError(forRequest: request, topic: "topic") - let timestamp = Date(timeIntervalSince1970: TimeInterval(request.id / 1000 / 1000)) + let request = RPCRequest(method: "method") + let response = RPCResponse(matchingRequest: request, error: JSONRPCError(code: 0, message: "message")) + let timestamp = Date(timeIntervalSince1970: TimeInterval(request.id!.right! / 1000 / 1000)) - XCTAssertEqual(response.timestamp, timestamp) - XCTAssertTrue(Calendar.current.isDateInToday(response.timestamp)) + XCTAssertEqual(response.id!.timestamp, timestamp) + XCTAssertTrue(Calendar.current.isDateInToday(response.id!.timestamp)) } } diff --git a/Tests/WalletConnectUtilsTests/WalletConnectURITests.swift b/Tests/WalletConnectUtilsTests/WalletConnectURITests.swift index fc2f8c170..5f65b5c28 100644 --- a/Tests/WalletConnectUtilsTests/WalletConnectURITests.swift +++ b/Tests/WalletConnectUtilsTests/WalletConnectURITests.swift @@ -1,17 +1,15 @@ import XCTest @testable import WalletConnectUtils -private func stubURI(api: WalletConnectURI.TargetAPI? = nil) -> (uri: WalletConnectURI, string: String) { +private func stubURI() -> (uri: WalletConnectURI, string: String) { let topic = Data.randomBytes(count: 32).toHexString() let symKey = Data.randomBytes(count: 32).toHexString() let protocolName = "irn" - let uriBase = api == nil ? "wc:" : "wc:\(api!.rawValue)-" - let uriString = "\(uriBase)\(topic)@2?symKey=\(symKey)&relay-protocol=\(protocolName)" + let uriString = "wc:\(topic)@2?symKey=\(symKey)&relay-protocol=\(protocolName)" let uri = WalletConnectURI( topic: topic, symKey: symKey, - relay: RelayProtocolOptions(protocol: protocolName, data: nil), - api: api) + relay: RelayProtocolOptions(protocol: protocolName, data: nil)) return (uri, uriString) } @@ -42,25 +40,6 @@ final class WalletConnectURITests: XCTestCase { XCTAssertEqual(expectedString, outputURIString) } - // MARK: - Init URI with prefix API identifier - - func testInitFromPrefixedURIString() { - WalletConnectURI.TargetAPI.allCases.forEach { api in - let uriString = stubURI(api: api).string - let uri = WalletConnectURI(string: uriString) - XCTAssertEqual(uri?.api, api) - XCTAssertEqual(uri?.absoluteString, uriString) - } - } - - func testAbsentPrefixFallbackToSign() { - let input = stubURI() - let uriFromParams = input.uri - let uriFromString = WalletConnectURI(string: input.string) - XCTAssertEqual(uriFromParams.api, .sign) - XCTAssertEqual(uriFromString?.api, .sign) - } - // MARK: - Init URI failure cases func testInitFailsBadScheme() {