diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bcbcf09cf..a81d10cb4 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,6 +37,12 @@ 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 }} diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index e23637099..645d75745 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -44,6 +44,7 @@ 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 */; }; @@ -235,6 +236,7 @@ 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 = ""; }; @@ -581,6 +583,14 @@ path = Connect; sourceTree = ""; }; + 84CEC64728D8A98900D081A8 /* Pairing */ = { + isa = PBXGroup; + children = ( + 84CEC64528D89D6B00D081A8 /* PairingTests.swift */, + ); + path = Pairing; + sourceTree = ""; + }; 84D2A66728A4F5260088AE09 /* Auth */ = { isa = PBXGroup; children = ( @@ -1019,6 +1029,7 @@ A5E03DEE286464DB00888481 /* IntegrationTests */ = { isa = PBXGroup; children = ( + 84CEC64728D8A98900D081A8 /* Pairing */, A5E03E0B28646AA500888481 /* Relay */, A5E03E0A28646A8A00888481 /* Stubs */, A5E03E0928646A8100888481 /* Sign */, @@ -1455,6 +1466,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 84CEC64628D89D6B00D081A8 /* PairingTests.swift in Sources */, 767DC83528997F8E00080FA9 /* EthSendTransaction.swift in Sources */, A5E03E03286466F400888481 /* ChatTests.swift in Sources */, 84D2A66628A4F51E0088AE09 /* AuthTests.swift in Sources */, diff --git a/Example/IntegrationTests/Pairing/PairingTests.swift b/Example/IntegrationTests/Pairing/PairingTests.swift new file mode 100644 index 000000000..01085df49 --- /dev/null +++ b/Example/IntegrationTests/Pairing/PairingTests.swift @@ -0,0 +1,56 @@ +import Foundation +import XCTest +import WalletConnectUtils +@testable import WalletConnectKMS +import WalletConnectRelay +import Combine +import WalletConnectNetworking +@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]() + + override func setUp() { + (appPairingClient, appPushClient) = makeClients(prefix: "🤖 App") + (walletPairingClient, walletPushClient) = makeClients(prefix: "🐶 Wallet") + } + + func makeClients(prefix: String) -> (PairingClient, PushClient) { + let keychain = KeychainStorageMock() + let logger = ConsoleLogger(suffix: prefix, loggingLevel: .debug) + let projectId = "3ca2919724fbfa5456a25194e369a8b4" + let relayClient = RelayClient(relayHost: URLConfig.relayHost, projectId: projectId, keychainStorage: keychain, socketFactory: SocketFactory(), logger: logger) + + let pairingClient = PairingClientFactory.create(logger: logger, keyValueStorage: RuntimeKeyValueStorage(), keychainStorage: keychain, relayClient: relayClient) + + let pushClient = PushClientFactory.create(logger: logger, keyValueStorage: RuntimeKeyValueStorage(), keychainStorage: keychain, relayClient: relayClient, pairingClient: pairingClient) + return (pairingClient, pushClient) + + } + + func testProposePushOnPairing() async throws { + let exp = expectation(description: "testProposePushOnPairing") + + walletPushClient.proposalPublisher.sink { _ in + exp.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: [exp], timeout: 2) + } +} + diff --git a/Sources/WalletConnectNetworking/NetworkInteracting.swift b/Sources/WalletConnectNetworking/NetworkInteracting.swift index 665834735..f23d5b66d 100644 --- a/Sources/WalletConnectNetworking/NetworkInteracting.swift +++ b/Sources/WalletConnectNetworking/NetworkInteracting.swift @@ -6,6 +6,7 @@ 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, protocolMethod: ProtocolMethod, envelopeType: Envelope.EnvelopeType) async throws diff --git a/Sources/WalletConnectNetworking/NetworkInteractor.swift b/Sources/WalletConnectNetworking/NetworkInteractor.swift index 6b8cdecbd..00efdc43b 100644 --- a/Sources/WalletConnectNetworking/NetworkInteractor.swift +++ b/Sources/WalletConnectNetworking/NetworkInteractor.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() } @@ -56,13 +56,13 @@ public class NetworkingInteractor: NetworkInteracting { } } - public func requestSubscription(on request: ProtocolMethod) -> AnyPublisher, Never> { + public func requestSubscription(on request: ProtocolMethod) -> AnyPublisher, Never> { return requestPublisher .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() diff --git a/Sources/WalletConnectPairing/PairingClient.swift b/Sources/WalletConnectPairing/PairingClient.swift new file mode 100644 index 000000000..de16ec03b --- /dev/null +++ b/Sources/WalletConnectPairing/PairingClient.swift @@ -0,0 +1,48 @@ +import Foundation +import WalletConnectUtils +import WalletConnectRelay +import WalletConnectNetworking +import Combine + +public class PairingClient: PairingRegisterer { + private let walletPairService: WalletPairService + private let appPairService: AppPairService + public let socketConnectionStatusPublisher: AnyPublisher + private let logger: ConsoleLogging + private let networkingInteractor: NetworkInteracting + private let pairingRequestsSubscriber: PairingRequestsSubscriber + + init(appPairService: AppPairService, + networkingInteractor: NetworkInteracting, + logger: ConsoleLogging, + walletPairService: WalletPairService, + pairingRequestsSubscriber: PairingRequestsSubscriber, + socketConnectionStatusPublisher: AnyPublisher + ) { + self.appPairService = appPairService + self.walletPairService = walletPairService + self.networkingInteractor = networkingInteractor + self.socketConnectionStatusPublisher = socketConnectionStatusPublisher + self.logger = logger + self.pairingRequestsSubscriber = pairingRequestsSubscriber + } + /// 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 register(method: ProtocolMethod) { + pairingRequestsSubscriber.subscribeForRequest(method) + } +} + diff --git a/Sources/WalletConnectPairing/PairingClientFactory.swift b/Sources/WalletConnectPairing/PairingClientFactory.swift new file mode 100644 index 000000000..63a447038 --- /dev/null +++ b/Sources/WalletConnectPairing/PairingClientFactory.swift @@ -0,0 +1,36 @@ +import Foundation +import WalletConnectRelay +import WalletConnectUtils +import WalletConnectKMS +import WalletConnectNetworking + +public struct PairingClientFactory { + + public static func create(relayClient: RelayClient) -> 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, relayClient: relayClient) + } + + static func create(logger: ConsoleLogging, keyValueStorage: KeyValueStorage, keychainStorage: KeychainStorageProtocol, relayClient: RelayClient) -> PairingClient { + let kms = KeyManagementService(keychain: keychainStorage) + let serializer = Serializer(kms: kms) + let history = RPCHistoryFactory.createForNetwork(keyValueStorage: keyValueStorage) + let networkingInteractor = NetworkingInteractor(relayClient: relayClient, serializer: serializer, logger: logger, rpcHistory: history) + let pairingStore = PairingStorage(storage: SequenceStore(store: .init(defaults: keyValueStorage, identifier: ""))) + let appPairService = AppPairService(networkingInteractor: networkingInteractor, kms: kms, pairingStorage: pairingStore) + let walletPairService = WalletPairService(networkingInteractor: networkingInteractor, kms: kms, pairingStorage: pairingStore) + let pairingRequestsSubscriber = PairingRequestsSubscriber(networkingInteractor: networkingInteractor, pairingStorage: pairingStore, logger: logger) + + return PairingClient( + appPairService: appPairService, + networkingInteractor: networkingInteractor, + logger: logger, + walletPairService: walletPairService, + pairingRequestsSubscriber: pairingRequestsSubscriber, + socketConnectionStatusPublisher: relayClient.socketConnectionStatusPublisher + ) + } +} + diff --git a/Sources/WalletConnectPairing/PairingRegisterer.swift b/Sources/WalletConnectPairing/PairingRegisterer.swift new file mode 100644 index 000000000..6eafa7b1e --- /dev/null +++ b/Sources/WalletConnectPairing/PairingRegisterer.swift @@ -0,0 +1,6 @@ +import Foundation +import WalletConnectNetworking + +public protocol PairingRegisterer { + func register(method: ProtocolMethod) +} diff --git a/Sources/WalletConnectPairing/PairingRequester.swift b/Sources/WalletConnectPairing/PairingRequester.swift new file mode 100644 index 000000000..535f9eb34 --- /dev/null +++ b/Sources/WalletConnectPairing/PairingRequester.swift @@ -0,0 +1,27 @@ +import Foundation +import Combine +import JSONRPC +import WalletConnectUtils +import WalletConnectKMS +import WalletConnectNetworking + + +public 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: AnyCodable) async throws { + let protocolMethod = PushProposeProtocolMethod() + let request = RPCRequest(method: protocolMethod.method, params: params) + try await networkingInteractor.requestNetworkAck(request, topic: topic, protocolMethod: protocolMethod) + } +} diff --git a/Sources/WalletConnectPairing/PairingRequestsSubscriber.swift b/Sources/WalletConnectPairing/PairingRequestsSubscriber.swift new file mode 100644 index 000000000..ed3ede277 --- /dev/null +++ b/Sources/WalletConnectPairing/PairingRequestsSubscriber.swift @@ -0,0 +1,36 @@ +import Foundation +import Combine +import WalletConnectUtils +import WalletConnectNetworking + +public class PairingRequestsSubscriber { + private let networkingInteractor: NetworkInteracting + private let pairingStorage: PairingStorage + private var publishers = Set() + + init(networkingInteractor: NetworkInteracting, pairingStorage: PairingStorage, logger: ConsoleLogging) { + self.networkingInteractor = networkingInteractor + self.pairingStorage = pairingStorage + } + + func subscribeForRequest(_ protocolMethod: ProtocolMethod) { + networkingInteractor.requestPublisher + // Pairing requests only + .filter { [unowned self] payload in + return pairingStorage.hasPairing(forTopic: payload.topic) + } + // Wrong method + .filter { payload in + return payload.request.method != protocolMethod.method + } + // Respond error + .sink { [unowned self] topic, request in + Task(priority: .high) { + // TODO - spec tag + try await networkingInteractor.respondError(topic: topic, requestId: request.id!, protocolMethod: protocolMethod, reason: PairError.methodUnsupported) + } + + }.store(in: &publishers) + } + +} diff --git a/Sources/WalletConnectPairing/Push/PushClient.swift b/Sources/WalletConnectPairing/Push/PushClient.swift new file mode 100644 index 000000000..5dee1b892 --- /dev/null +++ b/Sources/WalletConnectPairing/Push/PushClient.swift @@ -0,0 +1,59 @@ +import Foundation +import JSONRPC +import Combine +import WalletConnectKMS +import WalletConnectUtils +import WalletConnectNetworking + +public class PushClient { + + private var publishers = Set() + + let requestPublisherSubject = PassthroughSubject<(topic: String, params: PushRequestParams), Never>() + + var proposalPublisher: AnyPublisher<(topic: String, params: PushRequestParams), Never> { + requestPublisherSubject.eraseToAnyPublisher() + } + + public let logger: ConsoleLogging + + private let pushProposer: PushProposer + private let networkInteractor: NetworkInteracting + private let pairingRegisterer: PairingRegisterer + + init(networkInteractor: NetworkInteracting, + logger: ConsoleLogging, + kms: KeyManagementServiceProtocol, + pushProposer: PushProposer, + pairingRegisterer: PairingRegisterer) { + self.networkInteractor = networkInteractor + self.logger = logger + self.pushProposer = pushProposer + self.pairingRegisterer = pairingRegisterer + + setupPairingSubscriptions() + } + + public func propose(topic: String) async throws { + try await pushProposer.request(topic: topic, params: AnyCodable(PushRequestParams())) + } +} + +private extension PushClient { + + func setupPairingSubscriptions() { + let protocolMethod = PushProposeProtocolMethod() + + pairingRegisterer.register(method: protocolMethod) + + networkInteractor.responseErrorSubscription(on: protocolMethod) + .sink { [unowned self] (payload: ResponseSubscriptionErrorPayload) in + logger.error(payload.error.localizedDescription) + }.store(in: &publishers) + + networkInteractor.requestSubscription(on: protocolMethod) + .sink { [unowned self] (payload: RequestSubscriptionPayload) in + requestPublisherSubject.send((payload.topic, payload.request)) + }.store(in: &publishers) + } +} diff --git a/Sources/WalletConnectPairing/Push/PushClientFactory.swift b/Sources/WalletConnectPairing/Push/PushClientFactory.swift new file mode 100644 index 000000000..21c4cf74c --- /dev/null +++ b/Sources/WalletConnectPairing/Push/PushClientFactory.swift @@ -0,0 +1,24 @@ +import Foundation +import WalletConnectRelay +import WalletConnectUtils +import WalletConnectKMS +import WalletConnectNetworking + +public struct PushClientFactory { + + static func create(logger: ConsoleLogging, keyValueStorage: KeyValueStorage, keychainStorage: KeychainStorageProtocol, relayClient: RelayClient, pairingClient: PairingClient) -> PushClient { + let kms = KeyManagementService(keychain: keychainStorage) + let serializer = Serializer(kms: kms) + let history = RPCHistoryFactory.createForNetwork(keyValueStorage: keyValueStorage) + let networkingInteractor = NetworkingInteractor(relayClient: relayClient, serializer: serializer, logger: logger, rpcHistory: history) + let pushProposer = PushProposer(networkingInteractor: networkingInteractor, kms: kms, logger: logger) + + return PushClient( + networkInteractor: networkingInteractor, + logger: logger, + kms: kms, + pushProposer: pushProposer, + pairingRegisterer: pairingClient + ) + } +} diff --git a/Sources/WalletConnectPairing/Push/PushProposeProtocolMethod.swift b/Sources/WalletConnectPairing/Push/PushProposeProtocolMethod.swift new file mode 100644 index 000000000..1c5e62906 --- /dev/null +++ b/Sources/WalletConnectPairing/Push/PushProposeProtocolMethod.swift @@ -0,0 +1,12 @@ +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) +} + +struct PushRequestParams: Codable {} diff --git a/Sources/WalletConnectPairing/Services/App/AppPairService.swift b/Sources/WalletConnectPairing/Services/App/AppPairService.swift new file mode 100644 index 000000000..9d223d06a --- /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, api: .auth) + pairingStorage.setPairing(pairing) + return uri + } +} diff --git a/Sources/WalletConnectPairing/Services/PairingPingService.swift b/Sources/WalletConnectPairing/Services/Common/Ping/PairingPingService.swift similarity index 100% rename from Sources/WalletConnectPairing/Services/PairingPingService.swift rename to Sources/WalletConnectPairing/Services/Common/Ping/PairingPingService.swift diff --git a/Sources/WalletConnectPairing/Services/PingRequester.swift b/Sources/WalletConnectPairing/Services/Common/Ping/PingRequester.swift similarity index 100% rename from Sources/WalletConnectPairing/Services/PingRequester.swift rename to Sources/WalletConnectPairing/Services/Common/Ping/PingRequester.swift diff --git a/Sources/WalletConnectPairing/Services/PingResponder.swift b/Sources/WalletConnectPairing/Services/Common/Ping/PingResponder.swift similarity index 100% rename from Sources/WalletConnectPairing/Services/PingResponder.swift rename to Sources/WalletConnectPairing/Services/Common/Ping/PingResponder.swift diff --git a/Sources/WalletConnectPairing/Services/PingResponseSubscriber.swift b/Sources/WalletConnectPairing/Services/Common/Ping/PingResponseSubscriber.swift similarity index 100% rename from Sources/WalletConnectPairing/Services/PingResponseSubscriber.swift rename to Sources/WalletConnectPairing/Services/Common/Ping/PingResponseSubscriber.swift diff --git a/Sources/WalletConnectPairing/Services/Wallet/WalletPairService.swift b/Sources/WalletConnectPairing/Services/Wallet/WalletPairService.swift new file mode 100644 index 000000000..199b5a9a9 --- /dev/null +++ b/Sources/WalletConnectPairing/Services/Wallet/WalletPairService.swift @@ -0,0 +1,38 @@ +import Foundation +import WalletConnectKMS +import WalletConnectNetworking +import WalletConnectUtils + +actor WalletPairService { + enum Errors: Error { + case pairingAlreadyExist + } + + let networkingInteractor: NetworkInteracting + let kms: KeyManagementServiceProtocol + private let pairingStorage: WCPairingStorage + + init(networkingInteractor: NetworkInteracting, + kms: KeyManagementServiceProtocol, + pairingStorage: WCPairingStorage) { + self.networkingInteractor = networkingInteractor + self.kms = kms + self.pairingStorage = pairingStorage + } + + func pair(_ uri: WalletConnectURI) async throws { + guard !hasPairing(for: uri.topic) else { + throw Errors.pairingAlreadyExist + } + var pairing = WCPairing(uri: uri) + try await networkingInteractor.subscribe(topic: pairing.topic) + let symKey = try SymmetricKey(hex: uri.symKey) + try kms.setSymmetricKey(symKey, for: pairing.topic) + pairing.activate() + pairingStorage.setPairing(pairing) + } + + func hasPairing(for topic: String) -> Bool { + return pairingStorage.hasPairing(forTopic: topic) + } +} diff --git a/Sources/WalletConnectPairing/Types/PairError.swift b/Sources/WalletConnectPairing/Types/PairError.swift new file mode 100644 index 000000000..99e93f336 --- /dev/null +++ b/Sources/WalletConnectPairing/Types/PairError.swift @@ -0,0 +1,16 @@ +import WalletConnectNetworking + +public enum PairError: Codable, Equatable, Error, Reason { + case methodUnsupported + + public var code: Int { + //TODO - spec code + return 44444 + } + + //TODO - spec message + public var message: String { + return "Method Unsupported" + } + +} diff --git a/Sources/WalletConnectPairing/PairingProtocolMethod.swift b/Sources/WalletConnectPairing/Types/PairingProtocolMethod.swift similarity index 100% rename from Sources/WalletConnectPairing/PairingProtocolMethod.swift rename to Sources/WalletConnectPairing/Types/PairingProtocolMethod.swift diff --git a/Tests/TestingUtils/NetworkingInteractorMock.swift b/Tests/TestingUtils/NetworkingInteractorMock.swift index 171602eba..36bee9173 100644 --- a/Tests/TestingUtils/NetworkingInteractorMock.swift +++ b/Tests/TestingUtils/NetworkingInteractorMock.swift @@ -22,6 +22,8 @@ public class NetworkingInteractorMock: NetworkInteracting { private(set) var requestCallCount = 0 var didCallRequest: Bool { requestCallCount > 0 } + var onSubscribeCalled: (() -> Void)? + public let socketConnectionStatusPublisherSubject = PassthroughSubject() public var socketConnectionStatusPublisher: AnyPublisher { socketConnectionStatusPublisherSubject.eraseToAnyPublisher() @@ -30,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() } @@ -79,6 +81,7 @@ public class NetworkingInteractorMock: NetworkInteracting { } public func subscribe(topic: String) async throws { + defer { onSubscribeCalled?() } subscriptions.append(topic) didCallSubscribe = true } diff --git a/Tests/WalletConnectSignTests/PairingEngineTests.swift b/Tests/WalletConnectSignTests/PairingEngineTests.swift index 6bd6538cc..0f99d16d0 100644 --- a/Tests/WalletConnectSignTests/PairingEngineTests.swift +++ b/Tests/WalletConnectSignTests/PairingEngineTests.swift @@ -87,6 +87,7 @@ final class PairingEngineTests: XCTestCase { } func testHandleSessionProposeResponse() async { + let exp = expectation(description: "testHandleSessionProposeResponse") let uri = try! await engine.create() let pairing = storageMock.getPairing(forTopic: uri.topic)! let topicA = pairing.topic @@ -107,10 +108,17 @@ final class PairingEngineTests: XCTestCase { let response = RPCResponse(id: request.id!, result: RPCResult.response(AnyCodable(proposalResponse))) + 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)