From 3def1c6a98916968c4ae9b5d5dd092f1aeb8f7f5 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Fri, 1 Nov 2024 13:22:18 -0700 Subject: [PATCH] swift format is finally here --- Sources/XMTPiOS/Client.swift | 416 ++++++++++++++++++---------- Tests/XMTPTests/V3ClientTests.swift | 387 ++++++++++++++++---------- 2 files changed, 513 insertions(+), 290 deletions(-) diff --git a/Sources/XMTPiOS/Client.swift b/Sources/XMTPiOS/Client.swift index 059b7681..bbd9cd0a 100644 --- a/Sources/XMTPiOS/Client.swift +++ b/Sources/XMTPiOS/Client.swift @@ -48,7 +48,10 @@ public struct ClientOptions { /// /// Optional: Specify self-reported version e.g. XMTPInbox/v1.0.0. public var appVersion: String? - public init(env: XMTPEnvironment = .dev, isSecure: Bool = true, appVersion: String? = nil) { + public init( + env: XMTPEnvironment = .dev, isSecure: Bool = true, + appVersion: String? = nil + ) { self.env = env self.isSecure = isSecure self.appVersion = appVersion @@ -63,9 +66,9 @@ public struct ClientOptions { /// `preCreateIdentityCallback` will be called immediately before a Create Identity wallet signature is requested from the user. public var preCreateIdentityCallback: PreEventCallback? - - /// `preAuthenticateToInboxCallback` will be called immediately before an Auth Inbox signature is requested from the user - public var preAuthenticateToInboxCallback: PreEventCallback? + + /// `preAuthenticateToInboxCallback` will be called immediately before an Auth Inbox signature is requested from the user + public var preAuthenticateToInboxCallback: PreEventCallback? public var enableV3 = false public var dbEncryptionKey: Data? @@ -77,7 +80,7 @@ public struct ClientOptions { codecs: [any ContentCodec] = [], preEnableIdentityCallback: PreEventCallback? = nil, preCreateIdentityCallback: PreEventCallback? = nil, - preAuthenticateToInboxCallback: PreEventCallback? = nil, + preAuthenticateToInboxCallback: PreEventCallback? = nil, enableV3: Bool = false, encryptionKey: Data? = nil, dbDirectory: String? = nil, @@ -91,14 +94,16 @@ public struct ClientOptions { self.enableV3 = enableV3 self.dbEncryptionKey = encryptionKey self.dbDirectory = dbDirectory - if (historySyncUrl == nil) { + if historySyncUrl == nil { switch api.env { case .production: - self.historySyncUrl = "https://message-history.production.ephemera.network/" + self.historySyncUrl = + "https://message-history.production.ephemera.network/" case .local: self.historySyncUrl = "http://0.0.0.0:5558" default: - self.historySyncUrl = "https://message-history.dev.ephemera.network/" + self.historySyncUrl = + "https://message-history.dev.ephemera.network/" } } else { self.historySyncUrl = historySyncUrl @@ -126,7 +131,6 @@ public final class Client { public let inboxID: String public var hasV2Client: Bool = true - /// Access ``Conversations`` for this Client. public lazy var conversations: Conversations = .init(client: self) @@ -134,7 +138,8 @@ public final class Client { public lazy var contacts: Contacts = .init(client: self) /// The XMTP environment which specifies which network this Client is connected to. - public lazy var environment: XMTPEnvironment = apiClient?.environment ?? .dev + public lazy var environment: XMTPEnvironment = + apiClient?.environment ?? .dev var codecRegistry = CodecRegistry() @@ -143,16 +148,20 @@ public final class Client { } /// Creates a client. - public static func create(account: SigningKey, options: ClientOptions? = nil) async throws -> Client { + public static func create( + account: SigningKey, options: ClientOptions? = nil + ) async throws -> Client { let options = options ?? ClientOptions() do { - let client = try await LibXMTP.createV2Client(host: options.api.env.url, isSecure: options.api.env.isSecure) + let client = try await LibXMTP.createV2Client( + host: options.api.env.url, isSecure: options.api.env.isSecure) let apiClient = try GRPCApiClient( environment: options.api.env, secure: options.api.isSecure, rustClient: client ) - return try await create(account: account, apiClient: apiClient, options: options) + return try await create( + account: account, apiClient: apiClient, options: options) } catch { let detailedErrorMessage: String if let nsError = error as NSError? { @@ -163,7 +172,7 @@ public final class Client { throw ClientError.creationError(detailedErrorMessage) } } - + static func initializeClient( accountAddress: String, options: ClientOptions, @@ -199,9 +208,12 @@ public final class Client { return client } - public static func createV3(account: SigningKey, options: ClientOptions) async throws -> Client { + public static func createV3(account: SigningKey, options: ClientOptions) + async throws -> Client + { let accountAddress = account.address.lowercased() - let inboxId = try await getOrCreateInboxId(options: options, address: accountAddress) + let inboxId = try await getOrCreateInboxId( + options: options, address: accountAddress) return try await initializeClient( accountAddress: accountAddress, @@ -210,10 +222,13 @@ public final class Client { inboxId: inboxId ) } - - public static func buildV3(address: String, options: ClientOptions) async throws -> Client { + + public static func buildV3(address: String, options: ClientOptions) + async throws -> Client + { let accountAddress = address.lowercased() - let inboxId = try await getOrCreateInboxId(options: options, address: accountAddress) + let inboxId = try await getOrCreateInboxId( + options: options, address: accountAddress) return try await initializeClient( accountAddress: accountAddress, @@ -232,18 +247,22 @@ public final class Client { ) async throws -> (FfiXmtpClient?, String) { if options?.enableV3 == true { let address = accountAddress.lowercased() - + let mlsDbDirectory = options?.dbDirectory var directoryURL: URL if let mlsDbDirectory = mlsDbDirectory { let fileManager = FileManager.default - directoryURL = URL(fileURLWithPath: mlsDbDirectory, isDirectory: true) + directoryURL = URL( + fileURLWithPath: mlsDbDirectory, isDirectory: true) // Check if the directory exists, if not, create it if !fileManager.fileExists(atPath: directoryURL.path) { do { - try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil) + try fileManager.createDirectory( + at: directoryURL, withIntermediateDirectories: true, + attributes: nil) } catch { - throw ClientError.creationError("Failed db directory \(mlsDbDirectory)") + throw ClientError.creationError( + "Failed db directory \(mlsDbDirectory)") } } } else { @@ -252,10 +271,12 @@ public final class Client { let alias = "xmtp-\(options?.api.env.rawValue ?? "")-\(inboxId).db3" let dbURL = directoryURL.appendingPathComponent(alias).path - + let encryptionKey = options?.dbEncryptionKey - if (encryptionKey == nil) { - throw ClientError.creationError("No encryption key passed for the database. Please store and provide a secure encryption key.") + if encryptionKey == nil { + throw ClientError.creationError( + "No encryption key passed for the database. Please store and provide a secure encryption key." + ) } let v3Client = try await LibXMTP.createClient( @@ -267,34 +288,48 @@ public final class Client { inboxId: inboxId, accountAddress: address, nonce: 0, - legacySignedPrivateKeyProto: try privateKeyBundleV1?.toV2().identityKey.serializedData(), + legacySignedPrivateKeyProto: try privateKeyBundleV1?.toV2() + .identityKey.serializedData(), historySyncUrl: options?.historySyncUrl ) - - try await options?.preAuthenticateToInboxCallback?() + + try await options?.preAuthenticateToInboxCallback?() if let signatureRequest = v3Client.signatureRequest() { if let signingKey = signingKey { do { if signingKey.type == WalletType.SCW { guard let chainId = signingKey.chainId else { - throw ClientError.creationError("Chain id must be present to sign Smart Contract Wallet") + throw ClientError.creationError( + "Chain id must be present to sign Smart Contract Wallet" + ) } - let signedData = try await signingKey.signSCW(message: signatureRequest.signatureText()) - try await signatureRequest.addScwSignature(signatureBytes: signedData, - address: signingKey.address, - chainId: UInt64(chainId), - blockNumber: signingKey.blockNumber.flatMap { $0 >= 0 ? UInt64($0) : nil }) + let signedData = try await signingKey.signSCW( + message: signatureRequest.signatureText()) + try await signatureRequest.addScwSignature( + signatureBytes: signedData, + address: signingKey.address, + chainId: UInt64(chainId), + blockNumber: signingKey.blockNumber.flatMap { + $0 >= 0 ? UInt64($0) : nil + }) } else { - let signedData = try await signingKey.sign(message: signatureRequest.signatureText()) - try await signatureRequest.addEcdsaSignature(signatureBytes: signedData.rawData) + let signedData = try await signingKey.sign( + message: signatureRequest.signatureText()) + try await signatureRequest.addEcdsaSignature( + signatureBytes: signedData.rawData) } - try await v3Client.registerIdentity(signatureRequest: signatureRequest) + try await v3Client.registerIdentity( + signatureRequest: signatureRequest) } catch { - throw ClientError.creationError("Failed to sign the message: \(error.localizedDescription)") + throw ClientError.creationError( + "Failed to sign the message: \(error.localizedDescription)" + ) } } else { - throw ClientError.creationError("No v3 keys found, you must pass a SigningKey in order to enable alpha MLS features") + throw ClientError.creationError( + "No v3 keys found, you must pass a SigningKey in order to enable alpha MLS features" + ) } } @@ -306,9 +341,13 @@ public final class Client { } } - static func create(account: SigningKey, apiClient: ApiClient, options: ClientOptions? = nil) async throws -> Client { - let privateKeyBundleV1 = try await loadOrCreateKeys(for: account, apiClient: apiClient, options: options) - let inboxId = try await getOrCreateInboxId(options: options ?? ClientOptions(), address: account.address) + static func create( + account: SigningKey, apiClient: ApiClient, options: ClientOptions? = nil + ) async throws -> Client { + let privateKeyBundleV1 = try await loadOrCreateKeys( + for: account, apiClient: apiClient, options: options) + let inboxId = try await getOrCreateInboxId( + options: options ?? ClientOptions(), address: account.address) let (v3Client, dbPath) = try await initV3Client( accountAddress: account.address, @@ -318,7 +357,11 @@ public final class Client { inboxId: inboxId ) - let client = try Client(address: account.address, privateKeyBundleV1: privateKeyBundleV1, apiClient: apiClient, v3Client: v3Client, dbPath: dbPath, installationID: v3Client?.installationId().toHex ?? "", inboxID: v3Client?.inboxId() ?? inboxId) + let client = try Client( + address: account.address, privateKeyBundleV1: privateKeyBundleV1, + apiClient: apiClient, v3Client: v3Client, dbPath: dbPath, + installationID: v3Client?.installationId().toHex ?? "", + inboxID: v3Client?.inboxId() ?? inboxId) let conversations = client.conversations let contacts = client.contacts try await client.ensureUserContactPublished() @@ -330,8 +373,13 @@ public final class Client { return client } - static func loadOrCreateKeys(for account: SigningKey, apiClient: ApiClient, options: ClientOptions? = nil) async throws -> PrivateKeyBundleV1 { - if let keys = try await loadPrivateKeys(for: account, apiClient: apiClient, options: options) { + static func loadOrCreateKeys( + for account: SigningKey, apiClient: ApiClient, + options: ClientOptions? = nil + ) async throws -> PrivateKeyBundleV1 { + if let keys = try await loadPrivateKeys( + for: account, apiClient: apiClient, options: options) + { print("loading existing private keys.") #if DEBUG print("Loaded existing private keys.") @@ -341,31 +389,43 @@ public final class Client { #if DEBUG print("No existing keys found, creating new bundle.") #endif - let keys = try await PrivateKeyBundleV1.generate(wallet: account, options: options) + let keys = try await PrivateKeyBundleV1.generate( + wallet: account, options: options) let keyBundle = PrivateKeyBundle(v1: keys) - let encryptedKeys = try await keyBundle.encrypted(with: account, preEnableIdentityCallback: options?.preEnableIdentityCallback) - var authorizedIdentity = AuthorizedIdentity(privateKeyBundleV1: keys) + let encryptedKeys = try await keyBundle.encrypted( + with: account, + preEnableIdentityCallback: options?.preEnableIdentityCallback) + var authorizedIdentity = AuthorizedIdentity( + privateKeyBundleV1: keys) authorizedIdentity.address = account.address let authToken = try await authorizedIdentity.createAuthToken() let apiClient = apiClient apiClient.setAuthToken(authToken) _ = try await apiClient.publish(envelopes: [ - Envelope(topic: .userPrivateStoreKeyBundle(account.address), timestamp: Date(), message: encryptedKeys.serializedData()), + Envelope( + topic: .userPrivateStoreKeyBundle(account.address), + timestamp: Date(), message: encryptedKeys.serializedData()) ]) return keys } } - static func loadPrivateKeys(for account: SigningKey, apiClient: ApiClient, options: ClientOptions? = nil) async throws -> PrivateKeyBundleV1? { + static func loadPrivateKeys( + for account: SigningKey, apiClient: ApiClient, + options: ClientOptions? = nil + ) async throws -> PrivateKeyBundleV1? { let res = try await apiClient.query( topic: .userPrivateStoreKeyBundle(account.address), pagination: nil ) for envelope in res.envelopes { - let encryptedBundle = try EncryptedPrivateKeyBundle(serializedData: envelope.message) - let bundle = try await encryptedBundle.decrypted(with: account, preEnableIdentityCallback: options?.preEnableIdentityCallback) + let encryptedBundle = try EncryptedPrivateKeyBundle( + serializedData: envelope.message) + let bundle = try await encryptedBundle.decrypted( + with: account, + preEnableIdentityCallback: options?.preEnableIdentityCallback) if case .v1 = bundle.version { return bundle.v1 } @@ -374,16 +434,19 @@ public final class Client { return nil } - - public static func getOrCreateInboxId(options: ClientOptions, address: String) async throws -> String { + + public static func getOrCreateInboxId( + options: ClientOptions, address: String + ) async throws -> String { var inboxId: String do { - inboxId = try await getInboxIdForAddress( - logger: XMTPLogger(), - host: options.api.env.url, - isSecure: options.api.env.isSecure == true, - accountAddress: address - ) ?? generateInboxId(accountAddress: address, nonce: 0) + inboxId = + try await getInboxIdForAddress( + logger: XMTPLogger(), + host: options.api.env.url, + isSecure: options.api.env.isSecure == true, + accountAddress: address + ) ?? generateInboxId(accountAddress: address, nonce: 0) } catch { inboxId = generateInboxId(accountAddress: address, nonce: 0) } @@ -394,11 +457,13 @@ public final class Client { guard let client = v3Client else { throw ClientError.noV3Client("Error no V3 client initialized") } - let canMessage = try await client.canMessage(accountAddresses: [address]) + let canMessage = try await client.canMessage(accountAddresses: [address] + ) return canMessage[address.lowercased()] ?? false } - public func canMessageV3(addresses: [String]) async throws -> [String: Bool] { + public func canMessageV3(addresses: [String]) async throws -> [String: Bool] + { guard let client = v3Client else { throw ClientError.noV3Client("Error no V3 client initialized") } @@ -406,7 +471,9 @@ public final class Client { return try await client.canMessage(accountAddresses: addresses) } - public static func from(bundle: PrivateKeyBundle, options: ClientOptions? = nil) async throws -> Client { + public static func from( + bundle: PrivateKeyBundle, options: ClientOptions? = nil + ) async throws -> Client { return try await from(v1Bundle: bundle.v1, options: options) } @@ -416,10 +483,12 @@ public final class Client { options: ClientOptions? = nil, signingKey: SigningKey? = nil ) async throws -> Client { - let address = try v1Bundle.identityKey.publicKey.recoverWalletSignerPublicKey().walletAddress + let address = try v1Bundle.identityKey.publicKey + .recoverWalletSignerPublicKey().walletAddress let options = options ?? ClientOptions() - - let inboxId = try await getOrCreateInboxId(options: options, address: address) + + let inboxId = try await getOrCreateInboxId( + options: options, address: address) let (v3Client, dbPath) = try await initV3Client( accountAddress: address, @@ -429,14 +498,19 @@ public final class Client { inboxId: inboxId ) - let client = try await LibXMTP.createV2Client(host: options.api.env.url, isSecure: options.api.env.isSecure) + let client = try await LibXMTP.createV2Client( + host: options.api.env.url, isSecure: options.api.env.isSecure) let apiClient = try GRPCApiClient( environment: options.api.env, secure: options.api.isSecure, rustClient: client ) - let result = try Client(address: address, privateKeyBundleV1: v1Bundle, apiClient: apiClient, v3Client: v3Client, dbPath: dbPath, installationID: v3Client?.installationId().toHex ?? "", inboxID: v3Client?.inboxId() ?? inboxId) + let result = try Client( + address: address, privateKeyBundleV1: v1Bundle, + apiClient: apiClient, v3Client: v3Client, dbPath: dbPath, + installationID: v3Client?.installationId().toHex ?? "", + inboxID: v3Client?.inboxId() ?? inboxId) let conversations = result.conversations let contacts = result.contacts for codec in options.codecs { @@ -446,7 +520,11 @@ public final class Client { return result } - init(address: String, privateKeyBundleV1: PrivateKeyBundleV1, apiClient: ApiClient, v3Client: LibXMTP.FfiXmtpClient?, dbPath: String = "", installationID: String, inboxID: String) throws { + init( + address: String, privateKeyBundleV1: PrivateKeyBundleV1, + apiClient: ApiClient, v3Client: LibXMTP.FfiXmtpClient?, + dbPath: String = "", installationID: String, inboxID: String + ) throws { self.address = address self.privateKeyBundleV1 = privateKeyBundleV1 self.apiClient = apiClient @@ -457,8 +535,11 @@ public final class Client { self.hasV2Client = true self.environment = apiClient.environment } - - init(address: String, v3Client: LibXMTP.FfiXmtpClient, dbPath: String, installationID: String, inboxID: String, environment: XMTPEnvironment) throws { + + init( + address: String, v3Client: LibXMTP.FfiXmtpClient, dbPath: String, + installationID: String, inboxID: String, environment: XMTPEnvironment + ) throws { self.address = address self.v3Client = v3Client self.dbPath = dbPath @@ -499,26 +580,34 @@ public final class Client { return try await query(topic: .contact(peerAddress)).envelopes.count > 0 } - public static func canMessage(_ peerAddress: String, options: ClientOptions? = nil) async throws -> Bool { + public static func canMessage( + _ peerAddress: String, options: ClientOptions? = nil + ) async throws -> Bool { let options = options ?? ClientOptions() - let client = try await LibXMTP.createV2Client(host: options.api.env.url, isSecure: options.api.env.isSecure) + let client = try await LibXMTP.createV2Client( + host: options.api.env.url, isSecure: options.api.env.isSecure) let apiClient = try GRPCApiClient( environment: options.api.env, secure: options.api.isSecure, rustClient: client ) - return try await apiClient.query(topic: Topic.contact(peerAddress)).envelopes.count > 0 + return try await apiClient.query(topic: Topic.contact(peerAddress)) + .envelopes.count > 0 } - public func importConversation(from conversationData: Data) throws -> Conversation? { + public func importConversation(from conversationData: Data) throws + -> Conversation? + { let jsonDecoder = JSONDecoder() do { - let v2Export = try jsonDecoder.decode(ConversationV2Export.self, from: conversationData) + let v2Export = try jsonDecoder.decode( + ConversationV2Export.self, from: conversationData) return try importV2Conversation(export: v2Export) } catch { do { - let v1Export = try jsonDecoder.decode(ConversationV1Export.self, from: conversationData) + let v1Export = try jsonDecoder.decode( + ConversationV1Export.self, from: conversationData) return try importV1Conversation(export: v1Export) } catch { throw ConversationImportError.invalidData @@ -526,35 +615,43 @@ public final class Client { } } - func importV2Conversation(export: ConversationV2Export) throws -> Conversation { - guard let keyMaterial = Data(base64Encoded: Data(export.keyMaterial.utf8)) else { + func importV2Conversation(export: ConversationV2Export) throws + -> Conversation + { + guard + let keyMaterial = Data(base64Encoded: Data(export.keyMaterial.utf8)) + else { throw ConversationImportError.invalidData } - var consentProof: ConsentProofPayload? = nil - if let exportConsentProof = export.consentProof { - var proof = ConsentProofPayload() - proof.signature = exportConsentProof.signature - proof.timestamp = exportConsentProof.timestamp - proof.payloadVersion = ConsentProofPayloadVersion.consentProofPayloadVersion1 - consentProof = proof - } - - return .v2(ConversationV2( - topic: export.topic, - keyMaterial: keyMaterial, - context: InvitationV1.Context( - conversationID: export.context?.conversationId ?? "", - metadata: export.context?.metadata ?? [:] - ), - peerAddress: export.peerAddress, - client: self, - header: SealedInvitationHeaderV1(), - consentProof: consentProof - )) - } - - func importV1Conversation(export: ConversationV1Export) throws -> Conversation { + var consentProof: ConsentProofPayload? = nil + if let exportConsentProof = export.consentProof { + var proof = ConsentProofPayload() + proof.signature = exportConsentProof.signature + proof.timestamp = exportConsentProof.timestamp + proof.payloadVersion = + ConsentProofPayloadVersion.consentProofPayloadVersion1 + consentProof = proof + } + + return .v2( + ConversationV2( + topic: export.topic, + keyMaterial: keyMaterial, + context: InvitationV1.Context( + conversationID: export.context?.conversationId ?? "", + metadata: export.context?.metadata ?? [:] + ), + peerAddress: export.peerAddress, + client: self, + header: SealedInvitationHeaderV1(), + consentProof: consentProof + )) + } + + func importV1Conversation(export: ConversationV1Export) throws + -> Conversation + { let formatter = ISO8601DateFormatter() formatter.formatOptions.insert(.withFractionalSeconds) @@ -562,17 +659,18 @@ public final class Client { throw ConversationImportError.invalidData } - return .v1(ConversationV1( - client: self, - peerAddress: export.peerAddress, - sentAt: sentAt - )) + return .v1( + ConversationV1( + client: self, + peerAddress: export.peerAddress, + sentAt: sentAt + )) } func ensureUserContactPublished() async throws { if let contact = try await getUserContact(peerAddress: address), - case .v2 = contact.version, - try keys.getPublicKeyBundle().equals(contact.v2.keyBundle) + case .v2 = contact.version, + try keys.getPublicKeyBundle().equals(contact.v2.keyBundle) { return } @@ -589,7 +687,8 @@ public final class Client { var envelope = Envelope() envelope.contentTopic = Topic.contact(address).description - envelope.timestampNs = UInt64(Date().millisecondsSinceEpoch * 1_000_000) + envelope.timestampNs = UInt64( + Date().millisecondsSinceEpoch * 1_000_000) envelope.message = try contactBundle.serializedData() envelopes.append(envelope) @@ -608,7 +707,9 @@ public final class Client { _ = try await publish(envelopes: envelopes) } - public func query(topic: Topic, pagination: Pagination? = nil) async throws -> QueryResponse { + public func query(topic: Topic, pagination: Pagination? = nil) async throws + -> QueryResponse + { guard let client = apiClient else { throw ClientError.noV2Client("Error no V2 client initialized") } @@ -618,7 +719,9 @@ public final class Client { ) } - public func batchQuery(request: BatchQueryRequest) async throws -> BatchQueryResponse { + public func batchQuery(request: BatchQueryRequest) async throws + -> BatchQueryResponse + { guard let client = apiClient else { throw ClientError.noV2Client("Error no V2 client initialized") } @@ -629,7 +732,9 @@ public final class Client { guard let client = apiClient else { throw ClientError.noV2Client("Error no V2 client initialized") } - let authorized = try AuthorizedIdentity(address: address, authorized: v1keys.identityKey.publicKey, identity: v1keys.identityKey) + let authorized = try AuthorizedIdentity( + address: address, authorized: v1keys.identityKey.publicKey, + identity: v1keys.identityKey) let authToken = try await authorized.createAuthToken() client.setAuthToken(authToken) @@ -641,7 +746,9 @@ public final class Client { topics: [String], callback: FfiV2SubscriptionCallback ) async throws -> FfiV2Subscription { - return try await subscribe2(request: FfiV2SubscribeRequest(contentTopics: topics), callback: callback) + return try await subscribe2( + request: FfiV2SubscribeRequest(contentTopics: topics), + callback: callback) } public func subscribe2( @@ -659,15 +766,19 @@ public final class Client { let fm = FileManager.default try fm.removeItem(atPath: dbPath) } - - @available(*, deprecated, message: "This function is delicate and should be used with caution. App will error if database not properly reconnected. See: reconnectLocalDatabase()") + + @available( + *, deprecated, + message: + "This function is delicate and should be used with caution. App will error if database not properly reconnected. See: reconnectLocalDatabase()" + ) public func dropLocalDatabaseConnection() throws { guard let client = v3Client else { throw ClientError.noV3Client("Error no V3 client initialized") } try client.releaseDbConnection() } - + public func reconnectLocalDatabase() async throws { guard let client = v3Client else { throw ClientError.noV3Client("Error no V3 client initialized") @@ -679,37 +790,41 @@ public final class Client { let peerAddress = EthereumAddress(peerAddress).toChecksumAddress() return try await contacts.find(peerAddress) } - + public func inboxIdFromAddress(address: String) async throws -> String? { guard let client = v3Client else { throw ClientError.noV3Client("Error no V3 client initialized") } return try await client.findInboxId(address: address.lowercased()) } - + public func findGroup(groupId: String) throws -> Group? { guard let client = v3Client else { throw ClientError.noV3Client("Error no V3 client initialized") } do { - return Group(ffiGroup: try client.conversation(conversationId: groupId.hexToData), client: self) + return Group( + ffiGroup: try client.conversation( + conversationId: groupId.hexToData), client: self) } catch { return nil } } - - public func findConversation(conversationId: String) throws -> Conversation? { + + public func findConversation(conversationId: String) throws -> Conversation? + { guard let client = v3Client else { throw ClientError.noV3Client("Error no V3 client initialized") } do { - let conversation = try client.conversation(conversationId: conversationId.hexToData) + let conversation = try client.conversation( + conversationId: conversationId.hexToData) return try conversation.toConversation(client: self) } catch { return nil } } - + public func findConversationByTopic(topic: String) throws -> Conversation? { guard let client = v3Client else { throw ClientError.noV3Client("Error no V3 client initialized") @@ -718,9 +833,13 @@ public final class Client { let regexPattern = #"/xmtp/mls/1/g-(.*?)/proto"# if let regex = try? NSRegularExpression(pattern: regexPattern) { let range = NSRange(location: 0, length: topic.utf16.count) - if let match = regex.firstMatch(in: topic, options: [], range: range) { - let conversationId = (topic as NSString).substring(with: match.range(at: 1)) - let conversation = try client.conversation(conversationId: conversationId.hexToData) + if let match = regex.firstMatch( + in: topic, options: [], range: range) + { + let conversationId = (topic as NSString).substring( + with: match.range(at: 1)) + let conversation = try client.conversation( + conversationId: conversationId.hexToData) return try conversation.toConversation(client: self) } } @@ -729,12 +848,13 @@ public final class Client { } return nil } - + public func findDm(address: String) async throws -> Dm? { guard let client = v3Client else { throw ClientError.noV3Client("Error no V3 client initialized") } - guard let inboxId = try await inboxIdFromAddress(address: address) else { + guard let inboxId = try await inboxIdFromAddress(address: address) + else { throw ClientError.creationError("No inboxId present") } do { @@ -750,38 +870,48 @@ public final class Client { throw ClientError.noV3Client("Error no V3 client initialized") } do { - return MessageV3(client: self, ffiMessage: try client.message(messageId: messageId.hexToData)) + return MessageV3( + client: self, + ffiMessage: try client.message(messageId: messageId.hexToData)) } catch { return nil } } - + public func requestMessageHistorySync() async throws { guard let client = v3Client else { throw ClientError.noV3Client("Error no V3 client initialized") } try await client.requestHistorySync() } - - public func revokeAllOtherInstallations(signingKey: SigningKey) async throws { + + public func revokeAllOtherInstallations(signingKey: SigningKey) async throws + { guard let client = v3Client else { throw ClientError.noV3Client("Error: No V3 client initialized") } - + let signatureRequest = try await client.revokeAllOtherInstallations() do { - let signedData = try await signingKey.sign(message: signatureRequest.signatureText()) - try await signatureRequest.addEcdsaSignature(signatureBytes: signedData.rawData) - try await client.applySignatureRequest(signatureRequest: signatureRequest) + let signedData = try await signingKey.sign( + message: signatureRequest.signatureText()) + try await signatureRequest.addEcdsaSignature( + signatureBytes: signedData.rawData) + try await client.applySignatureRequest( + signatureRequest: signatureRequest) } catch { - throw ClientError.creationError("Failed to sign the message: \(error.localizedDescription)") + throw ClientError.creationError( + "Failed to sign the message: \(error.localizedDescription)") } } - - public func inboxState(refreshFromNetwork: Bool) async throws -> InboxState { + + public func inboxState(refreshFromNetwork: Bool) async throws -> InboxState + { guard let client = v3Client else { throw ClientError.noV3Client("Error: No V3 client initialized") } - return InboxState(ffiInboxState: try await client.inboxState(refreshFromNetwork: refreshFromNetwork)) + return InboxState( + ffiInboxState: try await client.inboxState( + refreshFromNetwork: refreshFromNetwork)) } } diff --git a/Tests/XMTPTests/V3ClientTests.swift b/Tests/XMTPTests/V3ClientTests.swift index bd5bea63..fe6f8085 100644 --- a/Tests/XMTPTests/V3ClientTests.swift +++ b/Tests/XMTPTests/V3ClientTests.swift @@ -1,15 +1,16 @@ // // V3ClientTests.swift -// +// // // Created by Naomi Plasterer on 9/19/24. // -import XCTest -@testable import XMTPiOS import LibXMTP +import XCTest import XMTPTestHelpers +@testable import XMTPiOS + @available(iOS 16, *) class V3ClientTests: XCTestCase { // Use these fixtures to talk to the local node @@ -21,7 +22,7 @@ class V3ClientTests: XCTestCase { var boV3Client: Client! var caroV2V3Client: Client! } - + func localFixtures() async throws -> LocalFixtures { let key = try Crypto.secureRandomBytes(count: 32) let alixV2 = try PrivateKey.generate() @@ -49,7 +50,7 @@ class V3ClientTests: XCTestCase { encryptionKey: key ) ) - + return .init( alixV2: alixV2, boV3: boV3, @@ -59,314 +60,406 @@ class V3ClientTests: XCTestCase { caroV2V3Client: caroV2V3Client ) } - + func testsCanCreateGroup() async throws { let fixtures = try await localFixtures() - let group = try await fixtures.boV3Client.conversations.newGroup(with: [fixtures.caroV2V3.address]) + let group = try await fixtures.boV3Client.conversations.newGroup(with: [ + fixtures.caroV2V3.address + ]) let members = try await group.members.map(\.inboxId).sorted() - XCTAssertEqual([fixtures.caroV2V3Client.inboxID, fixtures.boV3Client.inboxID].sorted(), members) - + XCTAssertEqual( + [fixtures.caroV2V3Client.inboxID, fixtures.boV3Client.inboxID] + .sorted(), members) + await assertThrowsAsyncError( - try await fixtures.boV3Client.conversations.newGroup(with: [fixtures.alixV2.address]) + try await fixtures.boV3Client.conversations.newGroup(with: [ + fixtures.alixV2.address + ]) ) } - + func testCanCreateDm() async throws { let fixtures = try await localFixtures() - - let dm = try await fixtures.boV3Client.conversations.findOrCreateDm(with: fixtures.caroV2V3.walletAddress) + + let dm = try await fixtures.boV3Client.conversations.findOrCreateDm( + with: fixtures.caroV2V3.walletAddress) let members = try await dm.members XCTAssertEqual(members.count, 2) - - let sameDm = try await fixtures.boV3Client.findDm(address: fixtures.caroV2V3.walletAddress) + + let sameDm = try await fixtures.boV3Client.findDm( + address: fixtures.caroV2V3.walletAddress) XCTAssertEqual(sameDm?.id, dm.id) - + try await fixtures.caroV2V3Client.conversations.sync() - let caroDm = try await fixtures.caroV2V3Client.findDm(address: fixtures.boV3Client.address) + let caroDm = try await fixtures.caroV2V3Client.findDm( + address: fixtures.boV3Client.address) XCTAssertEqual(caroDm?.id, dm.id) - + await assertThrowsAsyncError( - try await fixtures.boV3Client.conversations.findOrCreateDm(with: fixtures.alixV2.walletAddress) + try await fixtures.boV3Client.conversations.findOrCreateDm( + with: fixtures.alixV2.walletAddress) ) } - + func testCanFindConversationByTopic() async throws { let fixtures = try await localFixtures() - - let group = try await fixtures.boV3Client.conversations.newGroup(with: [fixtures.caroV2V3.walletAddress]) - let dm = try await fixtures.boV3Client.conversations.findOrCreateDm(with: fixtures.caroV2V3.walletAddress) - - let sameDm = try fixtures.boV3Client.findConversationByTopic(topic: dm.topic) - let sameGroup = try fixtures.boV3Client.findConversationByTopic(topic: group.topic) - + + let group = try await fixtures.boV3Client.conversations.newGroup(with: [ + fixtures.caroV2V3.walletAddress + ]) + let dm = try await fixtures.boV3Client.conversations.findOrCreateDm( + with: fixtures.caroV2V3.walletAddress) + + let sameDm = try fixtures.boV3Client.findConversationByTopic( + topic: dm.topic) + let sameGroup = try fixtures.boV3Client.findConversationByTopic( + topic: group.topic) + XCTAssertEqual(group.id, try sameGroup?.id) XCTAssertEqual(dm.id, try sameDm?.id) } - + func testCanListConversations() async throws { let fixtures = try await localFixtures() - - let dm = try await fixtures.boV3Client.conversations.findOrCreateDm(with: fixtures.caroV2V3.walletAddress) - let group = try await fixtures.boV3Client.conversations.newGroup(with: [fixtures.caroV2V3.walletAddress]) - - let convoCount = try await fixtures.boV3Client.conversations.listConversations().count + + let dm = try await fixtures.boV3Client.conversations.findOrCreateDm( + with: fixtures.caroV2V3.walletAddress) + let group = try await fixtures.boV3Client.conversations.newGroup(with: [ + fixtures.caroV2V3.walletAddress + ]) + + let convoCount = try await fixtures.boV3Client.conversations + .listConversations().count let dmCount = try await fixtures.boV3Client.conversations.dms().count - let groupCount = try await fixtures.boV3Client.conversations.groups().count + let groupCount = try await fixtures.boV3Client.conversations.groups() + .count XCTAssertEqual(convoCount, 2) XCTAssertEqual(dmCount, 1) XCTAssertEqual(groupCount, 1) - + try await fixtures.caroV2V3Client.conversations.sync() - let convoCount2 = try await fixtures.caroV2V3Client.conversations.list(includeGroups: true).count - let groupCount2 = try await fixtures.caroV2V3Client.conversations.groups().count + let convoCount2 = try await fixtures.caroV2V3Client.conversations.list( + includeGroups: true + ).count + let groupCount2 = try await fixtures.caroV2V3Client.conversations + .groups().count XCTAssertEqual(convoCount2, 1) XCTAssertEqual(groupCount2, 1) } - + func testCanListConversationsFiltered() async throws { let fixtures = try await localFixtures() - - let dm = try await fixtures.boV3Client.conversations.findOrCreateDm(with: fixtures.caroV2V3.walletAddress) - let group = try await fixtures.boV3Client.conversations.newGroup(with: [fixtures.caroV2V3.walletAddress]) - - let convoCount = try await fixtures.boV3Client.conversations.listConversations().count - let convoCountConsent = try await fixtures.boV3Client.conversations.listConversations(consentState: .allowed).count - + + let dm = try await fixtures.boV3Client.conversations.findOrCreateDm( + with: fixtures.caroV2V3.walletAddress) + let group = try await fixtures.boV3Client.conversations.newGroup(with: [ + fixtures.caroV2V3.walletAddress + ]) + + let convoCount = try await fixtures.boV3Client.conversations + .listConversations().count + let convoCountConsent = try await fixtures.boV3Client.conversations + .listConversations(consentState: .allowed).count + XCTAssertEqual(convoCount, 2) XCTAssertEqual(convoCountConsent, 2) - + try await group.updateConsentState(state: .denied) - - let convoCountAllowed = try await fixtures.boV3Client.conversations.listConversations(consentState: .allowed).count - let convoCountDenied = try await fixtures.boV3Client.conversations.listConversations(consentState: .denied).count - + + let convoCountAllowed = try await fixtures.boV3Client.conversations + .listConversations(consentState: .allowed).count + let convoCountDenied = try await fixtures.boV3Client.conversations + .listConversations(consentState: .denied).count + XCTAssertEqual(convoCountAllowed, 1) XCTAssertEqual(convoCountDenied, 1) } - + func testCanListConversationsOrder() async throws { let fixtures = try await localFixtures() - - let dm = try await fixtures.boV3Client.conversations.findOrCreateDm(with: fixtures.caroV2V3.walletAddress) - let group1 = try await fixtures.boV3Client.conversations.newGroup(with: [fixtures.caroV2V3.walletAddress]) - let group2 = try await fixtures.boV3Client.conversations.newGroup(with: [fixtures.caroV2V3.walletAddress]) - + + let dm = try await fixtures.boV3Client.conversations.findOrCreateDm( + with: fixtures.caroV2V3.walletAddress) + let group1 = try await fixtures.boV3Client.conversations.newGroup( + with: [fixtures.caroV2V3.walletAddress]) + let group2 = try await fixtures.boV3Client.conversations.newGroup( + with: [fixtures.caroV2V3.walletAddress]) + _ = try await dm.send(content: "Howdy") _ = try await group2.send(content: "Howdy") _ = try await fixtures.boV3Client.conversations.syncAllConversations() - - let conversations = try await fixtures.boV3Client.conversations.listConversations() - let conversationsOrdered = try await fixtures.boV3Client.conversations.listConversations(order: .lastMessage) - + + let conversations = try await fixtures.boV3Client.conversations + .listConversations() + let conversationsOrdered = try await fixtures.boV3Client.conversations + .listConversations(order: .lastMessage) + XCTAssertEqual(conversations.count, 3) XCTAssertEqual(conversationsOrdered.count, 3) - - XCTAssertEqual(try conversations.map { try $0.id }, [dm.id, group1.id, group2.id]) - XCTAssertEqual(try conversationsOrdered.map { try $0.id }, [group2.id, dm.id, group1.id]) + + XCTAssertEqual( + try conversations.map { try $0.id }, [dm.id, group1.id, group2.id]) + XCTAssertEqual( + try conversationsOrdered.map { try $0.id }, + [group2.id, dm.id, group1.id]) } - + func testsCanSendMessages() async throws { let fixtures = try await localFixtures() - let group = try await fixtures.boV3Client.conversations.newGroup(with: [fixtures.caroV2V3.address]) + let group = try await fixtures.boV3Client.conversations.newGroup(with: [ + fixtures.caroV2V3.address + ]) try await group.send(content: "howdy") let messageId = try await group.send(content: "gm") try await group.sync() - + let groupMessages = try await group.messages() XCTAssertEqual(groupMessages.first?.body, "gm") XCTAssertEqual(groupMessages.first?.id, messageId) XCTAssertEqual(groupMessages.first?.deliveryStatus, .published) XCTAssertEqual(groupMessages.count, 3) - - + try await fixtures.caroV2V3Client.conversations.sync() - let sameGroup = try await fixtures.caroV2V3Client.conversations.groups().last + let sameGroup = try await fixtures.caroV2V3Client.conversations.groups() + .last try await sameGroup?.sync() - + let sameGroupMessages = try await sameGroup?.messages() XCTAssertEqual(sameGroupMessages?.count, 2) XCTAssertEqual(sameGroupMessages?.first?.body, "gm") } - + func testsCanSendMessagesToDm() async throws { let fixtures = try await localFixtures() - let dm = try await fixtures.boV3Client.conversations.findOrCreateDm(with: fixtures.caroV2V3.address) + let dm = try await fixtures.boV3Client.conversations.findOrCreateDm( + with: fixtures.caroV2V3.address) try await dm.send(content: "howdy") let messageId = try await dm.send(content: "gm") try await dm.sync() - + let dmMessages = try await dm.messages() XCTAssertEqual(dmMessages.first?.body, "gm") XCTAssertEqual(dmMessages.first?.id, messageId) XCTAssertEqual(dmMessages.first?.deliveryStatus, .published) XCTAssertEqual(dmMessages.count, 3) - - + try await fixtures.caroV2V3Client.conversations.sync() - let sameDm = try await fixtures.caroV2V3Client.findDm(address: fixtures.boV3Client.address) + let sameDm = try await fixtures.caroV2V3Client.findDm( + address: fixtures.boV3Client.address) try await sameDm?.sync() - + let sameDmMessages = try await sameDm?.messages() XCTAssertEqual(sameDmMessages?.count, 2) XCTAssertEqual(sameDmMessages?.first?.body, "gm") } - + func testGroupConsent() async throws { let fixtures = try await localFixtures() - let group = try await fixtures.boV3Client.conversations.newGroup(with: [fixtures.caroV2V3.address]) - let isAllowed = try await fixtures.boV3Client.contacts.isGroupAllowed(groupId: group.id) + let group = try await fixtures.boV3Client.conversations.newGroup(with: [ + fixtures.caroV2V3.address + ]) + let isAllowed = try await fixtures.boV3Client.contacts.isGroupAllowed( + groupId: group.id) XCTAssert(isAllowed) XCTAssertEqual(try group.consentState(), .allowed) - + try await fixtures.boV3Client.contacts.denyGroups(groupIds: [group.id]) - let isDenied = try await fixtures.boV3Client.contacts.isGroupDenied(groupId: group.id) + let isDenied = try await fixtures.boV3Client.contacts.isGroupDenied( + groupId: group.id) XCTAssert(isDenied) XCTAssertEqual(try group.consentState(), .denied) - + try await group.updateConsentState(state: .allowed) - let isAllowed2 = try await fixtures.boV3Client.contacts.isGroupAllowed(groupId: group.id) + let isAllowed2 = try await fixtures.boV3Client.contacts.isGroupAllowed( + groupId: group.id) XCTAssert(isAllowed2) XCTAssertEqual(try group.consentState(), .allowed) } - + func testCanAllowAndDenyInboxId() async throws { let fixtures = try await localFixtures() - let boGroup = try await fixtures.boV3Client.conversations.newGroup(with: [fixtures.caroV2V3.address]) - var isInboxAllowed = try await fixtures.boV3Client.contacts.isInboxAllowed(inboxId: fixtures.caroV2V3.address) - var isInboxDenied = try await fixtures.boV3Client.contacts.isInboxDenied(inboxId: fixtures.caroV2V3.address) + let boGroup = try await fixtures.boV3Client.conversations.newGroup( + with: [fixtures.caroV2V3.address]) + var isInboxAllowed = try await fixtures.boV3Client.contacts + .isInboxAllowed(inboxId: fixtures.caroV2V3.address) + var isInboxDenied = try await fixtures.boV3Client.contacts + .isInboxDenied(inboxId: fixtures.caroV2V3.address) XCTAssert(!isInboxAllowed) XCTAssert(!isInboxDenied) - - - try await fixtures.boV3Client.contacts.allowInboxes(inboxIds: [fixtures.caroV2V3Client.inboxID]) - var caroMember = try await boGroup.members.first(where: { member in member.inboxId == fixtures.caroV2V3Client.inboxID }) + + try await fixtures.boV3Client.contacts.allowInboxes(inboxIds: [ + fixtures.caroV2V3Client.inboxID + ]) + var caroMember = try await boGroup.members.first(where: { member in + member.inboxId == fixtures.caroV2V3Client.inboxID + }) XCTAssertEqual(caroMember?.consentState, .allowed) - - isInboxAllowed = try await fixtures.boV3Client.contacts.isInboxAllowed(inboxId: fixtures.caroV2V3Client.inboxID) + + isInboxAllowed = try await fixtures.boV3Client.contacts.isInboxAllowed( + inboxId: fixtures.caroV2V3Client.inboxID) XCTAssert(isInboxAllowed) - isInboxDenied = try await fixtures.boV3Client.contacts.isInboxDenied(inboxId: fixtures.caroV2V3Client.inboxID) + isInboxDenied = try await fixtures.boV3Client.contacts.isInboxDenied( + inboxId: fixtures.caroV2V3Client.inboxID) XCTAssert(!isInboxDenied) - var isAddressAllowed = try await fixtures.boV3Client.contacts.isAllowed(fixtures.caroV2V3Client.address) + var isAddressAllowed = try await fixtures.boV3Client.contacts.isAllowed( + fixtures.caroV2V3Client.address) XCTAssert(isAddressAllowed) - var isAddressDenied = try await fixtures.boV3Client.contacts.isDenied(fixtures.caroV2V3Client.address) + var isAddressDenied = try await fixtures.boV3Client.contacts.isDenied( + fixtures.caroV2V3Client.address) XCTAssert(!isAddressDenied) - - try await fixtures.boV3Client.contacts.denyInboxes(inboxIds: [fixtures.caroV2V3Client.inboxID]) - caroMember = try await boGroup.members.first(where: { member in member.inboxId == fixtures.caroV2V3Client.inboxID }) + + try await fixtures.boV3Client.contacts.denyInboxes(inboxIds: [ + fixtures.caroV2V3Client.inboxID + ]) + caroMember = try await boGroup.members.first(where: { member in + member.inboxId == fixtures.caroV2V3Client.inboxID + }) XCTAssertEqual(caroMember?.consentState, .denied) - - isInboxAllowed = try await fixtures.boV3Client.contacts.isInboxAllowed(inboxId: fixtures.caroV2V3Client.inboxID) - isInboxDenied = try await fixtures.boV3Client.contacts.isInboxDenied(inboxId: fixtures.caroV2V3Client.inboxID) + + isInboxAllowed = try await fixtures.boV3Client.contacts.isInboxAllowed( + inboxId: fixtures.caroV2V3Client.inboxID) + isInboxDenied = try await fixtures.boV3Client.contacts.isInboxDenied( + inboxId: fixtures.caroV2V3Client.inboxID) XCTAssert(!isInboxAllowed) XCTAssert(isInboxDenied) - - try await fixtures.boV3Client.contacts.allow(addresses: [fixtures.alixV2.address]) - isAddressAllowed = try await fixtures.boV3Client.contacts.isAllowed(fixtures.alixV2.address) - isAddressDenied = try await fixtures.boV3Client.contacts.isDenied(fixtures.alixV2.address) + + try await fixtures.boV3Client.contacts.allow(addresses: [ + fixtures.alixV2.address + ]) + isAddressAllowed = try await fixtures.boV3Client.contacts.isAllowed( + fixtures.alixV2.address) + isAddressDenied = try await fixtures.boV3Client.contacts.isDenied( + fixtures.alixV2.address) XCTAssert(isAddressAllowed) XCTAssert(!isAddressDenied) } - + func testCanStreamAllMessagesFromV3Users() async throws { let fixtures = try await localFixtures() - + let expectation1 = XCTestExpectation(description: "got a conversation") expectation1.expectedFulfillmentCount = 2 - let convo = try await fixtures.boV3Client.conversations.findOrCreateDm(with: fixtures.caroV2V3.address) - let group = try await fixtures.caroV2V3Client.conversations.newGroup(with: [fixtures.boV3.address]) + let convo = try await fixtures.boV3Client.conversations.findOrCreateDm( + with: fixtures.caroV2V3.address) + let group = try await fixtures.caroV2V3Client.conversations.newGroup( + with: [fixtures.boV3.address]) try await fixtures.boV3Client.conversations.sync() Task(priority: .userInitiated) { - for try await _ in await fixtures.boV3Client.conversations.streamAllConversationMessages() { + for try await _ in await fixtures.boV3Client.conversations + .streamAllConversationMessages() + { expectation1.fulfill() } } - + _ = try await group.send(content: "hi") _ = try await convo.send(content: "hi") - + await fulfillment(of: [expectation1], timeout: 3) } - + func testCanStreamAllDecryptedMessagesFromV3Users() async throws { let fixtures = try await localFixtures() - + let expectation1 = XCTestExpectation(description: "got a conversation") expectation1.expectedFulfillmentCount = 2 - let convo = try await fixtures.boV3Client.conversations.findOrCreateDm(with: fixtures.caroV2V3.address) - let group = try await fixtures.caroV2V3Client.conversations.newGroup(with: [fixtures.boV3.address]) + let convo = try await fixtures.boV3Client.conversations.findOrCreateDm( + with: fixtures.caroV2V3.address) + let group = try await fixtures.caroV2V3Client.conversations.newGroup( + with: [fixtures.boV3.address]) try await fixtures.boV3Client.conversations.sync() Task(priority: .userInitiated) { - for try await _ in await fixtures.boV3Client.conversations.streamAllDecryptedConversationMessages() { + for try await _ in await fixtures.boV3Client.conversations + .streamAllDecryptedConversationMessages() + { expectation1.fulfill() } } - + _ = try await group.send(content: "hi") _ = try await convo.send(content: "hi") - + await fulfillment(of: [expectation1], timeout: 3) } - + func testCanStreamGroupsAndConversationsFromV3Users() async throws { let fixtures = try await localFixtures() - + let expectation1 = XCTestExpectation(description: "got a conversation") expectation1.expectedFulfillmentCount = 2 - + Task(priority: .userInitiated) { - for try await _ in await fixtures.boV3Client.conversations.streamConversations() { + for try await _ in await fixtures.boV3Client.conversations + .streamConversations() + { expectation1.fulfill() } } - - _ = try await fixtures.caroV2V3Client.conversations.newGroup(with: [fixtures.boV3.address]) - _ = try await fixtures.boV3Client.conversations.findOrCreateDm(with: fixtures.caroV2V3.address) - + + _ = try await fixtures.caroV2V3Client.conversations.newGroup(with: [ + fixtures.boV3.address + ]) + _ = try await fixtures.boV3Client.conversations.findOrCreateDm( + with: fixtures.caroV2V3.address) + await fulfillment(of: [expectation1], timeout: 3) } - + func testCanStreamAllMessagesFromV2andV3Users() async throws { let fixtures = try await localFixtures() - + let expectation1 = XCTestExpectation(description: "got a conversation") expectation1.expectedFulfillmentCount = 2 - let convo = try await fixtures.alixV2Client.conversations.newConversation(with: fixtures.caroV2V3.address) - let group = try await fixtures.boV3Client.conversations.newGroup(with: [fixtures.caroV2V3.address]) + let convo = try await fixtures.alixV2Client.conversations + .newConversation(with: fixtures.caroV2V3.address) + let group = try await fixtures.boV3Client.conversations.newGroup(with: [ + fixtures.caroV2V3.address + ]) try await fixtures.caroV2V3Client.conversations.sync() Task(priority: .userInitiated) { - for try await _ in await fixtures.caroV2V3Client.conversations.streamAllMessages(includeGroups: true) { + for try await _ in await fixtures.caroV2V3Client.conversations + .streamAllMessages(includeGroups: true) + { expectation1.fulfill() } } - + _ = try await group.send(content: "hi") _ = try await convo.send(content: "hi") - + await fulfillment(of: [expectation1], timeout: 3) } - + func testCanStreamGroupsAndConversationsFromV2andV3Users() async throws { let fixtures = try await localFixtures() - + let expectation1 = XCTestExpectation(description: "got a conversation") expectation1.expectedFulfillmentCount = 2 - + Task(priority: .userInitiated) { - for try await _ in await fixtures.caroV2V3Client.conversations.streamAll() { + for try await _ in await fixtures.caroV2V3Client.conversations + .streamAll() + { expectation1.fulfill() } } - - _ = try await fixtures.boV3Client.conversations.newGroup(with: [fixtures.caroV2V3.address]) - _ = try await fixtures.alixV2Client.conversations.newConversation(with: fixtures.caroV2V3.address) - + + _ = try await fixtures.boV3Client.conversations.newGroup(with: [ + fixtures.caroV2V3.address + ]) + _ = try await fixtures.alixV2Client.conversations.newConversation( + with: fixtures.caroV2V3.address) + await fulfillment(of: [expectation1], timeout: 3) } - - func createDms(client: Client, peers: [Client], numMessages: Int) async throws -> [Dm] { + + func createDms(client: Client, peers: [Client], numMessages: Int) + async throws -> [Dm] + { var dms: [Dm] = [] for peer in peers { - let dm = try await peer.conversations.findOrCreateDm(with: client.address) + let dm = try await peer.conversations.findOrCreateDm( + with: client.address) dms.append(dm) for i in 0..