From 543c5cac350acf04d652a31498002bf2a64a5be5 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Fri, 3 Jun 2022 14:20:44 +0500 Subject: [PATCH 01/13] Approve method moved ApproveEngine --- .../Engine/Common/ApproveEngine.swift | 57 +++++++++++++++++++ .../Engine/Common/PairingEngine.swift | 32 +---------- .../WalletConnectSign/Sign/SignClient.swift | 11 +++- .../ApproveEngineTests.swift | 43 ++++++++++++++ .../PairingEngineTests.swift | 14 ----- 5 files changed, 109 insertions(+), 48 deletions(-) create mode 100644 Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift create mode 100644 Tests/WalletConnectSignTests/ApproveEngineTests.swift diff --git a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift new file mode 100644 index 000000000..a610b3249 --- /dev/null +++ b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift @@ -0,0 +1,57 @@ +import Foundation +import WalletConnectUtils +import WalletConnectKMS + +final class ApproveEngine { + + private let networkingInteractor: NetworkInteracting + private let proposalPayloadsStore: CodableStore + private let kms: KeyManagementServiceProtocol + private let logger: ConsoleLogging + + init(networkingInteractor: NetworkInteracting, + proposalPayloadsStore: CodableStore, + kms: KeyManagementServiceProtocol, + logger: ConsoleLogging) { + self.networkingInteractor = networkingInteractor + self.proposalPayloadsStore = proposalPayloadsStore + self.kms = kms + self.logger = logger + } + + func approveProposal(proposerPubKey: String, validating sessionNamespaces: [String: SessionNamespace]) throws -> (String, SessionProposal) { + + let payload = try proposalPayloadsStore.get(key: proposerPubKey) + + guard let payload = payload, case .sessionPropose(let proposal) = payload.wcRequest.params + else { throw ApproveEngineError.wrongRequestParams } + + proposalPayloadsStore.delete(forKey: proposerPubKey) + + try Namespace.validate(sessionNamespaces) + try Namespace.validateApproved(sessionNamespaces, against: proposal.requiredNamespaces) + + let selfPublicKey = try kms.createX25519KeyPair() + let agreementKey = try? kms.performKeyAgreement(selfPublicKey: selfPublicKey, peerPublicKey: proposal.proposer.publicKey) + guard let agreementKey = agreementKey else { + networkingInteractor.respondError(for: payload, reason: .missingOrInvalid("agreement keys")) + throw ApproveEngineError.agreementMissingOrInvalid + } + // TODO: Extend pairing + let sessionTopic = agreementKey.derivedTopic() + try kms.setAgreementSecret(agreementKey, topic: sessionTopic) + + guard let relay = proposal.relays.first else { throw ApproveEngineError.relayNotFound } + let proposeResponse = SessionType.ProposeResponse(relay: relay, responderPublicKey: selfPublicKey.hexRepresentation) + let response = JSONRPCResponse(id: payload.wcRequest.id, result: AnyCodable(proposeResponse)) + networkingInteractor.respond(topic: payload.topic, response: .response(response)) { _ in } + + return (sessionTopic, proposal) + } +} + +enum ApproveEngineError: Error { + case wrongRequestParams + case relayNotFound + case agreementMissingOrInvalid +} diff --git a/Sources/WalletConnectSign/Engine/Common/PairingEngine.swift b/Sources/WalletConnectSign/Engine/Common/PairingEngine.swift index 4d242e7e8..f0bfea815 100644 --- a/Sources/WalletConnectSign/Engine/Common/PairingEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/PairingEngine.swift @@ -26,7 +26,7 @@ final class PairingEngine { metadata: AppMetadata, logger: ConsoleLogging, topicGenerator: @escaping () -> String = String.generateTopic, - proposalPayloadsStore: CodableStore = CodableStore(defaults: RuntimeKeyValueStorage(), identifier: StorageDomainIdentifiers.proposals.rawValue)) { + proposalPayloadsStore: CodableStore) { self.networkingInteractor = networkingInteractor self.kms = kms self.metadata = metadata @@ -115,36 +115,6 @@ final class PairingEngine { networkingInteractor.respondError(for: payload, reason: reason) // todo - delete pairing if inactive } - - func approveProposal(proposerPubKey: String, validating sessionNamespaces: [String: SessionNamespace]) -> (String, SessionProposal)? { - guard let payload = try? proposalPayloadsStore.get(key: proposerPubKey), - case .sessionPropose(let proposal) = payload.wcRequest.params else { - //TODO - throws - return nil - } - let proposedNamespaces = proposal.requiredNamespaces - proposalPayloadsStore.delete(forKey: proposerPubKey) - - do { - try Namespace.validate(sessionNamespaces) - try Namespace.validateApproved(sessionNamespaces, against: proposedNamespaces) - let selfPublicKey = try! kms.createX25519KeyPair() - let agreementKey = try kms.performKeyAgreement(selfPublicKey: selfPublicKey, peerPublicKey: proposal.proposer.publicKey) - //todo - extend pairing - let sessionTopic = agreementKey.derivedTopic() - - try! kms.setAgreementSecret(agreementKey, topic: sessionTopic) - guard let relay = proposal.relays.first else {return nil} - let proposeResponse = SessionType.ProposeResponse(relay: relay, responderPublicKey: selfPublicKey.hexRepresentation) - let response = JSONRPCResponse(id: payload.wcRequest.id, result: AnyCodable(proposeResponse)) - logger.debug("Responding session propose") - networkingInteractor.respond(topic: payload.topic, response: .response(response)) { _ in } - return (sessionTopic, proposal) - } catch { - networkingInteractor.respondError(for: payload, reason: .missingOrInvalid("agreement keys")) - return nil - } - } //MARK: - Private diff --git a/Sources/WalletConnectSign/Sign/SignClient.swift b/Sources/WalletConnectSign/Sign/SignClient.swift index b9ccd902e..f7ae66b18 100644 --- a/Sources/WalletConnectSign/Sign/SignClient.swift +++ b/Sources/WalletConnectSign/Sign/SignClient.swift @@ -28,6 +28,7 @@ public final class SignClient { private let pairingEngine: PairingEngine private let pairEngine: PairEngine private let sessionEngine: SessionEngine + private let approveEngine: ApproveEngine private let nonControllerSessionStateMachine: NonControllerSessionStateMachine private let controllerSessionStateMachine: ControllerSessionStateMachine private let networkingInteractor: NetworkInteracting @@ -61,11 +62,13 @@ public final class SignClient { 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) - self.pairingEngine = PairingEngine(networkingInteractor: networkingInteractor, kms: kms, pairingStore: pairingStore, sessionToPairingTopic: sessionToPairingTopic, metadata: metadata, logger: logger) + let proposalPayloadsStore = CodableStore(defaults: RuntimeKeyValueStorage(), identifier: StorageDomainIdentifiers.proposals.rawValue) + self.pairingEngine = PairingEngine(networkingInteractor: networkingInteractor, kms: kms, pairingStore: pairingStore, sessionToPairingTopic: sessionToPairingTopic, metadata: metadata, logger: logger, proposalPayloadsStore: proposalPayloadsStore) self.sessionEngine = SessionEngine(networkingInteractor: networkingInteractor, kms: kms, pairingStore: pairingStore, sessionStore: sessionStore, sessionToPairingTopic: sessionToPairingTopic, metadata: metadata, logger: logger) self.nonControllerSessionStateMachine = NonControllerSessionStateMachine(networkingInteractor: networkingInteractor, kms: kms, sessionStore: sessionStore, logger: logger) self.controllerSessionStateMachine = ControllerSessionStateMachine(networkingInteractor: networkingInteractor, kms: kms, sessionStore: sessionStore, logger: logger) self.pairEngine = PairEngine(networkingInteractor: networkingInteractor, kms: kms, pairingStore: pairingStore) + self.approveEngine = ApproveEngine(networkingInteractor: networkingInteractor, proposalPayloadsStore: proposalPayloadsStore, kms: kms, logger: logger) self.cleanupService = CleanupService(pairingStore: pairingStore, sessionStore: sessionStore, kms: kms, sessionToPairingTopic: sessionToPairingTopic) setUpConnectionObserving(relayClient: relayClient) setUpEnginesCallbacks() @@ -93,8 +96,10 @@ public final class SignClient { 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) - self.pairingEngine = PairingEngine(networkingInteractor: networkingInteractor, kms: kms, pairingStore: pairingStore, sessionToPairingTopic: sessionToPairingTopic, metadata: metadata, logger: logger) + let proposalPayloadsStore = CodableStore(defaults: RuntimeKeyValueStorage(), identifier: StorageDomainIdentifiers.proposals.rawValue) + self.pairingEngine = PairingEngine(networkingInteractor: networkingInteractor, kms: kms, pairingStore: pairingStore, sessionToPairingTopic: sessionToPairingTopic, metadata: metadata, logger: logger, proposalPayloadsStore: proposalPayloadsStore) self.sessionEngine = SessionEngine(networkingInteractor: networkingInteractor, kms: kms, pairingStore: pairingStore, sessionStore: sessionStore, sessionToPairingTopic: sessionToPairingTopic, metadata: metadata, logger: logger) + self.approveEngine = ApproveEngine(networkingInteractor: networkingInteractor, proposalPayloadsStore: proposalPayloadsStore, kms: kms, logger: logger) self.nonControllerSessionStateMachine = NonControllerSessionStateMachine(networkingInteractor: networkingInteractor, kms: kms, sessionStore: sessionStore, logger: logger) self.controllerSessionStateMachine = ControllerSessionStateMachine(networkingInteractor: networkingInteractor, kms: kms, sessionStore: sessionStore, logger: logger) self.pairEngine = PairEngine(networkingInteractor: networkingInteractor, kms: kms, pairingStore: pairingStore) @@ -157,7 +162,7 @@ public final class SignClient { namespaces: [String: SessionNamespace] ) throws { //TODO - accounts should be validated for matching namespaces BEFORE responding proposal - guard let (sessionTopic, proposal) = pairingEngine.approveProposal(proposerPubKey: proposalId, validating: namespaces) else {return} + let (sessionTopic, proposal) = try approveEngine.approveProposal(proposerPubKey: proposalId, validating: namespaces) try sessionEngine.settle(topic: sessionTopic, proposal: proposal, namespaces: namespaces) } diff --git a/Tests/WalletConnectSignTests/ApproveEngineTests.swift b/Tests/WalletConnectSignTests/ApproveEngineTests.swift new file mode 100644 index 000000000..ae6ad40f1 --- /dev/null +++ b/Tests/WalletConnectSignTests/ApproveEngineTests.swift @@ -0,0 +1,43 @@ +import XCTest +@testable import WalletConnectSign +@testable import TestingUtils +@testable import WalletConnectKMS +import WalletConnectUtils + +final class ApproveEngineTests: XCTestCase { + + var engine: ApproveEngine! + var networkingInteractor: MockedWCRelay! + var cryptoMock: KeyManagementServiceMock! + + override func setUp() { + networkingInteractor = MockedWCRelay() + cryptoMock = KeyManagementServiceMock() + engine = ApproveEngine( + networkingInteractor: networkingInteractor, + proposalPayloadsStore: .init(defaults: RuntimeKeyValueStorage(), identifier: ""), + kms: cryptoMock, + logger: ConsoleLoggerMock() + ) + } + + override func tearDown() { + networkingInteractor = nil + cryptoMock = nil + engine = nil + } + + func testApproveProposal() throws { + // Client receives a proposal + let topicA = String.generateTopic() + 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) + let (topicB, _) = try engine.approveProposal(proposerPubKey: proposal.proposer.publicKey, validating: SessionNamespace.stubDictionary()) + + XCTAssert(cryptoMock.hasAgreementSecret(for: topicB), "Responder must store agreement key for topic B") + XCTAssertEqual(networkingInteractor.didRespondOnTopic!, topicA, "Responder must respond on topic A") + } +} diff --git a/Tests/WalletConnectSignTests/PairingEngineTests.swift b/Tests/WalletConnectSignTests/PairingEngineTests.swift index 26177b51a..dfd31204b 100644 --- a/Tests/WalletConnectSignTests/PairingEngineTests.swift +++ b/Tests/WalletConnectSignTests/PairingEngineTests.swift @@ -92,20 +92,6 @@ final class PairingEngineTests: XCTestCase { XCTAssertTrue(sessionProposed) } - func testRespondProposal() { - // Client receives a proposal - let topicA = String.generateTopic() - 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) - let (topicB, _) = engine.approveProposal(proposerPubKey: proposal.proposer.publicKey, validating: SessionNamespace.stubDictionary())! - - XCTAssert(cryptoMock.hasAgreementSecret(for: topicB), "Responder must store agreement key for topic B") - XCTAssertEqual(networkingInteractor.didRespondOnTopic!, topicA, "Responder must respond on topic A") - } - func testHandleSessionProposeResponse() async { let uri = try! await engine.create() let pairing = storageMock.getPairing(forTopic: uri.topic)! From ed3f290e849af7cbecca08fe7f2a00afe3482cf4 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Fri, 3 Jun 2022 17:08:31 +0500 Subject: [PATCH 02/13] Reject and wcSessionSubscriptions for ApproveEngine --- .../Engine/Common/ApproveEngine.swift | 131 +++++++++++++++- .../Engine/Common/PairingEngine.swift | 144 ++++-------------- .../Engine/Common/SessionEngine.swift | 51 ++++--- .../NetworkInteractor/NetworkInteractor.swift | 26 +--- .../WalletConnectSign/Sign/SignClient.swift | 33 ++-- .../ApproveEngineTests.swift | 40 ++++- .../Mocks/MockedRelay.swift | 18 +-- .../PairingEngineTests.swift | 58 ++++--- .../SessionEngineTests.swift | 4 +- 9 files changed, 278 insertions(+), 227 deletions(-) diff --git a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift index a610b3249..b18014b8a 100644 --- a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift @@ -1,22 +1,47 @@ import Foundation +import Combine import WalletConnectUtils import WalletConnectKMS final class ApproveEngine { + enum Response { + case proposeResponse(topic: String, proposal: SessionProposal) + case sessionProposal(Session.Proposal) + case sessionRejected(proposal: Session.Proposal, reason: SessionType.Reason) + } + private let networkingInteractor: NetworkInteracting + private let pairingStore: WCPairingStorage private let proposalPayloadsStore: CodableStore + private let sessionToPairingTopic: CodableStore private let kms: KeyManagementServiceProtocol private let logger: ConsoleLogging - init(networkingInteractor: NetworkInteracting, - proposalPayloadsStore: CodableStore, - kms: KeyManagementServiceProtocol, - logger: ConsoleLogging) { + private var publishers = Set() + + private let channel = PassthroughSubject() + + var approvePublisher: AnyPublisher { + channel.eraseToAnyPublisher() + } + + init( + networkingInteractor: NetworkInteracting, + proposalPayloadsStore: CodableStore, + sessionToPairingTopic: CodableStore, + kms: KeyManagementServiceProtocol, + logger: ConsoleLogging, + pairingStore: WCPairingStorage + ) { self.networkingInteractor = networkingInteractor self.proposalPayloadsStore = proposalPayloadsStore + self.sessionToPairingTopic = sessionToPairingTopic self.kms = kms self.logger = logger + self.pairingStore = pairingStore + + setupNetworkingSubscriptions() } func approveProposal(proposerPubKey: String, validating sessionNamespaces: [String: SessionNamespace]) throws -> (String, SessionProposal) { @@ -48,6 +73,104 @@ final class ApproveEngine { return (sessionTopic, proposal) } + + func reject(proposal: SessionProposal, reason: ReasonCode) { + guard let payload = try? proposalPayloadsStore.get(key: proposal.proposer.publicKey) else { + return + } + proposalPayloadsStore.delete(forKey: proposal.proposer.publicKey) + networkingInteractor.respondError(for: payload, reason: reason) +// todo - delete pairing if inactive + } + + private func setupNetworkingSubscriptions() { + networkingInteractor.responsePublisher + .sink { [unowned self] response in + self.handleResponse(response) + }.store(in: &publishers) + + networkingInteractor.wcRequestPublisher + .sink { [unowned self] subscriptionPayload in + switch subscriptionPayload.wcRequest.params { + case .sessionPropose(let proposeParams): + wcSessionPropose(subscriptionPayload, proposal: proposeParams) + default: + return + } + }.store(in: &publishers) + } + + private func wcSessionPropose(_ payload: WCRequestSubscriptionPayload, proposal: SessionType.ProposeParams) { + logger.debug("Received Session Proposal") + do { + try Namespace.validate(proposal.requiredNamespaces) + } catch { + // TODO: respond error + return + } + proposalPayloadsStore.set(payload, forKey: proposal.proposer.publicKey) + channel.send(.sessionProposal(proposal.publicRepresentation())) + } + + private func handleResponse(_ response: WCResponse) { + switch response.requestParams { + case .sessionPropose(let proposal): + handleProposeResponse(pairingTopic: response.topic, proposal: proposal, result: response.result) + default: + break + } + } + + private func handleProposeResponse(pairingTopic: String, proposal: SessionProposal, result: JsonRpcResult) { + guard var pairing = pairingStore.getPairing(forTopic: pairingTopic) else { + return + } + switch result { + case .response(let response): + + // Activate the pairing + if !pairing.active { + pairing.activate() + } else { + try? pairing.updateExpiry() + } + + pairingStore.setPairing(pairing) + + let selfPublicKey = try! AgreementPublicKey(hex: proposal.proposer.publicKey) + var agreementKeys: AgreementKeys! + + do { + let proposeResponse = try response.result.get(SessionType.ProposeResponse.self) + agreementKeys = try kms.performKeyAgreement(selfPublicKey: selfPublicKey, peerPublicKey: proposeResponse.responderPublicKey) + } catch { + //TODO - handle error + logger.debug(error) + return + } + + let sessionTopic = agreementKeys.derivedTopic() + logger.debug("Received Session Proposal response") + + try? kms.setAgreementSecret(agreementKeys, topic: sessionTopic) + sessionToPairingTopic.set(pairingTopic, forKey: sessionTopic) + channel.send(.proposeResponse(topic: sessionTopic, proposal: proposal)) + + case .error(let error): + if !pairing.active { + kms.deleteSymmetricKey(for: pairing.topic) + networkingInteractor.unsubscribe(topic: pairing.topic) + pairingStore.delete(topic: pairingTopic) + } + logger.debug("Session Proposal has been rejected") + kms.deletePrivateKey(for: proposal.proposer.publicKey) + + channel.send(.sessionRejected( + proposal: proposal.publicRepresentation(), + reason: SessionType.Reason(code: error.error.code, message: error.error.message) + )) + } + } } enum ApproveEngineError: Error { diff --git a/Sources/WalletConnectSign/Engine/Common/PairingEngine.swift b/Sources/WalletConnectSign/Engine/Common/PairingEngine.swift index f0bfea815..abd00d19a 100644 --- a/Sources/WalletConnectSign/Engine/Common/PairingEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/PairingEngine.swift @@ -5,42 +5,31 @@ import WalletConnectKMS final class PairingEngine { - var onSessionProposal: ((Session.Proposal)->())? - var onProposeResponse: ((String, SessionProposal)->())? - var onSessionRejected: ((Session.Proposal, SessionType.Reason)->())? - - private let proposalPayloadsStore: CodableStore + private let networkingInteractor: NetworkInteracting private let kms: KeyManagementServiceProtocol private let pairingStore: WCPairingStorage - private let sessionToPairingTopic: CodableStore private var metadata: AppMetadata private var publishers = [AnyCancellable]() private let logger: ConsoleLogging private let topicInitializer: () -> String - - init(networkingInteractor: NetworkInteracting, - kms: KeyManagementServiceProtocol, - pairingStore: WCPairingStorage, - sessionToPairingTopic: CodableStore, - metadata: AppMetadata, - logger: ConsoleLogging, - topicGenerator: @escaping () -> String = String.generateTopic, - proposalPayloadsStore: CodableStore) { + + 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 - self.sessionToPairingTopic = sessionToPairingTopic - self.proposalPayloadsStore = proposalPayloadsStore - setUpWCRequestHandling() + setupNetworkingSubscriptions() setupExpirationHandling() - restoreSubscriptions() - networkingInteractor.onPairingResponse = { [weak self] in - self?.handleResponse($0) - } } func hasPairing(for topic: String) -> Bool { @@ -106,54 +95,30 @@ final class PairingEngine { } } } - - func reject(proposal: SessionProposal, reason: ReasonCode) { - guard let payload = try? proposalPayloadsStore.get(key: proposal.proposer.publicKey) else { - return - } - proposalPayloadsStore.delete(forKey: proposal.proposer.publicKey) - networkingInteractor.respondError(for: payload, reason: reason) -// todo - delete pairing if inactive - } //MARK: - Private - private func setUpWCRequestHandling() { - networkingInteractor.wcRequestPublisher.sink { [unowned self] subscriptionPayload in - switch subscriptionPayload.wcRequest.params { - case .pairingPing(_): - wcPairingPing(subscriptionPayload) - case .sessionPropose(let proposeParams): - wcSessionPropose(subscriptionPayload, proposal: proposeParams) - default: - return - } - }.store(in: &publishers) - } - - private func wcSessionPropose(_ payload: WCRequestSubscriptionPayload, proposal: SessionType.ProposeParams) { - logger.debug("Received Session Proposal") - do { - try Namespace.validate(proposal.requiredNamespaces) - } catch { - // TODO: respond error - return - } - try? proposalPayloadsStore.set(payload, forKey: proposal.proposer.publicKey) - onSessionProposal?(proposal.publicRepresentation()) - } - - private func wcPairingPing(_ payload: WCRequestSubscriptionPayload) { - networkingInteractor.respondSuccess(for: payload) - } - - private func restoreSubscriptions() { + private 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) + } + + private func wcPairingPing(_ payload: WCRequestSubscriptionPayload) { + networkingInteractor.respondSuccess(for: payload) } private func setupExpirationHandling() { @@ -162,61 +127,4 @@ final class PairingEngine { self?.networkingInteractor.unsubscribe(topic: pairing.topic) } } - - private func handleResponse(_ response: WCResponse) { - switch response.requestParams { - case .sessionPropose(let proposal): - handleProposeResponse(pairingTopic: response.topic, proposal: proposal, result: response.result) - default: - break - } - } - - private func handleProposeResponse(pairingTopic: String, proposal: SessionProposal, result: JsonRpcResult) { - guard var pairing = pairingStore.getPairing(forTopic: pairingTopic) else { - return - } - switch result { - case .response(let response): - - // Activate the pairing - if !pairing.active { - pairing.activate() - } else { - try? pairing.updateExpiry() - } - - pairingStore.setPairing(pairing) - - let selfPublicKey = try! AgreementPublicKey(hex: proposal.proposer.publicKey) - var agreementKeys: AgreementKeys! - - do { - let proposeResponse = try response.result.get(SessionType.ProposeResponse.self) - agreementKeys = try kms.performKeyAgreement(selfPublicKey: selfPublicKey, peerPublicKey: proposeResponse.responderPublicKey) - } catch { - //TODO - handle error - logger.debug(error) - return - } - - let sessionTopic = agreementKeys.derivedTopic() - logger.debug("Received Session Proposal response") - - try? kms.setAgreementSecret(agreementKeys, topic: sessionTopic) - try! sessionToPairingTopic.set(pairingTopic, forKey: sessionTopic) - onProposeResponse?(sessionTopic, proposal) - - case .error(let error): - if !pairing.active { - kms.deleteSymmetricKey(for: pairing.topic) - networkingInteractor.unsubscribe(topic: pairing.topic) - pairingStore.delete(topic: pairingTopic) - } - logger.debug("Session Proposal has been rejected") - kms.deletePrivateKey(for: proposal.proposer.publicKey) - onSessionRejected?(proposal.publicRepresentation(), SessionType.Reason(code: error.error.code, message: error.error.message)) - return - } - } } diff --git a/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift index 56f930780..86d6c93fa 100644 --- a/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift @@ -24,14 +24,16 @@ final class SessionEngine { private let logger: ConsoleLogging private let topicInitializer: () -> String - init(networkingInteractor: NetworkInteracting, - kms: KeyManagementServiceProtocol, - pairingStore: WCPairingStorage, - sessionStore: WCSessionStorage, - sessionToPairingTopic: CodableStore, - metadata: AppMetadata, - logger: ConsoleLogging, - topicGenerator: @escaping () -> String = String.generateTopic) { + init( + networkingInteractor: NetworkInteracting, + kms: KeyManagementServiceProtocol, + pairingStore: WCPairingStorage, + sessionStore: WCSessionStorage, + sessionToPairingTopic: CodableStore, + metadata: AppMetadata, + logger: ConsoleLogging, + topicGenerator: @escaping () -> String = String.generateTopic + ) { self.networkingInteractor = networkingInteractor self.kms = kms self.metadata = metadata @@ -40,13 +42,9 @@ final class SessionEngine { self.sessionToPairingTopic = sessionToPairingTopic self.logger = logger self.topicInitializer = topicGenerator - setUpWCRequestHandling() - setupExpirationHandling() - restoreSubscriptions() - networkingInteractor.onResponse = { [weak self] in - self?.handleResponse($0) - } + setupNetworkingSubscriptions() + setupExpirationSubscriptions() } func setSubscription(topic: String) { @@ -150,7 +148,7 @@ final class SessionEngine { //MARK: - Private - private func setUpWCRequestHandling() { + private func setupNetworkingSubscriptions() { networkingInteractor.wcRequestPublisher.sink { [unowned self] subscriptionPayload in switch subscriptionPayload.wcRequest.params { case .sessionSettle(let settleParams): @@ -167,6 +165,17 @@ final class SessionEngine { return } }.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) } } + }.store(in: &publishers) + + networkingInteractor.responsePublisher + .sink { [unowned self] response in + self.handleResponse(response) + }.store(in: &publishers) } private func onSessionSettle(payload: WCRequestSubscriptionPayload, settleParams: SessionType.SettleParams) { @@ -267,21 +276,13 @@ final class SessionEngine { onEventReceived?(topic, event.publicRepresentation(), eventParams.chainId) } - private func setupExpirationHandling() { + private func setupExpirationSubscriptions() { sessionStore.onSessionExpiration = { [weak self] session in self?.kms.deletePrivateKey(for: session.selfParticipant.publicKey) self?.kms.deleteAgreementSecret(for: session.topic) } } - - private func restoreSubscriptions() { - networkingInteractor.transportConnectionPublisher - .sink { [unowned self] (_) in - let topics = sessionStore.getAll().map{$0.topic} - topics.forEach{ topic in Task { try? await networkingInteractor.subscribe(topic: topic) } } - }.store(in: &publishers) - } - + private func handleResponse(_ response: WCResponse) { switch response.requestParams { case .sessionSettle: diff --git a/Sources/WalletConnectSign/NetworkInteractor/NetworkInteractor.swift b/Sources/WalletConnectSign/NetworkInteractor/NetworkInteractor.swift index 4855d460d..c113fccce 100644 --- a/Sources/WalletConnectSign/NetworkInteractor/NetworkInteractor.swift +++ b/Sources/WalletConnectSign/NetworkInteractor/NetworkInteractor.swift @@ -13,11 +13,6 @@ struct WCResponse: Codable { } protocol NetworkInteracting: AnyObject { - //// TODO - both methods to remove, use Publishers instead - var onPairingResponse: ((WCResponse) -> Void)? {get set} // Temporary workaround - var onResponse: ((WCResponse) -> Void)? {get set} - /////// - var transportConnectionPublisher: AnyPublisher {get} var wcRequestPublisher: AnyPublisher {get} var responsePublisher: AnyPublisher {get} @@ -41,31 +36,26 @@ extension NetworkInteracting { } class NetworkInteractor: NetworkInteracting { - - var onPairingResponse: ((WCResponse) -> Void)? - var onResponse: ((WCResponse) -> Void)? - private var publishers = [AnyCancellable]() + private var publishers = [AnyCancellable]() 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() } - private let transportConnectionPublisherSubject = PassthroughSubject() - - //rename to request publisher var wcRequestPublisher: AnyPublisher { wcRequestPublisherSubject.eraseToAnyPublisher() } - private let wcRequestPublisherSubject = PassthroughSubject() - var responsePublisher: AnyPublisher { responsePublisherSubject.eraseToAnyPublisher() } - private let responsePublisherSubject = PassthroughSubject() let logger: ConsoleLogging @@ -184,12 +174,14 @@ class NetworkInteractor: NetworkInteracting { } //MARK: - Private + private func setUpPublishers() { relayClient.socketConnectionStatusPublisher.sink { [weak self] status in if status == .connected { self?.transportConnectionPublisherSubject.send() } }.store(in: &publishers) + relayClient.onMessage = { [unowned self] topic, message in manageSubscription(topic, message) } @@ -229,8 +221,6 @@ class NetworkInteractor: NetworkInteracting { requestParams: record.request.params, result: JsonRpcResult.response(response)) responsePublisherSubject.send(wcResponse) - onPairingResponse?(wcResponse) - onResponse?(wcResponse) } catch { logger.info("Info: \(error.localizedDescription)") } @@ -246,8 +236,6 @@ class NetworkInteractor: NetworkInteracting { requestParams: record.request.params, result: JsonRpcResult.error(response)) responsePublisherSubject.send(wcResponse) - onPairingResponse?(wcResponse) - onResponse?(wcResponse) } catch { logger.info("Info: \(error.localizedDescription)") } diff --git a/Sources/WalletConnectSign/Sign/SignClient.swift b/Sources/WalletConnectSign/Sign/SignClient.swift index f7ae66b18..b2b101f1c 100644 --- a/Sources/WalletConnectSign/Sign/SignClient.swift +++ b/Sources/WalletConnectSign/Sign/SignClient.swift @@ -63,12 +63,12 @@ public final class SignClient { 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) - self.pairingEngine = PairingEngine(networkingInteractor: networkingInteractor, kms: kms, pairingStore: pairingStore, sessionToPairingTopic: sessionToPairingTopic, metadata: metadata, logger: logger, proposalPayloadsStore: proposalPayloadsStore) + self.pairingEngine = PairingEngine(networkingInteractor: networkingInteractor, kms: kms, pairingStore: pairingStore, metadata: metadata, logger: logger) self.sessionEngine = SessionEngine(networkingInteractor: networkingInteractor, kms: kms, pairingStore: pairingStore, sessionStore: sessionStore, sessionToPairingTopic: sessionToPairingTopic, metadata: metadata, logger: logger) self.nonControllerSessionStateMachine = NonControllerSessionStateMachine(networkingInteractor: networkingInteractor, kms: kms, sessionStore: sessionStore, logger: logger) self.controllerSessionStateMachine = ControllerSessionStateMachine(networkingInteractor: networkingInteractor, kms: kms, sessionStore: sessionStore, logger: logger) self.pairEngine = PairEngine(networkingInteractor: networkingInteractor, kms: kms, pairingStore: pairingStore) - self.approveEngine = ApproveEngine(networkingInteractor: networkingInteractor, proposalPayloadsStore: proposalPayloadsStore, kms: kms, logger: logger) + self.approveEngine = ApproveEngine(networkingInteractor: networkingInteractor, proposalPayloadsStore: proposalPayloadsStore, sessionToPairingTopic: sessionToPairingTopic, kms: kms, logger: logger, pairingStore: pairingStore) self.cleanupService = CleanupService(pairingStore: pairingStore, sessionStore: sessionStore, kms: kms, sessionToPairingTopic: sessionToPairingTopic) setUpConnectionObserving(relayClient: relayClient) setUpEnginesCallbacks() @@ -97,9 +97,9 @@ public final class SignClient { 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) - self.pairingEngine = PairingEngine(networkingInteractor: networkingInteractor, kms: kms, pairingStore: pairingStore, sessionToPairingTopic: sessionToPairingTopic, metadata: metadata, logger: logger, proposalPayloadsStore: proposalPayloadsStore) + self.pairingEngine = PairingEngine(networkingInteractor: networkingInteractor, kms: kms, pairingStore: pairingStore, metadata: metadata, logger: logger) self.sessionEngine = SessionEngine(networkingInteractor: networkingInteractor, kms: kms, pairingStore: pairingStore, sessionStore: sessionStore, sessionToPairingTopic: sessionToPairingTopic, metadata: metadata, logger: logger) - self.approveEngine = ApproveEngine(networkingInteractor: networkingInteractor, proposalPayloadsStore: proposalPayloadsStore, kms: kms, logger: logger) + self.approveEngine = ApproveEngine(networkingInteractor: networkingInteractor, proposalPayloadsStore: proposalPayloadsStore, sessionToPairingTopic: sessionToPairingTopic, kms: kms, logger: logger, pairingStore: pairingStore) self.nonControllerSessionStateMachine = NonControllerSessionStateMachine(networkingInteractor: networkingInteractor, kms: kms, sessionStore: sessionStore, logger: logger) self.controllerSessionStateMachine = ControllerSessionStateMachine(networkingInteractor: networkingInteractor, kms: kms, sessionStore: sessionStore, logger: logger) self.pairEngine = PairEngine(networkingInteractor: networkingInteractor, kms: kms, pairingStore: pairingStore) @@ -171,7 +171,7 @@ public final class SignClient { /// - proposal: Session Proposal received from peer client in a WalletConnect delegate. /// - reason: Reason why the session proposal was rejected. Conforms to CAIP25. public func reject(proposal: Session.Proposal, reason: RejectionReason) { - pairingEngine.reject(proposal: proposal.proposal, reason: reason.internalRepresentation()) + approveEngine.reject(proposal: proposal.proposal, reason: reason.internalRepresentation()) } /// For the responder to update session namespaces @@ -296,15 +296,22 @@ public final class SignClient { // MARK: - Private private func setUpEnginesCallbacks() { + approveEngine.approvePublisher.sink { [unowned self] response in + switch response { + case .proposeResponse(let topic, let proposal): + self.sessionEngine.settlingProposal = proposal + self.sessionEngine.setSubscription(topic: topic) + case .sessionProposal(let proposal): + self.delegate?.didReceive(sessionProposal: proposal) + case .sessionRejected(proposal: let proposal, reason: let reason): + self.delegate?.didReject(proposal: proposal, reason: reason.publicRepresentation()) + } + }.store(in: &publishers) + sessionEngine.onSessionSettle = { [unowned self] settledSession in delegate?.didSettle(session: settledSession) } - pairingEngine.onSessionProposal = { [unowned self] proposal in - delegate?.didReceive(sessionProposal: proposal) - } - pairingEngine.onSessionRejected = { [unowned self] proposal, reason in - delegate?.didReject(proposal: proposal, reason: reason.publicRepresentation()) - } + sessionEngine.onSessionRequest = { [unowned self] sessionRequest in delegate?.didReceive(sessionRequest: sessionRequest) } @@ -329,10 +336,6 @@ public final class SignClient { sessionEngine.onSessionResponse = { [unowned self] response in delegate?.didReceive(sessionResponse: response) } - pairingEngine.onProposeResponse = { [unowned self] sessionTopic, proposal in - sessionEngine.settlingProposal = proposal - sessionEngine.setSubscription(topic: sessionTopic) - } } #if DEBUG diff --git a/Tests/WalletConnectSignTests/ApproveEngineTests.swift b/Tests/WalletConnectSignTests/ApproveEngineTests.swift index ae6ad40f1..18de52551 100644 --- a/Tests/WalletConnectSignTests/ApproveEngineTests.swift +++ b/Tests/WalletConnectSignTests/ApproveEngineTests.swift @@ -1,4 +1,5 @@ import XCTest +import Combine @testable import WalletConnectSign @testable import TestingUtils @testable import WalletConnectKMS @@ -9,21 +10,30 @@ final class ApproveEngineTests: XCTestCase { var engine: ApproveEngine! var networkingInteractor: MockedWCRelay! var cryptoMock: KeyManagementServiceMock! + var storageMock: WCPairingStorageMock! + var proposalPayloadsStore: CodableStore! + + var publishers = Set() override func setUp() { networkingInteractor = MockedWCRelay() cryptoMock = KeyManagementServiceMock() + storageMock = WCPairingStorageMock() + proposalPayloadsStore = CodableStore(defaults: RuntimeKeyValueStorage(), identifier: "") engine = ApproveEngine( networkingInteractor: networkingInteractor, - proposalPayloadsStore: .init(defaults: RuntimeKeyValueStorage(), identifier: ""), + proposalPayloadsStore: proposalPayloadsStore, + sessionToPairingTopic: CodableStore(defaults: RuntimeKeyValueStorage(), identifier: ""), kms: cryptoMock, - logger: ConsoleLoggerMock() + logger: ConsoleLoggerMock(), + pairingStore: storageMock ) } override func tearDown() { networkingInteractor = nil cryptoMock = nil + storageMock = nil engine = nil } @@ -35,9 +45,35 @@ final class ApproveEngineTests: XCTestCase { let request = WCRequest(method: .sessionPropose, params: .sessionPropose(proposal)) let payload = WCRequestSubscriptionPayload(topic: topicA, wcRequest: request) networkingInteractor.wcRequestPublisherSubject.send(payload) + let (topicB, _) = try engine.approveProposal(proposerPubKey: proposal.proposer.publicKey, validating: SessionNamespace.stubDictionary()) XCTAssert(cryptoMock.hasAgreementSecret(for: topicB), "Responder must store agreement key for topic B") XCTAssertEqual(networkingInteractor.didRespondOnTopic!, topicA, "Responder must respond on topic A") } + + + func testReceiveProposal() { + let pairing = WCPairing.stub() + let topicA = pairing.topic + storageMock.setPairing(pairing) + 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.approvePublisher.sink { response in + switch response { + case .sessionProposal: + sessionProposed = true + default: + break + } + }.store(in: &publishers) + + networkingInteractor.wcRequestPublisherSubject.send(payload) + XCTAssertNotNil(try! proposalPayloadsStore.get(key: proposal.proposer.publicKey), "Proposer must store proposal payload") + XCTAssertTrue(sessionProposed) + } } diff --git a/Tests/WalletConnectSignTests/Mocks/MockedRelay.swift b/Tests/WalletConnectSignTests/Mocks/MockedRelay.swift index 9b23b9592..40ca53503 100644 --- a/Tests/WalletConnectSignTests/Mocks/MockedRelay.swift +++ b/Tests/WalletConnectSignTests/Mocks/MockedRelay.swift @@ -7,28 +7,22 @@ import WalletConnectUtils class MockedWCRelay: NetworkInteracting { - let responsePublisherSubject = PassthroughSubject() private(set) var subscriptions: [String] = [] private(set) var unsubscriptions: [String] = [] - var responsePublisher: AnyPublisher { - responsePublisherSubject.eraseToAnyPublisher() - } - - var onPairingResponse: ((WCResponse) -> Void)? - var onResponse: ((WCResponse) -> Void)? - - var onPairingApproveResponse: ((String) -> Void)? + let transportConnectionPublisherSubject = PassthroughSubject() + let responsePublisherSubject = PassthroughSubject() + let wcRequestPublisherSubject = PassthroughSubject() var transportConnectionPublisher: AnyPublisher { transportConnectionPublisherSubject.eraseToAnyPublisher() } - private let transportConnectionPublisherSubject = PassthroughSubject() - - let wcRequestPublisherSubject = PassthroughSubject() var wcRequestPublisher: AnyPublisher { wcRequestPublisherSubject.eraseToAnyPublisher() } + var responsePublisher: AnyPublisher { + responsePublisherSubject.eraseToAnyPublisher() + } var didCallSubscribe = false var didRespondOnTopic: String? = nil diff --git a/Tests/WalletConnectSignTests/PairingEngineTests.swift b/Tests/WalletConnectSignTests/PairingEngineTests.swift index dfd31204b..7328d65f7 100644 --- a/Tests/WalletConnectSignTests/PairingEngineTests.swift +++ b/Tests/WalletConnectSignTests/PairingEngineTests.swift @@ -1,4 +1,5 @@ import XCTest +import Combine @testable import WalletConnectSign @testable import TestingUtils @testable import WalletConnectKMS @@ -12,21 +13,21 @@ func deriveTopic(publicKey: String, privateKey: AgreementPrivateKey) -> String { final class PairingEngineTests: XCTestCase { var engine: PairingEngine! + var approveEngine: ApproveEngine! var networkingInteractor: MockedWCRelay! var storageMock: WCPairingStorageMock! var cryptoMock: KeyManagementServiceMock! - var proposalPayloadsStore: CodableStore! var topicGenerator: TopicGenerator! + var publishers = Set() override func setUp() { networkingInteractor = MockedWCRelay() storageMock = WCPairingStorageMock() cryptoMock = KeyManagementServiceMock() topicGenerator = TopicGenerator() - proposalPayloadsStore = CodableStore(defaults: RuntimeKeyValueStorage(), identifier: "") - setupEngine() + setupEngines() } override func tearDown() { @@ -35,20 +36,28 @@ final class PairingEngineTests: XCTestCase { cryptoMock = nil topicGenerator = nil engine = nil + approveEngine = nil } - func setupEngine() { + func setupEngines() { let meta = AppMetadata.stub() let logger = ConsoleLoggerMock() engine = PairingEngine( networkingInteractor: networkingInteractor, kms: cryptoMock, pairingStore: storageMock, - sessionToPairingTopic: CodableStore(defaults: RuntimeKeyValueStorage(), identifier: ""), metadata: meta, logger: logger, - topicGenerator: topicGenerator.getTopic, - proposalPayloadsStore: proposalPayloadsStore) + topicGenerator: topicGenerator.getTopic + ) + approveEngine = ApproveEngine( + networkingInteractor: networkingInteractor, + proposalPayloadsStore: .init(defaults: RuntimeKeyValueStorage(), identifier: ""), + sessionToPairingTopic: CodableStore(defaults: RuntimeKeyValueStorage(), identifier: ""), + kms: cryptoMock, + logger: logger, + pairingStore: storageMock + ) } func testCreate() async { @@ -75,23 +84,6 @@ final class PairingEngineTests: XCTestCase { XCTAssertEqual(publishTopic, topicA) } - func testReceiveProposal() { - let pairing = WCPairing.stub() - let topicA = pairing.topic - storageMock.setPairing(pairing) - 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) - XCTAssertNotNil(try! proposalPayloadsStore.get(key: proposal.proposer.publicKey), "Proposer must store proposal payload") - XCTAssertTrue(sessionProposed) - } - func testHandleSessionProposeResponse() async { let uri = try! await engine.create() let pairing = storageMock.getPairing(forTopic: uri.topic)! @@ -120,10 +112,16 @@ final class PairingEngineTests: XCTestCase { var sessionTopic: String! - engine.onProposeResponse = { topic, _ in - sessionTopic = topic - } - networkingInteractor.onPairingResponse?(response) + approveEngine.approvePublisher.sink { response in + switch response { + case .proposeResponse(let topic, _): + sessionTopic = topic + default: + XCTFail() + } + }.store(in: &publishers) + + networkingInteractor.responsePublisherSubject.send(response) let privateKey = try! cryptoMock.getPrivateKey(for: proposal.proposer.publicKey)! let topicB = deriveTopic(publicKey: responder.publicKey, privateKey: privateKey) @@ -149,7 +147,7 @@ final class PairingEngineTests: XCTestCase { } let response = WCResponse.stubError(forRequest: request, topic: topicA) - networkingInteractor.onPairingResponse?(response) + networkingInteractor.responsePublisherSubject.send(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.") @@ -177,7 +175,7 @@ final class PairingEngineTests: XCTestCase { storageMock.setPairing(storedPairing) let response = WCResponse.stubError(forRequest: request, topic: topicA) - networkingInteractor.onPairingResponse?(response) + networkingInteractor.responsePublisherSubject.send(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.") diff --git a/Tests/WalletConnectSignTests/SessionEngineTests.swift b/Tests/WalletConnectSignTests/SessionEngineTests.swift index cabea8efa..006014838 100644 --- a/Tests/WalletConnectSignTests/SessionEngineTests.swift +++ b/Tests/WalletConnectSignTests/SessionEngineTests.swift @@ -95,7 +95,7 @@ final class SessionEngineTests: XCTestCase { requestMethod: .sessionSettle, requestParams: .sessionSettle(SessionType.SettleParams.stub()), result: .response(settleResponse)) - networkingInteractor.onResponse?(response) + networkingInteractor.responsePublisherSubject.send(response) XCTAssertTrue(storageMock.getSession(forTopic: session.topic)!.acknowledged, "Responder must acknowledged session") } @@ -113,7 +113,7 @@ final class SessionEngineTests: XCTestCase { requestMethod: .sessionSettle, requestParams: .sessionSettle(SessionType.SettleParams.stub()), result: .error(JSONRPCErrorResponse(id: 1, error: JSONRPCErrorResponse.Error(code: 0, message: "")))) - networkingInteractor.onResponse?(response) + networkingInteractor.responsePublisherSubject.send(response) XCTAssertNil(storageMock.getSession(forTopic: session.topic), "Responder must remove session") XCTAssertTrue(networkingInteractor.didUnsubscribe(to: session.topic), "Responder must unsubscribe topic B") From c8b308612d978d7e6471d3fed119bccb323a040c Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Fri, 3 Jun 2022 18:15:17 +0500 Subject: [PATCH 03/13] Private methods moved in extension --- .../Engine/Common/ApproveEngine.swift | 36 +++++++++++-------- .../Engine/Common/PairingEngine.swift | 11 +++--- .../Engine/Common/SessionEngine.swift | 22 ++++++------ Sources/WalletConnectSign/Sign/Sign.swift | 4 +-- .../WalletConnectSign/Sign/SignClient.swift | 4 +-- 5 files changed, 44 insertions(+), 33 deletions(-) diff --git a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift index b18014b8a..017a3e2f1 100644 --- a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift @@ -20,12 +20,12 @@ final class ApproveEngine { private var publishers = Set() - private let channel = PassthroughSubject() + private let approvePublisherSubject = PassthroughSubject() var approvePublisher: AnyPublisher { - channel.eraseToAnyPublisher() + approvePublisherSubject.eraseToAnyPublisher() } - + init( networkingInteractor: NetworkInteracting, proposalPayloadsStore: CodableStore, @@ -74,16 +74,21 @@ final class ApproveEngine { return (sessionTopic, proposal) } - func reject(proposal: SessionProposal, reason: ReasonCode) { - guard let payload = try? proposalPayloadsStore.get(key: proposal.proposer.publicKey) else { - return - } + func reject(proposal: SessionProposal, reason: ReasonCode) throws { + guard let payload = try proposalPayloadsStore.get(key: proposal.proposer.publicKey) + else { throw ApproveEngineError.proposalPayloadsNotFound } + proposalPayloadsStore.delete(forKey: proposal.proposer.publicKey) networkingInteractor.respondError(for: payload, reason: reason) -// todo - delete pairing if inactive + // TODO: Delete pairing if inactive } +} + +// MARK: - Privates + +private extension ApproveEngine { - private func setupNetworkingSubscriptions() { + func setupNetworkingSubscriptions() { networkingInteractor.responsePublisher .sink { [unowned self] response in self.handleResponse(response) @@ -100,7 +105,7 @@ final class ApproveEngine { }.store(in: &publishers) } - private func wcSessionPropose(_ payload: WCRequestSubscriptionPayload, proposal: SessionType.ProposeParams) { + func wcSessionPropose(_ payload: WCRequestSubscriptionPayload, proposal: SessionType.ProposeParams) { logger.debug("Received Session Proposal") do { try Namespace.validate(proposal.requiredNamespaces) @@ -109,10 +114,10 @@ final class ApproveEngine { return } proposalPayloadsStore.set(payload, forKey: proposal.proposer.publicKey) - channel.send(.sessionProposal(proposal.publicRepresentation())) + approvePublisherSubject.send(.sessionProposal(proposal.publicRepresentation())) } - private func handleResponse(_ response: WCResponse) { + func handleResponse(_ response: WCResponse) { switch response.requestParams { case .sessionPropose(let proposal): handleProposeResponse(pairingTopic: response.topic, proposal: proposal, result: response.result) @@ -121,7 +126,7 @@ final class ApproveEngine { } } - private func handleProposeResponse(pairingTopic: String, proposal: SessionProposal, result: JsonRpcResult) { + func handleProposeResponse(pairingTopic: String, proposal: SessionProposal, result: JsonRpcResult) { guard var pairing = pairingStore.getPairing(forTopic: pairingTopic) else { return } @@ -154,7 +159,7 @@ final class ApproveEngine { try? kms.setAgreementSecret(agreementKeys, topic: sessionTopic) sessionToPairingTopic.set(pairingTopic, forKey: sessionTopic) - channel.send(.proposeResponse(topic: sessionTopic, proposal: proposal)) + approvePublisherSubject.send(.proposeResponse(topic: sessionTopic, proposal: proposal)) case .error(let error): if !pairing.active { @@ -165,7 +170,7 @@ final class ApproveEngine { logger.debug("Session Proposal has been rejected") kms.deletePrivateKey(for: proposal.proposer.publicKey) - channel.send(.sessionRejected( + approvePublisherSubject.send(.sessionRejected( proposal: proposal.publicRepresentation(), reason: SessionType.Reason(code: error.error.code, message: error.error.message) )) @@ -176,5 +181,6 @@ final class ApproveEngine { enum ApproveEngineError: Error { case wrongRequestParams case relayNotFound + case proposalPayloadsNotFound case agreementMissingOrInvalid } diff --git a/Sources/WalletConnectSign/Engine/Common/PairingEngine.swift b/Sources/WalletConnectSign/Engine/Common/PairingEngine.swift index abd00d19a..387b17e70 100644 --- a/Sources/WalletConnectSign/Engine/Common/PairingEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/PairingEngine.swift @@ -95,10 +95,13 @@ final class PairingEngine { } } } +} - //MARK: - Private +// MARK: Private - private func setupNetworkingSubscriptions() { +private extension PairingEngine { + + func setupNetworkingSubscriptions() { networkingInteractor.transportConnectionPublisher .sink { [unowned self] (_) in let topics = pairingStore.getAll() @@ -117,11 +120,11 @@ final class PairingEngine { }.store(in: &publishers) } - private func wcPairingPing(_ payload: WCRequestSubscriptionPayload) { + func wcPairingPing(_ payload: WCRequestSubscriptionPayload) { networkingInteractor.respondSuccess(for: payload) } - private func setupExpirationHandling() { + 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 86d6c93fa..516c636e9 100644 --- a/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift @@ -145,10 +145,12 @@ final class SessionEngine { let params = SessionType.EventParams(event: event, chainId: chainId) try await networkingInteractor.request(.wcSessionEvent(params), onTopic: topic) } +} +//MARK: - Privates - //MARK: - Private +private extension SessionEngine { - private func setupNetworkingSubscriptions() { + func setupNetworkingSubscriptions() { networkingInteractor.wcRequestPublisher.sink { [unowned self] subscriptionPayload in switch subscriptionPayload.wcRequest.params { case .sessionSettle(let settleParams): @@ -178,7 +180,7 @@ final class SessionEngine { }.store(in: &publishers) } - private func onSessionSettle(payload: WCRequestSubscriptionPayload, settleParams: SessionType.SettleParams) { + func onSessionSettle(payload: WCRequestSubscriptionPayload, settleParams: SessionType.SettleParams) { logger.debug("Did receive session settle request") guard let proposedNamespaces = settlingProposal?.requiredNamespaces else { // TODO: respond error @@ -216,7 +218,7 @@ final class SessionEngine { onSessionSettle?(session.publicRepresentation()) } - private func onSessionDelete(_ payload: WCRequestSubscriptionPayload, deleteParams: SessionType.DeleteParams) { + func onSessionDelete(_ payload: WCRequestSubscriptionPayload, deleteParams: SessionType.DeleteParams) { let topic = payload.topic guard sessionStore.hasSession(forTopic: topic) else { networkingInteractor.respondError(for: payload, reason: .noContextWithTopic(context: .session, topic: topic)) @@ -228,7 +230,7 @@ final class SessionEngine { onSessionDelete?(topic, deleteParams) } - private func onSessionRequest(_ payload: WCRequestSubscriptionPayload, payloadParams: SessionType.RequestParams) { + func onSessionRequest(_ payload: WCRequestSubscriptionPayload, payloadParams: SessionType.RequestParams) { let topic = payload.topic let jsonRpcRequest = JSONRPCRequest(id: payload.wcRequest.id, method: payloadParams.request.method, params: payloadParams.request.params) let request = Request( @@ -254,11 +256,11 @@ final class SessionEngine { onSessionRequest?(request) } - private func onSessionPing(_ payload: WCRequestSubscriptionPayload) { + func onSessionPing(_ payload: WCRequestSubscriptionPayload) { networkingInteractor.respondSuccess(for: payload) } - private func onSessionEvent(_ payload: WCRequestSubscriptionPayload, eventParams: SessionType.EventParams) { + func onSessionEvent(_ payload: WCRequestSubscriptionPayload, eventParams: SessionType.EventParams) { let event = eventParams.event let topic = payload.topic guard let session = sessionStore.getSession(forTopic: topic) else { @@ -276,14 +278,14 @@ final class SessionEngine { onEventReceived?(topic, event.publicRepresentation(), eventParams.chainId) } - private func setupExpirationSubscriptions() { + func setupExpirationSubscriptions() { sessionStore.onSessionExpiration = { [weak self] session in self?.kms.deletePrivateKey(for: session.selfParticipant.publicKey) self?.kms.deleteAgreementSecret(for: session.topic) } } - private func handleResponse(_ response: WCResponse) { + func handleResponse(_ response: WCResponse) { switch response.requestParams { case .sessionSettle: handleSessionSettleResponse(topic: response.topic, result: response.result) @@ -312,7 +314,7 @@ final class SessionEngine { } } - private func updatePairingMetadata(topic: String, metadata: AppMetadata) { + 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/WalletConnectSign/Sign/Sign.swift b/Sources/WalletConnectSign/Sign/Sign.swift index 29b980dac..af8ba3eae 100644 --- a/Sources/WalletConnectSign/Sign/Sign.swift +++ b/Sources/WalletConnectSign/Sign/Sign.swift @@ -157,8 +157,8 @@ extension Sign { /// - Parameters: /// - proposal: Session Proposal received from peer client in a WalletConnect delegate. /// - reason: Reason why the session proposal was rejected. Conforms to CAIP25. - public func reject(proposal: Session.Proposal, reason: RejectionReason) { - client.reject(proposal: proposal, reason: reason) + public func reject(proposal: Session.Proposal, reason: RejectionReason) throws { + try client.reject(proposal: proposal, reason: reason) } /// For the responder to update session methods diff --git a/Sources/WalletConnectSign/Sign/SignClient.swift b/Sources/WalletConnectSign/Sign/SignClient.swift index b2b101f1c..eafe63953 100644 --- a/Sources/WalletConnectSign/Sign/SignClient.swift +++ b/Sources/WalletConnectSign/Sign/SignClient.swift @@ -170,8 +170,8 @@ public final class SignClient { /// - Parameters: /// - proposal: Session Proposal received from peer client in a WalletConnect delegate. /// - reason: Reason why the session proposal was rejected. Conforms to CAIP25. - public func reject(proposal: Session.Proposal, reason: RejectionReason) { - approveEngine.reject(proposal: proposal.proposal, reason: reason.internalRepresentation()) + public func reject(proposal: Session.Proposal, reason: RejectionReason) throws { + try approveEngine.reject(proposal: proposal.proposal, reason: reason.internalRepresentation()) } /// For the responder to update session namespaces From 2fc679123d0fe42692409e99911712df48818a1b Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Fri, 3 Jun 2022 18:48:02 +0500 Subject: [PATCH 04/13] ApproveEngine errors handlers --- .../Engine/Common/ApproveEngine.swift | 83 +++++++++---------- 1 file changed, 41 insertions(+), 42 deletions(-) diff --git a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift index 017a3e2f1..45527ceb6 100644 --- a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift @@ -96,71 +96,73 @@ private extension ApproveEngine { networkingInteractor.wcRequestPublisher .sink { [unowned self] subscriptionPayload in - switch subscriptionPayload.wcRequest.params { - case .sessionPropose(let proposeParams): - wcSessionPropose(subscriptionPayload, proposal: proposeParams) - default: - return - } + guard case .sessionPropose(let proposal) = subscriptionPayload.wcRequest.params else { return } + handleSessionPropose(subscriptionPayload, proposal: proposal) }.store(in: &publishers) } - func wcSessionPropose(_ payload: WCRequestSubscriptionPayload, proposal: SessionType.ProposeParams) { - logger.debug("Received Session Proposal") + func handleSessionPropose(_ payload: WCRequestSubscriptionPayload, proposal: SessionType.ProposeParams) { do { + logger.debug("Received Session Proposal") try Namespace.validate(proposal.requiredNamespaces) - } catch { - // TODO: respond error - return + proposalPayloadsStore.set(payload, forKey: proposal.proposer.publicKey) + approvePublisherSubject.send(.sessionProposal(proposal.publicRepresentation())) + } + catch { + // TODO: Return reasons with 6000 code Issue: #253 + networkingInteractor.respondError(for: payload, reason: .invalidUpdateNamespaceRequest) } - proposalPayloadsStore.set(payload, forKey: proposal.proposer.publicKey) - approvePublisherSubject.send(.sessionProposal(proposal.publicRepresentation())) } func handleResponse(_ response: WCResponse) { - switch response.requestParams { - case .sessionPropose(let proposal): - handleProposeResponse(pairingTopic: response.topic, proposal: proposal, result: response.result) - default: - break + guard case .sessionPropose(let proposal) = response.requestParams else { return } + + do { + let sessionTopic = try handleProposeResponse( + pairingTopic: response.topic, + proposal: proposal, + result: response.result + ) + approvePublisherSubject.send(.proposeResponse(topic: sessionTopic, proposal: proposal)) + } + catch { + guard let error = error as? JSONRPCErrorResponse else { + return logger.debug(error.localizedDescription) + } + approvePublisherSubject.send(.sessionRejected( + proposal: proposal.publicRepresentation(), + reason: SessionType.Reason(code: error.error.code, message: error.error.message) + )) } } - func handleProposeResponse(pairingTopic: String, proposal: SessionProposal, result: JsonRpcResult) { - guard var pairing = pairingStore.getPairing(forTopic: pairingTopic) else { - return - } + func handleProposeResponse(pairingTopic: String, proposal: SessionProposal, result: JsonRpcResult) throws -> String { + guard var pairing = pairingStore.getPairing(forTopic: pairingTopic) + else { throw ApproveEngineError.pairingNotFound } + switch result { case .response(let response): - // Activate the pairing if !pairing.active { pairing.activate() } else { - try? pairing.updateExpiry() + try pairing.updateExpiry() } pairingStore.setPairing(pairing) - let selfPublicKey = try! AgreementPublicKey(hex: proposal.proposer.publicKey) - var agreementKeys: AgreementKeys! - - do { - let proposeResponse = try response.result.get(SessionType.ProposeResponse.self) - agreementKeys = try kms.performKeyAgreement(selfPublicKey: selfPublicKey, peerPublicKey: proposeResponse.responderPublicKey) - } catch { - //TODO - handle error - logger.debug(error) - return - } + 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 sessionTopic = agreementKeys.derivedTopic() logger.debug("Received Session Proposal response") - try? kms.setAgreementSecret(agreementKeys, topic: sessionTopic) + try kms.setAgreementSecret(agreementKeys, topic: sessionTopic) sessionToPairingTopic.set(pairingTopic, forKey: sessionTopic) - approvePublisherSubject.send(.proposeResponse(topic: sessionTopic, proposal: proposal)) + return sessionTopic + case .error(let error): if !pairing.active { kms.deleteSymmetricKey(for: pairing.topic) @@ -169,11 +171,7 @@ private extension ApproveEngine { } logger.debug("Session Proposal has been rejected") kms.deletePrivateKey(for: proposal.proposer.publicKey) - - approvePublisherSubject.send(.sessionRejected( - proposal: proposal.publicRepresentation(), - reason: SessionType.Reason(code: error.error.code, message: error.error.message) - )) + throw error } } } @@ -182,5 +180,6 @@ enum ApproveEngineError: Error { case wrongRequestParams case relayNotFound case proposalPayloadsNotFound + case pairingNotFound case agreementMissingOrInvalid } From d70ed7e6864e7d681fa0bbbfb3a959a148eaa54b Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Fri, 3 Jun 2022 18:54:46 +0500 Subject: [PATCH 05/13] Try on reject --- Example/ExampleApp/Wallet/WalletViewController.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Example/ExampleApp/Wallet/WalletViewController.swift b/Example/ExampleApp/Wallet/WalletViewController.swift index 59fe0fe04..d83df0661 100644 --- a/Example/ExampleApp/Wallet/WalletViewController.swift +++ b/Example/ExampleApp/Wallet/WalletViewController.swift @@ -168,7 +168,11 @@ extension WalletViewController: ProposalViewControllerDelegate { print("did reject session") let proposal = currentProposal! currentProposal = nil - Sign.instance.reject(proposal: proposal, reason: .disapprovedChains) + do { + try Sign.instance.reject(proposal: proposal, reason: .disapprovedChains) + } catch { + print("Session rejection error: \(error.localizedDescription)") + } } } From e4d8fef2ef387511724e1a4c7073510ef89a8791 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Mon, 6 Jun 2022 17:21:14 +0500 Subject: [PATCH 06/13] ApproveEngine moved to callbacks --- .../Engine/Common/ApproveEngine.swift | 181 +++++++++++++----- .../Engine/Common/SessionEngine.swift | 93 ++------- .../WalletConnectSign/Sign/SignClient.swift | 34 ++-- .../ApproveEngineTests.swift | 78 ++++++-- .../PairingEngineTests.swift | 15 +- .../SessionEngineTests.swift | 54 +----- 6 files changed, 236 insertions(+), 219 deletions(-) diff --git a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift index 45527ceb6..8bae718ea 100644 --- a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift @@ -4,52 +4,66 @@ import WalletConnectUtils import WalletConnectKMS final class ApproveEngine { - - enum Response { - case proposeResponse(topic: String, proposal: SessionProposal) - case sessionProposal(Session.Proposal) - case sessionRejected(proposal: Session.Proposal, reason: SessionType.Reason) - } + enum Error: String, Swift.Error { + case wrongRequestParams + case relayNotFound + case proposalPayloadsNotFound + case pairingNotFound + case agreementMissingOrInvalid + } + + typealias ProposeResponseCallback = (String, SessionProposal) -> Void + typealias SessionProposalCallback = (Session.Proposal) -> Void + typealias SessionRejectedCallback = (Session.Proposal, SessionType.Reason) -> Void + typealias SessionSettleCallback = (Session) -> Void + + var onProposeResponse: ProposeResponseCallback? + var onSessionProposal: SessionProposalCallback? + var onSessionRejected: SessionRejectedCallback? + var onSessionSettle: SessionSettleCallback? + + var settlingProposal: SessionProposal? + private let networkingInteractor: NetworkInteracting private let pairingStore: WCPairingStorage + private let sessionStore: WCSessionStorage private let proposalPayloadsStore: CodableStore private let sessionToPairingTopic: CodableStore + private let metadata: AppMetadata private let kms: KeyManagementServiceProtocol private let logger: ConsoleLogging private var publishers = Set() - - private let approvePublisherSubject = PassthroughSubject() - - var approvePublisher: AnyPublisher { - approvePublisherSubject.eraseToAnyPublisher() - } init( networkingInteractor: NetworkInteracting, proposalPayloadsStore: CodableStore, sessionToPairingTopic: CodableStore, + metadata: AppMetadata, kms: KeyManagementServiceProtocol, logger: ConsoleLogging, - pairingStore: WCPairingStorage + pairingStore: WCPairingStorage, + sessionStore: WCSessionStorage ) { self.networkingInteractor = networkingInteractor self.proposalPayloadsStore = proposalPayloadsStore self.sessionToPairingTopic = sessionToPairingTopic + self.metadata = metadata self.kms = kms self.logger = logger self.pairingStore = pairingStore + self.sessionStore = sessionStore setupNetworkingSubscriptions() } func approveProposal(proposerPubKey: String, validating sessionNamespaces: [String: SessionNamespace]) throws -> (String, SessionProposal) { - let payload = try proposalPayloadsStore.get(key: proposerPubKey) - - guard let payload = payload, case .sessionPropose(let proposal) = payload.wcRequest.params - else { throw ApproveEngineError.wrongRequestParams } + + guard let payload = payload, case .sessionPropose(let proposal) = payload.wcRequest.params else { + throw Error.wrongRequestParams + } proposalPayloadsStore.delete(forKey: proposerPubKey) @@ -57,16 +71,20 @@ final class ApproveEngine { try Namespace.validateApproved(sessionNamespaces, against: proposal.requiredNamespaces) let selfPublicKey = try kms.createX25519KeyPair() - let agreementKey = try? kms.performKeyAgreement(selfPublicKey: selfPublicKey, peerPublicKey: proposal.proposer.publicKey) - guard let agreementKey = agreementKey else { - networkingInteractor.respondError(for: payload, reason: .missingOrInvalid("agreement keys")) - throw ApproveEngineError.agreementMissingOrInvalid - } + + guard let agreementKey = try? kms.performKeyAgreement( + selfPublicKey: selfPublicKey, + peerPublicKey: proposal.proposer.publicKey + ) else { throw Error.agreementMissingOrInvalid } + // TODO: Extend pairing let sessionTopic = agreementKey.derivedTopic() try kms.setAgreementSecret(agreementKey, topic: sessionTopic) - guard let relay = proposal.relays.first else { throw ApproveEngineError.relayNotFound } + guard let relay = proposal.relays.first else { + throw Error.relayNotFound + } + let proposeResponse = SessionType.ProposeResponse(relay: relay, responderPublicKey: selfPublicKey.hexRepresentation) let response = JSONRPCResponse(id: payload.wcRequest.id, result: AnyCodable(proposeResponse)) networkingInteractor.respond(topic: payload.topic, response: .response(response)) { _ in } @@ -75,13 +93,50 @@ final class ApproveEngine { } func reject(proposal: SessionProposal, reason: ReasonCode) throws { - guard let payload = try proposalPayloadsStore.get(key: proposal.proposer.publicKey) - else { throw ApproveEngineError.proposalPayloadsNotFound } - + guard let payload = try proposalPayloadsStore.get(key: proposal.proposer.publicKey) else { + throw Error.proposalPayloadsNotFound + } proposalPayloadsStore.delete(forKey: proposal.proposer.publicKey) networkingInteractor.respondError(for: payload, reason: reason) // TODO: Delete pairing if inactive } + + func settle(topic: String, proposal: SessionProposal, namespaces: [String: SessionNamespace]) throws { + guard let agreementKeys = try kms.getAgreementSecret(for: topic) else { + throw Error.agreementMissingOrInvalid + } + let selfParticipant = Participant( + publicKey: agreementKeys.publicKey.hexRepresentation, + metadata: metadata + ) + let expectedExpiryTimeStamp = Date().addingTimeInterval(TimeInterval(WCSession.defaultTimeToLive)) + + guard let relay = proposal.relays.first else { + throw Error.relayNotFound + } + + // TODO: Test expiration times + let settleParams = SessionType.SettleParams( + relay: relay, + controller: selfParticipant, + namespaces: namespaces, + expiry: Int64(expectedExpiryTimeStamp.timeIntervalSince1970) + ) + let session = WCSession( + topic: topic, + selfParticipant: selfParticipant, + peerParticipant: proposal.proposer, + settleParams: settleParams, + acknowledged: false + ) + + logger.debug("Sending session settle request") + + Task { try? await networkingInteractor.subscribe(topic: topic) } + sessionStore.setSession(session) + networkingInteractor.request(.wcSessionSettle(settleParams), onTopic: topic) + onSessionSettle?(session.publicRepresentation()) + } } // MARK: - Privates @@ -96,8 +151,14 @@ private extension ApproveEngine { networkingInteractor.wcRequestPublisher .sink { [unowned self] subscriptionPayload in - guard case .sessionPropose(let proposal) = subscriptionPayload.wcRequest.params else { return } - handleSessionPropose(subscriptionPayload, proposal: proposal) + switch subscriptionPayload.wcRequest.params { + case .sessionPropose(let proposal): + handleSessionPropose(subscriptionPayload, proposal: proposal) + case .sessionSettle(let settleParams): + handleSessionSettle(payload: subscriptionPayload, settleParams: settleParams) + default: + return + } }.store(in: &publishers) } @@ -106,7 +167,7 @@ private extension ApproveEngine { logger.debug("Received Session Proposal") try Namespace.validate(proposal.requiredNamespaces) proposalPayloadsStore.set(payload, forKey: proposal.proposer.publicKey) - approvePublisherSubject.send(.sessionProposal(proposal.publicRepresentation())) + onSessionProposal?(proposal.publicRepresentation()) } catch { // TODO: Return reasons with 6000 code Issue: #253 @@ -123,22 +184,20 @@ private extension ApproveEngine { proposal: proposal, result: response.result ) - approvePublisherSubject.send(.proposeResponse(topic: sessionTopic, proposal: proposal)) + settlingProposal = proposal + onProposeResponse?(sessionTopic, proposal) } catch { guard let error = error as? JSONRPCErrorResponse else { return logger.debug(error.localizedDescription) } - approvePublisherSubject.send(.sessionRejected( - proposal: proposal.publicRepresentation(), - reason: SessionType.Reason(code: error.error.code, message: error.error.message) - )) + 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 ApproveEngineError.pairingNotFound } + else { throw Error.pairingNotFound } switch result { case .response(let response): @@ -174,12 +233,48 @@ private extension ApproveEngine { throw error } } -} - -enum ApproveEngineError: Error { - case wrongRequestParams - case relayNotFound - case proposalPayloadsNotFound - case pairingNotFound - case agreementMissingOrInvalid + + func handleSessionSettle(payload: WCRequestSubscriptionPayload, settleParams: SessionType.SettleParams) { + logger.debug("Did receive session settle request") + guard let proposedNamespaces = settlingProposal?.requiredNamespaces else { + // TODO: respond error + return + } + settlingProposal = nil + let sessionNamespaces = settleParams.namespaces + do { + try Namespace.validate(proposedNamespaces) + try Namespace.validate(sessionNamespaces) + try Namespace.validateApproved(sessionNamespaces, against: proposedNamespaces) + } catch { + // TODO: respond error + return + } + + let topic = payload.topic + + let agreementKeys = try! kms.getAgreementSecret(for: topic)! + + let selfParticipant = Participant(publicKey: agreementKeys.publicKey.hexRepresentation, metadata: metadata) + + if let pairingTopic = try? sessionToPairingTopic.get(key: topic) { + updatePairingMetadata(topic: pairingTopic, metadata: settleParams.controller.metadata) + } + + let session = WCSession( + topic: topic, + selfParticipant: selfParticipant, + peerParticipant: settleParams.controller, + settleParams: settleParams, + acknowledged: true) + sessionStore.setSession(session) + networkingInteractor.respondSuccess(for: payload) + onSessionSettle?(session.publicRepresentation()) + } + + 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/WalletConnectSign/Engine/Common/SessionEngine.swift b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift index 516c636e9..24c4a2859 100644 --- a/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift @@ -5,21 +5,24 @@ import WalletConnectKMS final class SessionEngine { - var onSessionRequest: ((Request)->())? - var onSessionResponse: ((Response)->())? - var onSessionSettle: ((Session)->())? - var onSessionRejected: ((String, SessionType.Reason)->())? - var onSessionDelete: ((String, SessionType.Reason)->())? - var onEventReceived: ((String, Session.Event, Blockchain?)->())? - - var settlingProposal: SessionProposal? + typealias SessionRequestCallback = (Request) -> Void + typealias SessionResponseCallback = (Response) -> Void + typealias SessionRejectedCallback = (String, SessionType.Reason) -> Void + typealias SessionDeleteCallback = (String, SessionType.Reason) -> Void + typealias EventReceivedCallback = (String, Session.Event, Blockchain?) -> Void + + var onSessionRequest: SessionRequestCallback? + var onSessionResponse: SessionResponseCallback? + var onSessionRejected: SessionRejectedCallback? + var onSessionDelete: SessionDeleteCallback? + var onEventReceived: EventReceivedCallback? + private let sessionStore: WCSessionStorage private let pairingStore: WCPairingStorage private let sessionToPairingTopic: CodableStore private let networkingInteractor: NetworkInteracting private let kms: KeyManagementServiceProtocol - private var metadata: AppMetadata private var publishers = [AnyCancellable]() private let logger: ConsoleLogging private let topicInitializer: () -> String @@ -30,13 +33,11 @@ final class SessionEngine { pairingStore: WCPairingStorage, sessionStore: WCSessionStorage, sessionToPairingTopic: CodableStore, - metadata: AppMetadata, logger: ConsoleLogging, topicGenerator: @escaping () -> String = String.generateTopic ) { self.networkingInteractor = networkingInteractor self.kms = kms - self.metadata = metadata self.sessionStore = sessionStore self.pairingStore = pairingStore self.sessionToPairingTopic = sessionToPairingTopic @@ -59,30 +60,6 @@ final class SessionEngine { sessionStore.getAll().map{$0.publicRepresentation()} } - func settle(topic: String, proposal: SessionProposal, namespaces: [String: SessionNamespace]) throws { - let agreementKeys = try! kms.getAgreementSecret(for: topic)! - let selfParticipant = Participant(publicKey: agreementKeys.publicKey.hexRepresentation, metadata: metadata) - - let expectedExpiryTimeStamp = Date().addingTimeInterval(TimeInterval(WCSession.defaultTimeToLive)) - guard let relay = proposal.relays.first else {return} - let settleParams = SessionType.SettleParams( - relay: relay, - controller: selfParticipant, - namespaces: namespaces, - expiry: Int64(expectedExpiryTimeStamp.timeIntervalSince1970))//todo - test expiration times - let session = WCSession( - topic: topic, - selfParticipant: selfParticipant, - peerParticipant: proposal.proposer, - settleParams: settleParams, - acknowledged: false) - logger.debug("Sending session settle request") - Task { try? await networkingInteractor.subscribe(topic: topic) } - sessionStore.setSession(session) - networkingInteractor.request(.wcSessionSettle(settleParams), onTopic: topic) - onSessionSettle?(session.publicRepresentation()) - } - func delete(topic: String, reason: Reason) async throws { logger.debug("Will delete session for reason: message: \(reason.message) code: \(reason.code)") try await networkingInteractor.request(.wcSessionDelete(reason.internalRepresentation()), onTopic: topic) @@ -153,8 +130,6 @@ private extension SessionEngine { func setupNetworkingSubscriptions() { networkingInteractor.wcRequestPublisher.sink { [unowned self] subscriptionPayload in switch subscriptionPayload.wcRequest.params { - case .sessionSettle(let settleParams): - onSessionSettle(payload: subscriptionPayload, settleParams: settleParams) case .sessionDelete(let deleteParams): onSessionDelete(subscriptionPayload, deleteParams: deleteParams) case .sessionRequest(let sessionRequestParams): @@ -180,44 +155,6 @@ private extension SessionEngine { }.store(in: &publishers) } - func onSessionSettle(payload: WCRequestSubscriptionPayload, settleParams: SessionType.SettleParams) { - logger.debug("Did receive session settle request") - guard let proposedNamespaces = settlingProposal?.requiredNamespaces else { - // TODO: respond error - return - } - settlingProposal = nil - let sessionNamespaces = settleParams.namespaces - do { - try Namespace.validate(proposedNamespaces) - try Namespace.validate(sessionNamespaces) - try Namespace.validateApproved(sessionNamespaces, against: proposedNamespaces) - } catch { - // TODO: respond error - return - } - - let topic = payload.topic - - let agreementKeys = try! kms.getAgreementSecret(for: topic)! - - let selfParticipant = Participant(publicKey: agreementKeys.publicKey.hexRepresentation, metadata: metadata) - - if let pairingTopic = try? sessionToPairingTopic.get(key: topic) { - updatePairingMetadata(topic: pairingTopic, metadata: settleParams.controller.metadata) - } - - let session = WCSession( - topic: topic, - selfParticipant: selfParticipant, - peerParticipant: settleParams.controller, - settleParams: settleParams, - acknowledged: true) - sessionStore.setSession(session) - networkingInteractor.respondSuccess(for: payload) - onSessionSettle?(session.publicRepresentation()) - } - func onSessionDelete(_ payload: WCRequestSubscriptionPayload, deleteParams: SessionType.DeleteParams) { let topic = payload.topic guard sessionStore.hasSession(forTopic: topic) else { @@ -313,10 +250,4 @@ private extension SessionEngine { kms.deletePrivateKey(for: session.publicKey!) } } - - 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/WalletConnectSign/Sign/SignClient.swift b/Sources/WalletConnectSign/Sign/SignClient.swift index eafe63953..0eb65c967 100644 --- a/Sources/WalletConnectSign/Sign/SignClient.swift +++ b/Sources/WalletConnectSign/Sign/SignClient.swift @@ -64,11 +64,11 @@ public final class SignClient { let sessionToPairingTopic = CodableStore(defaults: RuntimeKeyValueStorage(), identifier: StorageDomainIdentifiers.sessionToPairingTopic.rawValue) let proposalPayloadsStore = CodableStore(defaults: RuntimeKeyValueStorage(), identifier: StorageDomainIdentifiers.proposals.rawValue) self.pairingEngine = PairingEngine(networkingInteractor: networkingInteractor, kms: kms, pairingStore: pairingStore, metadata: metadata, logger: logger) - self.sessionEngine = SessionEngine(networkingInteractor: networkingInteractor, kms: kms, pairingStore: pairingStore, sessionStore: sessionStore, sessionToPairingTopic: sessionToPairingTopic, metadata: metadata, logger: logger) + self.sessionEngine = SessionEngine(networkingInteractor: networkingInteractor, kms: kms, pairingStore: pairingStore, sessionStore: sessionStore, sessionToPairingTopic: sessionToPairingTopic, logger: logger) self.nonControllerSessionStateMachine = NonControllerSessionStateMachine(networkingInteractor: networkingInteractor, kms: kms, sessionStore: sessionStore, logger: logger) self.controllerSessionStateMachine = ControllerSessionStateMachine(networkingInteractor: networkingInteractor, kms: kms, sessionStore: sessionStore, logger: logger) self.pairEngine = PairEngine(networkingInteractor: networkingInteractor, kms: kms, pairingStore: pairingStore) - self.approveEngine = ApproveEngine(networkingInteractor: networkingInteractor, proposalPayloadsStore: proposalPayloadsStore, sessionToPairingTopic: sessionToPairingTopic, kms: kms, logger: logger, pairingStore: pairingStore) + self.approveEngine = ApproveEngine(networkingInteractor: networkingInteractor, proposalPayloadsStore: proposalPayloadsStore, sessionToPairingTopic: sessionToPairingTopic, metadata: metadata, kms: kms, logger: logger, pairingStore: pairingStore, sessionStore: sessionStore) self.cleanupService = CleanupService(pairingStore: pairingStore, sessionStore: sessionStore, kms: kms, sessionToPairingTopic: sessionToPairingTopic) setUpConnectionObserving(relayClient: relayClient) setUpEnginesCallbacks() @@ -98,8 +98,8 @@ public final class SignClient { let sessionToPairingTopic = CodableStore(defaults: RuntimeKeyValueStorage(), identifier: StorageDomainIdentifiers.sessionToPairingTopic.rawValue) let proposalPayloadsStore = CodableStore(defaults: RuntimeKeyValueStorage(), identifier: StorageDomainIdentifiers.proposals.rawValue) self.pairingEngine = PairingEngine(networkingInteractor: networkingInteractor, kms: kms, pairingStore: pairingStore, metadata: metadata, logger: logger) - self.sessionEngine = SessionEngine(networkingInteractor: networkingInteractor, kms: kms, pairingStore: pairingStore, sessionStore: sessionStore, sessionToPairingTopic: sessionToPairingTopic, metadata: metadata, logger: logger) - self.approveEngine = ApproveEngine(networkingInteractor: networkingInteractor, proposalPayloadsStore: proposalPayloadsStore, sessionToPairingTopic: sessionToPairingTopic, kms: kms, logger: logger, pairingStore: pairingStore) + self.sessionEngine = SessionEngine(networkingInteractor: networkingInteractor, kms: kms, pairingStore: pairingStore, sessionStore: sessionStore, sessionToPairingTopic: sessionToPairingTopic, logger: logger) + self.approveEngine = ApproveEngine(networkingInteractor: networkingInteractor, proposalPayloadsStore: proposalPayloadsStore, sessionToPairingTopic: sessionToPairingTopic, metadata: metadata, kms: kms, logger: logger, pairingStore: pairingStore, sessionStore: sessionStore) self.nonControllerSessionStateMachine = NonControllerSessionStateMachine(networkingInteractor: networkingInteractor, kms: kms, sessionStore: sessionStore, logger: logger) self.controllerSessionStateMachine = ControllerSessionStateMachine(networkingInteractor: networkingInteractor, kms: kms, sessionStore: sessionStore, logger: logger) self.pairEngine = PairEngine(networkingInteractor: networkingInteractor, kms: kms, pairingStore: pairingStore) @@ -163,7 +163,7 @@ public final class SignClient { ) throws { //TODO - accounts should be validated for matching namespaces BEFORE responding proposal let (sessionTopic, proposal) = try approveEngine.approveProposal(proposerPubKey: proposalId, validating: namespaces) - try sessionEngine.settle(topic: sessionTopic, proposal: proposal, namespaces: namespaces) + try approveEngine.settle(topic: sessionTopic, proposal: proposal, namespaces: namespaces) } /// For the responder to reject a session proposal. @@ -296,22 +296,18 @@ public final class SignClient { // MARK: - Private private func setUpEnginesCallbacks() { - approveEngine.approvePublisher.sink { [unowned self] response in - switch response { - case .proposeResponse(let topic, let proposal): - self.sessionEngine.settlingProposal = proposal - self.sessionEngine.setSubscription(topic: topic) - case .sessionProposal(let proposal): - self.delegate?.didReceive(sessionProposal: proposal) - case .sessionRejected(proposal: let proposal, reason: let reason): - self.delegate?.didReject(proposal: proposal, reason: reason.publicRepresentation()) - } - }.store(in: &publishers) - - sessionEngine.onSessionSettle = { [unowned self] settledSession in + approveEngine.onProposeResponse = { [unowned self] topic, proposal in + sessionEngine.setSubscription(topic: topic) + } + approveEngine.onSessionProposal = { [unowned self] proposal in + delegate?.didReceive(sessionProposal: proposal) + } + approveEngine.onSessionRejected = { [unowned self] proposal, reason in + delegate?.didReject(proposal: proposal, reason: reason.publicRepresentation()) + } + approveEngine.onSessionSettle = { [unowned self] settledSession in delegate?.didSettle(session: settledSession) } - sessionEngine.onSessionRequest = { [unowned self] sessionRequest in delegate?.didReceive(sessionRequest: sessionRequest) } diff --git a/Tests/WalletConnectSignTests/ApproveEngineTests.swift b/Tests/WalletConnectSignTests/ApproveEngineTests.swift index 18de52551..1f6d50632 100644 --- a/Tests/WalletConnectSignTests/ApproveEngineTests.swift +++ b/Tests/WalletConnectSignTests/ApproveEngineTests.swift @@ -8,32 +8,39 @@ import WalletConnectUtils final class ApproveEngineTests: XCTestCase { var engine: ApproveEngine! + var metadata: AppMetadata! var networkingInteractor: MockedWCRelay! var cryptoMock: KeyManagementServiceMock! - var storageMock: WCPairingStorageMock! + var pairingStorageMock: WCPairingStorageMock! + var sessionStorageMock: WCSessionStorageMock! var proposalPayloadsStore: CodableStore! var publishers = Set() override func setUp() { + metadata = AppMetadata.stub() networkingInteractor = MockedWCRelay() cryptoMock = KeyManagementServiceMock() - storageMock = WCPairingStorageMock() + pairingStorageMock = WCPairingStorageMock() + sessionStorageMock = WCSessionStorageMock() proposalPayloadsStore = CodableStore(defaults: RuntimeKeyValueStorage(), identifier: "") engine = ApproveEngine( networkingInteractor: networkingInteractor, proposalPayloadsStore: proposalPayloadsStore, sessionToPairingTopic: CodableStore(defaults: RuntimeKeyValueStorage(), identifier: ""), + metadata: metadata, kms: cryptoMock, logger: ConsoleLoggerMock(), - pairingStore: storageMock + pairingStore: pairingStorageMock, + sessionStore: sessionStorageMock ) } override func tearDown() { networkingInteractor = nil + metadata = nil cryptoMock = nil - storageMock = nil + pairingStorageMock = nil engine = nil } @@ -52,28 +59,71 @@ final class ApproveEngineTests: XCTestCase { XCTAssertEqual(networkingInteractor.didRespondOnTopic!, topicA, "Responder must respond on topic A") } - func testReceiveProposal() { let pairing = WCPairing.stub() let topicA = pairing.topic - storageMock.setPairing(pairing) + pairingStorageMock.setPairing(pairing) 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.approvePublisher.sink { response in - switch response { - case .sessionProposal: - sessionProposed = true - default: - break - } - }.store(in: &publishers) + engine.onSessionProposal = { _ in + sessionProposed = true + } networkingInteractor.wcRequestPublisherSubject.send(payload) XCTAssertNotNil(try! proposalPayloadsStore.get(key: proposal.proposer.publicKey), "Proposer must store proposal payload") XCTAssertTrue(sessionProposed) } + + func testSessionSettle() { + let agreementKeys = AgreementKeys.stub() + let topicB = String.generateTopic() + cryptoMock.setAgreementSecret(agreementKeys, topic: topicB) + let proposal = SessionProposal.stub(proposerPubKey: AgreementPrivateKey().publicKey.hexRepresentation) + try? engine.settle(topic: topicB, proposal: proposal, namespaces: SessionNamespace.stubDictionary()) + usleep(100) + XCTAssertTrue(sessionStorageMock.hasSession(forTopic: topicB), "Responder must persist session on topic B") + XCTAssert(networkingInteractor.didSubscribe(to: topicB), "Responder must subscribe for topic B") + XCTAssertTrue(networkingInteractor.didCallRequest, "Responder must send session settle payload on topic B") + } + + func testHandleSessionSettle() { + let sessionTopic = String.generateTopic() + cryptoMock.setAgreementSecret(AgreementKeys.stub(), topic: sessionTopic) + var didCallBackOnSessionApproved = false + engine.onSessionSettle = { _ in + didCallBackOnSessionApproved = true + } + + engine.settlingProposal = SessionProposal.stub() + networkingInteractor.wcRequestPublisherSubject.send(WCRequestSubscriptionPayload.stubSettle(topic: sessionTopic)) + + XCTAssertTrue(sessionStorageMock.getSession(forTopic: sessionTopic)!.acknowledged, "Proposer must store acknowledged session on topic B") + XCTAssertTrue(networkingInteractor.didRespondSuccess, "Proposer must send acknowledge on settle request") + XCTAssertTrue(didCallBackOnSessionApproved, "Proposer's engine must call back with session") + } + + func testHandleSessionSettleAcknowledge() { + let session = WCSession.stub(isSelfController: true, acknowledged: false) + sessionStorageMock.setSession(session) + var didCallBackOnSessionApproved = false + engine.onSessionSettle = { _ in + didCallBackOnSessionApproved = true + } + + 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) + + XCTAssertTrue(didCallBackOnSessionApproved) + XCTAssertTrue(sessionStorageMock.getSession(forTopic: session.topic)!.acknowledged, "Responder must acknowledged session") + } } diff --git a/Tests/WalletConnectSignTests/PairingEngineTests.swift b/Tests/WalletConnectSignTests/PairingEngineTests.swift index 7328d65f7..25d260644 100644 --- a/Tests/WalletConnectSignTests/PairingEngineTests.swift +++ b/Tests/WalletConnectSignTests/PairingEngineTests.swift @@ -54,9 +54,11 @@ final class PairingEngineTests: XCTestCase { networkingInteractor: networkingInteractor, proposalPayloadsStore: .init(defaults: RuntimeKeyValueStorage(), identifier: ""), sessionToPairingTopic: CodableStore(defaults: RuntimeKeyValueStorage(), identifier: ""), + metadata: meta, kms: cryptoMock, logger: logger, - pairingStore: storageMock + pairingStore: storageMock, + sessionStore: WCSessionStorageMock() ) } @@ -112,14 +114,9 @@ final class PairingEngineTests: XCTestCase { var sessionTopic: String! - approveEngine.approvePublisher.sink { response in - switch response { - case .proposeResponse(let topic, _): - sessionTopic = topic - default: - XCTFail() - } - }.store(in: &publishers) + approveEngine.onProposeResponse = { topic, _ in + sessionTopic = topic + } networkingInteractor.responsePublisherSubject.send(response) let privateKey = try! cryptoMock.getPrivateKey(for: proposal.proposer.publicKey)! diff --git a/Tests/WalletConnectSignTests/SessionEngineTests.swift b/Tests/WalletConnectSignTests/SessionEngineTests.swift index 006014838..eb0b745f0 100644 --- a/Tests/WalletConnectSignTests/SessionEngineTests.swift +++ b/Tests/WalletConnectSignTests/SessionEngineTests.swift @@ -19,9 +19,7 @@ final class SessionEngineTests: XCTestCase { var cryptoMock: KeyManagementServiceMock! var topicGenerator: TopicGenerator! - - var metadata: AppMetadata! - + override func setUp() { networkingInteractor = MockedWCRelay() storageMock = WCSessionStorageMock() @@ -39,7 +37,6 @@ final class SessionEngineTests: XCTestCase { } func setupEngine() { - metadata = AppMetadata.stub() let logger = ConsoleLoggerMock() engine = SessionEngine( networkingInteractor: networkingInteractor, @@ -47,59 +44,10 @@ final class SessionEngineTests: XCTestCase { pairingStore: WCPairingStorageMock(), sessionStore: storageMock, sessionToPairingTopic: CodableStore(defaults: RuntimeKeyValueStorage(), identifier: ""), - metadata: metadata, logger: logger, topicGenerator: topicGenerator.getTopic) } - func testSessionSettle() { - let agreementKeys = AgreementKeys.stub() - let topicB = String.generateTopic() - cryptoMock.setAgreementSecret(agreementKeys, topic: topicB) - let proposal = SessionProposal.stub(proposerPubKey: AgreementPrivateKey().publicKey.hexRepresentation) - try? engine.settle(topic: topicB, proposal: proposal, namespaces: SessionNamespace.stubDictionary()) - usleep(100) - XCTAssertTrue(storageMock.hasSession(forTopic: topicB), "Responder must persist session on topic B") - XCTAssert(networkingInteractor.didSubscribe(to: topicB), "Responder must subscribe for topic B") - XCTAssertTrue(networkingInteractor.didCallRequest, "Responder must send session settle payload on topic B") - } - - func testHandleSessionSettle() { - let sessionTopic = String.generateTopic() - cryptoMock.setAgreementSecret(AgreementKeys.stub(), topic: sessionTopic) - var didCallBackOnSessionApproved = false - engine.onSessionSettle = { _ in - didCallBackOnSessionApproved = true - } - - engine.settlingProposal = SessionProposal.stub() - networkingInteractor.wcRequestPublisherSubject.send(WCRequestSubscriptionPayload.stubSettle(topic: sessionTopic)) - - XCTAssertTrue(storageMock.getSession(forTopic: sessionTopic)!.acknowledged, "Proposer must store acknowledged session on topic B") - XCTAssertTrue(networkingInteractor.didRespondSuccess, "Proposer must send acknowledge on settle request") - XCTAssertTrue(didCallBackOnSessionApproved, "Proposer's engine must call back with session") - } - - func testHandleSessionSettleAcknowledge() { - let session = WCSession.stub(isSelfController: true, acknowledged: false) - storageMock.setSession(session) - var didCallBackOnSessionApproved = false - engine.onSessionSettle = { _ in - didCallBackOnSessionApproved = true - } - - 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) - - XCTAssertTrue(storageMock.getSession(forTopic: session.topic)!.acknowledged, "Responder must acknowledged session") - } - func testHandleSessionSettleError() { let privateKey = AgreementPrivateKey() let session = WCSession.stub(isSelfController: false, selfPrivateKey: privateKey, acknowledged: false) From 8533da5622c8e1b77adceafbb80f39e9749a7381 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Tue, 7 Jun 2022 15:28:44 +0500 Subject: [PATCH 07/13] Session Settle moved to approve Engine --- .../Engine/Common/ApproveEngine.swift | 78 +++++++++++++------ .../Engine/Common/SessionEngine.swift | 30 +------ .../WalletConnectSign/Sign/SignClient.swift | 4 +- .../ApproveEngineTests.swift | 26 +++++-- .../SessionEngineTests.swift | 71 ----------------- 5 files changed, 78 insertions(+), 131 deletions(-) delete mode 100644 Tests/WalletConnectSignTests/SessionEngineTests.swift diff --git a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift index 8bae718ea..e1d8f0814 100644 --- a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift @@ -146,38 +146,38 @@ private extension ApproveEngine { func setupNetworkingSubscriptions() { networkingInteractor.responsePublisher .sink { [unowned self] response in - self.handleResponse(response) + switch response.requestParams { + case .sessionPropose(let proposal): + handleSessionProposeResponse(response: response, proposal: proposal) + case .sessionSettle: + handleSessionSettleResponse(response: response) + default: + break + } }.store(in: &publishers) networkingInteractor.wcRequestPublisher .sink { [unowned self] subscriptionPayload in switch subscriptionPayload.wcRequest.params { case .sessionPropose(let proposal): - handleSessionPropose(subscriptionPayload, proposal: proposal) + handleSessionProposeRequest(payload: subscriptionPayload, proposal: proposal) case .sessionSettle(let settleParams): - handleSessionSettle(payload: subscriptionPayload, settleParams: settleParams) + handleSessionSettleRequest(payload: subscriptionPayload, settleParams: settleParams) default: return } }.store(in: &publishers) } - func handleSessionPropose(_ payload: WCRequestSubscriptionPayload, proposal: SessionType.ProposeParams) { - do { - logger.debug("Received Session Proposal") - try Namespace.validate(proposal.requiredNamespaces) - proposalPayloadsStore.set(payload, forKey: proposal.proposer.publicKey) - onSessionProposal?(proposal.publicRepresentation()) - } - catch { - // TODO: Return reasons with 6000 code Issue: #253 - networkingInteractor.respondError(for: payload, reason: .invalidUpdateNamespaceRequest) - } + func updatePairingMetadata(topic: String, metadata: AppMetadata) { + guard var pairing = pairingStore.getPairing(forTopic: topic) else { return } + pairing.peerMetadata = metadata + pairingStore.setPairing(pairing) } - func handleResponse(_ response: WCResponse) { - guard case .sessionPropose(let proposal) = response.requestParams else { return } - + // MARK: SessionProposeResponse + + func handleSessionProposeResponse(response: WCResponse, proposal: SessionType.ProposeParams) { do { let sessionTopic = try handleProposeResponse( pairingTopic: response.topic, @@ -234,7 +234,43 @@ private extension ApproveEngine { } } - func handleSessionSettle(payload: WCRequestSubscriptionPayload, settleParams: SessionType.SettleParams) { + // 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!) + } + } + + // MARK: SessionProposeRequest + + func handleSessionProposeRequest(payload: WCRequestSubscriptionPayload, proposal: SessionType.ProposeParams) { + do { + logger.debug("Received Session Proposal") + try Namespace.validate(proposal.requiredNamespaces) + proposalPayloadsStore.set(payload, forKey: proposal.proposer.publicKey) + onSessionProposal?(proposal.publicRepresentation()) + } + catch { + // TODO: Return reasons with 6000 code Issue: #253 + networkingInteractor.respondError(for: payload, reason: .invalidUpdateNamespaceRequest) + } + } + + // MARK: SessionSettleRequest + + func handleSessionSettleRequest(payload: WCRequestSubscriptionPayload, settleParams: SessionType.SettleParams) { logger.debug("Did receive session settle request") guard let proposedNamespaces = settlingProposal?.requiredNamespaces else { // TODO: respond error @@ -271,10 +307,4 @@ private extension ApproveEngine { networkingInteractor.respondSuccess(for: payload) onSessionSettle?(session.publicRepresentation()) } - - 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/WalletConnectSign/Engine/Common/SessionEngine.swift b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift index 24c4a2859..3d44a2b01 100644 --- a/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift @@ -19,30 +19,21 @@ final class SessionEngine { var onEventReceived: EventReceivedCallback? private let sessionStore: WCSessionStorage - private let pairingStore: WCPairingStorage - private let sessionToPairingTopic: CodableStore private let networkingInteractor: NetworkInteracting private let kms: KeyManagementServiceProtocol private var publishers = [AnyCancellable]() private let logger: ConsoleLogging - private let topicInitializer: () -> String init( networkingInteractor: NetworkInteracting, kms: KeyManagementServiceProtocol, - pairingStore: WCPairingStorage, sessionStore: WCSessionStorage, - sessionToPairingTopic: CodableStore, - logger: ConsoleLogging, - topicGenerator: @escaping () -> String = String.generateTopic + logger: ConsoleLogging ) { self.networkingInteractor = networkingInteractor self.kms = kms self.sessionStore = sessionStore - self.pairingStore = pairingStore - self.sessionToPairingTopic = sessionToPairingTopic self.logger = logger - self.topicInitializer = topicGenerator setupNetworkingSubscriptions() setupExpirationSubscriptions() @@ -224,8 +215,6 @@ private extension SessionEngine { func handleResponse(_ response: WCResponse) { switch response.requestParams { - case .sessionSettle: - handleSessionSettleResponse(topic: response.topic, result: response.result) case .sessionRequest: let response = Response(topic: response.topic, chainId: response.chainId, result: response.result) onSessionResponse?(response) @@ -233,21 +222,4 @@ private extension SessionEngine { break } } - - func handleSessionSettleResponse(topic: String, result: JsonRpcResult) { - guard let session = sessionStore.getSession(forTopic: topic) else {return} - switch result { - case .response: - logger.debug("Received session settle response") - guard var session = sessionStore.getSession(forTopic: topic) else {return} - session.acknowledge() - sessionStore.setSession(session) - case .error(let error): - logger.error("Error - session rejected, Reason: \(error)") - networkingInteractor.unsubscribe(topic: topic) - sessionStore.delete(topic: topic) - kms.deleteAgreementSecret(for: topic) - kms.deletePrivateKey(for: session.publicKey!) - } - } } diff --git a/Sources/WalletConnectSign/Sign/SignClient.swift b/Sources/WalletConnectSign/Sign/SignClient.swift index 0eb65c967..3e01a6771 100644 --- a/Sources/WalletConnectSign/Sign/SignClient.swift +++ b/Sources/WalletConnectSign/Sign/SignClient.swift @@ -64,7 +64,7 @@ public final class SignClient { let sessionToPairingTopic = CodableStore(defaults: RuntimeKeyValueStorage(), identifier: StorageDomainIdentifiers.sessionToPairingTopic.rawValue) let proposalPayloadsStore = CodableStore(defaults: RuntimeKeyValueStorage(), identifier: StorageDomainIdentifiers.proposals.rawValue) self.pairingEngine = PairingEngine(networkingInteractor: networkingInteractor, kms: kms, pairingStore: pairingStore, metadata: metadata, logger: logger) - self.sessionEngine = SessionEngine(networkingInteractor: networkingInteractor, kms: kms, pairingStore: pairingStore, sessionStore: sessionStore, sessionToPairingTopic: sessionToPairingTopic, logger: logger) + self.sessionEngine = SessionEngine(networkingInteractor: networkingInteractor, kms: kms, sessionStore: sessionStore, logger: logger) self.nonControllerSessionStateMachine = NonControllerSessionStateMachine(networkingInteractor: networkingInteractor, kms: kms, sessionStore: sessionStore, logger: logger) self.controllerSessionStateMachine = ControllerSessionStateMachine(networkingInteractor: networkingInteractor, kms: kms, sessionStore: sessionStore, logger: logger) self.pairEngine = PairEngine(networkingInteractor: networkingInteractor, kms: kms, pairingStore: pairingStore) @@ -98,7 +98,7 @@ public final class SignClient { let sessionToPairingTopic = CodableStore(defaults: RuntimeKeyValueStorage(), identifier: StorageDomainIdentifiers.sessionToPairingTopic.rawValue) let proposalPayloadsStore = CodableStore(defaults: RuntimeKeyValueStorage(), identifier: StorageDomainIdentifiers.proposals.rawValue) self.pairingEngine = PairingEngine(networkingInteractor: networkingInteractor, kms: kms, pairingStore: pairingStore, metadata: metadata, logger: logger) - self.sessionEngine = SessionEngine(networkingInteractor: networkingInteractor, kms: kms, pairingStore: pairingStore, sessionStore: sessionStore, sessionToPairingTopic: sessionToPairingTopic, logger: logger) + self.sessionEngine = SessionEngine(networkingInteractor: networkingInteractor, kms: kms, sessionStore: sessionStore, logger: logger) self.approveEngine = ApproveEngine(networkingInteractor: networkingInteractor, proposalPayloadsStore: proposalPayloadsStore, sessionToPairingTopic: sessionToPairingTopic, metadata: metadata, kms: kms, logger: logger, pairingStore: pairingStore, sessionStore: sessionStore) self.nonControllerSessionStateMachine = NonControllerSessionStateMachine(networkingInteractor: networkingInteractor, kms: kms, sessionStore: sessionStore, logger: logger) self.controllerSessionStateMachine = ControllerSessionStateMachine(networkingInteractor: networkingInteractor, kms: kms, sessionStore: sessionStore, logger: logger) diff --git a/Tests/WalletConnectSignTests/ApproveEngineTests.swift b/Tests/WalletConnectSignTests/ApproveEngineTests.swift index 1f6d50632..24e5e679e 100644 --- a/Tests/WalletConnectSignTests/ApproveEngineTests.swift +++ b/Tests/WalletConnectSignTests/ApproveEngineTests.swift @@ -109,10 +109,6 @@ final class ApproveEngineTests: XCTestCase { func testHandleSessionSettleAcknowledge() { let session = WCSession.stub(isSelfController: true, acknowledged: false) sessionStorageMock.setSession(session) - var didCallBackOnSessionApproved = false - engine.onSessionSettle = { _ in - didCallBackOnSessionApproved = true - } let settleResponse = JSONRPCResponse(id: 1, result: AnyCodable(true)) let response = WCResponse( @@ -123,7 +119,27 @@ final class ApproveEngineTests: XCTestCase { result: .response(settleResponse)) networkingInteractor.responsePublisherSubject.send(response) - XCTAssertTrue(didCallBackOnSessionApproved) XCTAssertTrue(sessionStorageMock.getSession(forTopic: session.topic)!.acknowledged, "Responder must acknowledged session") } + + func testHandleSessionSettleError() { + let privateKey = AgreementPrivateKey() + let session = WCSession.stub(isSelfController: false, selfPrivateKey: privateKey, acknowledged: false) + sessionStorageMock.setSession(session) + 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) + + XCTAssertNil(sessionStorageMock.getSession(forTopic: session.topic), "Responder must remove session") + XCTAssertTrue(networkingInteractor.didUnsubscribe(to: session.topic), "Responder must unsubscribe topic B") + XCTAssertFalse(cryptoMock.hasAgreementSecret(for: session.topic), "Responder must remove agreement secret") + XCTAssertFalse(cryptoMock.hasPrivateKey(for: session.self.publicKey!), "Responder must remove private key") + } } diff --git a/Tests/WalletConnectSignTests/SessionEngineTests.swift b/Tests/WalletConnectSignTests/SessionEngineTests.swift deleted file mode 100644 index eb0b745f0..000000000 --- a/Tests/WalletConnectSignTests/SessionEngineTests.swift +++ /dev/null @@ -1,71 +0,0 @@ -import XCTest -import WalletConnectUtils -@testable import TestingUtils -import WalletConnectKMS -@testable import WalletConnectSign - -extension Collection where Self.Element == String { - func toAccountSet() -> Set { - Set(self.map { Account($0)! }) - } -} - -final class SessionEngineTests: XCTestCase { - - var engine: SessionEngine! - - var networkingInteractor: MockedWCRelay! - var storageMock: WCSessionStorageMock! - var cryptoMock: KeyManagementServiceMock! - - var topicGenerator: TopicGenerator! - - override func setUp() { - networkingInteractor = MockedWCRelay() - storageMock = WCSessionStorageMock() - cryptoMock = KeyManagementServiceMock() - topicGenerator = TopicGenerator() - setupEngine() - } - - override func tearDown() { - networkingInteractor = nil - storageMock = nil - cryptoMock = nil - topicGenerator = nil - engine = nil - } - - func setupEngine() { - let logger = ConsoleLoggerMock() - engine = SessionEngine( - networkingInteractor: networkingInteractor, - kms: cryptoMock, - pairingStore: WCPairingStorageMock(), - sessionStore: storageMock, - sessionToPairingTopic: CodableStore(defaults: RuntimeKeyValueStorage(), identifier: ""), - logger: logger, - topicGenerator: topicGenerator.getTopic) - } - - func testHandleSessionSettleError() { - let privateKey = AgreementPrivateKey() - let session = WCSession.stub(isSelfController: false, selfPrivateKey: privateKey, acknowledged: false) - storageMock.setSession(session) - 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) - - XCTAssertNil(storageMock.getSession(forTopic: session.topic), "Responder must remove session") - XCTAssertTrue(networkingInteractor.didUnsubscribe(to: session.topic), "Responder must unsubscribe topic B") - XCTAssertFalse(cryptoMock.hasAgreementSecret(for: session.topic), "Responder must remove agreement secret") - XCTAssertFalse(cryptoMock.hasPrivateKey(for: session.self.publicKey!), "Responder must remove private key") - } -} From 3c3d9be1a91cea0b70dc5ba7e1936f0183d1f766 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Tue, 7 Jun 2022 16:10:39 +0500 Subject: [PATCH 08/13] onProposeResponse subscription removed --- .../Engine/Common/ApproveEngine.swift | 5 ++--- .../Engine/Common/SessionEngine.swift | 4 ---- Sources/WalletConnectSign/Sign/SignClient.swift | 3 --- Tests/WalletConnectSignTests/PairingEngineTests.swift | 10 +++------- 4 files changed, 5 insertions(+), 17 deletions(-) diff --git a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift index e1d8f0814..a12d13fc0 100644 --- a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift @@ -13,12 +13,10 @@ final class ApproveEngine { case agreementMissingOrInvalid } - typealias ProposeResponseCallback = (String, SessionProposal) -> Void typealias SessionProposalCallback = (Session.Proposal) -> Void typealias SessionRejectedCallback = (Session.Proposal, SessionType.Reason) -> Void typealias SessionSettleCallback = (Session) -> Void - var onProposeResponse: ProposeResponseCallback? var onSessionProposal: SessionProposalCallback? var onSessionRejected: SessionRejectedCallback? var onSessionSettle: SessionSettleCallback? @@ -185,7 +183,8 @@ private extension ApproveEngine { result: response.result ) settlingProposal = proposal - onProposeResponse?(sessionTopic, proposal) + + Task { try? await networkingInteractor.subscribe(topic: sessionTopic) } } catch { guard let error = error as? JSONRPCErrorResponse else { diff --git a/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift index 3d44a2b01..c20e79cb9 100644 --- a/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift @@ -39,10 +39,6 @@ final class SessionEngine { setupExpirationSubscriptions() } - func setSubscription(topic: String) { - Task { try? await networkingInteractor.subscribe(topic: topic) } - } - func hasSession(for topic: String) -> Bool { return sessionStore.hasSession(forTopic: topic) } diff --git a/Sources/WalletConnectSign/Sign/SignClient.swift b/Sources/WalletConnectSign/Sign/SignClient.swift index 3e01a6771..e0c1d541e 100644 --- a/Sources/WalletConnectSign/Sign/SignClient.swift +++ b/Sources/WalletConnectSign/Sign/SignClient.swift @@ -296,9 +296,6 @@ public final class SignClient { // MARK: - Private private func setUpEnginesCallbacks() { - approveEngine.onProposeResponse = { [unowned self] topic, proposal in - sessionEngine.setSubscription(topic: topic) - } approveEngine.onSessionProposal = { [unowned self] proposal in delegate?.didReceive(sessionProposal: proposal) } diff --git a/Tests/WalletConnectSignTests/PairingEngineTests.swift b/Tests/WalletConnectSignTests/PairingEngineTests.swift index 25d260644..bc08fcfdd 100644 --- a/Tests/WalletConnectSignTests/PairingEngineTests.swift +++ b/Tests/WalletConnectSignTests/PairingEngineTests.swift @@ -86,6 +86,7 @@ final class PairingEngineTests: XCTestCase { XCTAssertEqual(publishTopic, topicA) } + @MainActor func testHandleSessionProposeResponse() async { let uri = try! await engine.create() let pairing = storageMock.getPairing(forTopic: uri.topic)! @@ -111,19 +112,14 @@ final class PairingEngineTests: XCTestCase { requestMethod: request.method, requestParams: request.params, result: .response(jsonRpcResponse)) - - var sessionTopic: String! - - approveEngine.onProposeResponse = { topic, _ in - sessionTopic = topic - } networkingInteractor.responsePublisherSubject.send(response) let privateKey = try! cryptoMock.getPrivateKey(for: proposal.proposer.publicKey)! let topicB = deriveTopic(publicKey: responder.publicKey, privateKey: privateKey) - let storedPairing = storageMock.getPairing(forTopic: topicA)! + let sessionTopic = networkingInteractor.subscriptions.last! + XCTAssertTrue(networkingInteractor.didCallSubscribe) XCTAssert(storedPairing.active) XCTAssertEqual(topicB, sessionTopic, "Responder engine calls back with session topic") } From 4671a34d5b5f332ee31692ef93a08ac07e88e1c5 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Tue, 7 Jun 2022 16:25:46 +0500 Subject: [PATCH 09/13] Reject by proposalId --- .../ExampleApp/Wallet/WalletViewController.swift | 2 +- .../Engine/Common/ApproveEngine.swift | 6 +++--- Sources/WalletConnectSign/Sign/Sign.swift | 8 ++++---- Sources/WalletConnectSign/Sign/SignClient.swift | 13 +++++-------- 4 files changed, 13 insertions(+), 16 deletions(-) diff --git a/Example/ExampleApp/Wallet/WalletViewController.swift b/Example/ExampleApp/Wallet/WalletViewController.swift index d83df0661..f5ede5ab5 100644 --- a/Example/ExampleApp/Wallet/WalletViewController.swift +++ b/Example/ExampleApp/Wallet/WalletViewController.swift @@ -169,7 +169,7 @@ extension WalletViewController: ProposalViewControllerDelegate { let proposal = currentProposal! currentProposal = nil do { - try Sign.instance.reject(proposal: proposal, reason: .disapprovedChains) + try Sign.instance.reject(proposalId: proposal.id, reason: .disapprovedChains) } catch { print("Session rejection error: \(error.localizedDescription)") } diff --git a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift index a12d13fc0..21910ca5c 100644 --- a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift @@ -90,11 +90,11 @@ final class ApproveEngine { return (sessionTopic, proposal) } - func reject(proposal: SessionProposal, reason: ReasonCode) throws { - guard let payload = try proposalPayloadsStore.get(key: proposal.proposer.publicKey) else { + func reject(proposerPubKey: String, reason: ReasonCode) throws { + guard let payload = try proposalPayloadsStore.get(key: proposerPubKey) else { throw Error.proposalPayloadsNotFound } - proposalPayloadsStore.delete(forKey: proposal.proposer.publicKey) + proposalPayloadsStore.delete(forKey: proposerPubKey) networkingInteractor.respondError(for: payload, reason: reason) // TODO: Delete pairing if inactive } diff --git a/Sources/WalletConnectSign/Sign/Sign.swift b/Sources/WalletConnectSign/Sign/Sign.swift index af8ba3eae..907e5d226 100644 --- a/Sources/WalletConnectSign/Sign/Sign.swift +++ b/Sources/WalletConnectSign/Sign/Sign.swift @@ -145,7 +145,7 @@ extension Sign { /// For the responder to approve a session proposal. /// - Parameters: - /// - proposal: Session Proposal received from peer client in a WalletConnect delegate function: `didReceive(sessionProposal: Session.Proposal)` + /// - proposalId: Session Proposal Public key received from peer client in a WalletConnect delegate function: `didReceive(sessionProposal: Session.Proposal)` /// - accounts: A Set of accounts that the dapp will be allowed to request methods executions on. /// - methods: A Set of methods that the dapp will be allowed to request. /// - events: A Set of events @@ -155,10 +155,10 @@ extension Sign { /// For the responder to reject a session proposal. /// - Parameters: - /// - proposal: Session Proposal received from peer client in a WalletConnect delegate. + /// - proposalId: Session Proposal Public key received from peer client in a WalletConnect delegate. /// - reason: Reason why the session proposal was rejected. Conforms to CAIP25. - public func reject(proposal: Session.Proposal, reason: RejectionReason) throws { - try client.reject(proposal: proposal, reason: reason) + public func reject(proposalId: String, reason: RejectionReason) throws { + try client.reject(proposalId: proposalId, reason: reason) } /// For the responder to update session methods diff --git a/Sources/WalletConnectSign/Sign/SignClient.swift b/Sources/WalletConnectSign/Sign/SignClient.swift index e0c1d541e..2030928e0 100644 --- a/Sources/WalletConnectSign/Sign/SignClient.swift +++ b/Sources/WalletConnectSign/Sign/SignClient.swift @@ -153,14 +153,11 @@ public final class SignClient { /// For the responder to approve a session proposal. /// - Parameters: - /// - proposal: Session Proposal received from peer client in a WalletConnect delegate function: `didReceive(sessionProposal: Session.Proposal)` + /// - proposalId: Session Proposal Public key received from peer client in a WalletConnect delegate function: `didReceive(sessionProposal: Session.Proposal)` /// - accounts: A Set of accounts that the dapp will be allowed to request methods executions on. /// - methods: A Set of methods that the dapp will be allowed to request. /// - events: A Set of events - public func approve( - proposalId: String, - namespaces: [String: SessionNamespace] - ) throws { + public func approve(proposalId: String, namespaces: [String: SessionNamespace]) throws { //TODO - accounts should be validated for matching namespaces BEFORE responding proposal let (sessionTopic, proposal) = try approveEngine.approveProposal(proposerPubKey: proposalId, validating: namespaces) try approveEngine.settle(topic: sessionTopic, proposal: proposal, namespaces: namespaces) @@ -168,10 +165,10 @@ public final class SignClient { /// For the responder to reject a session proposal. /// - Parameters: - /// - proposal: Session Proposal received from peer client in a WalletConnect delegate. + /// - proposalId: Session Proposal Public key received from peer client in a WalletConnect delegate. /// - reason: Reason why the session proposal was rejected. Conforms to CAIP25. - public func reject(proposal: Session.Proposal, reason: RejectionReason) throws { - try approveEngine.reject(proposal: proposal.proposal, reason: reason.internalRepresentation()) + public func reject(proposalId: String, reason: RejectionReason) throws { + try approveEngine.reject(proposerPubKey: proposalId, reason: reason.internalRepresentation()) } /// For the responder to update session namespaces From 16336ca1462b7d3384345733b21281c8a5c49882 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Tue, 7 Jun 2022 16:45:10 +0500 Subject: [PATCH 10/13] Settle moved to approve --- .../Engine/Common/ApproveEngine.swift | 4 ++-- Sources/WalletConnectSign/Sign/SignClient.swift | 3 +-- Tests/WalletConnectSignTests/ApproveEngineTests.swift | 10 ++++++++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift index 21910ca5c..631af6b1a 100644 --- a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift @@ -56,7 +56,7 @@ final class ApproveEngine { setupNetworkingSubscriptions() } - func approveProposal(proposerPubKey: String, validating sessionNamespaces: [String: SessionNamespace]) throws -> (String, SessionProposal) { + func approveProposal(proposerPubKey: String, validating sessionNamespaces: [String: SessionNamespace]) throws { let payload = try proposalPayloadsStore.get(key: proposerPubKey) guard let payload = payload, case .sessionPropose(let proposal) = payload.wcRequest.params else { @@ -87,7 +87,7 @@ final class ApproveEngine { let response = JSONRPCResponse(id: payload.wcRequest.id, result: AnyCodable(proposeResponse)) networkingInteractor.respond(topic: payload.topic, response: .response(response)) { _ in } - return (sessionTopic, proposal) + try settle(topic: sessionTopic, proposal: proposal, namespaces: sessionNamespaces) } func reject(proposerPubKey: String, reason: ReasonCode) throws { diff --git a/Sources/WalletConnectSign/Sign/SignClient.swift b/Sources/WalletConnectSign/Sign/SignClient.swift index 2030928e0..54bc2b724 100644 --- a/Sources/WalletConnectSign/Sign/SignClient.swift +++ b/Sources/WalletConnectSign/Sign/SignClient.swift @@ -159,8 +159,7 @@ public final class SignClient { /// - events: A Set of events public func approve(proposalId: String, namespaces: [String: SessionNamespace]) throws { //TODO - accounts should be validated for matching namespaces BEFORE responding proposal - let (sessionTopic, proposal) = try approveEngine.approveProposal(proposerPubKey: proposalId, validating: namespaces) - try approveEngine.settle(topic: sessionTopic, proposal: proposal, namespaces: namespaces) + try approveEngine.approveProposal(proposerPubKey: proposalId, validating: namespaces) } /// For the responder to reject a session proposal. diff --git a/Tests/WalletConnectSignTests/ApproveEngineTests.swift b/Tests/WalletConnectSignTests/ApproveEngineTests.swift index 24e5e679e..a0222fb29 100644 --- a/Tests/WalletConnectSignTests/ApproveEngineTests.swift +++ b/Tests/WalletConnectSignTests/ApproveEngineTests.swift @@ -44,7 +44,8 @@ final class ApproveEngineTests: XCTestCase { engine = nil } - func testApproveProposal() throws { + @MainActor + func testApproveProposal() async throws { // Client receives a proposal let topicA = String.generateTopic() let proposerPubKey = AgreementPrivateKey().publicKey.hexRepresentation @@ -53,8 +54,13 @@ final class ApproveEngineTests: XCTestCase { let payload = WCRequestSubscriptionPayload(topic: topicA, wcRequest: request) networkingInteractor.wcRequestPublisherSubject.send(payload) - let (topicB, _) = try engine.approveProposal(proposerPubKey: proposal.proposer.publicKey, validating: SessionNamespace.stubDictionary()) + try engine.approveProposal(proposerPubKey: proposal.proposer.publicKey, validating: SessionNamespace.stubDictionary()) + + usleep(100) + + let topicB = networkingInteractor.subscriptions.last! + XCTAssertTrue(networkingInteractor.didCallSubscribe) XCTAssert(cryptoMock.hasAgreementSecret(for: topicB), "Responder must store agreement key for topic B") XCTAssertEqual(networkingInteractor.didRespondOnTopic!, topicA, "Responder must respond on topic A") } From 01950831a666d172646895bf02e1d8cf20502973 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Tue, 7 Jun 2022 17:39:58 +0500 Subject: [PATCH 11/13] ApproveEngine moved to Controller folder --- Sources/WalletConnectSign/Engine/Common/PairingEngine.swift | 1 - Sources/WalletConnectSign/Engine/Common/SessionEngine.swift | 2 +- .../Engine/{Common => Controller}/ApproveEngine.swift | 0 3 files changed, 1 insertion(+), 2 deletions(-) rename Sources/WalletConnectSign/Engine/{Common => Controller}/ApproveEngine.swift (100%) diff --git a/Sources/WalletConnectSign/Engine/Common/PairingEngine.swift b/Sources/WalletConnectSign/Engine/Common/PairingEngine.swift index 387b17e70..1ea9359dd 100644 --- a/Sources/WalletConnectSign/Engine/Common/PairingEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/PairingEngine.swift @@ -3,7 +3,6 @@ import Combine import WalletConnectUtils import WalletConnectKMS - final class PairingEngine { private let networkingInteractor: NetworkInteracting diff --git a/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift index c20e79cb9..ef8f4e39e 100644 --- a/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift @@ -3,7 +3,6 @@ import Combine import WalletConnectUtils import WalletConnectKMS - final class SessionEngine { typealias SessionRequestCallback = (Request) -> Void @@ -110,6 +109,7 @@ final class SessionEngine { try await networkingInteractor.request(.wcSessionEvent(params), onTopic: topic) } } + //MARK: - Privates private extension SessionEngine { diff --git a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift b/Sources/WalletConnectSign/Engine/Controller/ApproveEngine.swift similarity index 100% rename from Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift rename to Sources/WalletConnectSign/Engine/Controller/ApproveEngine.swift From ba3fb7681d090e2a7caecc6150122d465a30aa46 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Wed, 8 Jun 2022 13:23:04 +0500 Subject: [PATCH 12/13] typealias removed --- .../Engine/Common/SessionEngine.swift | 18 ++++++------------ .../Engine/Controller/ApproveEngine.swift | 12 ++++-------- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift index ef8f4e39e..e61541731 100644 --- a/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift @@ -4,18 +4,12 @@ import WalletConnectUtils import WalletConnectKMS final class SessionEngine { - - typealias SessionRequestCallback = (Request) -> Void - typealias SessionResponseCallback = (Response) -> Void - typealias SessionRejectedCallback = (String, SessionType.Reason) -> Void - typealias SessionDeleteCallback = (String, SessionType.Reason) -> Void - typealias EventReceivedCallback = (String, Session.Event, Blockchain?) -> Void - - var onSessionRequest: SessionRequestCallback? - var onSessionResponse: SessionResponseCallback? - var onSessionRejected: SessionRejectedCallback? - var onSessionDelete: SessionDeleteCallback? - var onEventReceived: EventReceivedCallback? + + var onSessionRequest: ((Request) -> Void)? + var onSessionResponse: ((Response) -> Void)? + var onSessionRejected: ((String, SessionType.Reason) -> Void)? + var onSessionDelete: ((String, SessionType.Reason) -> Void)? + var onEventReceived: ((String, Session.Event, Blockchain?) -> Void)? private let sessionStore: WCSessionStorage private let networkingInteractor: NetworkInteracting diff --git a/Sources/WalletConnectSign/Engine/Controller/ApproveEngine.swift b/Sources/WalletConnectSign/Engine/Controller/ApproveEngine.swift index 631af6b1a..82e07139b 100644 --- a/Sources/WalletConnectSign/Engine/Controller/ApproveEngine.swift +++ b/Sources/WalletConnectSign/Engine/Controller/ApproveEngine.swift @@ -12,14 +12,10 @@ final class ApproveEngine { case pairingNotFound case agreementMissingOrInvalid } - - typealias SessionProposalCallback = (Session.Proposal) -> Void - typealias SessionRejectedCallback = (Session.Proposal, SessionType.Reason) -> Void - typealias SessionSettleCallback = (Session) -> Void - - var onSessionProposal: SessionProposalCallback? - var onSessionRejected: SessionRejectedCallback? - var onSessionSettle: SessionSettleCallback? + + var onSessionProposal: ((Session.Proposal) -> Void)? + var onSessionRejected: ((Session.Proposal, SessionType.Reason) -> Void)? + var onSessionSettle: ((Session) -> Void)? var settlingProposal: SessionProposal? From 6bd3c6b10af4b1929b9a0e3730b34b414566def4 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Wed, 8 Jun 2022 15:48:35 +0500 Subject: [PATCH 13/13] TODO for SettleEngine --- .../Engine/{Controller => Common}/ApproveEngine.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) rename Sources/WalletConnectSign/Engine/{Controller => Common}/ApproveEngine.swift (99%) diff --git a/Sources/WalletConnectSign/Engine/Controller/ApproveEngine.swift b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift similarity index 99% rename from Sources/WalletConnectSign/Engine/Controller/ApproveEngine.swift rename to Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift index 82e07139b..dceb8f3ac 100644 --- a/Sources/WalletConnectSign/Engine/Controller/ApproveEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift @@ -170,7 +170,7 @@ private extension ApproveEngine { } // MARK: SessionProposeResponse - + // TODO: Move to Non-Controller SettleEngine func handleSessionProposeResponse(response: WCResponse, proposal: SessionType.ProposeParams) { do { let sessionTopic = try handleProposeResponse( @@ -264,7 +264,6 @@ private extension ApproveEngine { } // MARK: SessionSettleRequest - func handleSessionSettleRequest(payload: WCRequestSubscriptionPayload, settleParams: SessionType.SettleParams) { logger.debug("Did receive session settle request") guard let proposedNamespaces = settlingProposal?.requiredNamespaces else {