From a3fcc06e97a17750b0c85f3aa9a1467be5cce075 Mon Sep 17 00:00:00 2001 From: Pat Nakajima Date: Wed, 19 Jul 2023 14:53:59 -0700 Subject: [PATCH] Run swiftformat --- Sources/XMTP/ApiClient.swift | 160 ++--- Sources/XMTP/Client.swift | 42 +- Sources/XMTP/Conversation.swift | 2 +- Sources/XMTP/ConversationV1.swift | 2 +- Sources/XMTP/ConversationV2.swift | 2 +- Sources/XMTP/Conversations.swift | 674 +++++++++--------- Sources/XMTP/Crypto.swift | 16 +- Sources/XMTP/GroupChat.swift | 2 +- Sources/XMTP/Messages/Invitation.swift | 27 +- Sources/XMTP/Messages/PagingInfo.swift | 6 +- Sources/XMTP/Messages/PrivateKey.swift | 2 +- Sources/XMTP/Messages/PrivateKeyBundle.swift | 1 - .../XMTP/Messages/PrivateKeyBundleV1.swift | 1 - Sources/XMTP/Messages/PublicKey.swift | 4 +- Sources/XMTP/Messages/PublicKeyBundle.swift | 2 - Sources/XMTP/Messages/SealedInvitation.swift | 5 +- .../Messages/SealedInvitationHeaderV1.swift | 1 - .../XMTP/Messages/SealedInvitationV1.swift | 1 - Sources/XMTP/Messages/SignedContent.swift | 1 - Sources/XMTP/Messages/SignedPrivateKey.swift | 1 - Sources/XMTP/Messages/SignedPublicKey.swift | 2 +- .../XMTP/Messages/SignedPublicKeyBundle.swift | 2 - Sources/XMTP/Messages/Token.swift | 1 - Sources/XMTP/Messages/Topic.swift | 4 +- Sources/XMTP/Messages/UnsignedPublicKey.swift | 1 - Sources/XMTP/Util.swift | 10 +- Sources/XMTPTestHelpers/TestHelpers.swift | 430 ++++++----- Tests/XMTPTests/ClientTests.swift | 6 +- Tests/XMTPTests/CodecTests.swift | 2 +- Tests/XMTPTests/ConversationTests.swift | 50 +- Tests/XMTPTests/ConversationsTest.swift | 5 +- Tests/XMTPTests/IntegrationTests.swift | 226 +++--- Tests/XMTPTests/InvitationTests.swift | 86 +-- Tests/XMTPTests/MessageTests.swift | 12 +- Tests/XMTPTests/PaginationTests.swift | 5 +- .../Account/WalletConnection.swift | 1 - 36 files changed, 890 insertions(+), 905 deletions(-) diff --git a/Sources/XMTP/ApiClient.swift b/Sources/XMTP/ApiClient.swift index 07c7a69d..1f10cc38 100644 --- a/Sources/XMTP/ApiClient.swift +++ b/Sources/XMTP/ApiClient.swift @@ -19,44 +19,44 @@ typealias QueryResponse = Xmtp_MessageApi_V1_QueryResponse typealias SubscribeRequest = Xmtp_MessageApi_V1_SubscribeRequest public enum ApiClientError: Error { - case batchQueryError(String) - case queryError(String) - case publishError(String) - case subscribeError(String) + case batchQueryError(String) + case queryError(String) + case publishError(String) + case subscribeError(String) } protocol ApiClient { var environment: XMTPEnvironment { get } init(environment: XMTPEnvironment, secure: Bool, rustClient: XMTPRust.RustClient) throws func setAuthToken(_ token: String) - func batchQuery(request: BatchQueryRequest) async throws -> BatchQueryResponse + func batchQuery(request: BatchQueryRequest) async throws -> BatchQueryResponse func query(topic: String, pagination: Pagination?, cursor: Xmtp_MessageApi_V1_Cursor?) async throws -> QueryResponse func query(topic: Topic, pagination: Pagination?) async throws -> QueryResponse - func query(request: QueryRequest) async throws -> QueryResponse + func query(request: QueryRequest) async throws -> QueryResponse func envelopes(topic: String, pagination: Pagination?) async throws -> [Envelope] func publish(envelopes: [Envelope]) async throws -> PublishResponse - func publish(request: PublishRequest) async throws -> PublishResponse + func publish(request: PublishRequest) async throws -> PublishResponse func subscribe(topics: [String]) -> AsyncThrowingStream } func makeQueryRequest(topic: String, pagination: Pagination? = nil, cursor: Cursor? = nil) -> QueryRequest { - return QueryRequest.with { - $0.contentTopics = [topic] - if let pagination { - $0.pagingInfo = pagination.pagingInfo - } - if let endAt = pagination?.before { - $0.endTimeNs = UInt64(endAt.millisecondsSinceEpoch) * 1_000_000 - $0.pagingInfo.direction = .descending - } - if let startAt = pagination?.after { - $0.startTimeNs = UInt64(startAt.millisecondsSinceEpoch) * 1_000_000 - $0.pagingInfo.direction = .descending - } - if let cursor { - $0.pagingInfo.cursor = cursor - } - } + return QueryRequest.with { + $0.contentTopics = [topic] + if let pagination { + $0.pagingInfo = pagination.pagingInfo + } + if let endAt = pagination?.before { + $0.endTimeNs = UInt64(endAt.millisecondsSinceEpoch) * 1_000_000 + $0.pagingInfo.direction = .descending + } + if let startAt = pagination?.after { + $0.startTimeNs = UInt64(startAt.millisecondsSinceEpoch) * 1_000_000 + $0.pagingInfo.direction = .descending + } + if let cursor { + $0.pagingInfo.cursor = cursor + } + } } class GRPCApiClient: ApiClient { @@ -85,33 +85,33 @@ class GRPCApiClient: ApiClient { authToken = token } - func batchQuery(request: BatchQueryRequest) async throws -> BatchQueryResponse { - do { - let req = RustVec(try request.serializedData()) - let res: RustVec = try await rustClient.batch_query(req) - return try BatchQueryResponse(serializedData: Data(res)) - } catch let error as RustString { - throw ApiClientError.batchQueryError(error.toString()) - } - } - - func query(request: QueryRequest) async throws -> QueryResponse { - do { - let req = RustVec(try request.serializedData()) - let res: RustVec = try await rustClient.query(req) - return try QueryResponse(serializedData: Data(res)) - } catch let error as RustString { - throw ApiClientError.queryError(error.toString()) - } - } - - func query(topic: String, pagination: Pagination? = nil, cursor: Cursor? = nil) async throws -> QueryResponse { - return try await query(request: makeQueryRequest(topic: topic, pagination: pagination, cursor: cursor)) - } - - func query(topic: Topic, pagination: Pagination? = nil) async throws -> QueryResponse { - return try await query(request: makeQueryRequest(topic: topic.description, pagination: pagination)) - } + func batchQuery(request: BatchQueryRequest) async throws -> BatchQueryResponse { + do { + let req = RustVec(try request.serializedData()) + let res: RustVec = try await rustClient.batch_query(req) + return try BatchQueryResponse(serializedData: Data(res)) + } catch let error as RustString { + throw ApiClientError.batchQueryError(error.toString()) + } + } + + func query(request: QueryRequest) async throws -> QueryResponse { + do { + let req = RustVec(try request.serializedData()) + let res: RustVec = try await rustClient.query(req) + return try QueryResponse(serializedData: Data(res)) + } catch let error as RustString { + throw ApiClientError.queryError(error.toString()) + } + } + + func query(topic: String, pagination: Pagination? = nil, cursor: Cursor? = nil) async throws -> QueryResponse { + return try await query(request: makeQueryRequest(topic: topic, pagination: pagination, cursor: cursor)) + } + + func query(topic: Topic, pagination: Pagination? = nil) async throws -> QueryResponse { + return try await query(request: makeQueryRequest(topic: topic.description, pagination: pagination)) + } func envelopes(topic: String, pagination: Pagination? = nil) async throws -> [Envelope] { var envelopes: [Envelope] = [] @@ -133,40 +133,40 @@ class GRPCApiClient: ApiClient { func subscribe(topics: [String]) -> AsyncThrowingStream { return AsyncThrowingStream { continuation in Task { - let request = SubscribeRequest.with { $0.contentTopics = topics } - let req = RustVec(try request.serializedData()) - do { - let subscription = try await self.rustClient.subscribe(req) - // Run a continuous for loop polling and sleeping for a bit each loop. - while true { - let buf = try subscription.get_envelopes_as_query_response() - // Note: it uses QueryResponse as a convenient envelopes wrapper. - let res = try QueryResponse(serializedData: Data(buf)) - for envelope in res.envelopes { - continuation.yield(envelope) - } - try await Task.sleep(nanoseconds: 50_000_000) // 50ms - } - } catch let error as RustString { - throw ApiClientError.subscribeError(error.toString()) - } + let request = SubscribeRequest.with { $0.contentTopics = topics } + let req = RustVec(try request.serializedData()) + do { + let subscription = try await self.rustClient.subscribe(req) + // Run a continuous for loop polling and sleeping for a bit each loop. + while true { + let buf = try subscription.get_envelopes_as_query_response() + // Note: it uses QueryResponse as a convenient envelopes wrapper. + let res = try QueryResponse(serializedData: Data(buf)) + for envelope in res.envelopes { + continuation.yield(envelope) + } + try await Task.sleep(nanoseconds: 50_000_000) // 50ms + } + } catch let error as RustString { + throw ApiClientError.subscribeError(error.toString()) + } } } } - func publish(request: PublishRequest) async throws -> PublishResponse { - do { - let req = RustVec(try request.serializedData()) - let res: RustVec = try await rustClient.publish(authToken.intoRustString(), req) - return try PublishResponse(serializedData: Data(res)) - } catch let error as RustString { - throw ApiClientError.publishError(error.toString()) - } - } + func publish(request: PublishRequest) async throws -> PublishResponse { + do { + let req = RustVec(try request.serializedData()) + let res: RustVec = try await rustClient.publish(authToken.intoRustString(), req) + return try PublishResponse(serializedData: Data(res)) + } catch let error as RustString { + throw ApiClientError.publishError(error.toString()) + } + } @discardableResult func publish(envelopes: [Envelope]) async throws -> PublishResponse { - return try await publish(request: PublishRequest.with { - $0.envelopes = envelopes - }) + return try await publish(request: PublishRequest.with { + $0.envelopes = envelopes + }) } } diff --git a/Sources/XMTP/Client.swift b/Sources/XMTP/Client.swift index 6a51664c..86de9492 100644 --- a/Sources/XMTP/Client.swift +++ b/Sources/XMTP/Client.swift @@ -10,7 +10,7 @@ import web3 import XMTPRust public enum ClientError: Error { - case creationError(String) + case creationError(String) } /// Specify configuration options for creating a ``Client``. @@ -78,17 +78,17 @@ public class Client { /// Creates a client. public static func create(account: SigningKey, options: ClientOptions? = nil) async throws -> Client { let options = options ?? ClientOptions() - do { - let client = try await XMTPRust.create_client(GRPCApiClient.envToUrl(env: options.api.env), options.api.env != .local) - let apiClient = try GRPCApiClient( - environment: options.api.env, - secure: options.api.isSecure, - rustClient: client - ) - return try await create(account: account, apiClient: apiClient) - } catch let error as RustString { - throw ClientError.creationError(error.toString()) - } + do { + let client = try await XMTPRust.create_client(GRPCApiClient.envToUrl(env: options.api.env), options.api.env != .local) + let apiClient = try GRPCApiClient( + environment: options.api.env, + secure: options.api.isSecure, + rustClient: client + ) + return try await create(account: account, apiClient: apiClient) + } catch let error as RustString { + throw ClientError.creationError(error.toString()) + } } static func create(account: SigningKey, apiClient: ApiClient) async throws -> Client { @@ -104,7 +104,7 @@ public class Client { // swiftlint:disable no_optional_try if let keys = try await loadPrivateKeys(for: account, apiClient: apiClient) { // swiftlint:enable no_optional_try - print("loading existing private keys.") + print("loading existing private keys.") #if DEBUG print("Loaded existing private keys.") #endif @@ -140,10 +140,10 @@ public class Client { for envelope in res.envelopes { let encryptedBundle = try EncryptedPrivateKeyBundle(serializedData: envelope.message) let bundle = try await encryptedBundle.decrypted(with: account) - if case .v1 = bundle.version { - return bundle.v1 - } - print("discarding unsupported stored key bundle") + if case .v1 = bundle.version { + return bundle.v1 + } + print("discarding unsupported stored key bundle") } return nil @@ -176,7 +176,7 @@ public class Client { } public func enableGroupChat() { - self.isGroupChatEnabled = true + isGroupChatEnabled = true GroupChat.registerCodecs() } @@ -291,9 +291,9 @@ public class Client { ) } - func batchQuery(request: BatchQueryRequest) async throws -> BatchQueryResponse { - return try await apiClient.batchQuery(request: request) - } + func batchQuery(request: BatchQueryRequest) async throws -> BatchQueryResponse { + return try await apiClient.batchQuery(request: request) + } @discardableResult func publish(envelopes: [Envelope]) async throws -> PublishResponse { let authorized = AuthorizedIdentity(address: address, authorized: privateKeyBundleV1.identityKey.publicKey, identity: privateKeyBundleV1.identityKey) diff --git a/Sources/XMTP/Conversation.swift b/Sources/XMTP/Conversation.swift index 1d203ce5..a5903dad 100644 --- a/Sources/XMTP/Conversation.swift +++ b/Sources/XMTP/Conversation.swift @@ -91,7 +91,7 @@ public enum Conversation: Sendable { /// See Conversations.importTopicData() public func toTopicData() -> Xmtp_KeystoreApi_V1_TopicMap.TopicData { Xmtp_KeystoreApi_V1_TopicMap.TopicData.with { - $0.createdNs = UInt64(createdAt.timeIntervalSince1970 * 1_000) * 1_000_000 + $0.createdNs = UInt64(createdAt.timeIntervalSince1970 * 1000) * 1_000_000 $0.peerAddress = peerAddress if case let .v2(cv2) = self { $0.invitation = Xmtp_MessageContents_InvitationV1.with { diff --git a/Sources/XMTP/ConversationV1.swift b/Sources/XMTP/ConversationV1.swift index 0d75bafb..993d4f65 100644 --- a/Sources/XMTP/ConversationV1.swift +++ b/Sources/XMTP/ConversationV1.swift @@ -147,7 +147,7 @@ public struct ConversationV1 { let pagination = Pagination(limit: limit, before: before, after: after) let envelopes = try await client.apiClient.envelopes( - topic: Topic.directMessageV1(client.address, peerAddress).description, + topic: Topic.directMessageV1(client.address, peerAddress).description, pagination: pagination ) diff --git a/Sources/XMTP/ConversationV2.swift b/Sources/XMTP/ConversationV2.swift index 5938afa0..934741da 100644 --- a/Sources/XMTP/ConversationV2.swift +++ b/Sources/XMTP/ConversationV2.swift @@ -116,7 +116,7 @@ public struct ConversationV2 { return envelopes.compactMap { envelope in do { - return try decode(envelope: envelope) + return try decode(envelope: envelope) } catch { print("Error decoding envelope \(error)") return nil diff --git a/Sources/XMTP/Conversations.swift b/Sources/XMTP/Conversations.swift index 1e7a639b..c37427d6 100644 --- a/Sources/XMTP/Conversations.swift +++ b/Sources/XMTP/Conversations.swift @@ -1,345 +1,345 @@ import Foundation public enum ConversationError: Error { - case recipientNotOnNetwork, recipientIsSender, v1NotSupported(String) + case recipientNotOnNetwork, recipientIsSender, v1NotSupported(String) } /// Handles listing and creating Conversations. public class Conversations { - var client: Client - var conversationsByTopic: [String: Conversation] = [:] - - init(client: Client) { - self.client = client - } - - /// Import a previously seen conversation. - /// See Conversation.toTopicData() - public func importTopicData(data: Xmtp_KeystoreApi_V1_TopicMap.TopicData) -> Conversation { - let conversation: Conversation; - if (!data.hasInvitation) { - let sentAt = Date(timeIntervalSince1970: TimeInterval(data.createdNs / 1_000_000_000)) - conversation = .v1(ConversationV1(client: client, peerAddress: data.peerAddress, sentAt: sentAt)) - } else { - conversation = .v2(ConversationV2( - topic: data.invitation.topic, - keyMaterial: data.invitation.aes256GcmHkdfSha256.keyMaterial, - context: data.invitation.context, - peerAddress: data.peerAddress, - client: client - )) - } - conversationsByTopic[conversation.topic] = conversation - return conversation - } - - public func listBatchMessages(topics: [String: Pagination?]) async throws -> [DecodedMessage] { - let requests = topics.map { (topic, page) in - makeQueryRequest(topic: topic, pagination: page) - } - /// The maximum number of requests permitted in a single batch call. - let maxQueryRequestsPerBatch = 50 - let batches = requests.chunks(maxQueryRequestsPerBatch) - .map { (requests) in BatchQueryRequest.with { $0.requests = requests } } - var messages: [DecodedMessage] = [] - // TODO: consider using a task group here for parallel batch calls - for batch in batches { - messages += try await client.apiClient.batchQuery(request: batch) - .responses.flatMap { (res) in - try res.envelopes.compactMap { (envelope) in - let conversation = conversationsByTopic[envelope.contentTopic] - if conversation == nil { - print("discarding message, unknown conversation \(envelope)") - return nil - } - let msg = try conversation?.decode(envelope) - if msg == nil { - print("discarding message, unable to decode \(envelope)") - return nil - } - return msg - } - } - } - return messages - } - - public func streamAllMessages() async throws -> AsyncThrowingStream { - return AsyncThrowingStream { continuation in - Task { - while true { - var topics: [String] = [ - Topic.userInvite(client.address).description, - Topic.userIntro(client.address).description, - ] - - for conversation in try await list() { - topics.append(conversation.topic) - } - - do { - for try await envelope in client.subscribe(topics: topics) { - if let conversation = conversationsByTopic[envelope.contentTopic] { - let decoded = try conversation.decode(envelope) - continuation.yield(decoded) - } else if envelope.contentTopic.hasPrefix("/xmtp/0/invite-") { - let conversation = try fromInvite(envelope: envelope) - conversationsByTopic[conversation.topic] = conversation - break // Break so we can resubscribe with the new conversation - } else if envelope.contentTopic.hasPrefix("/xmtp/0/intro-") { - let conversation = try fromIntro(envelope: envelope) - conversationsByTopic[conversation.topic] = conversation - let decoded = try conversation.decode(envelope) - continuation.yield(decoded) - break // Break so we can resubscribe with the new conversation - } else { - print("huh \(envelope)") - } - } - } catch { - continuation.finish(throwing: error) - } - } - } - } - } - - public func fromInvite(envelope: Envelope) throws -> Conversation { - let sealedInvitation = try SealedInvitation(serializedData: envelope.message) - let unsealed = try sealedInvitation.v1.getInvitation(viewer: client.keys) - - return .v2(try ConversationV2.create(client: client, invitation: unsealed, header: sealedInvitation.v1.header)) - } - - public func fromIntro(envelope: Envelope) throws -> Conversation { - let messageV1 = try MessageV1.fromBytes(envelope.message) - let senderAddress = try messageV1.header.sender.walletAddress - let recipientAddress = try messageV1.header.recipient.walletAddress - - let peerAddress = client.address == senderAddress ? recipientAddress : senderAddress - let conversationV1 = ConversationV1(client: client, peerAddress: peerAddress, sentAt: messageV1.sentAt) - - return .v1(conversationV1) - } - - private func findExistingConversation(with peerAddress: String, conversationID: String?) -> Conversation? { - return conversationsByTopic.first(where: { $0.value.peerAddress == peerAddress && - (($0.value.conversationID ?? "") == (conversationID ?? "")) - })?.value - } - - public func newConversation(with peerAddress: String, context: InvitationV1.Context? = nil) async throws -> Conversation { - if peerAddress.lowercased() == client.address.lowercased() { - throw ConversationError.recipientIsSender - } - print("\(client.address) starting conversation with \(peerAddress)") - if let existing = findExistingConversation(with: peerAddress, conversationID: context?.conversationID) { - return existing - } - - guard let contact = try await client.contacts.find(peerAddress) else { - throw ConversationError.recipientNotOnNetwork - } - - _ = try await list() // cache old conversations and check again - if let existing = findExistingConversation(with: peerAddress, conversationID: context?.conversationID) { - return existing - } - - // We don't have an existing conversation, make a v2 one - let recipient = try contact.toSignedPublicKeyBundle() - let invitation = try InvitationV1.createDeterministic( - sender: client.keys, - recipient: recipient, - context: context) - let sealedInvitation = try await sendInvitation(recipient: recipient, invitation: invitation, created: Date()) - let conversationV2 = try ConversationV2.create(client: client, invitation: invitation, header: sealedInvitation.v1.header) - - let conversation: Conversation = .v2(conversationV2) - conversationsByTopic[conversation.topic] = conversation - return conversation - } - - public func stream() -> AsyncThrowingStream { - AsyncThrowingStream { continuation in - Task { - var streamedConversationTopics: Set = [] - - for try await envelope in client.subscribe(topics: [.userIntro(client.address), .userInvite(client.address)]) { - if envelope.contentTopic == Topic.userIntro(client.address).description { - let conversationV1 = try fromIntro(envelope: envelope) - - if streamedConversationTopics.contains(conversationV1.topic.description) { - continue - } - - streamedConversationTopics.insert(conversationV1.topic.description) - continuation.yield(conversationV1) - } - - if envelope.contentTopic == Topic.userInvite(client.address).description { - let conversationV2 = try fromInvite(envelope: envelope) - - if streamedConversationTopics.contains(conversationV2.topic) { - continue - } - - streamedConversationTopics.insert(conversationV2.topic) - continuation.yield(conversationV2) - } - } - } - } - } - - private func makeConversation(from sealedInvitation: SealedInvitation, isGroup: Bool = false) throws -> ConversationV2 { - let unsealed = try sealedInvitation.v1.getInvitation(viewer: client.keys) - let conversation = try ConversationV2.create(client: client, invitation: unsealed, header: sealedInvitation.v1.header, isGroup: isGroup) - - return conversation - } - - - public func list() async throws -> [Conversation] { - var newConversations: [Conversation] = [] - let mostRecent = conversationsByTopic.values.max { a, b in - a.createdAt < b.createdAt - } - let pagination = Pagination(after: mostRecent?.createdAt) - do { - let seenPeers = try await listIntroductionPeers(pagination: pagination) - for (peerAddress, sentAt) in seenPeers { - newConversations.append( - Conversation.v1( - ConversationV1( - client: client, - peerAddress: peerAddress, - sentAt: sentAt - ) - ) - ) - } - } catch { - print("Error loading introduction peers: \(error)") - } - - for sealedInvitation in try await listInvitations(pagination: pagination) { - do { - newConversations.append( - Conversation.v2(try makeConversation(from: sealedInvitation)) - ) - } catch { - print("Error loading invitations: \(error)") - } - } - - for sealedInvitation in try await listGroupInvitations(pagination: pagination) { - do { - newConversations.append( - Conversation.v2(try makeConversation(from: sealedInvitation, isGroup: true)) - ) - } catch { - print("Error loading invitations: \(error)") - } - } - - newConversations - .filter { $0.peerAddress != client.address } - .forEach { conversationsByTopic[$0.topic] = $0 } - - // TODO(perf): use DB to persist + sort - return conversationsByTopic.values.sorted { a, b in - a.createdAt < b.createdAt - } - } - - private func listIntroductionPeers(pagination: Pagination?) async throws -> [String: Date] { - let envelopes = try await client.apiClient.query( - topic: .userIntro(client.address), - pagination: pagination - ).envelopes - - let messages = envelopes.compactMap { envelope in - do { - let message = try MessageV1.fromBytes(envelope.message) - - // Attempt to decrypt, just to make sure we can - _ = try message.decrypt(with: client.privateKeyBundleV1) - - return message - } catch { - return nil - } - } - - var seenPeers: [String: Date] = [:] - for message in messages { - guard let recipientAddress = message.recipientAddress, - let senderAddress = message.senderAddress - else { - continue - } - - let sentAt = message.sentAt - let peerAddress = recipientAddress == client.address ? senderAddress : recipientAddress - - guard let existing = seenPeers[peerAddress] else { - seenPeers[peerAddress] = sentAt - continue - } - - if existing > sentAt { - seenPeers[peerAddress] = sentAt - } - } - - return seenPeers - } - - private func listGroupInvitations(pagination: Pagination?) async throws -> [SealedInvitation] { - if !client.isGroupChatEnabled { - return [] - } - let envelopes = try await client.apiClient.envelopes( - topic: Topic.groupInvite(client.address).description, - pagination: pagination - ) - - return envelopes.compactMap { envelope in - // swiftlint:disable no_optional_try - try? SealedInvitation(serializedData: envelope.message) - // swiftlint:enable no_optional_try - } - } - - private func listInvitations(pagination: Pagination?) async throws -> [SealedInvitation] { - var envelopes = try await client.apiClient.envelopes( - topic: Topic.userInvite(client.address).description, - pagination: pagination - ) - - return envelopes.compactMap { envelope in - // swiftlint:disable no_optional_try - try? SealedInvitation(serializedData: envelope.message) - // swiftlint:enable no_optional_try - } - } - - func sendInvitation(recipient: SignedPublicKeyBundle, invitation: InvitationV1, created: Date) async throws -> SealedInvitation { - let sealed = try SealedInvitation.createV1( - sender: client.keys, - recipient: recipient, - created: created, - invitation: invitation - ) - - let peerAddress = try recipient.walletAddress - - try await client.publish(envelopes: [ - Envelope(topic: .userInvite(client.address), timestamp: created, message: try sealed.serializedData()), - Envelope(topic: .userInvite(peerAddress), timestamp: created, message: try sealed.serializedData()), - ]) - - return sealed - } + var client: Client + var conversationsByTopic: [String: Conversation] = [:] + + init(client: Client) { + self.client = client + } + + /// Import a previously seen conversation. + /// See Conversation.toTopicData() + public func importTopicData(data: Xmtp_KeystoreApi_V1_TopicMap.TopicData) -> Conversation { + let conversation: Conversation + if !data.hasInvitation { + let sentAt = Date(timeIntervalSince1970: TimeInterval(data.createdNs / 1_000_000_000)) + conversation = .v1(ConversationV1(client: client, peerAddress: data.peerAddress, sentAt: sentAt)) + } else { + conversation = .v2(ConversationV2( + topic: data.invitation.topic, + keyMaterial: data.invitation.aes256GcmHkdfSha256.keyMaterial, + context: data.invitation.context, + peerAddress: data.peerAddress, + client: client + )) + } + conversationsByTopic[conversation.topic] = conversation + return conversation + } + + public func listBatchMessages(topics: [String: Pagination?]) async throws -> [DecodedMessage] { + let requests = topics.map { topic, page in + makeQueryRequest(topic: topic, pagination: page) + } + /// The maximum number of requests permitted in a single batch call. + let maxQueryRequestsPerBatch = 50 + let batches = requests.chunks(maxQueryRequestsPerBatch) + .map { requests in BatchQueryRequest.with { $0.requests = requests } } + var messages: [DecodedMessage] = [] + // TODO: consider using a task group here for parallel batch calls + for batch in batches { + messages += try await client.apiClient.batchQuery(request: batch) + .responses.flatMap { res in + try res.envelopes.compactMap { envelope in + let conversation = conversationsByTopic[envelope.contentTopic] + if conversation == nil { + print("discarding message, unknown conversation \(envelope)") + return nil + } + let msg = try conversation?.decode(envelope) + if msg == nil { + print("discarding message, unable to decode \(envelope)") + return nil + } + return msg + } + } + } + return messages + } + + public func streamAllMessages() async throws -> AsyncThrowingStream { + return AsyncThrowingStream { continuation in + Task { + while true { + var topics: [String] = [ + Topic.userInvite(client.address).description, + Topic.userIntro(client.address).description, + ] + + for conversation in try await list() { + topics.append(conversation.topic) + } + + do { + for try await envelope in client.subscribe(topics: topics) { + if let conversation = conversationsByTopic[envelope.contentTopic] { + let decoded = try conversation.decode(envelope) + continuation.yield(decoded) + } else if envelope.contentTopic.hasPrefix("/xmtp/0/invite-") { + let conversation = try fromInvite(envelope: envelope) + conversationsByTopic[conversation.topic] = conversation + break // Break so we can resubscribe with the new conversation + } else if envelope.contentTopic.hasPrefix("/xmtp/0/intro-") { + let conversation = try fromIntro(envelope: envelope) + conversationsByTopic[conversation.topic] = conversation + let decoded = try conversation.decode(envelope) + continuation.yield(decoded) + break // Break so we can resubscribe with the new conversation + } else { + print("huh \(envelope)") + } + } + } catch { + continuation.finish(throwing: error) + } + } + } + } + } + + public func fromInvite(envelope: Envelope) throws -> Conversation { + let sealedInvitation = try SealedInvitation(serializedData: envelope.message) + let unsealed = try sealedInvitation.v1.getInvitation(viewer: client.keys) + + return .v2(try ConversationV2.create(client: client, invitation: unsealed, header: sealedInvitation.v1.header)) + } + + public func fromIntro(envelope: Envelope) throws -> Conversation { + let messageV1 = try MessageV1.fromBytes(envelope.message) + let senderAddress = try messageV1.header.sender.walletAddress + let recipientAddress = try messageV1.header.recipient.walletAddress + + let peerAddress = client.address == senderAddress ? recipientAddress : senderAddress + let conversationV1 = ConversationV1(client: client, peerAddress: peerAddress, sentAt: messageV1.sentAt) + + return .v1(conversationV1) + } + + private func findExistingConversation(with peerAddress: String, conversationID: String?) -> Conversation? { + return conversationsByTopic.first(where: { $0.value.peerAddress == peerAddress && + (($0.value.conversationID ?? "") == (conversationID ?? "")) + })?.value + } + + public func newConversation(with peerAddress: String, context: InvitationV1.Context? = nil) async throws -> Conversation { + if peerAddress.lowercased() == client.address.lowercased() { + throw ConversationError.recipientIsSender + } + print("\(client.address) starting conversation with \(peerAddress)") + if let existing = findExistingConversation(with: peerAddress, conversationID: context?.conversationID) { + return existing + } + + guard let contact = try await client.contacts.find(peerAddress) else { + throw ConversationError.recipientNotOnNetwork + } + + _ = try await list() // cache old conversations and check again + if let existing = findExistingConversation(with: peerAddress, conversationID: context?.conversationID) { + return existing + } + + // We don't have an existing conversation, make a v2 one + let recipient = try contact.toSignedPublicKeyBundle() + let invitation = try InvitationV1.createDeterministic( + sender: client.keys, + recipient: recipient, + context: context + ) + let sealedInvitation = try await sendInvitation(recipient: recipient, invitation: invitation, created: Date()) + let conversationV2 = try ConversationV2.create(client: client, invitation: invitation, header: sealedInvitation.v1.header) + + let conversation: Conversation = .v2(conversationV2) + conversationsByTopic[conversation.topic] = conversation + return conversation + } + + public func stream() -> AsyncThrowingStream { + AsyncThrowingStream { continuation in + Task { + var streamedConversationTopics: Set = [] + + for try await envelope in client.subscribe(topics: [.userIntro(client.address), .userInvite(client.address)]) { + if envelope.contentTopic == Topic.userIntro(client.address).description { + let conversationV1 = try fromIntro(envelope: envelope) + + if streamedConversationTopics.contains(conversationV1.topic.description) { + continue + } + + streamedConversationTopics.insert(conversationV1.topic.description) + continuation.yield(conversationV1) + } + + if envelope.contentTopic == Topic.userInvite(client.address).description { + let conversationV2 = try fromInvite(envelope: envelope) + + if streamedConversationTopics.contains(conversationV2.topic) { + continue + } + + streamedConversationTopics.insert(conversationV2.topic) + continuation.yield(conversationV2) + } + } + } + } + } + + private func makeConversation(from sealedInvitation: SealedInvitation, isGroup: Bool = false) throws -> ConversationV2 { + let unsealed = try sealedInvitation.v1.getInvitation(viewer: client.keys) + let conversation = try ConversationV2.create(client: client, invitation: unsealed, header: sealedInvitation.v1.header, isGroup: isGroup) + + return conversation + } + + public func list() async throws -> [Conversation] { + var newConversations: [Conversation] = [] + let mostRecent = conversationsByTopic.values.max { a, b in + a.createdAt < b.createdAt + } + let pagination = Pagination(after: mostRecent?.createdAt) + do { + let seenPeers = try await listIntroductionPeers(pagination: pagination) + for (peerAddress, sentAt) in seenPeers { + newConversations.append( + Conversation.v1( + ConversationV1( + client: client, + peerAddress: peerAddress, + sentAt: sentAt + ) + ) + ) + } + } catch { + print("Error loading introduction peers: \(error)") + } + + for sealedInvitation in try await listInvitations(pagination: pagination) { + do { + newConversations.append( + Conversation.v2(try makeConversation(from: sealedInvitation)) + ) + } catch { + print("Error loading invitations: \(error)") + } + } + + for sealedInvitation in try await listGroupInvitations(pagination: pagination) { + do { + newConversations.append( + Conversation.v2(try makeConversation(from: sealedInvitation, isGroup: true)) + ) + } catch { + print("Error loading invitations: \(error)") + } + } + + newConversations + .filter { $0.peerAddress != client.address } + .forEach { conversationsByTopic[$0.topic] = $0 } + + // TODO(perf): use DB to persist + sort + return conversationsByTopic.values.sorted { a, b in + a.createdAt < b.createdAt + } + } + + private func listIntroductionPeers(pagination: Pagination?) async throws -> [String: Date] { + let envelopes = try await client.apiClient.query( + topic: .userIntro(client.address), + pagination: pagination + ).envelopes + + let messages = envelopes.compactMap { envelope in + do { + let message = try MessageV1.fromBytes(envelope.message) + + // Attempt to decrypt, just to make sure we can + _ = try message.decrypt(with: client.privateKeyBundleV1) + + return message + } catch { + return nil + } + } + + var seenPeers: [String: Date] = [:] + for message in messages { + guard let recipientAddress = message.recipientAddress, + let senderAddress = message.senderAddress + else { + continue + } + + let sentAt = message.sentAt + let peerAddress = recipientAddress == client.address ? senderAddress : recipientAddress + + guard let existing = seenPeers[peerAddress] else { + seenPeers[peerAddress] = sentAt + continue + } + + if existing > sentAt { + seenPeers[peerAddress] = sentAt + } + } + + return seenPeers + } + + private func listGroupInvitations(pagination: Pagination?) async throws -> [SealedInvitation] { + if !client.isGroupChatEnabled { + return [] + } + let envelopes = try await client.apiClient.envelopes( + topic: Topic.groupInvite(client.address).description, + pagination: pagination + ) + + return envelopes.compactMap { envelope in + // swiftlint:disable no_optional_try + try? SealedInvitation(serializedData: envelope.message) + // swiftlint:enable no_optional_try + } + } + + private func listInvitations(pagination: Pagination?) async throws -> [SealedInvitation] { + var envelopes = try await client.apiClient.envelopes( + topic: Topic.userInvite(client.address).description, + pagination: pagination + ) + + return envelopes.compactMap { envelope in + // swiftlint:disable no_optional_try + try? SealedInvitation(serializedData: envelope.message) + // swiftlint:enable no_optional_try + } + } + + func sendInvitation(recipient: SignedPublicKeyBundle, invitation: InvitationV1, created: Date) async throws -> SealedInvitation { + let sealed = try SealedInvitation.createV1( + sender: client.keys, + recipient: recipient, + created: created, + invitation: invitation + ) + + let peerAddress = try recipient.walletAddress + + try await client.publish(envelopes: [ + Envelope(topic: .userInvite(client.address), timestamp: created, message: try sealed.serializedData()), + Envelope(topic: .userInvite(peerAddress), timestamp: created, message: try sealed.serializedData()), + ]) + + return sealed + } } diff --git a/Sources/XMTP/Crypto.swift b/Sources/XMTP/Crypto.swift index 6b3fdd65..d033ef0d 100644 --- a/Sources/XMTP/Crypto.swift +++ b/Sources/XMTP/Crypto.swift @@ -36,7 +36,7 @@ enum Crypto { // with offsets like lowerBound=12, upperBound=224. Without copying, trying to index like payload[0] crashes // up until payload[12]. This is mostly a problem for unit tests where we decrypt what we encrypt in memory, as // serialization/deserialization acts as copying and avoids this issue. - var payloadData = Data(payload.ciphertext.subdata(in: 12 ..< payload.ciphertext.count+12)) + var payloadData = Data(payload.ciphertext.subdata(in: 12 ..< payload.ciphertext.count + 12)) let startTag = 12 + payload.ciphertext.count payloadData.append(payload.tag.subdata(in: startTag ..< startTag + payload.tag.count)) ciphertext.aes256GcmHkdfSha256.payload = payloadData @@ -76,14 +76,14 @@ enum Crypto { static func deriveKey(secret: Data, nonce: Data, info: Data) throws -> Data { let key = HKDF.deriveKey( - inputKeyMaterial: SymmetricKey(data: secret), - salt: nonce, - info: info, - outputByteCount: 32 + inputKeyMaterial: SymmetricKey(data: secret), + salt: nonce, + info: info, + outputByteCount: 32 ) - return key.withUnsafeBytes { body in - Data(body) - } + return key.withUnsafeBytes { body in + Data(body) + } } static func secureRandomBytes(count: Int) throws -> Data { diff --git a/Sources/XMTP/GroupChat.swift b/Sources/XMTP/GroupChat.swift index a0e5a8aa..0828a788 100644 --- a/Sources/XMTP/GroupChat.swift +++ b/Sources/XMTP/GroupChat.swift @@ -7,7 +7,7 @@ import Foundation -public struct GroupChat { +public enum GroupChat { public static func registerCodecs() { Client.register(codec: GroupChatMemberAddedCodec()) Client.register(codec: GroupChatTitleChangedCodec()) diff --git a/Sources/XMTP/Messages/Invitation.swift b/Sources/XMTP/Messages/Invitation.swift index a755c2b0..126d9d84 100644 --- a/Sources/XMTP/Messages/Invitation.swift +++ b/Sources/XMTP/Messages/Invitation.swift @@ -9,16 +9,17 @@ public typealias InvitationV1 = Xmtp_MessageContents_InvitationV1 extension InvitationV1 { static func createDeterministic( - sender: PrivateKeyBundleV2, - recipient: SignedPublicKeyBundle, - context: InvitationV1.Context? = nil + sender: PrivateKeyBundleV2, + recipient: SignedPublicKeyBundle, + context: InvitationV1.Context? = nil ) throws -> InvitationV1 { let context = context ?? InvitationV1.Context() let secret = try sender.sharedSecret( - peer: recipient, - myPreKey: sender.preKeys[0].publicKey, - isRecipient: false) + peer: recipient, + myPreKey: sender.preKeys[0].publicKey, + isRecipient: false + ) let addresses = [ try sender.toV1().walletAddress, try recipient.walletAddress, @@ -28,17 +29,19 @@ extension InvitationV1 { let topic = Topic.directMessageV2(topicId) let keyMaterial = try Crypto.deriveKey( - secret: secret, - nonce: Data("__XMTP__INVITATION__SALT__XMTP__".utf8), - info: Data((["0"] + addresses).joined(separator: "|").utf8)) + secret: secret, + nonce: Data("__XMTP__INVITATION__SALT__XMTP__".utf8), + info: Data((["0"] + addresses).joined(separator: "|").utf8) + ) var aes256GcmHkdfSha256 = InvitationV1.Aes256gcmHkdfsha256() aes256GcmHkdfSha256.keyMaterial = Data(keyMaterial) return try InvitationV1( - topic: topic, - context: context, - aes256GcmHkdfSha256: aes256GcmHkdfSha256) + topic: topic, + context: context, + aes256GcmHkdfSha256: aes256GcmHkdfSha256 + ) } init(topic: Topic, context: InvitationV1.Context? = nil, aes256GcmHkdfSha256: InvitationV1.Aes256gcmHkdfsha256) throws { diff --git a/Sources/XMTP/Messages/PagingInfo.swift b/Sources/XMTP/Messages/PagingInfo.swift index 667f1ea5..ec25d612 100644 --- a/Sources/XMTP/Messages/PagingInfo.swift +++ b/Sources/XMTP/Messages/PagingInfo.swift @@ -14,8 +14,8 @@ typealias PagingInfoSortDirection = Xmtp_MessageApi_V1_SortDirection public struct Pagination { public var limit: Int? var direction: PagingInfoSortDirection? - public var before: Date? - public var after: Date? + public var before: Date? + public var after: Date? var pagingInfo: PagingInfo { var info = PagingInfo() @@ -23,7 +23,7 @@ public struct Pagination { if let limit { info.limit = UInt32(limit) } - info.direction = direction ?? Xmtp_MessageApi_V1_SortDirection.descending + info.direction = direction ?? Xmtp_MessageApi_V1_SortDirection.descending return info } } diff --git a/Sources/XMTP/Messages/PrivateKey.swift b/Sources/XMTP/Messages/PrivateKey.swift index 35e7a3a3..a4cd94a9 100644 --- a/Sources/XMTP/Messages/PrivateKey.swift +++ b/Sources/XMTP/Messages/PrivateKey.swift @@ -5,9 +5,9 @@ // Created by Pat Nakajima on 11/17/22. // +import CryptoKit import Foundation import XMTPRust -import CryptoKit /// Represents a secp256k1 private key. ``PrivateKey`` conforms to ``SigningKey`` so you can use it /// to create a ``Client``. diff --git a/Sources/XMTP/Messages/PrivateKeyBundle.swift b/Sources/XMTP/Messages/PrivateKeyBundle.swift index 96331dd9..db8ffef7 100644 --- a/Sources/XMTP/Messages/PrivateKeyBundle.swift +++ b/Sources/XMTP/Messages/PrivateKeyBundle.swift @@ -7,7 +7,6 @@ import Foundation - public typealias PrivateKeyBundle = Xmtp_MessageContents_PrivateKeyBundle enum PrivateKeyBundleError: Error { diff --git a/Sources/XMTP/Messages/PrivateKeyBundleV1.swift b/Sources/XMTP/Messages/PrivateKeyBundleV1.swift index 5161f1d3..e31c9cc5 100644 --- a/Sources/XMTP/Messages/PrivateKeyBundleV1.swift +++ b/Sources/XMTP/Messages/PrivateKeyBundleV1.swift @@ -9,7 +9,6 @@ import CryptoKit import Foundation import XMTPRust - public typealias PrivateKeyBundleV1 = Xmtp_MessageContents_PrivateKeyBundleV1 extension PrivateKeyBundleV1 { diff --git a/Sources/XMTP/Messages/PublicKey.swift b/Sources/XMTP/Messages/PublicKey.swift index 6a3f7d79..6cb26f27 100644 --- a/Sources/XMTP/Messages/PublicKey.swift +++ b/Sources/XMTP/Messages/PublicKey.swift @@ -7,9 +7,9 @@ import Foundation -import XMTPRust -import web3 import CryptoKit +import web3 +import XMTPRust typealias PublicKey = Xmtp_MessageContents_PublicKey diff --git a/Sources/XMTP/Messages/PublicKeyBundle.swift b/Sources/XMTP/Messages/PublicKeyBundle.swift index 23691789..0945db63 100644 --- a/Sources/XMTP/Messages/PublicKeyBundle.swift +++ b/Sources/XMTP/Messages/PublicKeyBundle.swift @@ -5,8 +5,6 @@ // Created by Pat Nakajima on 11/23/22. // - - typealias PublicKeyBundle = Xmtp_MessageContents_PublicKeyBundle extension PublicKeyBundle { diff --git a/Sources/XMTP/Messages/SealedInvitation.swift b/Sources/XMTP/Messages/SealedInvitation.swift index 2d5cae01..199ccad8 100644 --- a/Sources/XMTP/Messages/SealedInvitation.swift +++ b/Sources/XMTP/Messages/SealedInvitation.swift @@ -7,7 +7,6 @@ import Foundation - typealias SealedInvitation = Xmtp_MessageContents_SealedInvitation enum SealedInvitationError: Error { @@ -41,8 +40,8 @@ extension SealedInvitation { func involves(_ contact: ContactBundle) -> Bool { do { let contactSignedPublicKeyBundle = try contact.toSignedPublicKeyBundle() - let walletAddress = try contactSignedPublicKeyBundle.walletAddress - return try v1.header.recipient.walletAddress == walletAddress || v1.header.sender.walletAddress == walletAddress + let walletAddress = try contactSignedPublicKeyBundle.walletAddress + return try v1.header.recipient.walletAddress == walletAddress || v1.header.sender.walletAddress == walletAddress } catch { return false } diff --git a/Sources/XMTP/Messages/SealedInvitationHeaderV1.swift b/Sources/XMTP/Messages/SealedInvitationHeaderV1.swift index 70bbafc2..ca2513ba 100644 --- a/Sources/XMTP/Messages/SealedInvitationHeaderV1.swift +++ b/Sources/XMTP/Messages/SealedInvitationHeaderV1.swift @@ -7,7 +7,6 @@ import Foundation - public typealias SealedInvitationHeaderV1 = Xmtp_MessageContents_SealedInvitationHeaderV1 extension SealedInvitationHeaderV1 { diff --git a/Sources/XMTP/Messages/SealedInvitationV1.swift b/Sources/XMTP/Messages/SealedInvitationV1.swift index 054274b0..b1376300 100644 --- a/Sources/XMTP/Messages/SealedInvitationV1.swift +++ b/Sources/XMTP/Messages/SealedInvitationV1.swift @@ -7,7 +7,6 @@ import Foundation - typealias SealedInvitationV1 = Xmtp_MessageContents_SealedInvitationV1 extension SealedInvitationV1 { diff --git a/Sources/XMTP/Messages/SignedContent.swift b/Sources/XMTP/Messages/SignedContent.swift index 42d4e3b8..682cea07 100644 --- a/Sources/XMTP/Messages/SignedContent.swift +++ b/Sources/XMTP/Messages/SignedContent.swift @@ -7,7 +7,6 @@ import Foundation - typealias SignedContent = Xmtp_MessageContents_SignedContent extension SignedContent { diff --git a/Sources/XMTP/Messages/SignedPrivateKey.swift b/Sources/XMTP/Messages/SignedPrivateKey.swift index 102ce248..adfb1af2 100644 --- a/Sources/XMTP/Messages/SignedPrivateKey.swift +++ b/Sources/XMTP/Messages/SignedPrivateKey.swift @@ -7,7 +7,6 @@ import Foundation - public typealias SignedPrivateKey = Xmtp_MessageContents_SignedPrivateKey extension SignedPrivateKey { diff --git a/Sources/XMTP/Messages/SignedPublicKey.swift b/Sources/XMTP/Messages/SignedPublicKey.swift index bf0aabbd..ce19942b 100644 --- a/Sources/XMTP/Messages/SignedPublicKey.swift +++ b/Sources/XMTP/Messages/SignedPublicKey.swift @@ -8,8 +8,8 @@ import CryptoKit import Foundation -import XMTPRust import web3 +import XMTPRust typealias SignedPublicKey = Xmtp_MessageContents_SignedPublicKey diff --git a/Sources/XMTP/Messages/SignedPublicKeyBundle.swift b/Sources/XMTP/Messages/SignedPublicKeyBundle.swift index 36cf81ce..417f87d8 100644 --- a/Sources/XMTP/Messages/SignedPublicKeyBundle.swift +++ b/Sources/XMTP/Messages/SignedPublicKeyBundle.swift @@ -5,8 +5,6 @@ // Created by Pat Nakajima on 11/23/22. // - - typealias SignedPublicKeyBundle = Xmtp_MessageContents_SignedPublicKeyBundle extension SignedPublicKeyBundle { diff --git a/Sources/XMTP/Messages/Token.swift b/Sources/XMTP/Messages/Token.swift index 5165e156..ec95aaa1 100644 --- a/Sources/XMTP/Messages/Token.swift +++ b/Sources/XMTP/Messages/Token.swift @@ -7,5 +7,4 @@ import Foundation - typealias Token = Xmtp_MessageApi_V1_Token diff --git a/Sources/XMTP/Messages/Topic.swift b/Sources/XMTP/Messages/Topic.swift index bb36ffd4..e8e780dc 100644 --- a/Sources/XMTP/Messages/Topic.swift +++ b/Sources/XMTP/Messages/Topic.swift @@ -5,14 +5,12 @@ // Created by Pat Nakajima on 11/17/22. // - - public enum Topic { case userPrivateStoreKeyBundle(String), contact(String), userIntro(String), userInvite(String), - groupInvite(String), + groupInvite(String), directMessageV1(String, String), directMessageV2(String) diff --git a/Sources/XMTP/Messages/UnsignedPublicKey.swift b/Sources/XMTP/Messages/UnsignedPublicKey.swift index 69e26afd..17fa3ee6 100644 --- a/Sources/XMTP/Messages/UnsignedPublicKey.swift +++ b/Sources/XMTP/Messages/UnsignedPublicKey.swift @@ -7,7 +7,6 @@ import Foundation - typealias UnsignedPublicKey = Xmtp_MessageContents_UnsignedPublicKey extension UnsignedPublicKey { diff --git a/Sources/XMTP/Util.swift b/Sources/XMTP/Util.swift index d8d88dba..2f40fdf4 100644 --- a/Sources/XMTP/Util.swift +++ b/Sources/XMTP/Util.swift @@ -15,9 +15,9 @@ enum Util { } extension Array { - func chunks(_ chunkSize: Int) -> [[Element]] { - return stride(from: 0, to: self.count, by: chunkSize).map { - Array(self[$0.. [[Element]] { + return stride(from: 0, to: count, by: chunkSize).map { + Array(self[$0 ..< Swift.min($0 + chunkSize, self.count)]) + } + } } diff --git a/Sources/XMTPTestHelpers/TestHelpers.swift b/Sources/XMTPTestHelpers/TestHelpers.swift index 3f910677..b95155d3 100644 --- a/Sources/XMTPTestHelpers/TestHelpers.swift +++ b/Sources/XMTPTestHelpers/TestHelpers.swift @@ -6,286 +6,284 @@ // #if canImport(XCTest) -import Combine -import XCTest -@testable import XMTP -import XMTPRust - -public struct TestConfig { - static let TEST_SERVER_ENABLED = _env("TEST_SERVER_ENABLED") == "true" - // TODO: change Client constructor to accept these explicitly (so we can config CI): - // static let TEST_SERVER_HOST = _env("TEST_SERVER_HOST") ?? "127.0.0.1" - // static let TEST_SERVER_PORT = Int(_env("TEST_SERVER_PORT")) ?? 5556 - // static let TEST_SERVER_IS_SECURE = _env("TEST_SERVER_IS_SECURE") == "true" - - static private func _env(_ key: String) -> String? { - ProcessInfo.processInfo.environment[key] - } - - static public func skipIfNotRunningLocalNodeTests() throws { - try XCTSkipIf(!TEST_SERVER_ENABLED, "requires local node") - } - - static public func skip(because: String) throws { - try XCTSkipIf(true, because) - } -} - -// Helper for tests gathering transcripts in a background task. -public actor TestTranscript { - public var messages: [String] = [] - public init() {} - public func add(_ message: String) { - messages.append(message) - } -} - -public struct FakeWallet: SigningKey { - public static func generate() throws -> FakeWallet { - let key = try PrivateKey.generate() - return FakeWallet(key) - } + import Combine + import XCTest + @testable import XMTP + import XMTPRust + + public enum TestConfig { + static let TEST_SERVER_ENABLED = _env("TEST_SERVER_ENABLED") == "true" + // TODO: change Client constructor to accept these explicitly (so we can config CI): + // static let TEST_SERVER_HOST = _env("TEST_SERVER_HOST") ?? "127.0.0.1" + // static let TEST_SERVER_PORT = Int(_env("TEST_SERVER_PORT")) ?? 5556 + // static let TEST_SERVER_IS_SECURE = _env("TEST_SERVER_IS_SECURE") == "true" + + private static func _env(_ key: String) -> String? { + ProcessInfo.processInfo.environment[key] + } - public var address: String { - key.walletAddress - } + public static func skipIfNotRunningLocalNodeTests() throws { + try XCTSkipIf(!TEST_SERVER_ENABLED, "requires local node") + } - public func sign(_ data: Data) async throws -> XMTP.Signature { - let signature = try await key.sign(data) - return signature + public static func skip(because: String) throws { + try XCTSkipIf(true, because) + } } - public func sign(message: String) async throws -> XMTP.Signature { - let signature = try await key.sign(message: message) - return signature + // Helper for tests gathering transcripts in a background task. + public actor TestTranscript { + public var messages: [String] = [] + public init() {} + public func add(_ message: String) { + messages.append(message) + } } - public var key: PrivateKey + public struct FakeWallet: SigningKey { + public static func generate() throws -> FakeWallet { + let key = try PrivateKey.generate() + return FakeWallet(key) + } - public init(_ key: PrivateKey) { - self.key = key - } -} + public var address: String { + key.walletAddress + } -enum FakeApiClientError: String, Error { - case noResponses, queryAssertionFailure -} + public func sign(_ data: Data) async throws -> XMTP.Signature { + let signature = try await key.sign(data) + return signature + } -class FakeStreamHolder: ObservableObject { - @Published var envelope: XMTP.Envelope? + public func sign(message: String) async throws -> XMTP.Signature { + let signature = try await key.sign(message: message) + return signature + } - func send(envelope: XMTP.Envelope) { - self.envelope = envelope - } -} + public var key: PrivateKey -@available(iOS 15, *) -public class FakeApiClient: ApiClient { - public func envelopes(topic: String, pagination: XMTP.Pagination?) async throws -> [XMTP.Envelope] { - try await query(topic: topic, pagination: pagination).envelopes + public init(_ key: PrivateKey) { + self.key = key + } } - public var environment: XMTPEnvironment - public var authToken: String = "" - private var responses: [String: [XMTP.Envelope]] = [:] - private var stream = FakeStreamHolder() - public var published: [XMTP.Envelope] = [] - var cancellable: AnyCancellable? - var forbiddingQueries = false - - deinit { - cancellable?.cancel() + enum FakeApiClientError: String, Error { + case noResponses, queryAssertionFailure } - public func assertNoPublish(callback: () async throws -> Void) async throws { - let oldCount = published.count - try await callback() - // swiftlint:disable no_optional_try - XCTAssertEqual(oldCount, published.count, "Published messages: \(String(describing: try? published[oldCount - 1 ..< published.count].map { try $0.jsonString() }))") - // swiftlint:enable no_optional_try - } + class FakeStreamHolder: ObservableObject { + @Published var envelope: XMTP.Envelope? - public func assertNoQuery(callback: () async throws -> Void) async throws { - forbiddingQueries = true - try await callback() - forbiddingQueries = false + func send(envelope: XMTP.Envelope) { + self.envelope = envelope + } } - public func register(message: [XMTP.Envelope], for topic: Topic) { - var responsesForTopic = responses[topic.description] ?? [] - responsesForTopic.append(contentsOf: message) - responses[topic.description] = responsesForTopic - } + @available(iOS 15, *) + public class FakeApiClient: ApiClient { + public func envelopes(topic: String, pagination: XMTP.Pagination?) async throws -> [XMTP.Envelope] { + try await query(topic: topic, pagination: pagination).envelopes + } - public init() { - environment = .local - } + public var environment: XMTPEnvironment + public var authToken: String = "" + private var responses: [String: [XMTP.Envelope]] = [:] + private var stream = FakeStreamHolder() + public var published: [XMTP.Envelope] = [] + var cancellable: AnyCancellable? + var forbiddingQueries = false - public func send(envelope: XMTP.Envelope) { - stream.send(envelope: envelope) - } + deinit { + cancellable?.cancel() + } - public func findPublishedEnvelope(_ topic: Topic) -> XMTP.Envelope? { - return findPublishedEnvelope(topic.description) - } + public func assertNoPublish(callback: () async throws -> Void) async throws { + let oldCount = published.count + try await callback() + // swiftlint:disable no_optional_try + XCTAssertEqual(oldCount, published.count, "Published messages: \(String(describing: try? published[oldCount - 1 ..< published.count].map { try $0.jsonString() }))") + // swiftlint:enable no_optional_try + } - public func findPublishedEnvelope(_ topic: String) -> XMTP.Envelope? { - return published.reversed().first { $0.contentTopic == topic.description } - } + public func assertNoQuery(callback: () async throws -> Void) async throws { + forbiddingQueries = true + try await callback() + forbiddingQueries = false + } - // MARK: ApiClient conformance + public func register(message: [XMTP.Envelope], for topic: Topic) { + var responsesForTopic = responses[topic.description] ?? [] + responsesForTopic.append(contentsOf: message) + responses[topic.description] = responsesForTopic + } - public required init(environment: XMTP.XMTPEnvironment, secure _: Bool, rustClient _: XMTPRust.RustClient) throws { - self.environment = environment - } + public init() { + environment = .local + } - public func subscribe(topics: [String]) -> AsyncThrowingStream { - AsyncThrowingStream { continuation in - self.cancellable = stream.$envelope.sink(receiveValue: { env in - if let env, topics.contains(env.contentTopic) { - continuation.yield(env) - } - }) + public func send(envelope: XMTP.Envelope) { + stream.send(envelope: envelope) } - } - public func setAuthToken(_ token: String) { - authToken = token - } + public func findPublishedEnvelope(_ topic: Topic) -> XMTP.Envelope? { + return findPublishedEnvelope(topic.description) + } - public func query(topic: String, pagination: Pagination? = nil, cursor _: Xmtp_MessageApi_V1_Cursor? = nil) async throws -> XMTP.QueryResponse { - if forbiddingQueries { - XCTFail("Attempted to query \(topic)") - throw FakeApiClientError.queryAssertionFailure + public func findPublishedEnvelope(_ topic: String) -> XMTP.Envelope? { + return published.reversed().first { $0.contentTopic == topic.description } } - var result: [XMTP.Envelope] = [] + // MARK: ApiClient conformance - if let response = responses.removeValue(forKey: topic) { - result.append(contentsOf: response) + public required init(environment: XMTP.XMTPEnvironment, secure _: Bool, rustClient _: XMTPRust.RustClient) throws { + self.environment = environment } - result.append(contentsOf: published.filter { $0.contentTopic == topic }.reversed()) - - if let startAt = pagination?.after { - result = result - .filter { $0.timestampNs > UInt64(startAt.millisecondsSinceEpoch * 1_000_000) } + public func subscribe(topics: [String]) -> AsyncThrowingStream { + AsyncThrowingStream { continuation in + self.cancellable = stream.$envelope.sink(receiveValue: { env in + if let env, topics.contains(env.contentTopic) { + continuation.yield(env) + } + }) + } } - if let endAt = pagination?.before { - result = result - .filter { $0.timestampNs < UInt64(endAt.millisecondsSinceEpoch * 1_000_000) } + public func setAuthToken(_ token: String) { + authToken = token } - if let limit = pagination?.limit { - if limit == 1 { - if let first = result.first { - result = [first] - } else { - result = [] - } - } else { - let maxBound = min(result.count, limit) - 1 + public func query(topic: String, pagination: Pagination? = nil, cursor _: Xmtp_MessageApi_V1_Cursor? = nil) async throws -> XMTP.QueryResponse { + if forbiddingQueries { + XCTFail("Attempted to query \(topic)") + throw FakeApiClientError.queryAssertionFailure + } + + var result: [XMTP.Envelope] = [] - if maxBound <= 0 { - result = [] + if let response = responses.removeValue(forKey: topic) { + result.append(contentsOf: response) + } + + result.append(contentsOf: published.filter { $0.contentTopic == topic }.reversed()) + + if let startAt = pagination?.after { + result = result + .filter { $0.timestampNs > UInt64(startAt.millisecondsSinceEpoch * 1_000_000) } + } + + if let endAt = pagination?.before { + result = result + .filter { $0.timestampNs < UInt64(endAt.millisecondsSinceEpoch * 1_000_000) } + } + + if let limit = pagination?.limit { + if limit == 1 { + if let first = result.first { + result = [first] + } else { + result = [] + } } else { - result = Array(result[0 ... maxBound]) + let maxBound = min(result.count, limit) - 1 + + if maxBound <= 0 { + result = [] + } else { + result = Array(result[0 ... maxBound]) + } } } - } - - var queryResponse = QueryResponse() - queryResponse.envelopes = result - return queryResponse - } + var queryResponse = QueryResponse() + queryResponse.envelopes = result - public func query(topic: XMTP.Topic, pagination: Pagination? = nil) async throws -> XMTP.QueryResponse { - return try await query(topic: topic.description, pagination: pagination, cursor: nil) - } + return queryResponse + } - public func publish(envelopes: [XMTP.Envelope]) async throws -> XMTP.PublishResponse { - for envelope in envelopes { - send(envelope: envelope) + public func query(topic: XMTP.Topic, pagination: Pagination? = nil) async throws -> XMTP.QueryResponse { + return try await query(topic: topic.description, pagination: pagination, cursor: nil) } - published.append(contentsOf: envelopes) + public func publish(envelopes: [XMTP.Envelope]) async throws -> XMTP.PublishResponse { + for envelope in envelopes { + send(envelope: envelope) + } - return PublishResponse() - } + published.append(contentsOf: envelopes) - public func batchQuery(request: XMTP.BatchQueryRequest) async throws -> XMTP.BatchQueryResponse { - let responses = try await withThrowingTaskGroup(of: QueryResponse.self) { group in - for r in request.requests { - group.addTask { - try await self.query(topic: r.contentTopics[0], pagination: Pagination(after: Date(timeIntervalSince1970: Double(r.startTimeNs / 1_000_000) / 1000))) - } - } + return PublishResponse() + } - var results: [QueryResponse] = [] - for try await response in group { - results.append(response) - } + public func batchQuery(request: XMTP.BatchQueryRequest) async throws -> XMTP.BatchQueryResponse { + let responses = try await withThrowingTaskGroup(of: QueryResponse.self) { group in + for r in request.requests { + group.addTask { + try await self.query(topic: r.contentTopics[0], pagination: Pagination(after: Date(timeIntervalSince1970: Double(r.startTimeNs / 1_000_000) / 1000))) + } + } - return results - } + var results: [QueryResponse] = [] + for try await response in group { + results.append(response) + } - var queryResponse = XMTP.BatchQueryResponse() - queryResponse.responses = responses - return queryResponse - - } + return results + } - public func query(request: XMTP.QueryRequest) async throws -> XMTP.QueryResponse { - abort() // Not supported on Fake - } + var queryResponse = XMTP.BatchQueryResponse() + queryResponse.responses = responses + return queryResponse + } - public func publish(request: XMTP.PublishRequest) async throws -> XMTP.PublishResponse { - abort() // Not supported on Fake - } + public func query(request _: XMTP.QueryRequest) async throws -> XMTP.QueryResponse { + abort() // Not supported on Fake + } -} + public func publish(request _: XMTP.PublishRequest) async throws -> XMTP.PublishResponse { + abort() // Not supported on Fake + } + } -@available(iOS 15, *) -public struct Fixtures { - public var fakeApiClient: FakeApiClient! + @available(iOS 15, *) + public struct Fixtures { + public var fakeApiClient: FakeApiClient! - public var alice: PrivateKey! - public var aliceClient: Client! + public var alice: PrivateKey! + public var aliceClient: Client! - public var bob: PrivateKey! - public var bobClient: Client! + public var bob: PrivateKey! + public var bobClient: Client! - init() async throws { - alice = try PrivateKey.generate() - bob = try PrivateKey.generate() + init() async throws { + alice = try PrivateKey.generate() + bob = try PrivateKey.generate() - fakeApiClient = FakeApiClient() + fakeApiClient = FakeApiClient() - aliceClient = try await Client.create(account: alice, apiClient: fakeApiClient) - bobClient = try await Client.create(account: bob, apiClient: fakeApiClient) - } + aliceClient = try await Client.create(account: alice, apiClient: fakeApiClient) + bobClient = try await Client.create(account: bob, apiClient: fakeApiClient) + } - public func publishLegacyContact(client: Client) async throws { - var contactBundle = ContactBundle() - contactBundle.v1.keyBundle = client.privateKeyBundleV1.toPublicKeyBundle() + public func publishLegacyContact(client: Client) async throws { + var contactBundle = ContactBundle() + contactBundle.v1.keyBundle = client.privateKeyBundleV1.toPublicKeyBundle() - var envelope = Envelope() - envelope.contentTopic = Topic.contact(client.address).description - envelope.timestampNs = UInt64(Date().millisecondsSinceEpoch * 1_000_000) - envelope.message = try contactBundle.serializedData() + var envelope = Envelope() + envelope.contentTopic = Topic.contact(client.address).description + envelope.timestampNs = UInt64(Date().millisecondsSinceEpoch * 1_000_000) + envelope.message = try contactBundle.serializedData() - try await client.publish(envelopes: [envelope]) + try await client.publish(envelopes: [envelope]) + } } -} -public extension XCTestCase { - @available(iOS 15, *) - func fixtures() async -> Fixtures { - // swiftlint:disable force_try - return try! await Fixtures() - // swiftlint:enable force_try + public extension XCTestCase { + @available(iOS 15, *) + func fixtures() async -> Fixtures { + // swiftlint:disable force_try + return try! await Fixtures() + // swiftlint:enable force_try + } } -} #endif diff --git a/Tests/XMTPTests/ClientTests.swift b/Tests/XMTPTests/ClientTests.swift index 0b19fb00..086b68e4 100644 --- a/Tests/XMTPTests/ClientTests.swift +++ b/Tests/XMTPTests/ClientTests.swift @@ -15,7 +15,7 @@ import XMTPTestHelpers @available(iOS 15, *) class ClientTests: XCTestCase { func testTakesAWallet() async throws { - try TestConfig.skip(because: "run manually against dev") + try TestConfig.skip(because: "run manually against dev") let fakeWallet = try PrivateKey.generate() _ = try await Client.create(account: fakeWallet) } @@ -42,7 +42,7 @@ class ClientTests: XCTestCase { } func testCanBeCreatedWithBundle() async throws { - try TestConfig.skip(because: "run manually against dev") + try TestConfig.skip(because: "run manually against dev") let fakeWallet = try PrivateKey.generate() let client = try await Client.create(account: fakeWallet) @@ -55,7 +55,7 @@ class ClientTests: XCTestCase { } func testCanBeCreatedWithV1Bundle() async throws { - try TestConfig.skip(because: "run manually against dev") + try TestConfig.skip(because: "run manually against dev") let fakeWallet = try PrivateKey.generate() let client = try await Client.create(account: fakeWallet) diff --git a/Tests/XMTPTests/CodecTests.swift b/Tests/XMTPTests/CodecTests.swift index 707f9e59..2e88bf84 100644 --- a/Tests/XMTPTests/CodecTests.swift +++ b/Tests/XMTPTests/CodecTests.swift @@ -51,7 +51,7 @@ class CodecTests: XCTestCase { } func testFallsBackToFallbackContentWhenCannotDecode() async throws { - Client.register(codec: NumberCodec()) + Client.register(codec: NumberCodec()) let fixtures = await fixtures() let aliceClient = fixtures.aliceClient! diff --git a/Tests/XMTPTests/ConversationTests.swift b/Tests/XMTPTests/ConversationTests.swift index 7bee724c..885b6489 100644 --- a/Tests/XMTPTests/ConversationTests.swift +++ b/Tests/XMTPTests/ConversationTests.swift @@ -72,7 +72,7 @@ class ConversationTests: XCTestCase { } func testDoesNotAllowConversationWithSelf() async throws { - try TestConfig.skipIfNotRunningLocalNodeTests() + try TestConfig.skipIfNotRunningLocalNodeTests() let expectation = expectation(description: "convo with self throws") let client = aliceClient! @@ -375,7 +375,6 @@ class ConversationTests: XCTestCase { } func testCanRetrieveAllMessages() async throws { - guard case let .v2(bobConversation) = try await bobClient.conversations.newConversation(with: alice.address, context: InvitationV1.Context(conversationID: "hi")) else { XCTFail("did not get a v2 conversation for bob") return @@ -386,7 +385,7 @@ class ConversationTests: XCTestCase { return } - for i in 0..<110 { + for i in 0 ..< 110 { do { let content = "hey alice \(i)" let sentAt = Date().addingTimeInterval(-1000) @@ -405,29 +404,28 @@ class ConversationTests: XCTestCase { let messages = try await aliceConversation.messages() XCTAssertEqual(110, messages.count) } - - func testCanRetrieveBatchMessages() async throws { - - guard case let .v2(bobConversation) = try await aliceClient.conversations.newConversation(with: bob.address, context: InvitationV1.Context(conversationID: "hi")) else { - XCTFail("did not get a v2 conversation for bob") - return - } - - for i in 0..<3 { - do { - let content = "hey alice \(i)" - let sentAt = Date().addingTimeInterval(-1000) - try await bobConversation.send(content: content, sentAt: sentAt) - } catch { - print("Error sending message:", error) - } - } - - let messages = try await aliceClient.conversations.listBatchMessages( - topics: [bobConversation.topic : Pagination(limit:3)] - ) - XCTAssertEqual(3, messages.count) - } + + func testCanRetrieveBatchMessages() async throws { + guard case let .v2(bobConversation) = try await aliceClient.conversations.newConversation(with: bob.address, context: InvitationV1.Context(conversationID: "hi")) else { + XCTFail("did not get a v2 conversation for bob") + return + } + + for i in 0 ..< 3 { + do { + let content = "hey alice \(i)" + let sentAt = Date().addingTimeInterval(-1000) + try await bobConversation.send(content: content, sentAt: sentAt) + } catch { + print("Error sending message:", error) + } + } + + let messages = try await aliceClient.conversations.listBatchMessages( + topics: [bobConversation.topic: Pagination(limit: 3)] + ) + XCTAssertEqual(3, messages.count) + } func testImportV1ConversationFromJS() async throws { let jsExportJSONData = Data(""" diff --git a/Tests/XMTPTests/ConversationsTest.swift b/Tests/XMTPTests/ConversationsTest.swift index e4cd2285..f23a4bc9 100644 --- a/Tests/XMTPTests/ConversationsTest.swift +++ b/Tests/XMTPTests/ConversationsTest.swift @@ -43,8 +43,9 @@ class ConversationsTests: XCTestCase { let newClient = try await Client.create(account: newWallet, apiClient: fixtures.fakeApiClient) let invitation = try InvitationV1.createDeterministic( - sender: newClient.keys, - recipient: client.keys.getPublicKeyBundle()) + sender: newClient.keys, + recipient: client.keys.getPublicKeyBundle() + ) let sealed = try SealedInvitation.createV1( sender: newClient.keys, recipient: client.keys.getPublicKeyBundle(), diff --git a/Tests/XMTPTests/IntegrationTests.swift b/Tests/XMTPTests/IntegrationTests.swift index be824e91..4bdbd94c 100644 --- a/Tests/XMTPTests/IntegrationTests.swift +++ b/Tests/XMTPTests/IntegrationTests.swift @@ -9,7 +9,6 @@ import Foundation import secp256k1 import web3 import XCTest -import XMTPRust @testable import XMTP import XMTPRust import XMTPTestHelpers @@ -18,7 +17,7 @@ import XMTPTestHelpers @available(iOS 16, *) final class IntegrationTests: XCTestCase { func testSaveKey() async throws { - try TestConfig.skipIfNotRunningLocalNodeTests() + try TestConfig.skipIfNotRunningLocalNodeTests() let alice = try PrivateKey.generate() let identity = try PrivateKey.generate() @@ -47,7 +46,7 @@ final class IntegrationTests: XCTestCase { } func testPublishingAndFetchingContactBundles() async throws { - try TestConfig.skipIfNotRunningLocalNodeTests() + try TestConfig.skipIfNotRunningLocalNodeTests() let opts = ClientOptions(api: ClientOptions.Api(env: .local, isSecure: false)) @@ -67,7 +66,7 @@ final class IntegrationTests: XCTestCase { } func testCanReceiveV1MessagesFromJS() async throws { - try TestConfig.skip(because: "run with locally orchestrated network") + try TestConfig.skip(because: "run with locally orchestrated network") let wallet = try FakeWallet.generate() let options = ClientOptions(api: ClientOptions.Api(env: .local, isSecure: false)) @@ -84,7 +83,7 @@ final class IntegrationTests: XCTestCase { } func testCanReceiveV2MessagesFromJS() async throws { - try TestConfig.skip(because: "run with locally orchestrated network") + try TestConfig.skip(because: "run with locally orchestrated network") let wallet = try PrivateKey.generate() let options = ClientOptions(api: ClientOptions.Api(env: .local, isSecure: false)) @@ -106,27 +105,27 @@ final class IntegrationTests: XCTestCase { } func testEndToEndConversation() async throws { - try TestConfig.skipIfNotRunningLocalNodeTests() + try TestConfig.skipIfNotRunningLocalNodeTests() let opt = ClientOptions(api: .init(env: .local, isSecure: false)) - let alice = try await Client.create(account: try PrivateKey.generate(), options: opt) - let bob = try await Client.create(account: try PrivateKey.generate(), options: opt) + let alice = try await Client.create(account: try PrivateKey.generate(), options: opt) + let bob = try await Client.create(account: try PrivateKey.generate(), options: opt) - let aliceConvo = try await alice.conversations.newConversation(with: bob.address) - _ = try await aliceConvo.send(text: "Hello Bob") - try await delayToPropagate() + let aliceConvo = try await alice.conversations.newConversation(with: bob.address) + _ = try await aliceConvo.send(text: "Hello Bob") + try await delayToPropagate() - let bobConvos = try await bob.conversations.list() - let bobConvo = bobConvos[0] - let bobSees = try await bobConvo.messages() - XCTAssertEqual("Hello Bob", bobSees[0].body) + let bobConvos = try await bob.conversations.list() + let bobConvo = bobConvos[0] + let bobSees = try await bobConvo.messages() + XCTAssertEqual("Hello Bob", bobSees[0].body) - try await bobConvo.send(text: "Oh, hello Alice") - try await delayToPropagate() + try await bobConvo.send(text: "Oh, hello Alice") + try await delayToPropagate() - let aliceSees = try await aliceConvo.messages() - XCTAssertEqual("Hello Bob", aliceSees[1].body) - XCTAssertEqual("Oh, hello Alice", aliceSees[0].body) + let aliceSees = try await aliceConvo.messages() + XCTAssertEqual("Hello Bob", aliceSees[1].body) + XCTAssertEqual("Oh, hello Alice", aliceSees[0].body) } func testUsingSavedCredentialsAndKeyMaterial() async throws { @@ -138,17 +137,18 @@ final class IntegrationTests: XCTestCase { // Alice starts a conversation with Bob let aliceConvo = try await alice.conversations.newConversation( - with: bob.address, - context: InvitationV1.Context.with { - $0.conversationID = "example.com/alice-bob-1" - $0.metadata["title"] = "Chatting Using Saved Credentials" - }) + with: bob.address, + context: InvitationV1.Context.with { + $0.conversationID = "example.com/alice-bob-1" + $0.metadata["title"] = "Chatting Using Saved Credentials" + } + ) _ = try await aliceConvo.send(text: "Hello Bob") try await delayToPropagate() // Alice stores her credentials and conversations to her device - let keyBundle = try alice.privateKeyBundle.serializedData(); - let topicData = try aliceConvo.toTopicData().serializedData(); + let keyBundle = try alice.privateKeyBundle.serializedData() + let topicData = try aliceConvo.toTopicData().serializedData() // Meanwhile, Bob sends a reply. let bobConvos = try await bob.conversations.list() @@ -163,7 +163,7 @@ final class IntegrationTests: XCTestCase { ) // And it uses the saved topic data for the conversation let aliceConvo2 = alice2.conversations.importTopicData( - data: try Xmtp_KeystoreApi_V1_TopicMap.TopicData(serializedData: topicData)) + data: try Xmtp_KeystoreApi_V1_TopicMap.TopicData(serializedData: topicData)) XCTAssertEqual("example.com/alice-bob-1", aliceConvo2.conversationID) // Now Alice should be able to load message using her saved key material. @@ -195,8 +195,9 @@ final class IntegrationTests: XCTestCase { // Alice should see the same topic and keyMaterial for both conversations. XCTAssertEqual(c1.topic, c2.topic) XCTAssertEqual( - c1.toTopicData().invitation.aes256GcmHkdfSha256.keyMaterial, - c2.toTopicData().invitation.aes256GcmHkdfSha256.keyMaterial) + c1.toTopicData().invitation.aes256GcmHkdfSha256.keyMaterial, + c2.toTopicData().invitation.aes256GcmHkdfSha256.keyMaterial + ) // And Bob should only see the one conversation. let bobConvos = try await bob.conversations.list() @@ -211,17 +212,17 @@ final class IntegrationTests: XCTestCase { } func testStreamMessagesInV1Conversation() async throws { - try TestConfig.skipIfNotRunningLocalNodeTests() + try TestConfig.skipIfNotRunningLocalNodeTests() let opt = ClientOptions(api: .init(env: .local, isSecure: false)) let alice = try await Client.create(account: try PrivateKey.generate(), options: opt) let bob = try await Client.create(account: try PrivateKey.generate(), options: opt) - try await alice.publishUserContact(legacy: true) + try await alice.publishUserContact(legacy: true) try await bob.publishUserContact(legacy: true) - try await delayToPropagate() + try await delayToPropagate() let aliceConversation = try await alice.conversations.newConversation(with: bob.address) try await aliceConversation.send(content: "greetings") - try await delayToPropagate() + try await delayToPropagate() let transcript = TestTranscript() @@ -238,15 +239,15 @@ final class IntegrationTests: XCTestCase { try await aliceConversation.send(content: "hi bob") try await delayToPropagate() try await bobConversation.send(content: "hi alice") - try await delayToPropagate() + try await delayToPropagate() - let messages = await transcript.messages + let messages = await transcript.messages XCTAssertEqual("hi bob", messages[0]) XCTAssertEqual("hi alice", messages[1]) } func testStreamMessagesInV2Conversation() async throws { - try TestConfig.skipIfNotRunningLocalNodeTests() + try TestConfig.skipIfNotRunningLocalNodeTests() let alice = try PrivateKey.generate() let bob = try PrivateKey.generate() @@ -264,20 +265,20 @@ final class IntegrationTests: XCTestCase { XCTAssertEqual(bobConversation.topic, aliceConversation.topic) Task(priority: .userInitiated) { - for try await message in bobConversation.streamMessages() { - await transcript.add(message.body) - } + for try await message in bobConversation.streamMessages() { + await transcript.add(message.body) + } } try await aliceConversation.send(text: "hi bob") try await delayToPropagate() - let messages = await transcript.messages + let messages = await transcript.messages XCTAssertEqual(1, messages.count) XCTAssertEqual("hi bob", messages[0]) } func testCanPaginateV1Messages() async throws { - try TestConfig.skipIfNotRunningLocalNodeTests() + try TestConfig.skipIfNotRunningLocalNodeTests() let bob = try FakeWallet.generate() let alice = try FakeWallet.generate() @@ -292,19 +293,19 @@ final class IntegrationTests: XCTestCase { // Say this message is sent in the past try await convo.send(content: "first") - try await delayToPropagate() + try await delayToPropagate() try await convo.send(content: "second") - try await delayToPropagate() + try await delayToPropagate() var messages = try await convo.messages(limit: 1) XCTAssertEqual(1, messages.count) XCTAssertEqual("second", messages[0].body) // most-recent first - let secondMessageSent = messages[0].sent + let secondMessageSent = messages[0].sent // -// messages = try await convo.messages(limit: 1, before: secondMessageSent) + // messages = try await convo.messages(limit: 1, before: secondMessageSent) // XCTAssertEqual(1, messages.count) -// XCTAssertEqual("first", messages[0].body) -// let firstMessageSent = messages[0].sent + // XCTAssertEqual("first", messages[0].body) + // let firstMessageSent = messages[0].sent // // messages = try await convo.messages(limit: 1, after: firstMessageSent) // XCTAssertEqual(1, messages.count) @@ -312,7 +313,7 @@ final class IntegrationTests: XCTestCase { } func testCanPaginateV2Messages() async throws { - try TestConfig.skipIfNotRunningLocalNodeTests() + try TestConfig.skipIfNotRunningLocalNodeTests() let bob = try FakeWallet.generate() let alice = try FakeWallet.generate() @@ -349,63 +350,65 @@ final class IntegrationTests: XCTestCase { XCTAssertEqual("now", nowMessage2.body) } - func testStreamingMessagesShouldBeReceived() async throws { - try TestConfig.skipIfNotRunningLocalNodeTests() - - let alice = try await Client.create(account: try FakeWallet.generate(), - options: ClientOptions(api: ClientOptions.Api(env: .local, isSecure: false))) - let bob = try await Client.create(account: try FakeWallet.generate(), - options: ClientOptions(api: ClientOptions.Api(env: .local, isSecure: false))) - let transcript = TestTranscript() - Task(priority: .userInitiated) { - for try await message in try await alice.conversations.streamAllMessages() { - await transcript.add(message.body) - } - } - let c1 = try await bob.conversations.newConversation(with: alice.address) - try await delayToPropagate() - _ = try await c1.send(text: "hello Alice") - try await delayToPropagate() - let messages = await transcript.messages - XCTAssertEqual(1, messages.count) - XCTAssertEqual("hello Alice", messages[0]) - } - - func testListingConversations() async throws { - try TestConfig.skipIfNotRunningLocalNodeTests() - - let alice = try await Client.create(account: try FakeWallet.generate(), - options: ClientOptions(api: ClientOptions.Api(env: .local, isSecure: false))) - let bob = try await Client.create(account: try FakeWallet.generate(), - options: ClientOptions(api: ClientOptions.Api(env: .local, isSecure: false))) - - let c1 = try await bob.conversations.newConversation( - with: alice.address, - context: InvitationV1.Context.with { - $0.conversationID = "example.com/alice-bob-1" - $0.metadata["title"] = "First Chat" - }) - try await c1.send(text: "hello Alice!") - try await delayToPropagate() - - var aliceConvoList = try await alice.conversations.list() - XCTAssertEqual(1, aliceConvoList.count) - XCTAssertEqual("example.com/alice-bob-1", aliceConvoList[0].conversationID) - - let c2 = try await bob.conversations.newConversation( - with: alice.address, - context: InvitationV1.Context.with { - $0.conversationID = "example.com/alice-bob-2" - $0.metadata["title"] = "Second Chat" - }) - try await c2.send(text: "hello again Alice!") - try await delayToPropagate() - - aliceConvoList = try await alice.conversations.list() - XCTAssertEqual(2, aliceConvoList.count) -// XCTAssertEqual("example.com/alice-bob-2", aliceConvoList[0].conversationID) -// XCTAssertEqual("example.com/alice-bob-1", aliceConvoList[1].conversationID) - } + func testStreamingMessagesShouldBeReceived() async throws { + try TestConfig.skipIfNotRunningLocalNodeTests() + + let alice = try await Client.create(account: try FakeWallet.generate(), + options: ClientOptions(api: ClientOptions.Api(env: .local, isSecure: false))) + let bob = try await Client.create(account: try FakeWallet.generate(), + options: ClientOptions(api: ClientOptions.Api(env: .local, isSecure: false))) + let transcript = TestTranscript() + Task(priority: .userInitiated) { + for try await message in try await alice.conversations.streamAllMessages() { + await transcript.add(message.body) + } + } + let c1 = try await bob.conversations.newConversation(with: alice.address) + try await delayToPropagate() + _ = try await c1.send(text: "hello Alice") + try await delayToPropagate() + let messages = await transcript.messages + XCTAssertEqual(1, messages.count) + XCTAssertEqual("hello Alice", messages[0]) + } + + func testListingConversations() async throws { + try TestConfig.skipIfNotRunningLocalNodeTests() + + let alice = try await Client.create(account: try FakeWallet.generate(), + options: ClientOptions(api: ClientOptions.Api(env: .local, isSecure: false))) + let bob = try await Client.create(account: try FakeWallet.generate(), + options: ClientOptions(api: ClientOptions.Api(env: .local, isSecure: false))) + + let c1 = try await bob.conversations.newConversation( + with: alice.address, + context: InvitationV1.Context.with { + $0.conversationID = "example.com/alice-bob-1" + $0.metadata["title"] = "First Chat" + } + ) + try await c1.send(text: "hello Alice!") + try await delayToPropagate() + + var aliceConvoList = try await alice.conversations.list() + XCTAssertEqual(1, aliceConvoList.count) + XCTAssertEqual("example.com/alice-bob-1", aliceConvoList[0].conversationID) + + let c2 = try await bob.conversations.newConversation( + with: alice.address, + context: InvitationV1.Context.with { + $0.conversationID = "example.com/alice-bob-2" + $0.metadata["title"] = "Second Chat" + } + ) + try await c2.send(text: "hello again Alice!") + try await delayToPropagate() + + aliceConvoList = try await alice.conversations.list() + XCTAssertEqual(2, aliceConvoList.count) + // XCTAssertEqual("example.com/alice-bob-2", aliceConvoList[0].conversationID) + // XCTAssertEqual("example.com/alice-bob-1", aliceConvoList[1].conversationID) + } // Test used to verify https://github.com/xmtp/xmtp-ios/issues/39 fix. func testExistingWallet() async throws { @@ -434,7 +437,7 @@ final class IntegrationTests: XCTestCase { } func testCanStreamV2Conversations() async throws { - try TestConfig.skipIfNotRunningLocalNodeTests() + try TestConfig.skipIfNotRunningLocalNodeTests() let alice = try PrivateKey.generate() let bob = try PrivateKey.generate() @@ -545,7 +548,6 @@ final class IntegrationTests: XCTestCase { key.secp256K1.bytes = Data(keyBytes) key.publicKey.secp256K1Uncompressed.bytes = Data(try XMTPRust.public_key_from_private_key_k256(RustVec(keyBytes))) - let client = try await XMTP.Client.create(account: key) let conversations = try await client.conversations.list() @@ -553,9 +555,9 @@ final class IntegrationTests: XCTestCase { XCTAssertEqual(200, conversations.count) } - // Helpers + // Helpers - func delayToPropagate() async throws { - try await Task.sleep(for: .milliseconds(500)) - } + func delayToPropagate() async throws { + try await Task.sleep(for: .milliseconds(500)) + } } diff --git a/Tests/XMTPTests/InvitationTests.swift b/Tests/XMTPTests/InvitationTests.swift index 11be5018..5e933c3c 100644 --- a/Tests/XMTPTests/InvitationTests.swift +++ b/Tests/XMTPTests/InvitationTests.swift @@ -12,7 +12,6 @@ import XMTPTestHelpers @available(iOS 16.0, *) class InvitationTests: XCTestCase { - func testDeterministicInvite() async throws { let aliceWallet = try FakeWallet.generate() let bobWallet = try FakeWallet.generate() @@ -22,23 +21,24 @@ class InvitationTests: XCTestCase { let makeInvite = { (conversationID: String) in try InvitationV1.createDeterministic( - sender: alice.toV2(), - recipient: bob.toV2().getPublicKeyBundle(), - context: InvitationV1.Context.with { - $0.conversationID = conversationID - }) + sender: alice.toV2(), + recipient: bob.toV2().getPublicKeyBundle(), + context: InvitationV1.Context.with { + $0.conversationID = conversationID + } + ) } // Repeatedly making the same invite should use the same topic/keys - let original = try makeInvite("example.com/conversation-foo"); - for i in 1...10 { - let invite = try makeInvite("example.com/conversation-foo"); - XCTAssertEqual(original.topic, invite.topic); + let original = try makeInvite("example.com/conversation-foo") + for i in 1 ... 10 { + let invite = try makeInvite("example.com/conversation-foo") + XCTAssertEqual(original.topic, invite.topic) } // But when the conversationId changes then it use a new topic/keys - let invite = try makeInvite("example.com/conversation-bar"); - XCTAssertNotEqual(original.topic, invite.topic); + let invite = try makeInvite("example.com/conversation-bar") + XCTAssertNotEqual(original.topic, invite.topic) } func testGenerateSealedInvitation() async throws { @@ -82,35 +82,35 @@ class InvitationTests: XCTestCase { XCTAssertEqual(bobInvite.aes256GcmHkdfSha256.keyMaterial, invitation.aes256GcmHkdfSha256.keyMaterial) } - func testGeneratesKnownDeterministicTopic() async throws { - // address = 0xF56d1F3b1290204441Cb3843C2Cac1C2f5AEd690 - let aliceKeyData = Data(("0x0a8a030ac20108c192a3f7923112220a2068d2eb2ef8c50c4916b42ce638c5610e44ff4eb3ecb098" + - "c9dacf032625c72f101a940108c192a3f7923112460a440a40fc9822283078c323c9319c45e60ab4" + - "2c65f6e1744ed8c23c52728d456d33422824c98d307e8b1c86a26826578523ba15fe6f04a17fca17" + - "6664ee8017ec8ba59310011a430a410498dc2315dd45d99f5e900a071e7b56142de344540f07fbc7" + - "3a0f9a5d5df6b52eb85db06a3825988ab5e04746bc221fcdf5310a44d9523009546d4bfbfbb89cfb" + - "12c20108eb92a3f7923112220a20788be9da8e1a1a08b05f7cbf22d86980bc056b130c482fa5bd26" + - "ccb8d29b30451a940108eb92a3f7923112460a440a40a7afa25cb6f3fbb98f9e5cd92a1df1898452" + - "e0dfa1d7e5affe9eaf9b72dd14bc546d86c399768badf983f07fa7dd16eee8d793357ce6fccd6768" + - "07d87bcc595510011a430a410422931e6295c3c93a5f6f5e729dc02e1754e916cb9be16d36dc163a" + - "300931f42a0cd5fde957d75c2068e1980c5f86843daf16aba8ae57e8160b8b9f0191def09e").web3.bytesFromHex!) - let aliceKeys = try PrivateKeyBundle(serializedData: aliceKeyData).v1.toV2() - - // address = 0x3De402A325323Bb97f00cE3ad5bFAc96A11F9A34 - let bobKeyData = Data(("0x0a88030ac001088cd68df7923112220a209057f8d813314a2aae74e6c4c30f909c1c496b6037ce32" + - "a12c613558a8e961681a9201088cd68df7923112440a420a40501ae9b4f75d5bb5bae3ca4ecfda4e" + - "de9edc5a9b7fc2d56dc7325b837957c23235cc3005b46bb9ef485f106404dcf71247097ed5096355" + - "90f4b7987b833d03661a430a4104e61a7ae511567f4a2b5551221024b6932d6cdb8ecf3876ec64cf" + - "29be4291dd5428fc0301963cdf6939978846e2c35fd38fcb70c64296a929f166ef6e4e91045712c2" + - "0108b8d68df7923112220a2027707399474d417bf6aae4baa3d73b285bf728353bc3e156b0e32461" + - "ebb48f8c1a940108b8d68df7923112460a440a40fb96fa38c3f013830abb61cf6b39776e0475eb13" + - "79c66013569c3d2daecdd48c7fbee945dcdbdc5717d1f4ffd342c4d3f1b7215912829751a94e3ae1" + - "1007e0a110011a430a4104952b7158cfe819d92743a4132e2e3ae867d72f6a08292aebf471d0a7a2" + - "907f3e9947719033e20edc9ca9665874bd88c64c6b62c01928065f6069c5c80c699924").web3.bytesFromHex!) - let bobKeys = try PrivateKeyBundle(serializedData: bobKeyData) - - let invite = try InvitationV1.createDeterministic(sender: aliceKeys, recipient: bobKeys.v1.toV2().getPublicKeyBundle(), context: InvitationV1.Context.with { $0.conversationID = "test" }) - - XCTAssertEqual(invite.topic, "/xmtp/0/m-4b52be1e8567d72d0bc407debe2d3c7fca2ae93a47e58c3f9b5c5068aff80ec5/proto") - } + func testGeneratesKnownDeterministicTopic() async throws { + // address = 0xF56d1F3b1290204441Cb3843C2Cac1C2f5AEd690 + let aliceKeyData = Data(("0x0a8a030ac20108c192a3f7923112220a2068d2eb2ef8c50c4916b42ce638c5610e44ff4eb3ecb098" + + "c9dacf032625c72f101a940108c192a3f7923112460a440a40fc9822283078c323c9319c45e60ab4" + + "2c65f6e1744ed8c23c52728d456d33422824c98d307e8b1c86a26826578523ba15fe6f04a17fca17" + + "6664ee8017ec8ba59310011a430a410498dc2315dd45d99f5e900a071e7b56142de344540f07fbc7" + + "3a0f9a5d5df6b52eb85db06a3825988ab5e04746bc221fcdf5310a44d9523009546d4bfbfbb89cfb" + + "12c20108eb92a3f7923112220a20788be9da8e1a1a08b05f7cbf22d86980bc056b130c482fa5bd26" + + "ccb8d29b30451a940108eb92a3f7923112460a440a40a7afa25cb6f3fbb98f9e5cd92a1df1898452" + + "e0dfa1d7e5affe9eaf9b72dd14bc546d86c399768badf983f07fa7dd16eee8d793357ce6fccd6768" + + "07d87bcc595510011a430a410422931e6295c3c93a5f6f5e729dc02e1754e916cb9be16d36dc163a" + + "300931f42a0cd5fde957d75c2068e1980c5f86843daf16aba8ae57e8160b8b9f0191def09e").web3.bytesFromHex!) + let aliceKeys = try PrivateKeyBundle(serializedData: aliceKeyData).v1.toV2() + + // address = 0x3De402A325323Bb97f00cE3ad5bFAc96A11F9A34 + let bobKeyData = Data(("0x0a88030ac001088cd68df7923112220a209057f8d813314a2aae74e6c4c30f909c1c496b6037ce32" + + "a12c613558a8e961681a9201088cd68df7923112440a420a40501ae9b4f75d5bb5bae3ca4ecfda4e" + + "de9edc5a9b7fc2d56dc7325b837957c23235cc3005b46bb9ef485f106404dcf71247097ed5096355" + + "90f4b7987b833d03661a430a4104e61a7ae511567f4a2b5551221024b6932d6cdb8ecf3876ec64cf" + + "29be4291dd5428fc0301963cdf6939978846e2c35fd38fcb70c64296a929f166ef6e4e91045712c2" + + "0108b8d68df7923112220a2027707399474d417bf6aae4baa3d73b285bf728353bc3e156b0e32461" + + "ebb48f8c1a940108b8d68df7923112460a440a40fb96fa38c3f013830abb61cf6b39776e0475eb13" + + "79c66013569c3d2daecdd48c7fbee945dcdbdc5717d1f4ffd342c4d3f1b7215912829751a94e3ae1" + + "1007e0a110011a430a4104952b7158cfe819d92743a4132e2e3ae867d72f6a08292aebf471d0a7a2" + + "907f3e9947719033e20edc9ca9665874bd88c64c6b62c01928065f6069c5c80c699924").web3.bytesFromHex!) + let bobKeys = try PrivateKeyBundle(serializedData: bobKeyData) + + let invite = try InvitationV1.createDeterministic(sender: aliceKeys, recipient: bobKeys.v1.toV2().getPublicKeyBundle(), context: InvitationV1.Context.with { $0.conversationID = "test" }) + + XCTAssertEqual(invite.topic, "/xmtp/0/m-4b52be1e8567d72d0bc407debe2d3c7fca2ae93a47e58c3f9b5c5068aff80ec5/proto") + } } diff --git a/Tests/XMTPTests/MessageTests.swift b/Tests/XMTPTests/MessageTests.swift index e1931553..ff99b355 100644 --- a/Tests/XMTPTests/MessageTests.swift +++ b/Tests/XMTPTests/MessageTests.swift @@ -7,8 +7,8 @@ import CryptoKit import XCTest -import XMTPRust @testable import XMTP +import XMTPRust import XMTPTestHelpers @available(iOS 16.0, *) @@ -44,7 +44,7 @@ class MessageTests: XCTestCase { } func testFullyEncodesDecodesMessagesV2() async throws { - try TestConfig.skipIfNotRunningLocalNodeTests() + try TestConfig.skipIfNotRunningLocalNodeTests() let aliceWallet = try PrivateKey.generate() let bobWallet = try PrivateKey.generate() @@ -56,9 +56,9 @@ class MessageTests: XCTestCase { invitationContext.conversationID = "https://example.com/1" let invitationv1 = try InvitationV1.createDeterministic( - sender: alice.toV2(), - recipient: bob.toV2().getPublicKeyBundle(), - context: invitationContext + sender: alice.toV2(), + recipient: bob.toV2().getPublicKeyBundle(), + context: invitationContext ) let sealedInvitation = try SealedInvitation.createV1(sender: alice.toV2(), recipient: bob.toV2().getPublicKeyBundle(), created: Date(), invitation: invitationv1) let encoder = TextCodec() @@ -102,7 +102,7 @@ class MessageTests: XCTestCase { } func testGetsV2ID() async throws { - try TestConfig.skip(because: "run manually against dev") + try TestConfig.skip(because: "run manually against dev") let envelopeMessageData = Data( "12bf040a470880dedf9dafc0ff9e17123b2f786d74702f302f6d2d32536b644e355161305a6d694649357433524662667749532d4f4c76356a7573716e6465656e544c764e672f70726f746f12f3030af0030a20439174a205643a50af33c7670341338526dbb9c1cf0560687ff8a742e957282d120c090ba2b385b40639867493ce1abd037648c947f72e5c62e8691d7748e78f9a346ff401c97a628ebecf627d722829ff9cfb7d7c3e0b9e26b5801f2b5a39fd58757cc5771427bfefad6243f52cfc84b384fa042873ebeb90948aa80ca34f26ff883d64720c9228ed6bcd1a5c46953a12ae8732fd70260651455674e2e2c23bc8d64ed35562fef4cdfc55d38e72ad9cf2d597e68f48b6909967b0f5d0b4f33c0af3efce55c739fbc93888d20b833df15811823970a356b26622936564d830434d3ecde9a013f7433142e366f1df5589131e440251be54d5d6deef9aaaa9facac26eb54fb7b74eb48c5a2a9a2e2956633b123cc5b91dec03e4dba30683be03bd7510f16103d3f81712dccf2be003f2f77f9e1f162bc47f6c1c38a1068abd3403952bef31d75e8024e7a62d9a8cbd48f1872a0156abb559d01de689b4370a28454658957061c46f47fc5594808d15753876d4b5408b3a3410d0555c016e427dfceae9c05a4a21fd7ce4cfbb11b2a696170443cf310e0083b0a48e357fc2f00c688c0b56821c8a14c2bb44ddfa31d680dfc85efe4811e86c6aa3adfc373ad5731ddab83960774d98d60075b8fd70228da5d748bfb7a5334bd07e1cc4a9fbf3d5de50860d0684bb27786b5b4e00d415".web3.bytesFromHex! ) diff --git a/Tests/XMTPTests/PaginationTests.swift b/Tests/XMTPTests/PaginationTests.swift index a1534edf..b7604f46 100644 --- a/Tests/XMTPTests/PaginationTests.swift +++ b/Tests/XMTPTests/PaginationTests.swift @@ -14,7 +14,6 @@ import XMTPTestHelpers @available(iOS 15, *) class PaginationTests: XCTestCase { - func newClientHelper(account: PrivateKey) async throws -> Client { let client = try await Client.create(account: account, options: ClientOptions(api: .init(env: .local, isSecure: false))) return client @@ -57,7 +56,7 @@ class PaginationTests: XCTestCase { XCTAssertEqual("hey alice 2", messages2[0].body) // Send many many more messages, such that it forces cursor saving and pagination - for i in 4..<101 { + for i in 4 ..< 101 { try await bobConversation.send(content: "hey alice \(i)", sentAt: Date()) } // Grab the messages 50 at a time @@ -73,7 +72,7 @@ class PaginationTests: XCTestCase { } func testCanStreamConversationsV2() async throws { - try TestConfig.skipIfNotRunningLocalNodeTests() + try TestConfig.skipIfNotRunningLocalNodeTests() let alice = try PrivateKey.generate() let bob = try PrivateKey.generate() diff --git a/XMTPiOSExample/XMTPiOSExample/Account/WalletConnection.swift b/XMTPiOSExample/XMTPiOSExample/Account/WalletConnection.swift index 830ec56d..fcf24cba 100644 --- a/XMTPiOSExample/XMTPiOSExample/Account/WalletConnection.swift +++ b/XMTPiOSExample/XMTPiOSExample/Account/WalletConnection.swift @@ -10,7 +10,6 @@ import UIKit import WalletConnectSwift import web3 import XMTP -import UIKit extension WCURL { var asURL: URL {