Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#chat/registry #262

Merged
merged 7 commits into from
Jun 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 36 additions & 10 deletions Sources/Chat/Chat.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<SocketConnectionStatus, Never>

var newThreadPublisherSubject = PassthroughSubject<Thread, Never>()
Expand All @@ -29,13 +30,13 @@ class Chat {
kms: KeyManagementService,
logger: ConsoleLogging = ConsoleLogger(loggingLevel: .off),
keyValueStorage: KeyValueStorage) {
let topicToInvitationPubKeyStore = CodableStore<String>(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<String>(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.topicToInvitationPubKey.rawValue)
let inviteStore = CodableStore<Invite>(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,
Expand All @@ -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) {

}

Expand Down
29 changes: 8 additions & 21 deletions Sources/Chat/Engine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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")
Expand Down
14 changes: 7 additions & 7 deletions Sources/Chat/NetworkingInteractor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<RequestSubscriptionPayload, Never> {
Expand Down Expand Up @@ -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) {
Expand Down
16 changes: 9 additions & 7 deletions Sources/Chat/Registry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What benefits actor gives us here?

We could make it class and make registryStore private. So it couldn't be mutated from outside too

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

true it is better registryStore is private
but I think making it an actor gives thread safety

So it couldn't be mutated from outside too

It could be mutated by calling register() from different threads
anyway this class is mimicking an http client right now


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
}
}

35 changes: 35 additions & 0 deletions Sources/Chat/RegistryManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@

import Foundation
import WalletConnectUtils
import WalletConnectKMS

actor RegistryManager {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What the difference between Manager and Engine naming?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am always scratching my head when giving them a names. in this context it just sounds better to me. Any suggestions?

let networkingInteractor: NetworkInteracting
let topicToInvitationPubKeyStore: CodableStore<String>
let registry: Registry
let logger: ConsoleLogging
let kms: KeyManagementServiceProtocol

init(registry: Registry,
networkingInteractor: NetworkInteracting,
kms: KeyManagementServiceProtocol,
logger: ConsoleLogging,
topicToInvitationPubKeyStore: CodableStore<String>) {
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
}
}
1 change: 1 addition & 0 deletions Sources/Chat/Types/ChatError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ import Foundation
enum ChatError: Error {
case noPublicKeyForInviteId
case noInviteForId
case recordNotFound
}
9 changes: 7 additions & 2 deletions Sources/Chat/Types/ChatRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}


Expand Down
2 changes: 1 addition & 1 deletion Sources/Chat/Types/Invite.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Foundation

struct Invite: Codable {
let pubKey: String
let message: String
let openingMessage: String

var id: String {
return pubKey
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
15 changes: 15 additions & 0 deletions Tests/ChatTests/Mocks/NetworkingInteractorMock.swift
Original file line number Diff line number Diff line change
@@ -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 }
}
}
37 changes: 37 additions & 0 deletions Tests/ChatTests/RegistryManagerTests.swift
Original file line number Diff line number Diff line change
@@ -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<String>!
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")
}
}

4 changes: 2 additions & 2 deletions Tests/WalletConnectSignTests/ApproveEngineTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand All @@ -19,7 +19,7 @@ final class ApproveEngineTests: XCTestCase {

override func setUp() {
metadata = AppMetadata.stub()
networkingInteractor = MockedWCRelay()
networkingInteractor = NetworkingInteractorMock()
cryptoMock = KeyManagementServiceMock()
pairingStorageMock = WCPairingStorageMock()
sessionStorageMock = WCSessionStorageMock()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
2 changes: 1 addition & 1 deletion Tests/WalletConnectSignTests/Mocks/MockedRelay.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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] = []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
Loading