diff --git a/Sources/Chat/Chat.swift b/Sources/Chat/Chat.swift index fe54d4efb..f45354d60 100644 --- a/Sources/Chat/Chat.swift +++ b/Sources/Chat/Chat.swift @@ -7,19 +7,20 @@ import Combine class Chat { private var publishers = [AnyCancellable]() let registry: Registry - let registryManager: RegistryManager - let engine: Engine + let registryService: RegistryService + let invitationHandlingService: InvitationHandlingService + let inviteService: InviteService let kms: KeyManagementService let socketConnectionStatusPublisher: AnyPublisher - var newThreadPublisherSubject = PassthroughSubject() - public var newThreadPublisher: AnyPublisher { + var newThreadPublisherSubject = PassthroughSubject() + public var newThreadPublisher: AnyPublisher { newThreadPublisherSubject.eraseToAnyPublisher() } - var invitePublisherSubject = PassthroughSubject() - public var invitePublisher: AnyPublisher { + var invitePublisherSubject = PassthroughSubject() + public var invitePublisher: AnyPublisher { invitePublisherSubject.eraseToAnyPublisher() } @@ -32,16 +33,23 @@ class Chat { self.registry = registry self.kms = kms let serialiser = Serializer(kms: kms) - let networkingInteractor = NetworkingInteractor(relayClient: relayClient, serializer: serialiser) - let inviteStore = CodableStore(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.invite.rawValue) - self.registryManager = RegistryManager(registry: registry, networkingInteractor: networkingInteractor, kms: kms, logger: logger, topicToInvitationPubKeyStore: topicToInvitationPubKeyStore) - self.engine = Engine(registry: registry, + let jsonRpcHistory = JsonRpcHistory(logger: logger, keyValueStore: CodableStore(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.jsonRpcHistory.rawValue)) + let networkingInteractor = NetworkingInteractor( + relayClient: relayClient, + serializer: serialiser, + logger: logger, + jsonRpcHistory: jsonRpcHistory) + let invitePayloadStore = CodableStore(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.invite.rawValue) + self.registryService = RegistryService(registry: registry, networkingInteractor: networkingInteractor, kms: kms, logger: logger, topicToInvitationPubKeyStore: topicToInvitationPubKeyStore) + let codec = ChaChaPolyCodec() + self.invitationHandlingService = InvitationHandlingService(registry: registry, networkingInteractor: networkingInteractor, - kms: kms, - logger: logger, - topicToInvitationPubKeyStore: topicToInvitationPubKeyStore, - inviteStore: inviteStore, - threadsStore: CodableStore(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.threads.rawValue)) + kms: kms, + logger: logger, + topicToInvitationPubKeyStore: topicToInvitationPubKeyStore, + invitePayloadStore: invitePayloadStore, + threadsStore: CodableStore(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.threads.rawValue), codec: codec) + self.inviteService = InviteService(networkingInteractor: networkingInteractor, kms: kms, logger: logger, codec: codec) socketConnectionStatusPublisher = relayClient.socketConnectionStatusPublisher setUpEnginesCallbacks() } @@ -51,7 +59,7 @@ class Chat { /// - Parameter account: CAIP10 blockchain account /// - Returns: public key func register(account: Account) async throws -> String { - try await registryManager.register(account: account) + try await registryService.register(account: account) } /// Queries the default keyserver with a blockchain account @@ -66,11 +74,14 @@ class Chat { /// - publicKey: publicKey associated with a peer /// - openingMessage: oppening message for a chat invite func invite(publicKey: String, openingMessage: String) async throws { - try await engine.invite(peerPubKey: publicKey, openingMessage: openingMessage) + // TODO - how to provide account? + // in init or in invite method's params + let tempAccount = Account("eip155:1:33e32e32")! + try await inviteService.invite(peerPubKey: publicKey, openingMessage: openingMessage, account: tempAccount) } func accept(inviteId: String) async throws { - try await engine.accept(inviteId: inviteId) + try await invitationHandlingService.accept(inviteId: inviteId) } /// Sends a chat message to an active chat thread @@ -88,10 +99,13 @@ class Chat { } private func setUpEnginesCallbacks() { - engine.onInvite = { [unowned self] invite in - invitePublisherSubject.send(invite) + invitationHandlingService.onInvite = { [unowned self] inviteEnvelope in + invitePublisherSubject.send(inviteEnvelope) } - engine.onNewThread = { [unowned self] newThread in + invitationHandlingService.onNewThread = { [unowned self] newThread in + newThreadPublisherSubject.send(newThread) + } + inviteService.onNewThread = { [unowned self] newThread in newThreadPublisherSubject.send(newThread) } } diff --git a/Sources/Chat/Engine.swift b/Sources/Chat/Engine.swift deleted file mode 100644 index 8a187e0f5..000000000 --- a/Sources/Chat/Engine.swift +++ /dev/null @@ -1,92 +0,0 @@ -import Foundation -import WalletConnectKMS -import WalletConnectUtils -import WalletConnectRelay -import Combine - -class Engine { - var onInvite: ((Invite) -> Void)? - var onNewThread: ((Thread) -> Void)? - let networkingInteractor: NetworkingInteractor - let inviteStore: CodableStore<(Invite)> - let topicToInvitationPubKeyStore: CodableStore - let registry: Registry - let logger: ConsoleLogging - let kms: KeyManagementService - let threadsStore: CodableStore - private var publishers = [AnyCancellable]() - - init(registry: Registry, - networkingInteractor: NetworkingInteractor, - kms: KeyManagementService, - logger: ConsoleLogging, - topicToInvitationPubKeyStore: CodableStore, - inviteStore: CodableStore, - threadsStore: CodableStore) { - self.registry = registry - self.kms = kms - self.networkingInteractor = networkingInteractor - self.logger = logger - self.topicToInvitationPubKeyStore = topicToInvitationPubKeyStore - self.inviteStore = inviteStore - self.threadsStore = threadsStore - setUpRequestHandling() - setUpResponseHandling() - } - - func invite(peerPubKey: String, openingMessage: String) async throws { - let pubKey = try kms.createX25519KeyPair() - let invite = Invite(pubKey: pubKey.hexRepresentation, openingMessage: openingMessage) - let topic = try AgreementPublicKey(hex: peerPubKey).rawRepresentation.sha256().toHexString() - let request = ChatRequest(params: .invite(invite)) - networkingInteractor.requestUnencrypted(request, topic: topic) - let agreementKeys = try kms.performKeyAgreement(selfPublicKey: pubKey, peerPublicKey: peerPubKey) - let threadTopic = agreementKeys.derivedTopic() - try await networkingInteractor.subscribe(topic: threadTopic) - logger.debug("invite sent on topic: \(topic)") - } - - func accept(inviteId: String) async throws { - guard let hexPubKey = try topicToInvitationPubKeyStore.get(key: "todo-topic") else { - throw ChatError.noPublicKeyForInviteId - } - let pubKey = try! AgreementPublicKey(hex: hexPubKey) - guard let invite = try inviteStore.get(key: inviteId) else { - throw ChatError.noInviteForId - } - logger.debug("accepting an invitation") - let agreementKeys = try! kms.performKeyAgreement(selfPublicKey: pubKey, peerPublicKey: invite.pubKey) - let topic = agreementKeys.derivedTopic() - try await networkingInteractor.subscribe(topic: topic) - fatalError("not implemented") - } - - private func handleInvite(_ invite: Invite) { - onInvite?(invite) - logger.debug("did receive an invite") - try? inviteStore.set(invite, forKey: invite.id) -// networkingInteractor.respondSuccess(for: RequestSubscriptionPayload) - } - - private func setUpRequestHandling() { - networkingInteractor.requestPublisher.sink { [unowned self] subscriptionPayload in - switch subscriptionPayload.request.params { - case .invite(let invite): - handleInvite(invite) - case .message(let message): - print("received message: \(message)") - } - }.store(in: &publishers) - } - - private func setUpResponseHandling() { - networkingInteractor.responsePublisher.sink { [unowned self] response in - switch response.requestParams { - case .invite(let invite): - fatalError("not implemented") - case .message(let message): - print("received message response: \(message)") - } - }.store(in: &publishers) - } -} diff --git a/Sources/Chat/NetworkingInteractor.swift b/Sources/Chat/NetworkingInteractor.swift index 66fced10a..65764b89c 100644 --- a/Sources/Chat/NetworkingInteractor.swift +++ b/Sources/Chat/NetworkingInteractor.swift @@ -4,12 +4,23 @@ import WalletConnectRelay import WalletConnectUtils protocol NetworkInteracting { + var requestPublisher: AnyPublisher {get} + var responsePublisher: AnyPublisher {get} func subscribe(topic: String) async throws + func requestUnencrypted(_ request: JSONRPCRequest, topic: String) async throws + func request(_ request: JSONRPCRequest, topic: String) async throws + func respond(topic: String, response: JsonRpcResult) async throws + } class NetworkingInteractor: NetworkInteracting { - let relayClient: RelayClient + enum Error: Swift.Error { + case failedToInitialiseMethodFromRecord + } + private let jsonRpcHistory: JsonRpcHistory private let serializer: Serializing + private let relayClient: RelayClient + private let logger: ConsoleLogging var requestPublisher: AnyPublisher { requestPublisherSubject.eraseToAnyPublisher() } @@ -21,27 +32,35 @@ class NetworkingInteractor: NetworkInteracting { private let responsePublisherSubject = PassthroughSubject() init(relayClient: RelayClient, - serializer: Serializing) { + serializer: Serializing, + logger: ConsoleLogging, + jsonRpcHistory: JsonRpcHistory + ) { self.relayClient = relayClient self.serializer = serializer - + self.jsonRpcHistory = jsonRpcHistory + self.logger = logger relayClient.onMessage = { [unowned self] topic, message in manageSubscription(topic, message) } } - func requestUnencrypted(_ request: ChatRequest, topic: String) { + // TODO - remove the method + func requestUnencrypted(_ request: JSONRPCRequest, topic: String) async throws { + try jsonRpcHistory.set(topic: topic, request: request) let message = try! request.json() - relayClient.publish(topic: topic, payload: message) {_ in -// print(error) - } + try await relayClient.publish(topic: topic, payload: message) } - func request(_ request: ChatRequest, topic: String) { + func request(_ request: JSONRPCRequest, topic: String) async throws { + try jsonRpcHistory.set(topic: topic, request: request) let message = try! serializer.serialize(topic: topic, encodable: request) - relayClient.publish(topic: topic, payload: message) {_ in -// print(error) - } + try await relayClient.publish(topic: topic, payload: message) + } + + func respond(topic: String, response: JsonRpcResult) async throws { + let message = try serializer.serialize(topic: topic, encodable: response.value) + try await relayClient.publish(topic: topic, payload: message, prompt: false) } func subscribe(topic: String) async throws { @@ -49,9 +68,9 @@ class NetworkingInteractor: NetworkInteracting { } private func manageSubscription(_ topic: String, _ message: String) { - if let deserializedJsonRpcRequest: ChatRequest = serializer.tryDeserialize(topic: topic, message: message) { + if let deserializedJsonRpcRequest: JSONRPCRequest = serializer.tryDeserialize(topic: topic, message: message) { handleWCRequest(topic: topic, request: deserializedJsonRpcRequest) - } else if let decodedJsonRpcRequest: ChatRequest = tryDecodeRequest(message: message) { + } else if let decodedJsonRpcRequest: JSONRPCRequest = tryDecodeRequest(message: message) { handleWCRequest(topic: topic, request: decodedJsonRpcRequest) } else if let deserializedJsonRpcResponse: JSONRPCResponse = serializer.tryDeserialize(topic: topic, message: message) { @@ -63,20 +82,31 @@ class NetworkingInteractor: NetworkInteracting { } } - private func tryDecodeRequest(message: String) -> ChatRequest? { + private func tryDecodeRequest(message: String) -> JSONRPCRequest? { guard let messageData = message.data(using: .utf8) else { return nil } - return try? JSONDecoder().decode(ChatRequest.self, from: messageData) + return try? JSONDecoder().decode(JSONRPCRequest.self, from: messageData) } - private func handleWCRequest(topic: String, request: ChatRequest) { + private func handleWCRequest(topic: String, request: JSONRPCRequest) { let payload = RequestSubscriptionPayload(topic: topic, request: request) requestPublisherSubject.send(payload) } private func handleJsonRpcResponse(response: JSONRPCResponse) { - // todo + do { + let record = try jsonRpcHistory.resolve(response: JsonRpcResult.response(response)) + let params = try record.request.params.get(ChatRequestParams.self) + let chatResponse = ChatResponse( + topic: record.topic, + requestMethod: record.request.method, + requestParams: params, + result: JsonRpcResult.response(response)) + responsePublisherSubject.send(chatResponse) + } catch { + logger.debug("Handle json rpc response error: \(error)") + } } private func handleJsonRpcErrorResponse(response: JSONRPCErrorResponse) { diff --git a/Sources/Chat/ProtocolServices/Common/MessagingService.swift b/Sources/Chat/ProtocolServices/Common/MessagingService.swift new file mode 100644 index 000000000..fecc4ab44 --- /dev/null +++ b/Sources/Chat/ProtocolServices/Common/MessagingService.swift @@ -0,0 +1 @@ +import Foundation diff --git a/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift b/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift new file mode 100644 index 000000000..0a33f7d13 --- /dev/null +++ b/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift @@ -0,0 +1,103 @@ +import Foundation +import WalletConnectKMS +import WalletConnectUtils +import WalletConnectRelay +import Combine + +class InvitationHandlingService { + enum Error: Swift.Error { + case inviteForIdNotFound + } + var onInvite: ((InviteEnvelope)->Void)? + var onNewThread: ((String)->Void)? + private let networkingInteractor: NetworkInteracting + private let invitePayloadStore: CodableStore<(RequestSubscriptionPayload)> + private let topicToInvitationPubKeyStore: CodableStore + private let registry: Registry + private let logger: ConsoleLogging + private let kms: KeyManagementService + private let threadsStore: CodableStore + private var publishers = [AnyCancellable]() + private let codec: Codec + + init(registry: Registry, + networkingInteractor: NetworkInteracting, + kms: KeyManagementService, + logger: ConsoleLogging, + topicToInvitationPubKeyStore: CodableStore, + invitePayloadStore: CodableStore, + threadsStore: CodableStore, + codec: Codec) { + self.registry = registry + self.kms = kms + self.networkingInteractor = networkingInteractor + self.logger = logger + self.topicToInvitationPubKeyStore = topicToInvitationPubKeyStore + self.invitePayloadStore = invitePayloadStore + self.threadsStore = threadsStore + self.codec = codec + setUpRequestHandling() + } + + func accept(inviteId: String) async throws { + + guard let payload = try invitePayloadStore.get(key: inviteId) else { throw Error.inviteForIdNotFound } + + let selfThreadPubKey = try kms.createX25519KeyPair() + + let inviteResponse = InviteResponse(pubKey: selfThreadPubKey.hexRepresentation) + + let response = JsonRpcResult.response(JSONRPCResponse(id: payload.request.id, result: AnyCodable(inviteResponse))) + + try await networkingInteractor.respond(topic: payload.topic, response: response) + + guard case .invite(let inviteParams) = payload.request.params else {return} + + let threadAgreementKeys = try kms.performKeyAgreement(selfPublicKey: selfThreadPubKey, peerPublicKey: inviteParams.pubKey) + + let threadTopic = threadAgreementKeys.derivedTopic() + + try await networkingInteractor.subscribe(topic: threadTopic) + + logger.debug("Accepting an invite") + + onNewThread?(threadTopic) + } + + private func setUpRequestHandling() { + networkingInteractor.requestPublisher.sink { [unowned self] subscriptionPayload in + switch subscriptionPayload.request.params { + case .invite(let invite): + do { + try handleInvite(invite, subscriptionPayload) + } catch { + logger.debug("Did not handle invite, error: \(error)") + } + case .message(let message): + print("received message: \(message)") + } + }.store(in: &publishers) + } + + private func handleInvite(_ inviteParams: InviteParams, _ payload: RequestSubscriptionPayload) throws { + logger.debug("did receive an invite") + guard let selfPubKeyHex = try? topicToInvitationPubKeyStore.get(key: payload.topic) else { + logger.debug("PubKey for invitation topic not found") + return + } + + let selfPubKey = try AgreementPublicKey(hex: selfPubKeyHex) + + let agreementKeysI = try kms.performKeyAgreement(selfPublicKey: selfPubKey, peerPublicKey: inviteParams.pubKey) + + let decryptedData = try codec.decode(sealboxString: inviteParams.invite, symmetricKey: agreementKeysI.sharedKey.rawRepresentation) + + let invite = try JSONDecoder().decode(Invite.self, from: decryptedData) + + try kms.setSymmetricKey(agreementKeysI.sharedKey, for: payload.topic) + + invitePayloadStore.set(payload, forKey: inviteParams.id) + + onInvite?(InviteEnvelope(pubKey: inviteParams.pubKey, invite: invite)) + } +} diff --git a/Sources/Chat/RegistryManager.swift b/Sources/Chat/ProtocolServices/Invitee/RegistryService.swift similarity index 98% rename from Sources/Chat/RegistryManager.swift rename to Sources/Chat/ProtocolServices/Invitee/RegistryService.swift index fde863809..019199a75 100644 --- a/Sources/Chat/RegistryManager.swift +++ b/Sources/Chat/ProtocolServices/Invitee/RegistryService.swift @@ -2,7 +2,7 @@ import Foundation import WalletConnectUtils import WalletConnectKMS -actor RegistryManager { +actor RegistryService { let networkingInteractor: NetworkInteracting let topicToInvitationPubKeyStore: CodableStore let registry: Registry diff --git a/Sources/Chat/ProtocolServices/Inviter/InviteService.swift b/Sources/Chat/ProtocolServices/Inviter/InviteService.swift new file mode 100644 index 000000000..0ad0a5644 --- /dev/null +++ b/Sources/Chat/ProtocolServices/Inviter/InviteService.swift @@ -0,0 +1,85 @@ +import Foundation +import WalletConnectKMS +import WalletConnectUtils +import Combine + +class InviteService { + private var publishers = [AnyCancellable]() + let networkingInteractor: NetworkInteracting + let logger: ConsoleLogging + let kms: KeyManagementService + let codec: Codec + + var onNewThread: ((String)->Void)? + var onInvite: ((InviteParams)->Void)? + + init(networkingInteractor: NetworkInteracting, + kms: KeyManagementService, + logger: ConsoleLogging, + codec: Codec) { + self.kms = kms + self.networkingInteractor = networkingInteractor + self.logger = logger + self.codec = codec + setUpResponseHandling() + } + + func invite(peerPubKey: String, openingMessage: String, account: Account) async throws { + let selfPubKeyY = try kms.createX25519KeyPair() + let invite = Invite(message: openingMessage, account: account) + + let symKeyI = try kms.performKeyAgreement(selfPublicKey: selfPubKeyY, peerPublicKey: peerPubKey) + let inviteTopic = try AgreementPublicKey(hex: peerPubKey).rawRepresentation.sha256().toHexString() + + try kms.setSymmetricKey(symKeyI.sharedKey, for: inviteTopic) + + let encodedInvite = try codec.encode(plaintext: invite.json(), symmetricKey: symKeyI.sharedKey.rawRepresentation) + let inviteRequestParams = InviteParams(pubKey: selfPubKeyY.hexRepresentation, invite: encodedInvite) + + let request = JSONRPCRequest(params: .invite(inviteRequestParams)) + + try await networkingInteractor.subscribe(topic: inviteTopic) + + try await networkingInteractor.requestUnencrypted(request, topic: inviteTopic) + + logger.debug("invite sent on topic: \(inviteTopic)") + } + + private func setUpResponseHandling() { + networkingInteractor.responsePublisher + .sink { [unowned self] response in + switch response.requestParams { + case .invite: + handleInviteResponse(response) + default: + return + } + }.store(in: &publishers) + } + + private func handleInviteResponse(_ response: ChatResponse) { + switch response.result { + case .response(let jsonrpc): + do { + let inviteResponse = try jsonrpc.result.get(InviteResponse.self) + logger.debug("Invite has been accepted") + guard case .invite(let inviteParams) = response.requestParams else { return } + Task { try await createThread(selfPubKeyHex: inviteParams.pubKey, peerPubKey: inviteResponse.pubKey)} + } catch { + logger.debug("Handling invite response has failed") + } + case .error: + logger.debug("Invite has been rejected") + // TODO - remove keys, clean storage + } + } + + private func createThread(selfPubKeyHex: String, peerPubKey: String) async throws { + let selfPubKey = try AgreementPublicKey(hex: selfPubKeyHex) + let agreementKeys = try kms.performKeyAgreement(selfPublicKey: selfPubKey, peerPublicKey: peerPubKey) + let threadTopic = agreementKeys.derivedTopic() + try await networkingInteractor.subscribe(topic: threadTopic) + onNewThread?(threadTopic) + // TODO - remove symKeyI + } +} diff --git a/Sources/Chat/StorageDomainIdentifiers.swift b/Sources/Chat/StorageDomainIdentifiers.swift index 574f5525b..4f7adbdae 100644 --- a/Sources/Chat/StorageDomainIdentifiers.swift +++ b/Sources/Chat/StorageDomainIdentifiers.swift @@ -4,4 +4,5 @@ enum StorageDomainIdentifiers: String { case topicToInvitationPubKey = "com.walletconnect.chat.topicToInvitationPubKey" case invite = "com.walletconnect.chat.invite" case threads = "com.walletconnect.chat.threads" + case jsonRpcHistory = "com.walletconnect.chat.jsonRpcHistory" } diff --git a/Sources/Chat/Types/ChatError.swift b/Sources/Chat/Types/ChatError.swift index 96b71ce0e..0672ba16f 100644 --- a/Sources/Chat/Types/ChatError.swift +++ b/Sources/Chat/Types/ChatError.swift @@ -1,7 +1,6 @@ import Foundation enum ChatError: Error { - case noPublicKeyForInviteId case noInviteForId case recordNotFound } diff --git a/Sources/Chat/Types/ChatRequest.swift b/Sources/Chat/Types/ChatRequest.swift deleted file mode 100644 index 9fe2b075d..000000000 --- a/Sources/Chat/Types/ChatRequest.swift +++ /dev/null @@ -1,73 +0,0 @@ -import Foundation -import WalletConnectUtils - -struct ChatRequest: Codable { - let id: Int64 - let jsonrpc: String - let method: Method - let params: Params - - enum CodingKeys: CodingKey { - case id - case jsonrpc - case method - case params - } - - internal init(id: Int64 = generateId(), jsonrpc: String = "2.0", params: Params) { - self.id = id - self.jsonrpc = jsonrpc - self.params = params - switch params { - case .invite: - self.method = Method.invite - case .message: - self.method = Method.message - } - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - id = try container.decode(Int64.self, forKey: .id) - jsonrpc = try container.decode(String.self, forKey: .jsonrpc) - method = try container.decode(Method.self, forKey: .method) - switch method { - case .invite: - let paramsValue = try container.decode(Invite.self, forKey: .params) - params = .invite(paramsValue) - case .message: - let paramsValue = try container.decode(String.self, forKey: .params) - params = .message(paramsValue) - } - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(id, forKey: .id) - try container.encode(jsonrpc, forKey: .jsonrpc) - try container.encode(method.rawValue, forKey: .method) - switch params { - case .message(let params): - try container.encode(params, forKey: .params) - case .invite(let params): - try container.encode(params, forKey: .params) - } - } - - private static func generateId() -> Int64 { - return Int64(Date().timeIntervalSince1970 * 1000)*1000 + Int64.random(in: 0..<1000) - } -} - -extension ChatRequest { - enum Method: String, Codable { - case invite = "wc_chatInvite" - case message = "wc_chatMessage" - } -} -extension ChatRequest { - enum Params: Codable { - case invite(Invite) - case message(String) - } -} diff --git a/Sources/Chat/Types/ChatRequestParams.swift b/Sources/Chat/Types/ChatRequestParams.swift new file mode 100644 index 000000000..d0ae75e86 --- /dev/null +++ b/Sources/Chat/Types/ChatRequestParams.swift @@ -0,0 +1,20 @@ +import Foundation +import WalletConnectUtils + +enum ChatRequestParams: Codable, Equatable { + case invite(InviteParams) + case message(String) +} + +extension JSONRPCRequest { + init(id: Int64 = JSONRPCRequest.generateId(), params: T) where T == ChatRequestParams { + var method: String! + switch params { + case .invite: + method = "invite" + case .message: + method = "message" + } + self.init(id: id, method: method, params: params) + } +} diff --git a/Sources/Chat/Types/ChatResponse.swift b/Sources/Chat/Types/ChatResponse.swift index 54ba8beb5..448abb7e8 100644 --- a/Sources/Chat/Types/ChatResponse.swift +++ b/Sources/Chat/Types/ChatResponse.swift @@ -3,7 +3,7 @@ import WalletConnectUtils struct ChatResponse: Codable { let topic: String - let requestMethod: ChatRequest.Method - let requestParams: ChatRequest.Params + let requestMethod: String + let requestParams: ChatRequestParams let result: JsonRpcResult } diff --git a/Sources/Chat/Types/Invite.swift b/Sources/Chat/Types/Invite.swift deleted file mode 100644 index 618ae4e48..000000000 --- a/Sources/Chat/Types/Invite.swift +++ /dev/null @@ -1,10 +0,0 @@ -import Foundation - -struct Invite: Codable { - let pubKey: String - let openingMessage: String - - var id: String { - return pubKey - } -} diff --git a/Sources/Chat/Types/InviteParams.swift b/Sources/Chat/Types/InviteParams.swift new file mode 100644 index 000000000..741ffeb61 --- /dev/null +++ b/Sources/Chat/Types/InviteParams.swift @@ -0,0 +1,25 @@ +import WalletConnectUtils +import Foundation + +struct InviteParams: Codable, Equatable { + let pubKey: String + let invite: String + + var id: String { + return pubKey + } +} + +struct InviteResponse: Codable { + let pubKey: String +} + +struct Invite: Codable { + let message: String + let account: Account +} + +public struct InviteEnvelope: Codable { + let pubKey: String + let invite: Invite +} diff --git a/Sources/Chat/Types/RequestSubscriptionPayload.swift b/Sources/Chat/Types/RequestSubscriptionPayload.swift index d7a8066e9..575af574a 100644 --- a/Sources/Chat/Types/RequestSubscriptionPayload.swift +++ b/Sources/Chat/Types/RequestSubscriptionPayload.swift @@ -1,6 +1,7 @@ import Foundation +import WalletConnectUtils struct RequestSubscriptionPayload: Codable { let topic: String - let request: ChatRequest + let request: JSONRPCRequest } diff --git a/Sources/WalletConnectKMS/Codec/ChaChaPolyCodec.swift b/Sources/WalletConnectKMS/Codec/ChaChaPolyCodec.swift index c95cfc5e1..396bd087a 100644 --- a/Sources/WalletConnectKMS/Codec/ChaChaPolyCodec.swift +++ b/Sources/WalletConnectKMS/Codec/ChaChaPolyCodec.swift @@ -3,26 +3,28 @@ import Foundation import CryptoKit -protocol Codec { +public protocol Codec { func encode(plaintext: String, symmetricKey: Data, nonce: ChaChaPoly.Nonce) throws -> String func decode(sealboxString: String, symmetricKey: Data) throws -> Data } -extension Codec { +public extension Codec { func encode(plaintext: String, symmetricKey: Data, nonce: ChaChaPoly.Nonce = ChaChaPoly.Nonce()) throws -> String { try encode(plaintext: plaintext, symmetricKey: symmetricKey, nonce: nonce) } } -class ChaChaPolyCodec: Codec { +public class ChaChaPolyCodec: Codec { + + public init() {} /// nonce should always be random, exposed in parameter for testing purpose only - func encode(plaintext: String, symmetricKey: Data, nonce: ChaChaPoly.Nonce) throws -> String { + public func encode(plaintext: String, symmetricKey: Data, nonce: ChaChaPoly.Nonce) throws -> String { let key = CryptoKit.SymmetricKey(data: symmetricKey) let dataToSeal = try data(string: plaintext) let sealBox = try ChaChaPoly.seal(dataToSeal, using: key, nonce: nonce) return sealBox.combined.base64EncodedString() } - func decode(sealboxString: String, symmetricKey: Data) throws -> Data { + public func decode(sealboxString: String, symmetricKey: Data) throws -> Data { guard let sealboxData = Data(base64Encoded: sealboxString) else { throw CodecError.malformedSealbox } diff --git a/Tests/ChatTests/EndToEndTests.swift b/Tests/ChatTests/EndToEndTests.swift index 10b13c541..ce62a3dd3 100644 --- a/Tests/ChatTests/EndToEndTests.swift +++ b/Tests/ChatTests/EndToEndTests.swift @@ -8,28 +8,28 @@ import WalletConnectRelay import Combine final class ChatTests: XCTestCase { - var client1: Chat! - var client2: Chat! + var invitee: Chat! + var inviter: Chat! var registry: KeyValueRegistry! private var publishers = [AnyCancellable]() override func setUp() { registry = KeyValueRegistry() - client1 = makeClient(prefix: "🦖") - client2 = makeClient(prefix: "🍄") + invitee = makeClient(prefix: "🦖 Registered") + inviter = makeClient(prefix: "🍄 Inviter") } private func waitClientsConnected() async { let group = DispatchGroup() group.enter() - client1.socketConnectionStatusPublisher.sink { status in + invitee.socketConnectionStatusPublisher.sink { status in if status == .connected { group.leave() } }.store(in: &publishers) group.enter() - client2.socketConnectionStatusPublisher.sink { status in + inviter.socketConnectionStatusPublisher.sink { status in if status == .connected { group.leave() } @@ -52,26 +52,34 @@ final class ChatTests: XCTestCase { await waitClientsConnected() let inviteExpectation = expectation(description: "invitation expectation") let account = Account(chainIdentifier: "eip155:1", address: "0x3627523167367216556273151")! - let pubKey = try! await client1.register(account: account) - try! await client2.invite(publicKey: pubKey, openingMessage: "") - client1.invitePublisher.sink { _ in + let pubKey = try! await invitee.register(account: account) + try! await inviter.invite(publicKey: pubKey, openingMessage: "") + invitee.invitePublisher.sink { _ in inviteExpectation.fulfill() }.store(in: &publishers) wait(for: [inviteExpectation], timeout: 4) } -// func testNewThread() async { -// await waitClientsConnected() -// let newThreadExpectation = expectation(description: "new thread expectation") -// let account = Account(chainIdentifier: "eip155:1", address: "0x3627523167367216556273151")! -// client1.register(account: account) -// client2.invite(account: account) -// client1.onInvite = { [unowned self] invite in -// client1.accept(invite: invite) -// } -// client1.onNewThread = { [unowned self] thread in -// newThreadExpectation.fulfill() -// } -// wait(for: [newThreadExpectation], timeout: 4) -// } + func testAcceptAndCreateNewThread() async { + await waitClientsConnected() + let newThreadInviterExpectation = expectation(description: "new thread on inviting client expectation") + let newThreadinviteeExpectation = expectation(description: "new thread on invitee client expectation") + let account = Account(chainIdentifier: "eip155:1", address: "0x3627523167367216556273151")! + let pubKey = try! await invitee.register(account: account) + try! await inviter.invite(publicKey: pubKey, openingMessage: "opening message") + + invitee.invitePublisher.sink { [unowned self] inviteEnvelope in + Task {try! await invitee.accept(inviteId: inviteEnvelope.pubKey)} + }.store(in: &publishers) + + invitee.newThreadPublisher.sink { _ in + newThreadinviteeExpectation.fulfill() + }.store(in: &publishers) + + inviter.newThreadPublisher.sink { _ in + newThreadInviterExpectation.fulfill() + }.store(in: &publishers) + + wait(for: [newThreadinviteeExpectation, newThreadInviterExpectation], timeout: 4) + } } diff --git a/Tests/ChatTests/Mocks/NetworkingInteractorMock.swift b/Tests/ChatTests/Mocks/NetworkingInteractorMock.swift index 890d00ecd..a461e1f65 100644 --- a/Tests/ChatTests/Mocks/NetworkingInteractorMock.swift +++ b/Tests/ChatTests/Mocks/NetworkingInteractorMock.swift @@ -1,7 +1,33 @@ import Foundation @testable import Chat +import Combine +import WalletConnectUtils class NetworkingInteractorMock: NetworkInteracting { + + let responsePublisherSubject = PassthroughSubject() + let requestPublisherSubject = PassthroughSubject() + + var requestPublisher: AnyPublisher { + requestPublisherSubject.eraseToAnyPublisher() + } + + var responsePublisher: AnyPublisher { + responsePublisherSubject.eraseToAnyPublisher() + } + + func requestUnencrypted(_ request: JSONRPCRequest, topic: String) async throws { + + } + + func request(_ request: JSONRPCRequest, topic: String) async throws { + + } + + func respond(topic: String, response: JsonRpcResult) async throws { + + } + private(set) var subscriptions: [String] = [] func subscribe(topic: String) async throws { diff --git a/Tests/ChatTests/RegistryManagerTests.swift b/Tests/ChatTests/RegistryManagerTests.swift index fc3ee495a..9a62f5248 100644 --- a/Tests/ChatTests/RegistryManagerTests.swift +++ b/Tests/ChatTests/RegistryManagerTests.swift @@ -6,7 +6,7 @@ import WalletConnectUtils @testable import TestingUtils final class RegistryManagerTests: XCTestCase { - var registryManager: RegistryManager! + var registryManager: RegistryService! var networkingInteractor: NetworkingInteractorMock! var topicToInvitationPubKeyStore: CodableStore! var registry: Registry! @@ -17,7 +17,7 @@ final class RegistryManagerTests: XCTestCase { networkingInteractor = NetworkingInteractorMock() kms = KeyManagementServiceMock() topicToInvitationPubKeyStore = CodableStore(defaults: RuntimeKeyValueStorage(), identifier: "") - registryManager = RegistryManager( + registryManager = RegistryService( registry: registry, networkingInteractor: networkingInteractor, kms: kms, diff --git a/Tests/WalletConnectSignTests/Mocks/MockedRelay.swift b/Tests/WalletConnectSignTests/Mocks/NetworkingInteractorMock.swift similarity index 100% rename from Tests/WalletConnectSignTests/Mocks/MockedRelay.swift rename to Tests/WalletConnectSignTests/Mocks/NetworkingInteractorMock.swift