diff --git a/Sources/Chat/Chat.swift b/Sources/Chat/Chat.swift index 2bf928666..01b5b42e7 100644 --- a/Sources/Chat/Chat.swift +++ b/Sources/Chat/Chat.swift @@ -9,9 +9,10 @@ import Combine class Chat { private var publishers = [AnyCancellable]() let registry: Registry + let registryManager: RegistryManager let engine: Engine let kms: KeyManagementService - var onConnected: (()->())? + let socketConnectionStatusPublisher: AnyPublisher var newThreadPublisherSubject = PassthroughSubject() @@ -29,13 +30,13 @@ class Chat { kms: KeyManagementService, logger: ConsoleLogging = ConsoleLogger(loggingLevel: .off), keyValueStorage: KeyValueStorage) { + let topicToInvitationPubKeyStore = CodableStore(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.topicToInvitationPubKey.rawValue) self.registry = registry - self.kms = kms let serialiser = Serializer(kms: kms) let networkingInteractor = NetworkingInteractor(relayClient: relayClient, serializer: serialiser) - let topicToInvitationPubKeyStore = CodableStore(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.topicToInvitationPubKey.rawValue) 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, networkingInteractor: networkingInteractor, kms: kms, @@ -47,19 +48,44 @@ class Chat { setUpEnginesCallbacks() } - func register(account: Account) { - engine.register(account: account) + /// Registers a new record on Chat keyserver, + /// record is a blockchain account with a client generated public key + /// - Parameter account: CAIP10 blockchain account + /// - Returns: public key + func register(account: Account) async throws -> String { + try await registryManager.register(account: account) } - func invite(account: Account) throws { - try engine.invite(account: account) + /// Queries the default keyserver with a blockchain account + /// - Parameter account: CAIP10 blockachain account + /// - Returns: public key associated with an account in chat's keyserver + func resolve(account: Account) async throws -> String { + try await registry.resolve(account: account) } - func accept(inviteId: String) throws { - try engine.accept(inviteId: inviteId) + /// Sends a chat invite with opening message + /// - Parameters: + /// - 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) + } + + func accept(inviteId: String) async throws { + try await engine.accept(inviteId: inviteId) + } + + /// Sends a chat message to an active chat thread + /// - Parameters: + /// - topic: thread topic + /// - message: chat message + func message(topic: String, message: String) { + } - func message(threadTopic: String, message: String) { + /// To Ping peer client + /// - Parameter topic: chat thread topic + func ping(topic: String) { } diff --git a/Sources/Chat/Engine.swift b/Sources/Chat/Engine.swift index a2b4a9fda..eaff34169 100644 --- a/Sources/Chat/Engine.swift +++ b/Sources/Chat/Engine.swift @@ -35,22 +35,20 @@ class Engine { setUpResponseHandling() } - func invite(account: Account) throws { - let peerPubKeyHex = registry.resolve(account: account)! - print("resolved pub key: \(peerPubKeyHex)") + func invite(peerPubKey: String, openingMessage: String) async throws { let pubKey = try kms.createX25519KeyPair() - let invite = Invite(pubKey: pubKey.hexRepresentation, message: "hello") - let topic = try AgreementPublicKey(hex: peerPubKeyHex).rawRepresentation.sha256().toHexString() - let request = ChatRequest(method: .invite, params: .invite(invite)) + 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: peerPubKeyHex) + let agreementKeys = try kms.performKeyAgreement(selfPublicKey: pubKey, peerPublicKey: peerPubKey) let threadTopic = agreementKeys.derivedTopic() - networkingInteractor.subscribe(topic: threadTopic) + try await networkingInteractor.subscribe(topic: threadTopic) logger.debug("invite sent on topic: \(topic)") } - func accept(inviteId: String) throws { + func accept(inviteId: String) async throws { guard let hexPubKey = try topicToInvitationPubKeyStore.get(key: "todo-topic") else { throw ChatError.noPublicKeyForInviteId } @@ -61,21 +59,10 @@ class Engine { logger.debug("accepting an invitation") let agreementKeys = try! kms.performKeyAgreement(selfPublicKey: pubKey, peerPublicKey: invite.pubKey) let topic = agreementKeys.derivedTopic() - networkingInteractor.subscribe(topic: topic) + try await networkingInteractor.subscribe(topic: topic) fatalError("not implemented") } - func register(account: Account) { - let pubKey = try! kms.createX25519KeyPair() - let pubKeyHex = pubKey.hexRepresentation - print("registered pubKey: \(pubKeyHex)") - registry.register(account: account, pubKey: pubKeyHex) - let topic = pubKey.rawRepresentation.sha256().toHexString() - try! topicToInvitationPubKeyStore.set(pubKeyHex, forKey: topic) - networkingInteractor.subscribe(topic: topic) - print("did register and is subscribing on topic: \(topic)") - } - private func handleInvite(_ invite: Invite) { onInvite?(invite) logger.debug("did receive an invite") diff --git a/Sources/Chat/NetworkingInteractor.swift b/Sources/Chat/NetworkingInteractor.swift index 27894d172..8cdd1b44c 100644 --- a/Sources/Chat/NetworkingInteractor.swift +++ b/Sources/Chat/NetworkingInteractor.swift @@ -4,7 +4,11 @@ import Combine import WalletConnectRelay import WalletConnectUtils -class NetworkingInteractor { +protocol NetworkInteracting { + func subscribe(topic: String) async throws +} + +class NetworkingInteractor: NetworkInteracting { let relayClient: RelayClient private let serializer: Serializing var requestPublisher: AnyPublisher { @@ -41,12 +45,8 @@ class NetworkingInteractor { } } - func subscribe(topic: String) { - relayClient.subscribe(topic: topic) { [weak self] error in -// if let error = error { -// print(error) -// } - } + func subscribe(topic: String) async throws { + try await relayClient.subscribe(topic: topic) } private func manageSubscription(_ topic: String, _ message: String) { diff --git a/Sources/Chat/Registry.swift b/Sources/Chat/Registry.swift index 57d18b908..f01eaa7fc 100644 --- a/Sources/Chat/Registry.swift +++ b/Sources/Chat/Registry.swift @@ -3,19 +3,21 @@ import Foundation import WalletConnectUtils protocol Registry { - func register(account: Account, pubKey: String) - func resolve(account: Account) -> String? + func register(account: Account, pubKey: String) async throws + func resolve(account: Account) async throws -> String } -class KeyValueRegistry: Registry { +actor KeyValueRegistry: Registry { - var registryStore: [Account: String] = [:] + private var registryStore: [Account: String] = [:] - func register(account address: Account, pubKey: String) { + func register(account address: Account, pubKey: String) async throws { registryStore[address] = pubKey } - func resolve(account: Account) -> String? { - return registryStore[account] + func resolve(account: Account) async throws -> String { + guard let record = registryStore[account] else { throw ChatError.recordNotFound} + return record } } + diff --git a/Sources/Chat/RegistryManager.swift b/Sources/Chat/RegistryManager.swift new file mode 100644 index 000000000..ff0d84116 --- /dev/null +++ b/Sources/Chat/RegistryManager.swift @@ -0,0 +1,35 @@ + +import Foundation +import WalletConnectUtils +import WalletConnectKMS + +actor RegistryManager { + let networkingInteractor: NetworkInteracting + let topicToInvitationPubKeyStore: CodableStore + let registry: Registry + let logger: ConsoleLogging + let kms: KeyManagementServiceProtocol + + init(registry: Registry, + networkingInteractor: NetworkInteracting, + kms: KeyManagementServiceProtocol, + logger: ConsoleLogging, + topicToInvitationPubKeyStore: CodableStore) { + self.registry = registry + self.kms = kms + self.networkingInteractor = networkingInteractor + self.logger = logger + self.topicToInvitationPubKeyStore = topicToInvitationPubKeyStore + } + + func register(account: Account) async throws -> String { + let pubKey = try kms.createX25519KeyPair() + let pubKeyHex = pubKey.hexRepresentation + try await registry.register(account: account, pubKey: pubKeyHex) + let topic = pubKey.rawRepresentation.sha256().toHexString() + topicToInvitationPubKeyStore.set(pubKeyHex, forKey: topic) + try await networkingInteractor.subscribe(topic: topic) + logger.debug("Did register an account: \(account) and is subscribing on topic: \(topic)") + return pubKeyHex + } +} diff --git a/Sources/Chat/Types/ChatError.swift b/Sources/Chat/Types/ChatError.swift index 3238d4c62..6c3dafbe9 100644 --- a/Sources/Chat/Types/ChatError.swift +++ b/Sources/Chat/Types/ChatError.swift @@ -4,4 +4,5 @@ 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 index b8f90cd6b..cbbf372a4 100644 --- a/Sources/Chat/Types/ChatRequest.swift +++ b/Sources/Chat/Types/ChatRequest.swift @@ -15,11 +15,16 @@ struct ChatRequest: Codable { case params } - internal init(id: Int64 = generateId(), jsonrpc: String = "2.0", method: Method, params: Params) { + internal init(id: Int64 = generateId(), jsonrpc: String = "2.0", params: Params) { self.id = id self.jsonrpc = jsonrpc - self.method = method self.params = params + switch params { + case .invite(_): + self.method = Method.invite + case .message(_): + self.method = Method.message + } } diff --git a/Sources/Chat/Types/Invite.swift b/Sources/Chat/Types/Invite.swift index a5c50c475..e772538b8 100644 --- a/Sources/Chat/Types/Invite.swift +++ b/Sources/Chat/Types/Invite.swift @@ -4,7 +4,7 @@ import Foundation struct Invite: Codable { let pubKey: String - let message: String + let openingMessage: String var id: String { return pubKey diff --git a/Tests/ChatTests/endToEndTests.swift b/Tests/ChatTests/EndToEndTests.swift similarity index 93% rename from Tests/ChatTests/endToEndTests.swift rename to Tests/ChatTests/EndToEndTests.swift index 9aa030bf1..e95c447c2 100644 --- a/Tests/ChatTests/endToEndTests.swift +++ b/Tests/ChatTests/EndToEndTests.swift @@ -53,9 +53,9 @@ final class ChatTests: XCTestCase { await waitClientsConnected() let inviteExpectation = expectation(description: "invitation expectation") let account = Account(chainIdentifier: "eip155:1", address: "0x3627523167367216556273151")! - client1.register(account: account) - try! client2.invite(account: account) - client1.invitePublisher.sink { invite in + let pubKey = try! await client1.register(account: account) + try! await client2.invite(publicKey: pubKey, openingMessage: "") + client1.invitePublisher.sink { _ in inviteExpectation.fulfill() }.store(in: &publishers) wait(for: [inviteExpectation], timeout: 4) diff --git a/Tests/ChatTests/Mocks/NetworkingInteractorMock.swift b/Tests/ChatTests/Mocks/NetworkingInteractorMock.swift new file mode 100644 index 000000000..30f6e4a50 --- /dev/null +++ b/Tests/ChatTests/Mocks/NetworkingInteractorMock.swift @@ -0,0 +1,15 @@ + +import Foundation +@testable import Chat + +class NetworkingInteractorMock: NetworkInteracting { + private(set) var subscriptions: [String] = [] + + func subscribe(topic: String) async throws { + subscriptions.append(topic) + } + + func didSubscribe(to topic: String) -> Bool { + subscriptions.contains { $0 == topic } + } +} diff --git a/Tests/ChatTests/RegistryManagerTests.swift b/Tests/ChatTests/RegistryManagerTests.swift new file mode 100644 index 000000000..f68398003 --- /dev/null +++ b/Tests/ChatTests/RegistryManagerTests.swift @@ -0,0 +1,37 @@ +import Foundation +import XCTest +@testable import Chat +import WalletConnectUtils +@testable import WalletConnectKMS +@testable import TestingUtils + +final class RegistryManagerTests: XCTestCase { + var registryManager: RegistryManager! + var networkingInteractor: NetworkingInteractorMock! + var topicToInvitationPubKeyStore: CodableStore! + var registry: Registry! + var kms: KeyManagementServiceMock! + + override func setUp() { + registry = KeyValueRegistry() + networkingInteractor = NetworkingInteractorMock() + kms = KeyManagementServiceMock() + topicToInvitationPubKeyStore = CodableStore(defaults: RuntimeKeyValueStorage(), identifier: "") + registryManager = RegistryManager( + registry: registry, + networkingInteractor: networkingInteractor, + kms: kms, + logger: ConsoleLoggerMock(), + topicToInvitationPubKeyStore: topicToInvitationPubKeyStore) + } + + func testRegister() async { + let account = Account("eip155:1:0xab16a96d359ec26a11e2c2b3d8f8b8942d5bfcdb")! + try! await registryManager.register(account: account) + XCTAssert(!networkingInteractor.subscriptions.isEmpty, "networkingInteractors subscribes to new topic") + let resolved = try! await registry.resolve(account: account) + XCTAssertNotNil(resolved, "register account is resolvable") + XCTAssertFalse(topicToInvitationPubKeyStore.getAll().isEmpty, "stores topic to invitation") + } +} + diff --git a/Tests/WalletConnectSignTests/ApproveEngineTests.swift b/Tests/WalletConnectSignTests/ApproveEngineTests.swift index a0222fb29..d237c49b9 100644 --- a/Tests/WalletConnectSignTests/ApproveEngineTests.swift +++ b/Tests/WalletConnectSignTests/ApproveEngineTests.swift @@ -9,7 +9,7 @@ final class ApproveEngineTests: XCTestCase { var engine: ApproveEngine! var metadata: AppMetadata! - var networkingInteractor: MockedWCRelay! + var networkingInteractor: NetworkingInteractorMock! var cryptoMock: KeyManagementServiceMock! var pairingStorageMock: WCPairingStorageMock! var sessionStorageMock: WCSessionStorageMock! @@ -19,7 +19,7 @@ final class ApproveEngineTests: XCTestCase { override func setUp() { metadata = AppMetadata.stub() - networkingInteractor = MockedWCRelay() + networkingInteractor = NetworkingInteractorMock() cryptoMock = KeyManagementServiceMock() pairingStorageMock = WCPairingStorageMock() sessionStorageMock = WCSessionStorageMock() diff --git a/Tests/WalletConnectSignTests/ControllerSessionStateMachineTests.swift b/Tests/WalletConnectSignTests/ControllerSessionStateMachineTests.swift index 04345ff41..766990cfe 100644 --- a/Tests/WalletConnectSignTests/ControllerSessionStateMachineTests.swift +++ b/Tests/WalletConnectSignTests/ControllerSessionStateMachineTests.swift @@ -7,12 +7,12 @@ import WalletConnectKMS class ControllerSessionStateMachineTests: XCTestCase { var sut: ControllerSessionStateMachine! - var networkingInteractor: MockedWCRelay! + var networkingInteractor: NetworkingInteractorMock! var storageMock: WCSessionStorageMock! var cryptoMock: KeyManagementServiceMock! override func setUp() { - networkingInteractor = MockedWCRelay() + networkingInteractor = NetworkingInteractorMock() storageMock = WCSessionStorageMock() cryptoMock = KeyManagementServiceMock() sut = ControllerSessionStateMachine(networkingInteractor: networkingInteractor, kms: cryptoMock, sessionStore: storageMock, logger: ConsoleLoggerMock()) diff --git a/Tests/WalletConnectSignTests/Mocks/MockedRelay.swift b/Tests/WalletConnectSignTests/Mocks/MockedRelay.swift index 40ca53503..6f5bb3bb9 100644 --- a/Tests/WalletConnectSignTests/Mocks/MockedRelay.swift +++ b/Tests/WalletConnectSignTests/Mocks/MockedRelay.swift @@ -5,7 +5,7 @@ import WalletConnectUtils @testable import WalletConnectSign @testable import TestingUtils -class MockedWCRelay: NetworkInteracting { +class NetworkingInteractorMock: NetworkInteracting { private(set) var subscriptions: [String] = [] private(set) var unsubscriptions: [String] = [] diff --git a/Tests/WalletConnectSignTests/NonControllerSessionStateMachineTests.swift b/Tests/WalletConnectSignTests/NonControllerSessionStateMachineTests.swift index 1f3dfa111..4ab099605 100644 --- a/Tests/WalletConnectSignTests/NonControllerSessionStateMachineTests.swift +++ b/Tests/WalletConnectSignTests/NonControllerSessionStateMachineTests.swift @@ -7,12 +7,12 @@ import WalletConnectKMS class NonControllerSessionStateMachineTests: XCTestCase { var sut: NonControllerSessionStateMachine! - var networkingInteractor: MockedWCRelay! + var networkingInteractor: NetworkingInteractorMock! var storageMock: WCSessionStorageMock! var cryptoMock: KeyManagementServiceMock! override func setUp() { - networkingInteractor = MockedWCRelay() + networkingInteractor = NetworkingInteractorMock() storageMock = WCSessionStorageMock() cryptoMock = KeyManagementServiceMock() sut = NonControllerSessionStateMachine(networkingInteractor: networkingInteractor, kms: cryptoMock, sessionStore: storageMock, logger: ConsoleLoggerMock()) diff --git a/Tests/WalletConnectSignTests/PairEngineTests.swift b/Tests/WalletConnectSignTests/PairEngineTests.swift index f6c85e4b8..b81337ecb 100644 --- a/Tests/WalletConnectSignTests/PairEngineTests.swift +++ b/Tests/WalletConnectSignTests/PairEngineTests.swift @@ -9,7 +9,7 @@ final class PairEngineTests: XCTestCase { var engine: PairEngine! - var networkingInteractor: MockedWCRelay! + var networkingInteractor: NetworkingInteractorMock! var storageMock: WCPairingStorageMock! var cryptoMock: KeyManagementServiceMock! var proposalPayloadsStore: CodableStore! @@ -17,7 +17,7 @@ final class PairEngineTests: XCTestCase { var topicGenerator: TopicGenerator! override func setUp() { - networkingInteractor = MockedWCRelay() + networkingInteractor = NetworkingInteractorMock() storageMock = WCPairingStorageMock() cryptoMock = KeyManagementServiceMock() topicGenerator = TopicGenerator() diff --git a/Tests/WalletConnectSignTests/PairingEngineTests.swift b/Tests/WalletConnectSignTests/PairingEngineTests.swift index bc08fcfdd..ce764b6da 100644 --- a/Tests/WalletConnectSignTests/PairingEngineTests.swift +++ b/Tests/WalletConnectSignTests/PairingEngineTests.swift @@ -15,7 +15,7 @@ final class PairingEngineTests: XCTestCase { var engine: PairingEngine! var approveEngine: ApproveEngine! - var networkingInteractor: MockedWCRelay! + var networkingInteractor: NetworkingInteractorMock! var storageMock: WCPairingStorageMock! var cryptoMock: KeyManagementServiceMock! @@ -23,7 +23,7 @@ final class PairingEngineTests: XCTestCase { var publishers = Set() override func setUp() { - networkingInteractor = MockedWCRelay() + networkingInteractor = NetworkingInteractorMock() storageMock = WCPairingStorageMock() cryptoMock = KeyManagementServiceMock() topicGenerator = TopicGenerator()