From ede81225aec813acb1061c76ffbb2fcc6b07fea3 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Fri, 13 Sep 2024 13:22:24 -0600 Subject: [PATCH 01/45] update package --- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a874b2a3..4d3d866a 100644 --- a/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -59,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/xmtp/libxmtp-swift.git", "state" : { - "revision" : "06e890646a32c3ae9b9ac78150a7ec4971e54c9d", - "version" : "0.5.8-beta3" + "revision" : "abd4f896f539e5bb090c85022177d775ad08dcb1", + "version" : "0.5.8-beta4" } }, { From 2f9cca8b451edf5ee3f2e29958c74550b15bfa39 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Fri, 20 Sep 2024 16:38:06 -0600 Subject: [PATCH 02/45] add chain id and SCW check --- Sources/XMTPiOS/SigningKey.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Sources/XMTPiOS/SigningKey.swift b/Sources/XMTPiOS/SigningKey.swift index 7eb93eab..4acd7011 100644 --- a/Sources/XMTPiOS/SigningKey.swift +++ b/Sources/XMTPiOS/SigningKey.swift @@ -19,6 +19,12 @@ import LibXMTP public protocol SigningKey { /// A wallet address for this key var address: String { get } + + /// If this signing key is a smart contract wallet + var isSmartContractWallet: Bool { get } + + /// The name of the chainId for example "eip155:1" + var chainId: String { get } /// Sign the data and return a secp256k1 compact recoverable signature. func sign(_ data: Data) async throws -> Signature @@ -29,6 +35,14 @@ public protocol SigningKey { } extension SigningKey { + public var isSmartContractWallet: Bool { + return false + } + + public var chainId: String { + return "eip155:1" + } + func createIdentity(_ identity: PrivateKey, preCreateIdentityCallback: PreEventCallback? = nil) async throws -> AuthorizedIdentity { var slimKey = PublicKey() slimKey.timestamp = UInt64(Date().millisecondsSinceEpoch) From 4e43786498a0ae0fafe945e2f495ea9c3ef83b31 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Fri, 20 Sep 2024 16:46:02 -0600 Subject: [PATCH 03/45] add implementation --- Sources/XMTPiOS/Client.swift | 27 ++++++++++++++++++++++++++- Sources/XMTPiOS/SigningKey.swift | 11 +++++++++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/Sources/XMTPiOS/Client.swift b/Sources/XMTPiOS/Client.swift index e0da501d..423a1a2c 100644 --- a/Sources/XMTPiOS/Client.swift +++ b/Sources/XMTPiOS/Client.swift @@ -247,7 +247,14 @@ public final class Client { if let signingKey = signingKey { do { let signedData = try await signingKey.sign(message: signatureRequest.signatureText()) - try await signatureRequest.addEcdsaSignature(signatureBytes: signedData.rawData) + if signingKey.isSmartContractWallet { + guard isValidAccountID(signingKey.address) else { + throw ClientError.creationError("Account address must conform to CAIP format") + } + try await signatureRequest.addScwSignature(signatureBytes: signedData.rawData, address: signingKey.address, chainId: signingKey.chainId, blockNumber: signingKey.blockNumber) + } else { + try await signatureRequest.addEcdsaSignature(signatureBytes: signedData.rawData) + } try await v3Client.registerIdentity(signatureRequest: signatureRequest) } catch { throw ClientError.creationError("Failed to sign the message: \(error.localizedDescription)") @@ -696,4 +703,22 @@ public final class Client { } return InboxState(ffiInboxState: try await client.inboxState(refreshFromNetwork: refreshFromNetwork)) } + + // See for more details https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-10.md + public func isValidAccountID(_ accountAddress: String) -> Bool { + // Define the regular expressions for chain_id and account_address + let chainIDPattern = "[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}" + let accountAddressPattern = "[-.%a-zA-Z0-9]{1,128}" + + // Combine them to match the entire account_id format + let accountIDPattern = "^\(chainIDPattern):\(accountAddressPattern)$" + + let regex = try? NSRegularExpression(pattern: accountIDPattern) + + if let match = regex?.firstMatch(in: accountAddress, options: [], range: NSRange(location: 0, length: accountAddress.utf16.count)) { + return match.range.location != NSNotFound + } else { + return false + } + } } diff --git a/Sources/XMTPiOS/SigningKey.swift b/Sources/XMTPiOS/SigningKey.swift index 4acd7011..9a2b0cca 100644 --- a/Sources/XMTPiOS/SigningKey.swift +++ b/Sources/XMTPiOS/SigningKey.swift @@ -24,7 +24,10 @@ public protocol SigningKey { var isSmartContractWallet: Bool { get } /// The name of the chainId for example "eip155:1" - var chainId: String { get } + var chainId: UInt64 { get } + + /// The blockNumber of the chain for example "1" + var blockNumber: UInt64 { get } /// Sign the data and return a secp256k1 compact recoverable signature. func sign(_ data: Data) async throws -> Signature @@ -39,7 +42,11 @@ extension SigningKey { return false } - public var chainId: String { + public var chainId: UInt64 { + return "eip155:1" + } + + public var blockNumber: UInt64 { return "eip155:1" } From ede8c2958518e4d3334bf8496345b51b0b378f5e Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Fri, 20 Sep 2024 18:15:10 -0600 Subject: [PATCH 04/45] fix a little formatting --- Sources/XMTPiOS/Client.swift | 21 --------------------- Sources/XMTPiOS/SigningKey.swift | 6 +++--- 2 files changed, 3 insertions(+), 24 deletions(-) diff --git a/Sources/XMTPiOS/Client.swift b/Sources/XMTPiOS/Client.swift index 423a1a2c..1aadce17 100644 --- a/Sources/XMTPiOS/Client.swift +++ b/Sources/XMTPiOS/Client.swift @@ -248,9 +248,6 @@ public final class Client { do { let signedData = try await signingKey.sign(message: signatureRequest.signatureText()) if signingKey.isSmartContractWallet { - guard isValidAccountID(signingKey.address) else { - throw ClientError.creationError("Account address must conform to CAIP format") - } try await signatureRequest.addScwSignature(signatureBytes: signedData.rawData, address: signingKey.address, chainId: signingKey.chainId, blockNumber: signingKey.blockNumber) } else { try await signatureRequest.addEcdsaSignature(signatureBytes: signedData.rawData) @@ -703,22 +700,4 @@ public final class Client { } return InboxState(ffiInboxState: try await client.inboxState(refreshFromNetwork: refreshFromNetwork)) } - - // See for more details https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-10.md - public func isValidAccountID(_ accountAddress: String) -> Bool { - // Define the regular expressions for chain_id and account_address - let chainIDPattern = "[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}" - let accountAddressPattern = "[-.%a-zA-Z0-9]{1,128}" - - // Combine them to match the entire account_id format - let accountIDPattern = "^\(chainIDPattern):\(accountAddressPattern)$" - - let regex = try? NSRegularExpression(pattern: accountIDPattern) - - if let match = regex?.firstMatch(in: accountAddress, options: [], range: NSRange(location: 0, length: accountAddress.utf16.count)) { - return match.range.location != NSNotFound - } else { - return false - } - } } diff --git a/Sources/XMTPiOS/SigningKey.swift b/Sources/XMTPiOS/SigningKey.swift index 9a2b0cca..2c4a63b1 100644 --- a/Sources/XMTPiOS/SigningKey.swift +++ b/Sources/XMTPiOS/SigningKey.swift @@ -23,7 +23,7 @@ public protocol SigningKey { /// If this signing key is a smart contract wallet var isSmartContractWallet: Bool { get } - /// The name of the chainId for example "eip155:1" + /// The name of the chainId for example "8453" is the chainId for base var chainId: UInt64 { get } /// The blockNumber of the chain for example "1" @@ -43,11 +43,11 @@ extension SigningKey { } public var chainId: UInt64 { - return "eip155:1" + return 8453 } public var blockNumber: UInt64 { - return "eip155:1" + return 0 } func createIdentity(_ identity: PrivateKey, preCreateIdentityCallback: PreEventCallback? = nil) async throws -> AuthorizedIdentity { From fd8ddc1822e15117f32bfdfb1397232eb90f863a Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Fri, 20 Sep 2024 18:16:19 -0600 Subject: [PATCH 05/45] change defaults --- Sources/XMTPiOS/SigningKey.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/XMTPiOS/SigningKey.swift b/Sources/XMTPiOS/SigningKey.swift index 2c4a63b1..8e40cace 100644 --- a/Sources/XMTPiOS/SigningKey.swift +++ b/Sources/XMTPiOS/SigningKey.swift @@ -23,7 +23,7 @@ public protocol SigningKey { /// If this signing key is a smart contract wallet var isSmartContractWallet: Bool { get } - /// The name of the chainId for example "8453" is the chainId for base + /// The name of the chainId for example "1" var chainId: UInt64 { get } /// The blockNumber of the chain for example "1" @@ -43,11 +43,11 @@ extension SigningKey { } public var chainId: UInt64 { - return 8453 + return 1 } public var blockNumber: UInt64 { - return 0 + return 1 } func createIdentity(_ identity: PrivateKey, preCreateIdentityCallback: PreEventCallback? = nil) async throws -> AuthorizedIdentity { From 65a4aaa7cf1cb09a693f6eb7441f971ffdeb9c55 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Tue, 24 Sep 2024 11:22:41 -0600 Subject: [PATCH 06/45] make a test release pod --- XMTP.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/XMTP.podspec b/XMTP.podspec index efe9d3b0..3a90780e 100644 --- a/XMTP.podspec +++ b/XMTP.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |spec| # spec.name = "XMTP" - spec.version = "0.14.14" + spec.version = "0.15.0-alpha0" spec.summary = "XMTP SDK Cocoapod" # This description is used to generate tags and improve search results. From a3665034445a241ce130ab2c7bf3b712ed60dcfa Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Tue, 24 Sep 2024 15:25:51 -0600 Subject: [PATCH 07/45] bump the latest libxmtp --- Package.swift | 2 +- Sources/XMTPiOS/Group.swift | 8 ++++---- XMTP.podspec | 4 ++-- .../xcshareddata/swiftpm/Package.resolved | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Package.swift b/Package.swift index fe417580..13882418 100644 --- a/Package.swift +++ b/Package.swift @@ -25,7 +25,7 @@ let package = Package( .package(url: "https://github.com/1024jp/GzipSwift", from: "5.2.0"), .package(url: "https://github.com/bufbuild/connect-swift", exact: "0.12.0"), .package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.0.0"), - .package(url: "https://github.com/xmtp/libxmtp-swift.git", exact: "0.5.8-beta5"), + .package(url: "https://github.com/xmtp/libxmtp-swift.git", exact: "0.5.9-alpha0"), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. diff --git a/Sources/XMTPiOS/Group.swift b/Sources/XMTPiOS/Group.swift index a255bd3a..3d43e151 100644 --- a/Sources/XMTPiOS/Group.swift +++ b/Sources/XMTPiOS/Group.swift @@ -112,16 +112,16 @@ public struct Group: Identifiable, Equatable, Hashable { } public var members: [Member] { - get throws { - return try ffiGroup.listMembers().map { ffiGroupMember in + get async throws { + return try await ffiGroup.listMembers().map { ffiGroupMember in Member(ffiGroupMember: ffiGroupMember) } } } public var peerInboxIds: [String] { - get throws { - var ids = try members.map(\.inboxId) + get async throws { + var ids = try await members.map(\.inboxId) if let index = ids.firstIndex(of: client.inboxID) { ids.remove(at: index) } diff --git a/XMTP.podspec b/XMTP.podspec index 3a90780e..3873598a 100644 --- a/XMTP.podspec +++ b/XMTP.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |spec| # spec.name = "XMTP" - spec.version = "0.15.0-alpha0" + spec.version = "0.15.0-alpha1" spec.summary = "XMTP SDK Cocoapod" # This description is used to generate tags and improve search results. @@ -44,5 +44,5 @@ Pod::Spec.new do |spec| spec.dependency "web3.swift" spec.dependency "GzipSwift" spec.dependency "Connect-Swift", "= 0.12.0" - spec.dependency 'LibXMTP', '= 0.5.8-beta5' + spec.dependency 'LibXMTP', '= 0.5.9-alpha0' end diff --git a/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 73812576..f5d7e620 100644 --- a/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -59,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/xmtp/libxmtp-swift.git", "state" : { - "revision" : "9d5153926ac1bfcab76802d5a7626c2cf47212a4", - "version" : "0.5.8-beta5" + "revision" : "8ebeeaf8ebfe056be0b306910f5128a21795aca9", + "version" : "0.5.9-alpha0" } }, { From a46302b82d3f61c376759e8caa1d1319147fedf2 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Tue, 24 Sep 2024 15:37:46 -0600 Subject: [PATCH 08/45] fix up all the async tests --- Sources/XMTPiOS/Conversation.swift | 4 +- Tests/XMTPTests/GroupPermissionsTests.swift | 6 +- Tests/XMTPTests/GroupTests.swift | 62 +++++++++++++-------- Tests/XMTPTests/V3ClientTests.swift | 2 +- 4 files changed, 44 insertions(+), 30 deletions(-) diff --git a/Sources/XMTPiOS/Conversation.swift b/Sources/XMTPiOS/Conversation.swift index ad0f88ba..a7de6f20 100644 --- a/Sources/XMTPiOS/Conversation.swift +++ b/Sources/XMTPiOS/Conversation.swift @@ -83,7 +83,7 @@ public enum Conversation: Sendable { case let .v2(conversationV2): return conversationV2.peerAddress case let .group(group): - return try group.peerInboxIds.joined(separator: ",") + throw GroupError.notSupportedByGroups } } } @@ -96,7 +96,7 @@ public enum Conversation: Sendable { case let .v2(conversationV2): return [conversationV2.peerAddress] case let .group(group): - return try group.peerInboxIds + throw GroupError.notSupportedByGroups } } } diff --git a/Tests/XMTPTests/GroupPermissionsTests.swift b/Tests/XMTPTests/GroupPermissionsTests.swift index d1293a91..8f362d6f 100644 --- a/Tests/XMTPTests/GroupPermissionsTests.swift +++ b/Tests/XMTPTests/GroupPermissionsTests.swift @@ -213,7 +213,7 @@ class GroupPermissionTests: XCTestCase { let aliceGroup = try await fixtures.aliceClient.conversations.groups().first! // Initial checks for group members and their permissions - var members = try bobGroup.members + var members = try await bobGroup.members var admins = members.filter { $0.permissionLevel == PermissionLevel.Admin } var superAdmins = members.filter { $0.permissionLevel == PermissionLevel.SuperAdmin } var regularMembers = members.filter { $0.permissionLevel == PermissionLevel.Member } @@ -227,7 +227,7 @@ class GroupPermissionTests: XCTestCase { try await bobGroup.sync() try await aliceGroup.sync() - members = try bobGroup.members + members = try await bobGroup.members admins = members.filter { $0.permissionLevel == PermissionLevel.Admin } superAdmins = members.filter { $0.permissionLevel == PermissionLevel.SuperAdmin } regularMembers = members.filter { $0.permissionLevel == PermissionLevel.Member } @@ -241,7 +241,7 @@ class GroupPermissionTests: XCTestCase { try await bobGroup.sync() try await aliceGroup.sync() - members = try bobGroup.members + members = try await bobGroup.members admins = members.filter { $0.permissionLevel == PermissionLevel.Admin } superAdmins = members.filter { $0.permissionLevel == PermissionLevel.SuperAdmin } regularMembers = members.filter { $0.permissionLevel == PermissionLevel.Member } diff --git a/Tests/XMTPTests/GroupTests.swift b/Tests/XMTPTests/GroupTests.swift index 7662a614..a255a29b 100644 --- a/Tests/XMTPTests/GroupTests.swift +++ b/Tests/XMTPTests/GroupTests.swift @@ -98,16 +98,20 @@ class GroupTests: XCTestCase { try await aliceGroup.addMembers(addresses: [fixtures.fred.address]) try await bobGroup.sync() - XCTAssertEqual(try aliceGroup.members.count, 3) - XCTAssertEqual(try bobGroup.members.count, 3) + let aliceMembers = try await aliceGroup.members.count + let bobMembers = try await bobGroup.members.count + XCTAssertEqual(aliceMembers, 3) + XCTAssertEqual(bobMembers, 3) try await bobGroup.addAdmin(inboxId: fixtures.aliceClient.inboxID) try await aliceGroup.removeMembers(addresses: [fixtures.fred.address]) try await bobGroup.sync() - XCTAssertEqual(try aliceGroup.members.count, 2) - XCTAssertEqual(try bobGroup.members.count, 2) + let aliceMembers2 = try await aliceGroup.members.count + let bobMembers2 = try await bobGroup.members.count + XCTAssertEqual(aliceMembers2, 2) + XCTAssertEqual(bobMembers2, 2) try await bobGroup.addMembers(addresses: [fixtures.fred.address]) try await aliceGroup.sync() @@ -115,8 +119,10 @@ class GroupTests: XCTestCase { try await bobGroup.removeAdmin(inboxId: fixtures.aliceClient.inboxID) try await aliceGroup.sync() - XCTAssertEqual(try aliceGroup.members.count, 3) - XCTAssertEqual(try bobGroup.members.count, 3) + let aliceMembers3 = try await aliceGroup.members.count + let bobMembers3 = try await bobGroup.members.count + XCTAssertEqual(aliceMembers3, 3) + XCTAssertEqual(bobMembers3, 3) XCTAssertEqual(try bobGroup.permissionPolicySet().addMemberPolicy, .allow) XCTAssertEqual(try aliceGroup.permissionPolicySet().addMemberPolicy, .allow) @@ -145,30 +151,38 @@ class GroupTests: XCTestCase { try await bobGroup.addMembers(addresses: [fixtures.fred.address]) try await aliceGroup.sync() - XCTAssertEqual(try aliceGroup.members.count, 3) - XCTAssertEqual(try bobGroup.members.count, 3) + let aliceMembers = try await aliceGroup.members.count + let bobMembers = try await bobGroup.members.count + XCTAssertEqual(aliceMembers, 3) + XCTAssertEqual(bobMembers, 3) await assertThrowsAsyncError( try await aliceGroup.removeMembers(addresses: [fixtures.fred.address]) ) try await bobGroup.sync() - XCTAssertEqual(try aliceGroup.members.count, 3) - XCTAssertEqual(try bobGroup.members.count, 3) + let aliceMembers1 = try await aliceGroup.members.count + let bobMembers1 = try await bobGroup.members.count + XCTAssertEqual(aliceMembers1, 3) + XCTAssertEqual(bobMembers1, 3) try await bobGroup.removeMembers(addresses: [fixtures.fred.address]) try await aliceGroup.sync() - XCTAssertEqual(try aliceGroup.members.count, 2) - XCTAssertEqual(try bobGroup.members.count, 2) + let aliceMembers2 = try await aliceGroup.members.count + let bobMembers2 = try await bobGroup.members.count + XCTAssertEqual(aliceMembers2, 2) + XCTAssertEqual(bobMembers2, 2) await assertThrowsAsyncError( try await aliceGroup.addMembers(addresses: [fixtures.fred.address]) ) try await bobGroup.sync() - XCTAssertEqual(try aliceGroup.members.count, 2) - XCTAssertEqual(try bobGroup.members.count, 2) + let aliceMembers3 = try await aliceGroup.members.count + let bobMembers3 = try await bobGroup.members.count + XCTAssertEqual(aliceMembers3, 2) + XCTAssertEqual(bobMembers2, 2) XCTAssertEqual(try bobGroup.permissionPolicySet().addMemberPolicy, .admin) XCTAssertEqual(try aliceGroup.permissionPolicySet().addMemberPolicy, .admin) @@ -210,7 +224,7 @@ class GroupTests: XCTestCase { let group = try await fixtures.aliceClient.conversations.newGroup(with: [fixtures.bob.address]) try await group.sync() - let members = try group.members.map(\.inboxId).sorted() + let members = try await group.members.map(\.inboxId).sorted() let peerMembers = try Conversation.group(group).peerAddresses.sorted() XCTAssertEqual([fixtures.bobClient.inboxID, fixtures.aliceClient.inboxID].sorted(), members) @@ -224,7 +238,7 @@ class GroupTests: XCTestCase { try await group.addMembers(addresses: [fixtures.fred.address]) try await group.sync() - let members = try group.members.map(\.inboxId).sorted() + let members = try await group.members.map(\.inboxId).sorted() XCTAssertEqual([ fixtures.bobClient.inboxID, @@ -243,7 +257,7 @@ class GroupTests: XCTestCase { try await group.addMembersByInboxId(inboxIds: [fixtures.fredClient.inboxID]) try await group.sync() - let members = try group.members.map(\.inboxId).sorted() + let members = try await group.members.map(\.inboxId).sorted() XCTAssertEqual([ fixtures.bobClient.inboxID, @@ -260,7 +274,7 @@ class GroupTests: XCTestCase { let group = try await fixtures.aliceClient.conversations.newGroup(with: [fixtures.bob.address, fixtures.fred.address]) try await group.sync() - let members = try group.members.map(\.inboxId).sorted() + let members = try await group.members.map(\.inboxId).sorted() XCTAssertEqual([ fixtures.bobClient.inboxID, @@ -272,7 +286,7 @@ class GroupTests: XCTestCase { try await group.sync() - let newMembers = try group.members.map(\.inboxId).sorted() + let newMembers = try await group.members.map(\.inboxId).sorted() XCTAssertEqual([ fixtures.bobClient.inboxID, fixtures.aliceClient.inboxID, @@ -287,7 +301,7 @@ class GroupTests: XCTestCase { let group = try await fixtures.aliceClient.conversations.newGroup(with: [fixtures.bob.address, fixtures.fred.address]) try await group.sync() - let members = try group.members.map(\.inboxId).sorted() + let members = try await group.members.map(\.inboxId).sorted() XCTAssertEqual([ fixtures.bobClient.inboxID, @@ -299,7 +313,7 @@ class GroupTests: XCTestCase { try await group.sync() - let newMembers = try group.members.map(\.inboxId).sorted() + let newMembers = try await group.members.map(\.inboxId).sorted() XCTAssertEqual([ fixtures.bobClient.inboxID, fixtures.aliceClient.inboxID, @@ -323,7 +337,7 @@ class GroupTests: XCTestCase { let group = try await fixtures.aliceClient.conversations.newGroup(with: [fixtures.bob.address, fixtures.fred.address]) try await group.sync() - let members = try group.members.map(\.inboxId).sorted() + let members = try await group.members.map(\.inboxId).sorted() XCTAssertEqual([ fixtures.bobClient.inboxID, @@ -345,7 +359,7 @@ class GroupTests: XCTestCase { try await group.sync() - let newMembers = try group.members.map(\.inboxId).sorted() + let newMembers = try await group.members.map(\.inboxId).sorted() XCTAssertEqual([ fixtures.bobClient.inboxID, fixtures.aliceClient.inboxID, @@ -907,7 +921,7 @@ class GroupTests: XCTestCase { await withThrowingTaskGroup(of: [Member].self) { taskGroup in for group in groups { taskGroup.addTask { - return try group.members + return try await group.members } } } diff --git a/Tests/XMTPTests/V3ClientTests.swift b/Tests/XMTPTests/V3ClientTests.swift index 1cbd5a6e..573e7874 100644 --- a/Tests/XMTPTests/V3ClientTests.swift +++ b/Tests/XMTPTests/V3ClientTests.swift @@ -64,7 +64,7 @@ class V3ClientTests: XCTestCase { func testsCanCreateGroup() async throws { let fixtures = try await localFixtures() let group = try await fixtures.boV3Client.conversations.newGroup(with: [fixtures.caroV2V3.address]) - let members = try group.members.map(\.inboxId).sorted() + let members = try await group.members.map(\.inboxId).sorted() XCTAssertEqual([fixtures.caroV2V3Client.inboxID, fixtures.boV3Client.inboxID].sorted(), members) await assertThrowsAsyncError( From 16e2e6d59f4a18115fc9860e232bc0d934b711dd Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Wed, 25 Sep 2024 23:22:52 -0600 Subject: [PATCH 09/45] add installation timestamps and async members --- Package.swift | 2 +- Sources/XMTPiOS/Group.swift | 8 +-- Sources/XMTPiOS/Mls/InboxState.swift | 4 +- Sources/XMTPiOS/Mls/Installation.swift | 27 ++++++++ Tests/XMTPTests/ClientTests.swift | 5 +- Tests/XMTPTests/GroupPermissionsTests.swift | 6 +- Tests/XMTPTests/GroupTests.swift | 66 +++++++++++-------- XMTP.podspec | 4 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- 9 files changed, 84 insertions(+), 42 deletions(-) create mode 100644 Sources/XMTPiOS/Mls/Installation.swift diff --git a/Package.swift b/Package.swift index f3431147..078c325f 100644 --- a/Package.swift +++ b/Package.swift @@ -25,7 +25,7 @@ let package = Package( .package(url: "https://github.com/1024jp/GzipSwift", from: "5.2.0"), .package(url: "https://github.com/bufbuild/connect-swift", exact: "0.12.0"), .package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.0.0"), - .package(url: "https://github.com/xmtp/libxmtp-swift.git", exact: "0.5.8-beta6"), + .package(url: "https://github.com/xmtp/libxmtp-swift.git", exact: "0.5.8-beta7"), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. diff --git a/Sources/XMTPiOS/Group.swift b/Sources/XMTPiOS/Group.swift index 05446058..b9ee9d24 100644 --- a/Sources/XMTPiOS/Group.swift +++ b/Sources/XMTPiOS/Group.swift @@ -112,16 +112,16 @@ public struct Group: Identifiable, Equatable, Hashable { } public var members: [Member] { - get throws { - return try ffiGroup.listMembers().map { ffiGroupMember in + get async throws { + return try await ffiGroup.listMembers().map { ffiGroupMember in Member(ffiGroupMember: ffiGroupMember) } } } public var peerInboxIds: [String] { - get throws { - var ids = try members.map(\.inboxId) + get async throws { + var ids = try await members.map(\.inboxId) if let index = ids.firstIndex(of: client.inboxID) { ids.remove(at: index) } diff --git a/Sources/XMTPiOS/Mls/InboxState.swift b/Sources/XMTPiOS/Mls/InboxState.swift index f604f15c..bad69551 100644 --- a/Sources/XMTPiOS/Mls/InboxState.swift +++ b/Sources/XMTPiOS/Mls/InboxState.swift @@ -23,8 +23,8 @@ public struct InboxState { ffiInboxState.accountAddresses } - public var installationIds: [String] { - ffiInboxState.installationIds.map { $0.toHex } + public var installations: [Installation] { + ffiInboxState.installations.map { Installation(ffiInstallation: $0) } } public var recoveryAddress: String { diff --git a/Sources/XMTPiOS/Mls/Installation.swift b/Sources/XMTPiOS/Mls/Installation.swift new file mode 100644 index 00000000..fa237ee0 --- /dev/null +++ b/Sources/XMTPiOS/Mls/Installation.swift @@ -0,0 +1,27 @@ +// +// Installation.swift +// +// +// Created by Naomi Plasterer on 9/25/24. +// + +import Foundation +import LibXMTP + +public struct Installation { + var ffiInstallation: FfiInstallation + + init(ffiInstallation: FfiInstallation) { + self.ffiInstallation = ffiInstallation + } + + public var id: String { + ffiInstallation.id.toHex + } + + var createdAt: Date? { + guard let timestampNs = ffiInstallation.clientTimestampNs else { return nil } + return Date(timeIntervalSince1970: TimeInterval(timestampNs) / 1_000_000_000) + } +} + diff --git a/Tests/XMTPTests/ClientTests.swift b/Tests/XMTPTests/ClientTests.swift index 234e6b7d..40b2495e 100644 --- a/Tests/XMTPTests/ClientTests.swift +++ b/Tests/XMTPTests/ClientTests.swift @@ -506,11 +506,12 @@ class ClientTests: XCTestCase { ) let state = try await alixClient3.inboxState(refreshFromNetwork: true) - XCTAssertEqual(state.installationIds.count, 3) + XCTAssertEqual(state.installations.count, 3) + XCTAssert(state.installations.first?.createdAt != nil) try await alixClient3.revokeAllOtherInstallations(signingKey: alix) let newState = try await alixClient3.inboxState(refreshFromNetwork: true) - XCTAssertEqual(newState.installationIds.count, 1) + XCTAssertEqual(newState.installations.count, 1) } } diff --git a/Tests/XMTPTests/GroupPermissionsTests.swift b/Tests/XMTPTests/GroupPermissionsTests.swift index d1293a91..8f362d6f 100644 --- a/Tests/XMTPTests/GroupPermissionsTests.swift +++ b/Tests/XMTPTests/GroupPermissionsTests.swift @@ -213,7 +213,7 @@ class GroupPermissionTests: XCTestCase { let aliceGroup = try await fixtures.aliceClient.conversations.groups().first! // Initial checks for group members and their permissions - var members = try bobGroup.members + var members = try await bobGroup.members var admins = members.filter { $0.permissionLevel == PermissionLevel.Admin } var superAdmins = members.filter { $0.permissionLevel == PermissionLevel.SuperAdmin } var regularMembers = members.filter { $0.permissionLevel == PermissionLevel.Member } @@ -227,7 +227,7 @@ class GroupPermissionTests: XCTestCase { try await bobGroup.sync() try await aliceGroup.sync() - members = try bobGroup.members + members = try await bobGroup.members admins = members.filter { $0.permissionLevel == PermissionLevel.Admin } superAdmins = members.filter { $0.permissionLevel == PermissionLevel.SuperAdmin } regularMembers = members.filter { $0.permissionLevel == PermissionLevel.Member } @@ -241,7 +241,7 @@ class GroupPermissionTests: XCTestCase { try await bobGroup.sync() try await aliceGroup.sync() - members = try bobGroup.members + members = try await bobGroup.members admins = members.filter { $0.permissionLevel == PermissionLevel.Admin } superAdmins = members.filter { $0.permissionLevel == PermissionLevel.SuperAdmin } regularMembers = members.filter { $0.permissionLevel == PermissionLevel.Member } diff --git a/Tests/XMTPTests/GroupTests.swift b/Tests/XMTPTests/GroupTests.swift index 1d4d25e7..6dfb9c61 100644 --- a/Tests/XMTPTests/GroupTests.swift +++ b/Tests/XMTPTests/GroupTests.swift @@ -98,16 +98,20 @@ class GroupTests: XCTestCase { try await aliceGroup.addMembers(addresses: [fixtures.fred.address]) try await bobGroup.sync() - XCTAssertEqual(try aliceGroup.members.count, 3) - XCTAssertEqual(try bobGroup.members.count, 3) + var aliceMembersCount = try await aliceGroup.members.count + var bobMembersCount = try await bobGroup.members.count + XCTAssertEqual(aliceMembersCount, 3) + XCTAssertEqual(bobMembersCount, 3) try await bobGroup.addAdmin(inboxId: fixtures.aliceClient.inboxID) try await aliceGroup.removeMembers(addresses: [fixtures.fred.address]) try await bobGroup.sync() - XCTAssertEqual(try aliceGroup.members.count, 2) - XCTAssertEqual(try bobGroup.members.count, 2) + aliceMembersCount = try await aliceGroup.members.count + bobMembersCount = try await bobGroup.members.count + XCTAssertEqual(aliceMembersCount, 2) + XCTAssertEqual(bobMembersCount, 2) try await bobGroup.addMembers(addresses: [fixtures.fred.address]) try await aliceGroup.sync() @@ -115,8 +119,10 @@ class GroupTests: XCTestCase { try await bobGroup.removeAdmin(inboxId: fixtures.aliceClient.inboxID) try await aliceGroup.sync() - XCTAssertEqual(try aliceGroup.members.count, 3) - XCTAssertEqual(try bobGroup.members.count, 3) + aliceMembersCount = try await aliceGroup.members.count + bobMembersCount = try await bobGroup.members.count + XCTAssertEqual(aliceMembersCount, 3) + XCTAssertEqual(bobMembersCount, 3) XCTAssertEqual(try bobGroup.permissionPolicySet().addMemberPolicy, .allow) XCTAssertEqual(try aliceGroup.permissionPolicySet().addMemberPolicy, .allow) @@ -145,30 +151,38 @@ class GroupTests: XCTestCase { try await bobGroup.addMembers(addresses: [fixtures.fred.address]) try await aliceGroup.sync() - XCTAssertEqual(try aliceGroup.members.count, 3) - XCTAssertEqual(try bobGroup.members.count, 3) + var aliceMembersCount = try await aliceGroup.members.count + var bobMembersCount = try await bobGroup.members.count + XCTAssertEqual(aliceMembersCount, 3) + XCTAssertEqual(bobMembersCount, 3) await assertThrowsAsyncError( try await aliceGroup.removeMembers(addresses: [fixtures.fred.address]) ) try await bobGroup.sync() - XCTAssertEqual(try aliceGroup.members.count, 3) - XCTAssertEqual(try bobGroup.members.count, 3) + aliceMembersCount = try await aliceGroup.members.count + bobMembersCount = try await bobGroup.members.count + XCTAssertEqual(aliceMembersCount, 3) + XCTAssertEqual(bobMembersCount, 3) try await bobGroup.removeMembers(addresses: [fixtures.fred.address]) try await aliceGroup.sync() - XCTAssertEqual(try aliceGroup.members.count, 2) - XCTAssertEqual(try bobGroup.members.count, 2) + aliceMembersCount = try await aliceGroup.members.count + bobMembersCount = try await bobGroup.members.count + XCTAssertEqual(aliceMembersCount, 2) + XCTAssertEqual(bobMembersCount, 2) await assertThrowsAsyncError( try await aliceGroup.addMembers(addresses: [fixtures.fred.address]) ) try await bobGroup.sync() - XCTAssertEqual(try aliceGroup.members.count, 2) - XCTAssertEqual(try bobGroup.members.count, 2) + aliceMembersCount = try await aliceGroup.members.count + bobMembersCount = try await bobGroup.members.count + XCTAssertEqual(aliceMembersCount, 2) + XCTAssertEqual(bobMembersCount, 2) XCTAssertEqual(try bobGroup.permissionPolicySet().addMemberPolicy, .admin) XCTAssertEqual(try aliceGroup.permissionPolicySet().addMemberPolicy, .admin) @@ -210,7 +224,7 @@ class GroupTests: XCTestCase { let group = try await fixtures.aliceClient.conversations.newGroup(with: [fixtures.bob.address]) try await group.sync() - let members = try group.members.map(\.inboxId).sorted() + let members = try await group.members.map(\.inboxId).sorted() let peerMembers = try Conversation.group(group).peerAddresses.sorted() XCTAssertEqual([fixtures.bobClient.inboxID, fixtures.aliceClient.inboxID].sorted(), members) @@ -224,7 +238,7 @@ class GroupTests: XCTestCase { try await group.addMembers(addresses: [fixtures.fred.address]) try await group.sync() - let members = try group.members.map(\.inboxId).sorted() + let members = try await group.members.map(\.inboxId).sorted() XCTAssertEqual([ fixtures.bobClient.inboxID, @@ -243,7 +257,7 @@ class GroupTests: XCTestCase { try await group.addMembersByInboxId(inboxIds: [fixtures.fredClient.inboxID]) try await group.sync() - let members = try group.members.map(\.inboxId).sorted() + let members = try await group.members.map(\.inboxId).sorted() XCTAssertEqual([ fixtures.bobClient.inboxID, @@ -260,7 +274,7 @@ class GroupTests: XCTestCase { let group = try await fixtures.aliceClient.conversations.newGroup(with: [fixtures.bob.address, fixtures.fred.address]) try await group.sync() - let members = try group.members.map(\.inboxId).sorted() + let members = try await group.members.map(\.inboxId).sorted() XCTAssertEqual([ fixtures.bobClient.inboxID, @@ -272,7 +286,7 @@ class GroupTests: XCTestCase { try await group.sync() - let newMembers = try group.members.map(\.inboxId).sorted() + let newMembers = try await group.members.map(\.inboxId).sorted() XCTAssertEqual([ fixtures.bobClient.inboxID, fixtures.aliceClient.inboxID, @@ -287,7 +301,7 @@ class GroupTests: XCTestCase { let group = try await fixtures.aliceClient.conversations.newGroup(with: [fixtures.bob.address, fixtures.fred.address]) try await group.sync() - let members = try group.members.map(\.inboxId).sorted() + let members = try await group.members.map(\.inboxId).sorted() XCTAssertEqual([ fixtures.bobClient.inboxID, @@ -299,7 +313,7 @@ class GroupTests: XCTestCase { try await group.sync() - let newMembers = try group.members.map(\.inboxId).sorted() + let newMembers = try await group.members.map(\.inboxId).sorted() XCTAssertEqual([ fixtures.bobClient.inboxID, fixtures.aliceClient.inboxID, @@ -323,7 +337,7 @@ class GroupTests: XCTestCase { let group = try await fixtures.aliceClient.conversations.newGroup(with: [fixtures.bob.address, fixtures.fred.address]) try await group.sync() - let members = try group.members.map(\.inboxId).sorted() + let members = try await group.members.map(\.inboxId).sorted() XCTAssertEqual([ fixtures.bobClient.inboxID, @@ -345,7 +359,7 @@ class GroupTests: XCTestCase { try await group.sync() - let newMembers = try group.members.map(\.inboxId).sorted() + let newMembers = try await group.members.map(\.inboxId).sorted() XCTAssertEqual([ fixtures.bobClient.inboxID, fixtures.aliceClient.inboxID, @@ -786,7 +800,7 @@ class GroupTests: XCTestCase { try await fixtures.bobClient.contacts.allowInboxes(inboxIds: [fixtures.aliceClient.inboxID]) - var alixMember = try boGroup.members.first(where: { member in member.inboxId == fixtures.aliceClient.inboxID }) + var alixMember = try await boGroup.members.first(where: { member in member.inboxId == fixtures.aliceClient.inboxID }) XCTAssertEqual(alixMember?.consentState, .allowed) isInboxAllowed = try await fixtures.bobClient.contacts.isInboxAllowed(inboxId: fixtures.aliceClient.inboxID) @@ -796,7 +810,7 @@ class GroupTests: XCTestCase { try await fixtures.bobClient.contacts.denyInboxes(inboxIds: [fixtures.aliceClient.inboxID]) - alixMember = try boGroup.members.first(where: { member in member.inboxId == fixtures.aliceClient.inboxID }) + alixMember = try await boGroup.members.first(where: { member in member.inboxId == fixtures.aliceClient.inboxID }) XCTAssertEqual(alixMember?.consentState, .denied) isInboxAllowed = try await fixtures.bobClient.contacts.isInboxAllowed(inboxId: fixtures.aliceClient.inboxID) @@ -941,7 +955,7 @@ class GroupTests: XCTestCase { await withThrowingTaskGroup(of: [Member].self) { taskGroup in for group in groups { taskGroup.addTask { - return try group.members + return try await group.members } } } diff --git a/XMTP.podspec b/XMTP.podspec index 2071549c..a6496c21 100644 --- a/XMTP.podspec +++ b/XMTP.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |spec| # spec.name = "XMTP" - spec.version = "0.14.14" + spec.version = "0.14.15" spec.summary = "XMTP SDK Cocoapod" # This description is used to generate tags and improve search results. @@ -44,5 +44,5 @@ Pod::Spec.new do |spec| spec.dependency "web3.swift" spec.dependency "GzipSwift" spec.dependency "Connect-Swift", "= 0.12.0" - spec.dependency 'LibXMTP', '= 0.5.8-beta6' + spec.dependency 'LibXMTP', '= 0.5.8-beta7' end diff --git a/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index b86f6275..60210e9d 100644 --- a/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -59,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/xmtp/libxmtp-swift.git", "state" : { - "revision" : "9398f5516b18044bb94e5d21dabd7a5ddfc25062", - "version" : "0.5.8-beta6" + "revision" : "859333faaddc04128443709182bbcbf41b2209a5", + "version" : "0.5.8-beta7" } }, { From f02d4898b042cf95c4af67b1b50538252eb63abf Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Wed, 25 Sep 2024 23:30:54 -0600 Subject: [PATCH 10/45] fix up the tests and bump the pod --- Sources/XMTPiOS/Conversation.swift | 4 ++-- Tests/XMTPTests/V3ClientTests.swift | 6 +++--- XMTP.podspec | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/XMTPiOS/Conversation.swift b/Sources/XMTPiOS/Conversation.swift index 807a5701..737bb1f2 100644 --- a/Sources/XMTPiOS/Conversation.swift +++ b/Sources/XMTPiOS/Conversation.swift @@ -83,7 +83,7 @@ public enum Conversation: Sendable { case let .v2(conversationV2): return conversationV2.peerAddress case let .group(group): - return try group.peerInboxIds.joined(separator: ",") + throw GroupError.notSupportedByGroups } } } @@ -96,7 +96,7 @@ public enum Conversation: Sendable { case let .v2(conversationV2): return [conversationV2.peerAddress] case let .group(group): - return try group.peerInboxIds + throw GroupError.notSupportedByGroups } } } diff --git a/Tests/XMTPTests/V3ClientTests.swift b/Tests/XMTPTests/V3ClientTests.swift index a4ed952d..4cb7d59e 100644 --- a/Tests/XMTPTests/V3ClientTests.swift +++ b/Tests/XMTPTests/V3ClientTests.swift @@ -64,7 +64,7 @@ class V3ClientTests: XCTestCase { func testsCanCreateGroup() async throws { let fixtures = try await localFixtures() let group = try await fixtures.boV3Client.conversations.newGroup(with: [fixtures.caroV2V3.address]) - let members = try group.members.map(\.inboxId).sorted() + let members = try await group.members.map(\.inboxId).sorted() XCTAssertEqual([fixtures.caroV2V3Client.inboxID, fixtures.boV3Client.inboxID].sorted(), members) await assertThrowsAsyncError( @@ -123,7 +123,7 @@ class V3ClientTests: XCTestCase { try await fixtures.boV3Client.contacts.allowInboxes(inboxIds: [fixtures.caroV2V3Client.inboxID]) - var caroMember = try boGroup.members.first(where: { member in member.inboxId == fixtures.caroV2V3Client.inboxID }) + var caroMember = try await boGroup.members.first(where: { member in member.inboxId == fixtures.caroV2V3Client.inboxID }) XCTAssertEqual(caroMember?.consentState, .allowed) isInboxAllowed = try await fixtures.boV3Client.contacts.isInboxAllowed(inboxId: fixtures.caroV2V3Client.inboxID) @@ -136,7 +136,7 @@ class V3ClientTests: XCTestCase { XCTAssert(!isAddressDenied) try await fixtures.boV3Client.contacts.denyInboxes(inboxIds: [fixtures.caroV2V3Client.inboxID]) - caroMember = try boGroup.members.first(where: { member in member.inboxId == fixtures.caroV2V3Client.inboxID }) + caroMember = try await boGroup.members.first(where: { member in member.inboxId == fixtures.caroV2V3Client.inboxID }) XCTAssertEqual(caroMember?.consentState, .denied) isInboxAllowed = try await fixtures.boV3Client.contacts.isInboxAllowed(inboxId: fixtures.caroV2V3Client.inboxID) diff --git a/XMTP.podspec b/XMTP.podspec index a6496c21..8601d828 100644 --- a/XMTP.podspec +++ b/XMTP.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |spec| # spec.name = "XMTP" - spec.version = "0.14.15" + spec.version = "0.14.16" spec.summary = "XMTP SDK Cocoapod" # This description is used to generate tags and improve search results. From e246103affdf1865d2af0f919b61113856cd1817 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Thu, 26 Sep 2024 08:51:23 -0600 Subject: [PATCH 11/45] bump to the next version --- Package.swift | 2 +- XMTP.podspec | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Package.swift b/Package.swift index 13882418..f08b3777 100644 --- a/Package.swift +++ b/Package.swift @@ -25,7 +25,7 @@ let package = Package( .package(url: "https://github.com/1024jp/GzipSwift", from: "5.2.0"), .package(url: "https://github.com/bufbuild/connect-swift", exact: "0.12.0"), .package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.0.0"), - .package(url: "https://github.com/xmtp/libxmtp-swift.git", exact: "0.5.9-alpha0"), + .package(url: "https://github.com/xmtp/libxmtp-swift.git", exact: "0.5.9-alpha1"), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. diff --git a/XMTP.podspec b/XMTP.podspec index 3873598a..bc36b56c 100644 --- a/XMTP.podspec +++ b/XMTP.podspec @@ -44,5 +44,5 @@ Pod::Spec.new do |spec| spec.dependency "web3.swift" spec.dependency "GzipSwift" spec.dependency "Connect-Swift", "= 0.12.0" - spec.dependency 'LibXMTP', '= 0.5.9-alpha0' + spec.dependency 'LibXMTP', '= 0.5.9-alpha1' end diff --git a/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index f5d7e620..7f528946 100644 --- a/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -59,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/xmtp/libxmtp-swift.git", "state" : { - "revision" : "8ebeeaf8ebfe056be0b306910f5128a21795aca9", - "version" : "0.5.9-alpha0" + "revision" : "c51b13046b35229b1a5fabb4f1a395c96ec262f2", + "version" : "0.5.9-alpha1" } }, { From ef8dea9a47aeb59cc2bd9d5a8b99d8cc23e64b00 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Thu, 26 Sep 2024 08:53:05 -0600 Subject: [PATCH 12/45] bad merge --- XMTP.podspec | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/XMTP.podspec b/XMTP.podspec index b8fe7480..755b1ba7 100644 --- a/XMTP.podspec +++ b/XMTP.podspec @@ -16,13 +16,7 @@ Pod::Spec.new do |spec| # spec.name = "XMTP" -<<<<<<< HEAD - spec.version = "0.15.0-alpha1" -||||||| e7ed584 - spec.version = "0.14.14" -======= - spec.version = "0.14.16" ->>>>>>> 25e45d18b1b962231eb7a7bf469d47bb094b4842 + spec.version = "0.15.0-alpha2" spec.summary = "XMTP SDK Cocoapod" # This description is used to generate tags and improve search results. From 67a88fc56c32d56c773a88884a3667b8ff36d26e Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Wed, 2 Oct 2024 14:04:48 -0600 Subject: [PATCH 13/45] update the package --- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 7f528946..74a34233 100644 --- a/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -59,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/xmtp/libxmtp-swift.git", "state" : { - "revision" : "c51b13046b35229b1a5fabb4f1a395c96ec262f2", - "version" : "0.5.9-alpha1" + "revision" : "156cbd2d5aa09db45f58137a2352ece6b4b186e7", + "version" : "0.5.9-alpha2" } }, { From 929ac8453ecd86deb35f1e4cdda6f035c156776d Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Wed, 2 Oct 2024 15:55:23 -0600 Subject: [PATCH 14/45] fix up bad merge --- XMTP.podspec | 1 - 1 file changed, 1 deletion(-) diff --git a/XMTP.podspec b/XMTP.podspec index 2a22e80b..e8d176c1 100644 --- a/XMTP.podspec +++ b/XMTP.podspec @@ -45,5 +45,4 @@ Pod::Spec.new do |spec| spec.dependency "GzipSwift" spec.dependency "Connect-Swift", "= 0.12.0" spec.dependency 'LibXMTP', '= 0.5.9-alpha2' ->>>>>>> 8197ea4bb2a8a61f1e3b483c0a96f706dba6be34 end From 63b79e1d2300045c4dc9868903e83bea1ddbc948 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Tue, 8 Oct 2024 19:52:44 -0700 Subject: [PATCH 15/45] make block number optional --- Sources/XMTPiOS/Client.swift | 6 +++++- Sources/XMTPiOS/SigningKey.swift | 10 +++++----- XMTP.podspec | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Sources/XMTPiOS/Client.swift b/Sources/XMTPiOS/Client.swift index 1aadce17..83febe9c 100644 --- a/Sources/XMTPiOS/Client.swift +++ b/Sources/XMTPiOS/Client.swift @@ -248,7 +248,11 @@ public final class Client { do { let signedData = try await signingKey.sign(message: signatureRequest.signatureText()) if signingKey.isSmartContractWallet { - try await signatureRequest.addScwSignature(signatureBytes: signedData.rawData, address: signingKey.address, chainId: signingKey.chainId, blockNumber: signingKey.blockNumber) + try await signatureRequest.addScwSignature(signatureBytes: signedData.rawData, + address: signingKey.address, + chainId: UInt64(signingKey.chainId), + blockNumber: signingKey.blockNumber.flatMap { $0 >= 0 ? UInt64($0) : nil }) + } else { try await signatureRequest.addEcdsaSignature(signatureBytes: signedData.rawData) } diff --git a/Sources/XMTPiOS/SigningKey.swift b/Sources/XMTPiOS/SigningKey.swift index 8e40cace..72ebb0f4 100644 --- a/Sources/XMTPiOS/SigningKey.swift +++ b/Sources/XMTPiOS/SigningKey.swift @@ -24,10 +24,10 @@ public protocol SigningKey { var isSmartContractWallet: Bool { get } /// The name of the chainId for example "1" - var chainId: UInt64 { get } + var chainId: Int64 { get } /// The blockNumber of the chain for example "1" - var blockNumber: UInt64 { get } + var blockNumber: Int64? { get } /// Sign the data and return a secp256k1 compact recoverable signature. func sign(_ data: Data) async throws -> Signature @@ -42,12 +42,12 @@ extension SigningKey { return false } - public var chainId: UInt64 { + public var chainId: Int64 { return 1 } - public var blockNumber: UInt64 { - return 1 + public var blockNumber: Int64? { + return nil } func createIdentity(_ identity: PrivateKey, preCreateIdentityCallback: PreEventCallback? = nil) async throws -> AuthorizedIdentity { diff --git a/XMTP.podspec b/XMTP.podspec index 76198185..ef7c81a7 100644 --- a/XMTP.podspec +++ b/XMTP.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |spec| # spec.name = "XMTP" - spec.version = "0.15.1" + spec.version = "0.15.1-alpha0" spec.summary = "XMTP SDK Cocoapod" # This description is used to generate tags and improve search results. From 402ee51c02b7ee360b43f87297a6a8f9bc5239d0 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Tue, 8 Oct 2024 20:49:46 -0700 Subject: [PATCH 16/45] add a test to reproduce the scw error --- Sources/XMTPTestHelpers/TestHelpers.swift | 40 +++++++++++++++++++++++ Tests/XMTPTests/ClientTests.swift | 13 ++++++++ Tests/XMTPTests/V3ClientTests.swift | 14 +++++++- 3 files changed, 66 insertions(+), 1 deletion(-) diff --git a/Sources/XMTPTestHelpers/TestHelpers.swift b/Sources/XMTPTestHelpers/TestHelpers.swift index d9a43f26..addd07d5 100644 --- a/Sources/XMTPTestHelpers/TestHelpers.swift +++ b/Sources/XMTPTestHelpers/TestHelpers.swift @@ -7,6 +7,7 @@ #if canImport(XCTest) import Combine +import CryptoKit import XCTest @testable import XMTPiOS import LibXMTP @@ -67,6 +68,45 @@ public struct FakeWallet: SigningKey { } } +public struct FakeSCWWallet: SigningKey { + public var walletAddress: String + private var internalSignature: String + + public init() throws { + // Simulate a wallet address (could be derived from a hash of some internal data) + self.walletAddress = UUID().uuidString // Using UUID for uniqueness in this fake example + self.internalSignature = Data(repeating: 0x01, count: 64).toHex // Fake internal signature + } + + public var address: String { + walletAddress + } + + public var isSmartContractWallet: Bool { + true + } + + public var chainId: Int64 { + 1 + } + + public static func generate() throws -> FakeSCWWallet { + return try FakeSCWWallet() + } + + public func sign(_ data: Data) async throws -> XMTPiOS.Signature { + let signature = XMTPiOS.Signature.with { + $0.ecdsaCompact.bytes = internalSignature.hexToData + } + return signature + } + + public func sign(message: String) async throws -> XMTPiOS.Signature { + let digest = SHA256.hash(data: message.data(using: .utf8)!) + return try await sign(Data(digest)) + } +} + @available(iOS 15, *) public struct Fixtures { public var alice: PrivateKey! diff --git a/Tests/XMTPTests/ClientTests.swift b/Tests/XMTPTests/ClientTests.swift index 40b2495e..eb687e23 100644 --- a/Tests/XMTPTests/ClientTests.swift +++ b/Tests/XMTPTests/ClientTests.swift @@ -514,4 +514,17 @@ class ClientTests: XCTestCase { let newState = try await alixClient3.inboxState(refreshFromNetwork: true) XCTAssertEqual(newState.installations.count, 1) } + + func testCanCreateASCW() async throws { + let key = try Crypto.secureRandomBytes(count: 32) + let davonSCW = try FakeSCWWallet.generate() + let davonSCWClient = try await Client.createOrBuild( + account: davonSCW, + options: .init( + api: .init(env: .local, isSecure: false), + enableV3: true, + encryptionKey: key + ) + ) + } } diff --git a/Tests/XMTPTests/V3ClientTests.swift b/Tests/XMTPTests/V3ClientTests.swift index 4cb7d59e..9a19b5e0 100644 --- a/Tests/XMTPTests/V3ClientTests.swift +++ b/Tests/XMTPTests/V3ClientTests.swift @@ -5,7 +5,6 @@ // Created by Naomi Plasterer on 9/19/24. // -import CryptoKit import XCTest @testable import XMTPiOS import LibXMTP @@ -18,9 +17,11 @@ class V3ClientTests: XCTestCase { var alixV2: PrivateKey! var boV3: PrivateKey! var caroV2V3: PrivateKey! + var davonSCW: FakeSCWWallet! var alixV2Client: Client! var boV3Client: Client! var caroV2V3Client: Client! + var davonSCWClient: Client! } func localFixtures() async throws -> LocalFixtures { @@ -51,6 +52,17 @@ class V3ClientTests: XCTestCase { ) ) + let davonSCW = try FakeSCWWallet.generate() + let davonSCWClient = try await Client.createOrBuild( + account: davonSCW, + options: .init( + api: .init(env: .local, isSecure: false), + enableV3: true, + encryptionKey: key + ) + ) + + return .init( alixV2: alixV2, boV3: boV3, From d1f509d0ada405b277cebb53f9e29b5de1423965 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Fri, 11 Oct 2024 07:56:57 -0700 Subject: [PATCH 17/45] update to latest libxmtp --- Package.swift | 2 +- XMTP.podspec | 4 ++-- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Package.swift b/Package.swift index f83b6b7b..8a1e166e 100644 --- a/Package.swift +++ b/Package.swift @@ -25,7 +25,7 @@ let package = Package( .package(url: "https://github.com/1024jp/GzipSwift", from: "5.2.0"), .package(url: "https://github.com/bufbuild/connect-swift", exact: "0.12.0"), .package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.0.0"), - .package(url: "https://github.com/xmtp/libxmtp-swift.git", exact: "0.5.9-beta1"), + .package(url: "https://github.com/xmtp/libxmtp-swift.git", exact: "0.5.9-beta2"), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. diff --git a/XMTP.podspec b/XMTP.podspec index ef7c81a7..5f024aeb 100644 --- a/XMTP.podspec +++ b/XMTP.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |spec| # spec.name = "XMTP" - spec.version = "0.15.1-alpha0" + spec.version = "0.15.1-alpha1" spec.summary = "XMTP SDK Cocoapod" # This description is used to generate tags and improve search results. @@ -44,5 +44,5 @@ Pod::Spec.new do |spec| spec.dependency "web3.swift" spec.dependency "GzipSwift" spec.dependency "Connect-Swift", "= 0.12.0" - spec.dependency 'LibXMTP', '= 0.5.9-beta1' + spec.dependency 'LibXMTP', '= 0.5.9-beta2' end diff --git a/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 321e8126..fc48b889 100644 --- a/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -59,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/xmtp/libxmtp-swift.git", "state" : { - "revision" : "f07383efdee7bed94120dd73efa4ff1c4dcead3c", - "version" : "0.5.9-beta1" + "revision" : "d5d5134ccd177bbad3d74d8ebb6bd2cdd7d2f197", + "version" : "0.5.9-beta2" } }, { From abe6af890b8ea9329de90e76a5cc3878133fd8fe Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Thu, 17 Oct 2024 21:35:31 -0700 Subject: [PATCH 18/45] update the signers --- Package.swift | 2 +- Sources/XMTPiOS/Client.swift | 57 ++++++++++++++----- Sources/XMTPiOS/SigningKey.swift | 3 + XMTP.podspec | 4 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- 5 files changed, 51 insertions(+), 19 deletions(-) diff --git a/Package.swift b/Package.swift index 8a1e166e..8e1f5b49 100644 --- a/Package.swift +++ b/Package.swift @@ -25,7 +25,7 @@ let package = Package( .package(url: "https://github.com/1024jp/GzipSwift", from: "5.2.0"), .package(url: "https://github.com/bufbuild/connect-swift", exact: "0.12.0"), .package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.0.0"), - .package(url: "https://github.com/xmtp/libxmtp-swift.git", exact: "0.5.9-beta2"), + .package(url: "https://github.com/xmtp/libxmtp-swift.git", exact: "0.5.9-beta3"), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. diff --git a/Sources/XMTPiOS/Client.swift b/Sources/XMTPiOS/Client.swift index 83febe9c..a53a06af 100644 --- a/Sources/XMTPiOS/Client.swift +++ b/Sources/XMTPiOS/Client.swift @@ -161,23 +161,26 @@ public final class Client { } } - // This is a V3 only feature - public static func createOrBuild(account: SigningKey, options: ClientOptions) async throws -> Client { - let inboxId = try await getOrCreateInboxId(options: options, address: account.address) - + private static func initializeClient( + accountAddress: String, + options: ClientOptions, + signingKey: SigningKey?, + inboxId: String + ) async throws -> Client { let (libxmtpClient, dbPath) = try await initV3Client( - accountAddress: account.address, + accountAddress: accountAddress, options: options, privateKeyBundleV1: nil, - signingKey: account, + signingKey: signingKey, inboxId: inboxId ) + guard let v3Client = libxmtpClient else { throw ClientError.noV3Client("Error no V3 client initialized") } let client = try Client( - address: account.address, + address: accountAddress, v3Client: v3Client, dbPath: dbPath, installationID: v3Client.installationId().toHex, @@ -185,15 +188,40 @@ public final class Client { environment: options.api.env ) - let conversations = client.conversations - let contacts = client.contacts - - for codec in (options.codecs) { + // Register codecs + for codec in options.codecs { client.register(codec: codec) } + return client } + public static func createV3(account: SigningKey, options: ClientOptions) async throws -> Client { + let accountAddress = if(account.isSmartContractWallet) { "eip155:\(String(describing: account.chainId)):\(account.address.lowercased())" } else { account.address } + + + let inboxId = try await getOrCreateInboxId(options: options, address: accountAddress) + + return try await initializeClient( + accountAddress: accountAddress, + options: options, + signingKey: account, + inboxId: inboxId + ) + } + + public static func buildV3(address: String, scwChainId: Int64?, options: ClientOptions) async throws -> Client { + let accountAddress = if(scwChainId != nil) { "eip155:\(String(describing: scwChainId)):\(address.lowercased())" } else { address } + let inboxId = try await getOrCreateInboxId(options: options, address: accountAddress) + + return try await initializeClient( + accountAddress: accountAddress, + options: options, + signingKey: nil, + inboxId: inboxId + ) + } + static func initV3Client( accountAddress: String, options: ClientOptions?, @@ -224,7 +252,7 @@ public final class Client { let alias = "xmtp-\(options?.api.env.rawValue ?? "")-\(inboxId).db3" let dbURL = directoryURL.appendingPathComponent(alias).path - var encryptionKey = options?.dbEncryptionKey + let encryptionKey = options?.dbEncryptionKey if (encryptionKey == nil) { throw ClientError.creationError("No encryption key passed for the database. Please store and provide a secure encryption key.") } @@ -246,14 +274,15 @@ public final class Client { if let signatureRequest = v3Client.signatureRequest() { if let signingKey = signingKey { do { - let signedData = try await signingKey.sign(message: signatureRequest.signatureText()) if signingKey.isSmartContractWallet { - try await signatureRequest.addScwSignature(signatureBytes: signedData.rawData, + let signedData = try await signingKey.signSCW(message: signatureRequest.signatureText()) + try await signatureRequest.addScwSignature(signatureBytes: signedData, address: signingKey.address, chainId: UInt64(signingKey.chainId), blockNumber: signingKey.blockNumber.flatMap { $0 >= 0 ? UInt64($0) : nil }) } else { + let signedData = try await signingKey.sign(message: signatureRequest.signatureText()) try await signatureRequest.addEcdsaSignature(signatureBytes: signedData.rawData) } try await v3Client.registerIdentity(signatureRequest: signatureRequest) diff --git a/Sources/XMTPiOS/SigningKey.swift b/Sources/XMTPiOS/SigningKey.swift index 72ebb0f4..4208cba6 100644 --- a/Sources/XMTPiOS/SigningKey.swift +++ b/Sources/XMTPiOS/SigningKey.swift @@ -35,6 +35,9 @@ public protocol SigningKey { /// Pass a personal Ethereum signed message string text to be signed, returning /// a secp256k1 compact recoverable signature. You can use ``Signature.ethPersonalMessage`` to generate this text. func sign(message: String) async throws -> Signature + + /// Pass a personal Ethereum signed message string text to be signed, return bytes to be verified + func signSCW(message: String) async throws -> Data } extension SigningKey { diff --git a/XMTP.podspec b/XMTP.podspec index 5f024aeb..509c5a04 100644 --- a/XMTP.podspec +++ b/XMTP.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |spec| # spec.name = "XMTP" - spec.version = "0.15.1-alpha1" + spec.version = "0.15.1-alpha2" spec.summary = "XMTP SDK Cocoapod" # This description is used to generate tags and improve search results. @@ -44,5 +44,5 @@ Pod::Spec.new do |spec| spec.dependency "web3.swift" spec.dependency "GzipSwift" spec.dependency "Connect-Swift", "= 0.12.0" - spec.dependency 'LibXMTP', '= 0.5.9-beta2' + spec.dependency 'LibXMTP', '= 0.5.9-beta3' end diff --git a/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index fc48b889..4568be0a 100644 --- a/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -59,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/xmtp/libxmtp-swift.git", "state" : { - "revision" : "d5d5134ccd177bbad3d74d8ebb6bd2cdd7d2f197", - "version" : "0.5.9-beta2" + "revision" : "a103132088fb9657b03c25006fe6a6686fa4d082", + "version" : "0.5.9-beta3" } }, { From 2cd20cefa9b87f3c6be87fd69e273431326534df Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Thu, 17 Oct 2024 21:46:17 -0700 Subject: [PATCH 19/45] update to the latest libxmtp functions --- Sources/XMTPiOS/Client.swift | 4 ++-- Sources/XMTPiOS/Contacts.swift | 2 +- Sources/XMTPiOS/Conversations.swift | 14 +++++++------- Sources/XMTPiOS/Extensions/Ffi.swift | 6 +++--- Sources/XMTPiOS/Group.swift | 8 ++++---- Sources/XMTPiOS/Mls/Member.swift | 4 ++-- Sources/XMTPiOS/SigningKey.swift | 12 ++++++++++++ Tests/XMTPTests/ClientTests.swift | 22 ++++++++-------------- Tests/XMTPTests/V3ClientTests.swift | 12 +----------- 9 files changed, 40 insertions(+), 44 deletions(-) diff --git a/Sources/XMTPiOS/Client.swift b/Sources/XMTPiOS/Client.swift index a53a06af..e9039bde 100644 --- a/Sources/XMTPiOS/Client.swift +++ b/Sources/XMTPiOS/Client.swift @@ -210,7 +210,7 @@ public final class Client { ) } - public static func buildV3(address: String, scwChainId: Int64?, options: ClientOptions) async throws -> Client { + public static func buildV3(address: String, scwChainId: Int64? = nil, options: ClientOptions) async throws -> Client { let accountAddress = if(scwChainId != nil) { "eip155:\(String(describing: scwChainId)):\(address.lowercased())" } else { address } let inboxId = try await getOrCreateInboxId(options: options, address: accountAddress) @@ -688,7 +688,7 @@ public final class Client { throw ClientError.noV3Client("Error no V3 client initialized") } do { - return Group(ffiGroup: try client.group(groupId: groupId.hexToData), client: self) + return Group(ffiGroup: try client.conversation(conversationId: groupId.hexToData), client: self) } catch { return nil } diff --git a/Sources/XMTPiOS/Contacts.swift b/Sources/XMTPiOS/Contacts.swift index c6c7e6ec..d7963054 100644 --- a/Sources/XMTPiOS/Contacts.swift +++ b/Sources/XMTPiOS/Contacts.swift @@ -248,7 +248,7 @@ public class ConsentList { func groupState(groupId: String) async throws -> ConsentState { if let client = client.v3Client { return try await client.getConsentState( - entityType: .groupId, + entityType: .conversationId, entity: groupId ).fromFFI } diff --git a/Sources/XMTPiOS/Conversations.swift b/Sources/XMTPiOS/Conversations.swift index 91c47095..98dc482b 100644 --- a/Sources/XMTPiOS/Conversations.swift +++ b/Sources/XMTPiOS/Conversations.swift @@ -54,7 +54,7 @@ final class GroupStreamCallback: FfiConversationCallback { self.callback = callback } - func onConversation(conversation: FfiGroup) { + func onConversation(conversation: FfiConversation) { self.callback(conversation.fromFFI(client: client)) } } @@ -119,7 +119,7 @@ public actor Conversations { guard let v3Client = client.v3Client else { return 0 } - return try await v3Client.conversations().syncAllGroups() + return try await v3Client.conversations().syncAllConversations() } public func groups(createdAfter: Date? = nil, createdBefore: Date? = nil, limit: Int? = nil) async throws -> [Group] { @@ -136,7 +136,7 @@ public actor Conversations { if let limit { options.limit = Int64(limit) } - return try await v3Client.conversations().list(opts: options).map { $0.fromFFI(client: client) } + return try await v3Client.conversations().listGroups(opts: options).map { $0.fromFFI(client: client) } } public func streamGroups() async throws -> AsyncThrowingStream { @@ -150,7 +150,7 @@ public actor Conversations { } continuation.yield(group) } - guard let stream = await self.client.v3Client?.conversations().stream(callback: groupCallback) else { + guard let stream = await self.client.v3Client?.conversations().streamGroups(callback: groupCallback) else { continuation.finish(throwing: GroupError.streamingFailure) return } @@ -175,7 +175,7 @@ public actor Conversations { AsyncThrowingStream { continuation in let ffiStreamActor = FfiStreamActor() let task = Task { - let stream = await self.client.v3Client?.conversations().stream( + let stream = await self.client.v3Client?.conversations().streamGroups( callback: GroupStreamCallback(client: self.client) { group in guard !Task.isCancelled else { continuation.finish() @@ -435,7 +435,7 @@ public actor Conversations { AsyncThrowingStream { continuation in let ffiStreamActor = FfiStreamActor() let task = Task { - let stream = await self.client.v3Client?.conversations().streamAllMessages( + let stream = await self.client.v3Client?.conversations().streamAllGroupMessages( messageCallback: MessageCallback(client: self.client) { message in guard !Task.isCancelled else { continuation.finish() @@ -500,7 +500,7 @@ public actor Conversations { AsyncThrowingStream { continuation in let ffiStreamActor = FfiStreamActor() let task = Task { - let stream = await self.client.v3Client?.conversations().streamAllMessages( + let stream = await self.client.v3Client?.conversations().streamAllGroupMessages( messageCallback: MessageCallback(client: self.client) { message in guard !Task.isCancelled else { continuation.finish() diff --git a/Sources/XMTPiOS/Extensions/Ffi.swift b/Sources/XMTPiOS/Extensions/Ffi.swift index 23c58f8f..60e6787b 100644 --- a/Sources/XMTPiOS/Extensions/Ffi.swift +++ b/Sources/XMTPiOS/Extensions/Ffi.swift @@ -195,13 +195,13 @@ extension FfiV2SubscribeRequest { // MARK: Group -extension FfiGroup { +extension FfiConversation { func fromFFI(client: Client) -> Group { Group(ffiGroup: self, client: client) } } -extension FfiGroupMember { +extension FfiConversationMember { var fromFFI: Member { Member(ffiGroupMember: self) } @@ -230,7 +230,7 @@ extension FfiConsentState { extension EntryType { var toFFI: FfiConsentEntityType{ switch (self) { - case .group_id: return FfiConsentEntityType.groupId + case .group_id: return FfiConsentEntityType.conversationId case .inbox_id: return FfiConsentEntityType.inboxId case .address: return FfiConsentEntityType.address } diff --git a/Sources/XMTPiOS/Group.swift b/Sources/XMTPiOS/Group.swift index b9ee9d24..dfc4a2e1 100644 --- a/Sources/XMTPiOS/Group.swift +++ b/Sources/XMTPiOS/Group.swift @@ -27,7 +27,7 @@ final class StreamHolder { } public struct Group: Identifiable, Equatable, Hashable { - var ffiGroup: FfiGroup + var ffiGroup: FfiConversation var client: Client let streamHolder = StreamHolder() @@ -39,7 +39,7 @@ public struct Group: Identifiable, Equatable, Hashable { Topic.groupMessage(id).description } - func metadata() throws -> FfiGroupMetadata { + func metadata() throws -> FfiConversationMetadata { return try ffiGroup.groupMetadata() } @@ -230,12 +230,12 @@ public struct Group: Identifiable, Equatable, Hashable { } public func processMessage(envelopeBytes: Data) async throws -> DecodedMessage { - let message = try await ffiGroup.processStreamedGroupMessage(envelopeBytes: envelopeBytes) + let message = try await ffiGroup.processStreamedConversationMessage(envelopeBytes: envelopeBytes) return try MessageV3(client: client, ffiMessage: message).decode() } public func processMessageDecrypted(envelopeBytes: Data) async throws -> DecryptedMessage { - let message = try await ffiGroup.processStreamedGroupMessage(envelopeBytes: envelopeBytes) + let message = try await ffiGroup.processStreamedConversationMessage(envelopeBytes: envelopeBytes) return try MessageV3(client: client, ffiMessage: message).decrypt() } diff --git a/Sources/XMTPiOS/Mls/Member.swift b/Sources/XMTPiOS/Mls/Member.swift index 1e0885e3..fcc4a1e4 100644 --- a/Sources/XMTPiOS/Mls/Member.swift +++ b/Sources/XMTPiOS/Mls/Member.swift @@ -13,9 +13,9 @@ public enum PermissionLevel { } public struct Member { - var ffiGroupMember: FfiGroupMember + var ffiGroupMember: FfiConversationMember - init(ffiGroupMember: FfiGroupMember) { + init(ffiGroupMember: FfiConversationMember) { self.ffiGroupMember = ffiGroupMember } diff --git a/Sources/XMTPiOS/SigningKey.swift b/Sources/XMTPiOS/SigningKey.swift index 4208cba6..20523242 100644 --- a/Sources/XMTPiOS/SigningKey.swift +++ b/Sources/XMTPiOS/SigningKey.swift @@ -74,4 +74,16 @@ extension SigningKey { return AuthorizedIdentity(address: address, authorized: authorized, identity: identity) } + + public func sign(_ data: Data) async throws -> Signature { + throw NSError(domain: "NotImplemented", code: 1, userInfo: [NSLocalizedDescriptionKey: "sign(Data) not implemented."]) + } + + public func sign(message: String) async throws -> Signature { + throw NSError(domain: "NotImplemented", code: 1, userInfo: [NSLocalizedDescriptionKey: "sign(String) not implemented."]) + } + + public func signSCW(message: String) async throws -> Data { + throw NSError(domain: "NotImplemented", code: 1, userInfo: [NSLocalizedDescriptionKey: "signSCW(String) not implemented."]) + } } diff --git a/Tests/XMTPTests/ClientTests.swift b/Tests/XMTPTests/ClientTests.swift index eb687e23..00843749 100644 --- a/Tests/XMTPTests/ClientTests.swift +++ b/Tests/XMTPTests/ClientTests.swift @@ -469,12 +469,19 @@ class ClientTests: XCTestCase { let inboxId = try await Client.getOrCreateInboxId(options: options, address: alix.address) - let alixClient = try await Client.createOrBuild( + let alixClient = try await Client.createV3( account: alix, options: options ) XCTAssertEqual(inboxId, alixClient.inboxID) + + let alixClient2 = try await Client.buildV3( + address: alix.address, + options: options + ) + + XCTAssertEqual(alixClient2.inboxID, alixClient.inboxID) } func testRevokesAllOtherInstallations() async throws { @@ -514,17 +521,4 @@ class ClientTests: XCTestCase { let newState = try await alixClient3.inboxState(refreshFromNetwork: true) XCTAssertEqual(newState.installations.count, 1) } - - func testCanCreateASCW() async throws { - let key = try Crypto.secureRandomBytes(count: 32) - let davonSCW = try FakeSCWWallet.generate() - let davonSCWClient = try await Client.createOrBuild( - account: davonSCW, - options: .init( - api: .init(env: .local, isSecure: false), - enableV3: true, - encryptionKey: key - ) - ) - } } diff --git a/Tests/XMTPTests/V3ClientTests.swift b/Tests/XMTPTests/V3ClientTests.swift index 9a19b5e0..91a70fd8 100644 --- a/Tests/XMTPTests/V3ClientTests.swift +++ b/Tests/XMTPTests/V3ClientTests.swift @@ -34,7 +34,7 @@ class V3ClientTests: XCTestCase { ) ) let boV3 = try PrivateKey.generate() - let boV3Client = try await Client.createOrBuild( + let boV3Client = try await Client.createV3( account: boV3, options: .init( api: .init(env: .local, isSecure: false), @@ -52,16 +52,6 @@ class V3ClientTests: XCTestCase { ) ) - let davonSCW = try FakeSCWWallet.generate() - let davonSCWClient = try await Client.createOrBuild( - account: davonSCW, - options: .init( - api: .init(env: .local, isSecure: false), - enableV3: true, - encryptionKey: key - ) - ) - return .init( alixV2: alixV2, From 2812b8a469897d682e8ab7b6ca35580db9c27d81 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Thu, 17 Oct 2024 21:58:35 -0700 Subject: [PATCH 20/45] fix the linter --- Sources/XMTPTestHelpers/TestHelpers.swift | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/Sources/XMTPTestHelpers/TestHelpers.swift b/Sources/XMTPTestHelpers/TestHelpers.swift index addd07d5..41a8fa52 100644 --- a/Sources/XMTPTestHelpers/TestHelpers.swift +++ b/Sources/XMTPTestHelpers/TestHelpers.swift @@ -94,16 +94,11 @@ public struct FakeSCWWallet: SigningKey { return try FakeSCWWallet() } - public func sign(_ data: Data) async throws -> XMTPiOS.Signature { - let signature = XMTPiOS.Signature.with { - $0.ecdsaCompact.bytes = internalSignature.hexToData - } - return signature - } - - public func sign(message: String) async throws -> XMTPiOS.Signature { + public func signSCW(message: String) async throws -> Data { + // swiftlint:disable force_unwrapping let digest = SHA256.hash(data: message.data(using: .utf8)!) - return try await sign(Data(digest)) + // swiftlint:enable force_unwrapping + return Data(digest) } } From b03101f0e5d8116921d336a976a71409d31f6065 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Sat, 19 Oct 2024 12:33:51 -0700 Subject: [PATCH 21/45] get on a working version --- Package.swift | 2 +- XMTP.podspec | 4 ++-- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Package.swift b/Package.swift index 8e1f5b49..3a990488 100644 --- a/Package.swift +++ b/Package.swift @@ -25,7 +25,7 @@ let package = Package( .package(url: "https://github.com/1024jp/GzipSwift", from: "5.2.0"), .package(url: "https://github.com/bufbuild/connect-swift", exact: "0.12.0"), .package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.0.0"), - .package(url: "https://github.com/xmtp/libxmtp-swift.git", exact: "0.5.9-beta3"), + .package(url: "https://github.com/xmtp/libxmtp-swift.git", exact: "0.5.9-beta4"), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. diff --git a/XMTP.podspec b/XMTP.podspec index 509c5a04..157446e1 100644 --- a/XMTP.podspec +++ b/XMTP.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |spec| # spec.name = "XMTP" - spec.version = "0.15.1-alpha2" + spec.version = "0.15.2" spec.summary = "XMTP SDK Cocoapod" # This description is used to generate tags and improve search results. @@ -44,5 +44,5 @@ Pod::Spec.new do |spec| spec.dependency "web3.swift" spec.dependency "GzipSwift" spec.dependency "Connect-Swift", "= 0.12.0" - spec.dependency 'LibXMTP', '= 0.5.9-beta3' + spec.dependency 'LibXMTP', '= 0.5.9-beta4' end diff --git a/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4568be0a..c7bf0b25 100644 --- a/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -59,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/xmtp/libxmtp-swift.git", "state" : { - "revision" : "a103132088fb9657b03c25006fe6a6686fa4d082", - "version" : "0.5.9-beta3" + "revision" : "dad96a78a12fdeeb22e5fea5c2e39d073084b0d7", + "version" : "0.5.9-beta4" } }, { From 263bf64e289b133413183c29933bf59ad24db901 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Sat, 19 Oct 2024 12:51:16 -0700 Subject: [PATCH 22/45] check the chain id --- Sources/XMTPiOS/Client.swift | 11 +++++++---- Sources/XMTPiOS/SigningKey.swift | 4 ++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/Sources/XMTPiOS/Client.swift b/Sources/XMTPiOS/Client.swift index e9039bde..f87c0e3f 100644 --- a/Sources/XMTPiOS/Client.swift +++ b/Sources/XMTPiOS/Client.swift @@ -161,7 +161,7 @@ public final class Client { } } - private static func initializeClient( + static func initializeClient( accountAddress: String, options: ClientOptions, signingKey: SigningKey?, @@ -210,8 +210,8 @@ public final class Client { ) } - public static func buildV3(address: String, scwChainId: Int64? = nil, options: ClientOptions) async throws -> Client { - let accountAddress = if(scwChainId != nil) { "eip155:\(String(describing: scwChainId)):\(address.lowercased())" } else { address } + public static func buildV3(address: String, chainId: Int64? = nil, options: ClientOptions) async throws -> Client { + let accountAddress = if(chainId != nil) { "eip155:\(String(describing: chainId)):\(address.lowercased())" } else { address } let inboxId = try await getOrCreateInboxId(options: options, address: accountAddress) return try await initializeClient( @@ -275,10 +275,13 @@ public final class Client { if let signingKey = signingKey { do { if signingKey.isSmartContractWallet { + guard let chainId = signingKey.chainId else { + throw ClientError.creationError("Chain id must be present to sign Smart Contract Wallet") + } let signedData = try await signingKey.signSCW(message: signatureRequest.signatureText()) try await signatureRequest.addScwSignature(signatureBytes: signedData, address: signingKey.address, - chainId: UInt64(signingKey.chainId), + chainId: UInt64(chainId), blockNumber: signingKey.blockNumber.flatMap { $0 >= 0 ? UInt64($0) : nil }) } else { diff --git a/Sources/XMTPiOS/SigningKey.swift b/Sources/XMTPiOS/SigningKey.swift index 20523242..70d29de7 100644 --- a/Sources/XMTPiOS/SigningKey.swift +++ b/Sources/XMTPiOS/SigningKey.swift @@ -45,8 +45,8 @@ extension SigningKey { return false } - public var chainId: Int64 { - return 1 + public var chainId: Int64? { + return nil } public var blockNumber: Int64? { From 14b0cffe3bca9bfd134c08458b269686fc75a121 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Sat, 19 Oct 2024 12:54:11 -0700 Subject: [PATCH 23/45] chain id is optional --- Sources/XMTPiOS/Client.swift | 2 -- Sources/XMTPiOS/SigningKey.swift | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Sources/XMTPiOS/Client.swift b/Sources/XMTPiOS/Client.swift index f87c0e3f..d7a6b257 100644 --- a/Sources/XMTPiOS/Client.swift +++ b/Sources/XMTPiOS/Client.swift @@ -198,8 +198,6 @@ public final class Client { public static func createV3(account: SigningKey, options: ClientOptions) async throws -> Client { let accountAddress = if(account.isSmartContractWallet) { "eip155:\(String(describing: account.chainId)):\(account.address.lowercased())" } else { account.address } - - let inboxId = try await getOrCreateInboxId(options: options, address: accountAddress) return try await initializeClient( diff --git a/Sources/XMTPiOS/SigningKey.swift b/Sources/XMTPiOS/SigningKey.swift index 70d29de7..5c720edf 100644 --- a/Sources/XMTPiOS/SigningKey.swift +++ b/Sources/XMTPiOS/SigningKey.swift @@ -24,7 +24,7 @@ public protocol SigningKey { var isSmartContractWallet: Bool { get } /// The name of the chainId for example "1" - var chainId: Int64 { get } + var chainId: Int64? { get } /// The blockNumber of the chain for example "1" var blockNumber: Int64? { get } From b98637037242867d7046c0f49190ebc38c1b83e4 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Sun, 20 Oct 2024 07:03:50 -0700 Subject: [PATCH 24/45] fix the lint issue --- Sources/XMTPiOS/Client.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Sources/XMTPiOS/Client.swift b/Sources/XMTPiOS/Client.swift index d7a6b257..010602d2 100644 --- a/Sources/XMTPiOS/Client.swift +++ b/Sources/XMTPiOS/Client.swift @@ -197,7 +197,9 @@ public final class Client { } public static func createV3(account: SigningKey, options: ClientOptions) async throws -> Client { - let accountAddress = if(account.isSmartContractWallet) { "eip155:\(String(describing: account.chainId)):\(account.address.lowercased())" } else { account.address } + let accountAddress = account.isSmartContractWallet ? + "eip155:\(String(describing: account.chainId)):\(account.address.lowercased())" : + account.address let inboxId = try await getOrCreateInboxId(options: options, address: accountAddress) return try await initializeClient( @@ -209,7 +211,9 @@ public final class Client { } public static func buildV3(address: String, chainId: Int64? = nil, options: ClientOptions) async throws -> Client { - let accountAddress = if(chainId != nil) { "eip155:\(String(describing: chainId)):\(address.lowercased())" } else { address } + let accountAddress = chainId != nil ? + "eip155:\(String(describing: chainId)):\(address.lowercased())" : + address let inboxId = try await getOrCreateInboxId(options: options, address: accountAddress) return try await initializeClient( From 87b8b4292061e86b2996679ab49cd5559c440dcf Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Sun, 20 Oct 2024 07:04:44 -0700 Subject: [PATCH 25/45] tag --- XMTP.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/XMTP.podspec b/XMTP.podspec index 157446e1..ecec0f30 100644 --- a/XMTP.podspec +++ b/XMTP.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |spec| # spec.name = "XMTP" - spec.version = "0.15.2" + spec.version = "0.15.2-alpha0" spec.summary = "XMTP SDK Cocoapod" # This description is used to generate tags and improve search results. From 4f36f8ccec63b2a6b9573ee650bdca460b9b4900 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Wed, 23 Oct 2024 12:26:51 -0700 Subject: [PATCH 26/45] remove chain id from inbox id creation --- Package.swift | 2 +- Sources/XMTPiOS/Client.swift | 13 ++++--------- XMTP.podspec | 4 ++-- .../xcshareddata/swiftpm/Package.resolved | 4 ++-- 4 files changed, 9 insertions(+), 14 deletions(-) diff --git a/Package.swift b/Package.swift index 3a990488..ffa43c0e 100644 --- a/Package.swift +++ b/Package.swift @@ -25,7 +25,7 @@ let package = Package( .package(url: "https://github.com/1024jp/GzipSwift", from: "5.2.0"), .package(url: "https://github.com/bufbuild/connect-swift", exact: "0.12.0"), .package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.0.0"), - .package(url: "https://github.com/xmtp/libxmtp-swift.git", exact: "0.5.9-beta4"), + .package(url: "https://github.com/xmtp/libxmtp-swift.git", exact: "0.5.10"), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. diff --git a/Sources/XMTPiOS/Client.swift b/Sources/XMTPiOS/Client.swift index 010602d2..bafcb6e4 100644 --- a/Sources/XMTPiOS/Client.swift +++ b/Sources/XMTPiOS/Client.swift @@ -197,9 +197,7 @@ public final class Client { } public static func createV3(account: SigningKey, options: ClientOptions) async throws -> Client { - let accountAddress = account.isSmartContractWallet ? - "eip155:\(String(describing: account.chainId)):\(account.address.lowercased())" : - account.address + let accountAddress = account.address let inboxId = try await getOrCreateInboxId(options: options, address: accountAddress) return try await initializeClient( @@ -210,14 +208,11 @@ public final class Client { ) } - public static func buildV3(address: String, chainId: Int64? = nil, options: ClientOptions) async throws -> Client { - let accountAddress = chainId != nil ? - "eip155:\(String(describing: chainId)):\(address.lowercased())" : - address - let inboxId = try await getOrCreateInboxId(options: options, address: accountAddress) + public static func buildV3(address: String, options: ClientOptions) async throws -> Client { + let inboxId = try await getOrCreateInboxId(options: options, address: address) return try await initializeClient( - accountAddress: accountAddress, + accountAddress: address, options: options, signingKey: nil, inboxId: inboxId diff --git a/XMTP.podspec b/XMTP.podspec index ecec0f30..e3f0e969 100644 --- a/XMTP.podspec +++ b/XMTP.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |spec| # spec.name = "XMTP" - spec.version = "0.15.2-alpha0" + spec.version = "0.15.2" spec.summary = "XMTP SDK Cocoapod" # This description is used to generate tags and improve search results. @@ -44,5 +44,5 @@ Pod::Spec.new do |spec| spec.dependency "web3.swift" spec.dependency "GzipSwift" spec.dependency "Connect-Swift", "= 0.12.0" - spec.dependency 'LibXMTP', '= 0.5.9-beta4' + spec.dependency 'LibXMTP', '= 0.5.10' end diff --git a/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index c7bf0b25..71ef282b 100644 --- a/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -59,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/xmtp/libxmtp-swift.git", "state" : { - "revision" : "dad96a78a12fdeeb22e5fea5c2e39d073084b0d7", - "version" : "0.5.9-beta4" + "revision" : "aea8058324fc349288bba50089b8edd2644971be", + "version" : "0.5.10" } }, { From e704cea8a722b55876259a3b75d80e9cec074855 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Wed, 23 Oct 2024 12:46:36 -0700 Subject: [PATCH 27/45] update the SCW functionality and message listing --- Sources/XMTPTestHelpers/TestHelpers.swift | 4 +-- Sources/XMTPiOS/Client.swift | 2 +- Sources/XMTPiOS/Group.swift | 37 +++++++++++++++++------ Sources/XMTPiOS/SigningKey.swift | 12 +++++--- Tests/XMTPTests/ClientTests.swift | 19 ++++++++++++ 5 files changed, 57 insertions(+), 17 deletions(-) diff --git a/Sources/XMTPTestHelpers/TestHelpers.swift b/Sources/XMTPTestHelpers/TestHelpers.swift index 41a8fa52..c6808a34 100644 --- a/Sources/XMTPTestHelpers/TestHelpers.swift +++ b/Sources/XMTPTestHelpers/TestHelpers.swift @@ -82,8 +82,8 @@ public struct FakeSCWWallet: SigningKey { walletAddress } - public var isSmartContractWallet: Bool { - true + public var type: WalletType { + WalletType.SCW } public var chainId: Int64 { diff --git a/Sources/XMTPiOS/Client.swift b/Sources/XMTPiOS/Client.swift index bafcb6e4..68ecf5be 100644 --- a/Sources/XMTPiOS/Client.swift +++ b/Sources/XMTPiOS/Client.swift @@ -271,7 +271,7 @@ public final class Client { if let signatureRequest = v3Client.signatureRequest() { if let signingKey = signingKey { do { - if signingKey.isSmartContractWallet { + if signingKey.type == WalletType.SCW { guard let chainId = signingKey.chainId else { throw ClientError.creationError("Chain id must be present to sign Smart Contract Wallet") } diff --git a/Sources/XMTPiOS/Group.swift b/Sources/XMTPiOS/Group.swift index dfc4a2e1..6f43cbc1 100644 --- a/Sources/XMTPiOS/Group.swift +++ b/Sources/XMTPiOS/Group.swift @@ -373,7 +373,8 @@ public struct Group: Identifiable, Equatable, Hashable { sentBeforeNs: nil, sentAfterNs: nil, limit: nil, - deliveryStatus: nil + deliveryStatus: nil, + direction: nil ) if let before { @@ -402,16 +403,20 @@ public struct Group: Identifiable, Equatable, Hashable { }() options.deliveryStatus = status + + let direction: FfiDirection? = { + switch direction { + case .ascending: + return FfiDirection.ascending + default: + return FfiDirection.descending + } + }() - let messages = try ffiGroup.findMessages(opts: options).compactMap { ffiMessage in - return MessageV3(client: self.client, ffiMessage: ffiMessage).decodeOrNull() - } + options.direction = direction - switch direction { - case .ascending: - return messages - default: - return messages.reversed() + return try ffiGroup.findMessages(opts: options).compactMap { ffiMessage in + return MessageV3(client: self.client, ffiMessage: ffiMessage).decodeOrNull() } } @@ -426,7 +431,8 @@ public struct Group: Identifiable, Equatable, Hashable { sentBeforeNs: nil, sentAfterNs: nil, limit: nil, - deliveryStatus: nil + deliveryStatus: nil, + direction: nil ) if let before { @@ -455,6 +461,17 @@ public struct Group: Identifiable, Equatable, Hashable { }() options.deliveryStatus = status + + let direction: FfiDirection? = { + switch direction { + case .ascending: + return FfiDirection.ascending + default: + return FfiDirection.descending + } + }() + + options.direction = direction let messages = try ffiGroup.findMessages(opts: options).compactMap { ffiMessage in return MessageV3(client: self.client, ffiMessage: ffiMessage).decryptOrNull() diff --git a/Sources/XMTPiOS/SigningKey.swift b/Sources/XMTPiOS/SigningKey.swift index 5c720edf..22a3fda9 100644 --- a/Sources/XMTPiOS/SigningKey.swift +++ b/Sources/XMTPiOS/SigningKey.swift @@ -9,6 +9,10 @@ import Foundation import web3 import LibXMTP +public enum WalletType { + case EOA, SCW +} + /// Defines a type that is used by a ``Client`` to sign keys and messages. /// /// You can use ``Account`` for an easier WalletConnect flow, or ``PrivateKey`` @@ -20,8 +24,8 @@ public protocol SigningKey { /// A wallet address for this key var address: String { get } - /// If this signing key is a smart contract wallet - var isSmartContractWallet: Bool { get } + /// The wallet type if Smart Contract Wallet this should be type SCW. Default EOA + var type: WalletType { get } /// The name of the chainId for example "1" var chainId: Int64? { get } @@ -41,8 +45,8 @@ public protocol SigningKey { } extension SigningKey { - public var isSmartContractWallet: Bool { - return false + public var type: WalletType { + return WalletType.EOA } public var chainId: Int64? { diff --git a/Tests/XMTPTests/ClientTests.swift b/Tests/XMTPTests/ClientTests.swift index 00843749..f5a6a9d4 100644 --- a/Tests/XMTPTests/ClientTests.swift +++ b/Tests/XMTPTests/ClientTests.swift @@ -521,4 +521,23 @@ class ClientTests: XCTestCase { let newState = try await alixClient3.inboxState(refreshFromNetwork: true) XCTAssertEqual(newState.installations.count, 1) } + + func testCreatesASCWClient() async throws { + let key = try Crypto.secureRandomBytes(count: 32) + let alix = try FakeSCWWallet.generate() + let options = ClientOptions.init( + api: .init(env: .local, isSecure: false), + enableV3: true, + encryptionKey: key + ) + + + let inboxId = try await Client.getOrCreateInboxId(options: options, address: alix.address) + let alixClient = try await Client.createV3( + account: alix, + options: options + ) + + XCTAssertEqual(inboxId, alixClient.inboxID) + } } From 39e52ef67c12f5e80635c78ec077db995c9bd81d Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Wed, 23 Oct 2024 12:48:38 -0700 Subject: [PATCH 28/45] small tweak to message listing --- Sources/XMTPiOS/Group.swift | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/Sources/XMTPiOS/Group.swift b/Sources/XMTPiOS/Group.swift index 6f43cbc1..5c57cf9a 100644 --- a/Sources/XMTPiOS/Group.swift +++ b/Sources/XMTPiOS/Group.swift @@ -473,15 +473,8 @@ public struct Group: Identifiable, Equatable, Hashable { options.direction = direction - let messages = try ffiGroup.findMessages(opts: options).compactMap { ffiMessage in + return try ffiGroup.findMessages(opts: options).compactMap { ffiMessage in return MessageV3(client: self.client, ffiMessage: ffiMessage).decryptOrNull() } - - switch direction { - case .ascending: - return messages - default: - return messages.reversed() - } } } From b1678a97acc2b854e5ad53471e34fa6c657c9449 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Wed, 23 Oct 2024 12:54:04 -0700 Subject: [PATCH 29/45] get closer --- Sources/XMTPTestHelpers/TestHelpers.swift | 2 +- Tests/XMTPTests/ClientTests.swift | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Sources/XMTPTestHelpers/TestHelpers.swift b/Sources/XMTPTestHelpers/TestHelpers.swift index c6808a34..d8cc65fc 100644 --- a/Sources/XMTPTestHelpers/TestHelpers.swift +++ b/Sources/XMTPTestHelpers/TestHelpers.swift @@ -86,7 +86,7 @@ public struct FakeSCWWallet: SigningKey { WalletType.SCW } - public var chainId: Int64 { + public var chainId: Int64? { 1 } diff --git a/Tests/XMTPTests/ClientTests.swift b/Tests/XMTPTests/ClientTests.swift index f5a6a9d4..21e4905e 100644 --- a/Tests/XMTPTests/ClientTests.swift +++ b/Tests/XMTPTests/ClientTests.swift @@ -523,6 +523,7 @@ class ClientTests: XCTestCase { } func testCreatesASCWClient() async throws { + throw XCTSkip("TODO: Need to write a SCW local deploy with anvil") let key = try Crypto.secureRandomBytes(count: 32) let alix = try FakeSCWWallet.generate() let options = ClientOptions.init( @@ -533,11 +534,15 @@ class ClientTests: XCTestCase { let inboxId = try await Client.getOrCreateInboxId(options: options, address: alix.address) + let alixClient = try await Client.createV3( account: alix, options: options ) - + + let alixClient2 = try await Client.buildV3(address: alix.address, options: options) XCTAssertEqual(inboxId, alixClient.inboxID) + XCTAssertEqual(alixClient2.inboxID, alixClient.inboxID) + } } From b169c71bf7653c75355dc71de463bd6ea541139c Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Wed, 23 Oct 2024 14:03:34 -0700 Subject: [PATCH 30/45] small test clean up --- Tests/XMTPTests/V3ClientTests.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Tests/XMTPTests/V3ClientTests.swift b/Tests/XMTPTests/V3ClientTests.swift index 91a70fd8..4b0015ff 100644 --- a/Tests/XMTPTests/V3ClientTests.swift +++ b/Tests/XMTPTests/V3ClientTests.swift @@ -17,11 +17,9 @@ class V3ClientTests: XCTestCase { var alixV2: PrivateKey! var boV3: PrivateKey! var caroV2V3: PrivateKey! - var davonSCW: FakeSCWWallet! var alixV2Client: Client! var boV3Client: Client! var caroV2V3Client: Client! - var davonSCWClient: Client! } func localFixtures() async throws -> LocalFixtures { @@ -51,8 +49,7 @@ class V3ClientTests: XCTestCase { encryptionKey: key ) ) - - + return .init( alixV2: alixV2, boV3: boV3, From 6c6a2f5d494d757a61bf3477ba82565dd4b889c9 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Wed, 23 Oct 2024 14:39:46 -0700 Subject: [PATCH 31/45] add the basic functionality to conversation and client --- Sources/XMTPiOS/Client.swift | 45 ++++ Sources/XMTPiOS/Conversation.swift | 108 ++++++--- Sources/XMTPiOS/Conversations.swift | 6 +- Sources/XMTPiOS/Dm.swift | 338 ++++++++++++++++++++++++++++ 4 files changed, 464 insertions(+), 33 deletions(-) create mode 100644 Sources/XMTPiOS/Dm.swift diff --git a/Sources/XMTPiOS/Client.swift b/Sources/XMTPiOS/Client.swift index 68ecf5be..cbb3d7c3 100644 --- a/Sources/XMTPiOS/Client.swift +++ b/Sources/XMTPiOS/Client.swift @@ -693,6 +693,51 @@ public final class Client { return nil } } + + public func findConversation(conversationId: String) throws -> Conversation? { + guard let client = v3Client else { + throw ClientError.noV3Client("Error no V3 client initialized") + } + let conversation = try client.conversation(conversationId: conversationId.hexToData) + return if (try conversation.groupMetadata().conversationType() == "dm") { + Conversation.dm(Dm(ffiConversation: conversation, client: self)) + } else if (try conversation.groupMetadata().conversationType() == "group") { + Conversation.group(Group(ffiGroup: conversation, client: self)) + } else { + nil + } + } + + public func findConversationByTopic(topic: String) throws -> Conversation? { + guard let client = v3Client else { + throw ClientError.noV3Client("Error no V3 client initialized") + } + let regexPattern = #"/xmtp/mls/1/g-(.*?)/proto"# + if let regex = try? NSRegularExpression(pattern: regexPattern) { + let range = NSRange(location: 0, length: topic.utf16.count) + if let match = regex.firstMatch(in: topic, options: [], range: range) { + let conversationId = (topic as NSString).substring(with: match.range(at: 1)) + let conversation = try client.conversation(conversationId: conversationId.hexToData) + if (try conversation.groupMetadata().conversationType() == "dm") { + return Conversation.dm(Dm(ffiConversation: conversation, client: self)) + } else if (try conversation.groupMetadata().conversationType() == "group") { + return Conversation.group(Group(ffiGroup: conversation, client: self)) + } + } + } + return nil + } + + public func findDm(address: String) async throws -> Dm? { + guard let client = v3Client else { + throw ClientError.noV3Client("Error no V3 client initialized") + } + guard let inboxId = try await inboxIdFromAddress(address: address) else { + throw ClientError.creationError("No inboxId present") + } + let conversation = try client.dmConversation(targetInboxId: inboxId) + return Dm(ffiConversation: conversation, client: self) + } public func findMessage(messageId: String) throws -> MessageV3? { guard let client = v3Client else { diff --git a/Sources/XMTPiOS/Conversation.swift b/Sources/XMTPiOS/Conversation.swift index 737bb1f2..cffdc09c 100644 --- a/Sources/XMTPiOS/Conversation.swift +++ b/Sources/XMTPiOS/Conversation.swift @@ -24,10 +24,10 @@ public enum ConversationContainer: Codable { /// Wrapper that provides a common interface between ``ConversationV1`` and ``ConversationV2`` objects. public enum Conversation: Sendable { // TODO: It'd be nice to not have to expose these types as public, maybe we make this a struct with an enum prop instead of just an enum - case v1(ConversationV1), v2(ConversationV2), group(Group) + case v1(ConversationV1), v2(ConversationV2), group(Group), dm(Dm) public enum Version { - case v1, v2, group + case v1, v2, group, dm } public func consentState() async throws -> ConsentState { @@ -38,6 +38,8 @@ public enum Conversation: Sendable { return try await conversationV2.client.contacts.consentList.state(address: peerAddress) case let .group(group): return try group.consentState() + case let .dm(dm): + return try dm.consentState() } } @@ -49,6 +51,8 @@ public enum Conversation: Sendable { return .v2 case .group: return .group + case let .dm(dm): + return .dm } } @@ -60,6 +64,8 @@ public enum Conversation: Sendable { return conversationV2.createdAt case let .group(group): return group.createdAt + case let .dm(dm): + return dm.createdAt } } @@ -69,8 +75,10 @@ public enum Conversation: Sendable { return .v1(conversationV1.encodedContainer) case let .v2(conversationV2): return .v2(conversationV2.encodedContainer) - case let .group(group): - throw GroupError.notSupportedByGroups + case .group(_): + throw ConversationError.v3NotSupported("encodedContainer") + case .dm(_): + throw ConversationError.v3NotSupported("encodedContainer") } } @@ -82,8 +90,10 @@ public enum Conversation: Sendable { return conversationV1.peerAddress case let .v2(conversationV2): return conversationV2.peerAddress - case let .group(group): - throw GroupError.notSupportedByGroups + case .group(_): + throw ConversationError.v3NotSupported("peerAddress use members inboxId instead") + case .dm(_): + throw ConversationError.v3NotSupported("peerAddress use members inboxId instead") } } } @@ -95,8 +105,10 @@ public enum Conversation: Sendable { return [conversationV1.peerAddress] case let .v2(conversationV2): return [conversationV2.peerAddress] - case let .group(group): - throw GroupError.notSupportedByGroups + case .group(_): + throw ConversationError.v3NotSupported("peerAddresses use members inboxIds instead") + case .dm(_): + throw ConversationError.v3NotSupported("peerAddresses use members inboxIds instead") } } } @@ -107,7 +119,9 @@ public enum Conversation: Sendable { return nil case let .v2(conversationV2): return conversationV2.keyMaterial - case let .group(group): + case .group(_): + return nil + case .dm(_): return nil } } @@ -121,7 +135,9 @@ public enum Conversation: Sendable { return nil case let .v2(conversation): return conversation.context.conversationID - case let .group(group): + case .group(_): + return nil + case .dm(_): return nil } } @@ -144,31 +160,29 @@ public enum Conversation: Sendable { } } - public func decode(_ envelope: Envelope, message: FfiMessage? = nil) throws -> DecodedMessage { + public func decode(_ envelope: Envelope) throws -> DecodedMessage { switch self { case let .v1(conversationV1): return try conversationV1.decode(envelope: envelope) case let .v2(conversationV2): return try conversationV2.decode(envelope: envelope) - case let .group(group): - guard let message = message else { - throw GroupError.groupsRequireMessagePassed - } - return try MessageV3(client: client, ffiMessage: message).decode() + case .group(_): + throw ConversationError.v3NotSupported("decode use decodeV3 instead") + case .dm(_): + throw ConversationError.v3NotSupported("decode use decodeV3 instead") } } - public func decrypt(_ envelope: Envelope, message: FfiMessage? = nil) throws -> DecryptedMessage { + public func decrypt(_ envelope: Envelope) throws -> DecryptedMessage { switch self { case let .v1(conversationV1): return try conversationV1.decrypt(envelope: envelope) case let .v2(conversationV2): return try conversationV2.decrypt(envelope: envelope) - case let .group(group): - guard let message = message else { - throw GroupError.groupsRequireMessagePassed - } - return try MessageV3(client: client, ffiMessage: message).decrypt() + case .group(_): + throw ConversationError.v3NotSupported("decrypt use decryptV3 instead") + case .dm(_): + throw ConversationError.v3NotSupported("decrypt use decryptV3 instead") } } @@ -178,8 +192,10 @@ public enum Conversation: Sendable { throw RemoteAttachmentError.v1NotSupported case let .v2(conversationV2): return try await conversationV2.encode(codec: codec, content: content) - case let .group(group): - throw GroupError.notSupportedByGroups + case .group(_): + throw ConversationError.v3NotSupported("encode") + case .dm(_): + throw ConversationError.v3NotSupported("encode") } } @@ -189,8 +205,10 @@ public enum Conversation: Sendable { return try await conversationV1.prepareMessage(encodedContent: encodedContent, options: options) case let .v2(conversationV2): return try await conversationV2.prepareMessage(encodedContent: encodedContent, options: options) - case let .group(group): - throw GroupError.notSupportedByGroups + case .group(_): + throw ConversationError.v3NotSupported("prepareMessage use prepareMessageV3 instead") + case .dm(_): + throw ConversationError.v3NotSupported("prepareMessage use prepareMessageV3 instead") } } @@ -200,8 +218,10 @@ public enum Conversation: Sendable { return try await conversationV1.prepareMessage(content: content, options: options ?? .init()) case let .v2(conversationV2): return try await conversationV2.prepareMessage(content: content, options: options ?? .init()) - case let .group(group): - throw GroupError.notSupportedByGroups + case .group(_): + throw ConversationError.v3NotSupported("prepareMessage use prepareMessageV3 instead") + case .dm(_): + throw ConversationError.v3NotSupported("prepareMessage use prepareMessageV3 instead") } } @@ -213,8 +233,10 @@ public enum Conversation: Sendable { return try await conversationV1.send(prepared: prepared) case let .v2(conversationV2): return try await conversationV2.send(prepared: prepared) - case let .group(group): - throw GroupError.notSupportedByGroups + case .group(_): + throw ConversationError.v3NotSupported("send(prepareMessage) use send(content) instead") + case .dm(_): + throw ConversationError.v3NotSupported("send(prepareMessage) use send(content) instead") } } @@ -226,6 +248,8 @@ public enum Conversation: Sendable { return try await conversationV2.send(content: content, options: options) case let .group(group): return try await group.send(content: content, options: options) + case let .dm(dm): + return try await dm.send(content: content, options: options) } } @@ -237,6 +261,8 @@ public enum Conversation: Sendable { return try await conversationV2.send(encodedContent: encodedContent, options: options) case let .group(group): return try await group.send(content: encodedContent, options: options) + case let .dm(dm): + return try await dm.send(content: encodedContent, options: options) } } @@ -249,6 +275,8 @@ public enum Conversation: Sendable { return try await conversationV2.send(content: text, options: options) case let .group(group): return try await group.send(content: text, options: options) + case let .dm(dm): + return try await dm.send(content: text, options: options) } } @@ -265,6 +293,8 @@ public enum Conversation: Sendable { return conversation.topic case let .group(group): return group.topic + case let .dm(dm): + return dm.topic } } @@ -274,8 +304,10 @@ public enum Conversation: Sendable { return conversation.streamEphemeral() case let .v2(conversation): return conversation.streamEphemeral() - case let .group(group): - throw GroupError.notSupportedByGroups + case .group(_): + throw ConversationError.v3NotSupported("streamEphemeral") + case .dm(_): + throw ConversationError.v3NotSupported("streamEphemeral") } } @@ -291,6 +323,8 @@ public enum Conversation: Sendable { return conversation.streamMessages() case let .group(group): return group.streamMessages() + case let .dm(dm): + return dm.streamMessages() } } @@ -302,6 +336,8 @@ public enum Conversation: Sendable { return conversation.streamDecryptedMessages() case let .group(group): return group.streamDecryptedMessages() + case let .dm(dm): + return dm.streamDecryptedMessages() } } @@ -314,6 +350,8 @@ public enum Conversation: Sendable { return try await conversationV2.messages(limit: limit, before: before, after: after, direction: direction) case let .group(group): return try await group.messages(before: before, after: after, limit: limit, direction: direction) + case let .dm(dm): + return try await dm.messages(before: before, after: after, limit: limit, direction: direction) } } @@ -325,6 +363,8 @@ public enum Conversation: Sendable { return try await conversationV2.decryptedMessages(limit: limit, before: before, after: after, direction: direction) case let .group(group): return try await group.decryptedMessages(before: before, after: after, limit: limit, direction: direction) + case let .dm(dm): + return try await dm.decryptedMessages(before: before, after: after, limit: limit, direction: direction) } } @@ -336,6 +376,8 @@ public enum Conversation: Sendable { return conversationV2.consentProof case .group(_): return nil + case let .dm(dm): + return nil } } @@ -347,6 +389,8 @@ public enum Conversation: Sendable { return conversationV2.client case let .group(group): return group.client + case let .dm(dm): + return dm.client } } } diff --git a/Sources/XMTPiOS/Conversations.swift b/Sources/XMTPiOS/Conversations.swift index 98dc482b..405d3605 100644 --- a/Sources/XMTPiOS/Conversations.swift +++ b/Sources/XMTPiOS/Conversations.swift @@ -2,7 +2,7 @@ import Foundation import LibXMTP public enum ConversationError: Error, CustomStringConvertible, LocalizedError { - case recipientNotOnNetwork, recipientIsSender, v1NotSupported(String) + case recipientNotOnNetwork, recipientIsSender, v1NotSupported(String), v2NotSupported(String), v3NotSupported(String) public var description: String { switch self { @@ -12,6 +12,10 @@ public enum ConversationError: Error, CustomStringConvertible, LocalizedError { return "ConversationError.recipientNotOnNetwork: Recipient is not on network" case .v1NotSupported(let str): return "ConversationError.v1NotSupported: V1 does not support: \(str)" + case .v2NotSupported(let str): + return "ConversationError.v2NotSupported: V2 does not support: \(str)" + case .v3NotSupported(let str): + return "ConversationError.v3NotSupported: V3 does not support: \(str)" } } diff --git a/Sources/XMTPiOS/Dm.swift b/Sources/XMTPiOS/Dm.swift new file mode 100644 index 00000000..7a956fa0 --- /dev/null +++ b/Sources/XMTPiOS/Dm.swift @@ -0,0 +1,338 @@ +// +// Dm.swift +// XMTPiOS +// +// Created by Naomi Plasterer on 10/23/24. +// + +import Foundation +import LibXMTP + +public struct Dm: Identifiable, Equatable, Hashable { + var ffiConversation: FfiConversation + var client: Client + let streamHolder = StreamHolder() + + public var id: String { + ffiConversation.id().toHex + } + + public var topic: String { + Topic.groupMessage(id).description + } + + func metadata() throws -> FfiConversationMetadata { + return try ffiConversation.groupMetadata() + } + + public func sync() async throws { + try await ffiConversation.sync() + } + + public static func == (lhs: Dm, rhs: Dm) -> Bool { + lhs.id == rhs.id + } + + public func hash(into hasher: inout Hasher) { + id.hash(into: &hasher) + } + + public func isCreator() throws -> Bool { + return try metadata().creatorInboxId() == client.inboxID + } + + public func creatorInboxId() throws -> String { + return try metadata().creatorInboxId() + } + + public func addedByInboxId() throws -> String { + return try ffiConversation.addedByInboxId() + } + + public var members: [Member] { + get async throws { + return try await ffiConversation.listMembers().map { ffiGroupMember in + Member(ffiGroupMember: ffiGroupMember) + } + } + } + + public var peerInboxIds: [String] { + get async throws { + var ids = try await members.map(\.inboxId) + if let index = ids.firstIndex(of: client.inboxID) { + ids.remove(at: index) + } + return ids + } + } + + public var createdAt: Date { + Date(millisecondsSinceEpoch: ffiConversation.createdAtNs()) + } + + public func updateConsentState(state: ConsentState) async throws { + if (client.hasV2Client) { + switch (state) { + case .allowed: try await client.contacts.allowGroups(groupIds: [id]) + case .denied: try await client.contacts.denyGroups(groupIds: [id]) + case .unknown: () + } + } + + try ffiConversation.updateConsentState(state: state.toFFI) + } + + public func consentState() throws -> ConsentState{ + return try ffiConversation.consentState().fromFFI + } + + public func processMessage(envelopeBytes: Data) async throws -> DecodedMessage { + let message = try await ffiConversation.processStreamedConversationMessage(envelopeBytes: envelopeBytes) + return try MessageV3(client: client, ffiMessage: message).decode() + } + + public func processMessageDecrypted(envelopeBytes: Data) async throws -> DecryptedMessage { + let message = try await ffiConversation.processStreamedConversationMessage(envelopeBytes: envelopeBytes) + return try MessageV3(client: client, ffiMessage: message).decrypt() + } + + public func send(content: T, options: SendOptions? = nil) async throws -> String { + let encodeContent = try await encodeContent(content: content, options: options) + return try await send(encodedContent: encodeContent) + } + + public func send(encodedContent: EncodedContent) async throws -> String { + if (try consentState() == .unknown) { + try await updateConsentState(state: .allowed) + } + + let messageId = try await ffiConversation.send(contentBytes: encodedContent.serializedData()) + return messageId.toHex + } + + public func encodeContent(content: T, options: SendOptions?) async throws -> EncodedContent { + let codec = client.codecRegistry.find(for: options?.contentType) + + func encode(codec: Codec, content: Any) throws -> EncodedContent { + if let content = content as? Codec.T { + return try codec.encode(content: content, client: client) + } else { + throw CodecError.invalidContent + } + } + + var encoded = try encode(codec: codec, content: content) + + func fallback(codec: Codec, content: Any) throws -> String? { + if let content = content as? Codec.T { + return try codec.fallback(content: content) + } else { + throw CodecError.invalidContent + } + } + + if let fallback = try fallback(codec: codec, content: content) { + encoded.fallback = fallback + } + + if let compression = options?.compression { + encoded = try encoded.compress(compression) + } + + return encoded + } + + public func prepareMessage(content: T, options: SendOptions? = nil) async throws -> String { + if (try consentState() == .unknown) { + try await updateConsentState(state: .allowed) + } + + let encodeContent = try await encodeContent(content: content, options: options) + return try ffiConversation.sendOptimistic(contentBytes: try encodeContent.serializedData()).toHex + } + + public func publishMessages() async throws { + try await ffiConversation.publishMessages() + } + + public func endStream() { + self.streamHolder.stream?.end() + } + + public func streamMessages() -> AsyncThrowingStream { + AsyncThrowingStream { continuation in + let task = Task.detached { + self.streamHolder.stream = await self.ffiConversation.stream( + messageCallback: MessageCallback(client: self.client) { message in + guard !Task.isCancelled else { + continuation.finish() + return + } + do { + continuation.yield(try MessageV3(client: self.client, ffiMessage: message).decode()) + } catch { + print("Error onMessage \(error)") + continuation.finish(throwing: error) + } + } + ) + + continuation.onTermination = { @Sendable reason in + self.streamHolder.stream?.end() + } + } + + continuation.onTermination = { @Sendable reason in + task.cancel() + self.streamHolder.stream?.end() + } + } + } + + public func streamDecryptedMessages() -> AsyncThrowingStream { + AsyncThrowingStream { continuation in + let task = Task.detached { + self.streamHolder.stream = await self.ffiConversation.stream( + messageCallback: MessageCallback(client: self.client) { message in + guard !Task.isCancelled else { + continuation.finish() + return + } + do { + continuation.yield(try MessageV3(client: self.client, ffiMessage: message).decrypt()) + } catch { + print("Error onMessage \(error)") + continuation.finish(throwing: error) + } + } + ) + + continuation.onTermination = { @Sendable reason in + self.streamHolder.stream?.end() + } + } + + continuation.onTermination = { @Sendable reason in + task.cancel() + self.streamHolder.stream?.end() + } + } + } + + public func messages( + before: Date? = nil, + after: Date? = nil, + limit: Int? = nil, + direction: PagingInfoSortDirection? = .descending, + deliveryStatus: MessageDeliveryStatus = .all + ) async throws -> [DecodedMessage] { + var options = FfiListMessagesOptions( + sentBeforeNs: nil, + sentAfterNs: nil, + limit: nil, + deliveryStatus: nil, + direction: nil + ) + + if let before { + options.sentBeforeNs = Int64(before.millisecondsSinceEpoch * 1_000_000) + } + + if let after { + options.sentAfterNs = Int64(after.millisecondsSinceEpoch * 1_000_000) + } + + if let limit { + options.limit = Int64(limit) + } + + let status: FfiDeliveryStatus? = { + switch deliveryStatus { + case .published: + return FfiDeliveryStatus.published + case .unpublished: + return FfiDeliveryStatus.unpublished + case .failed: + return FfiDeliveryStatus.failed + default: + return nil + } + }() + + options.deliveryStatus = status + + let direction: FfiDirection? = { + switch direction { + case .ascending: + return FfiDirection.ascending + default: + return FfiDirection.descending + } + }() + + options.direction = direction + + return try ffiConversation.findMessages(opts: options).compactMap { ffiMessage in + return MessageV3(client: self.client, ffiMessage: ffiMessage).decodeOrNull() + } + } + + public func decryptedMessages( + before: Date? = nil, + after: Date? = nil, + limit: Int? = nil, + direction: PagingInfoSortDirection? = .descending, + deliveryStatus: MessageDeliveryStatus? = .all + ) async throws -> [DecryptedMessage] { + var options = FfiListMessagesOptions( + sentBeforeNs: nil, + sentAfterNs: nil, + limit: nil, + deliveryStatus: nil, + direction: nil + ) + + if let before { + options.sentBeforeNs = Int64(before.millisecondsSinceEpoch * 1_000_000) + } + + if let after { + options.sentAfterNs = Int64(after.millisecondsSinceEpoch * 1_000_000) + } + + if let limit { + options.limit = Int64(limit) + } + + let status: FfiDeliveryStatus? = { + switch deliveryStatus { + case .published: + return FfiDeliveryStatus.published + case .unpublished: + return FfiDeliveryStatus.unpublished + case .failed: + return FfiDeliveryStatus.failed + default: + return nil + } + }() + + options.deliveryStatus = status + + let direction: FfiDirection? = { + switch direction { + case .ascending: + return FfiDirection.ascending + default: + return FfiDirection.descending + } + }() + + options.direction = direction + + return try ffiConversation.findMessages(opts: options).compactMap { ffiMessage in + return MessageV3(client: self.client, ffiMessage: ffiMessage).decryptOrNull() + } + } +} From c7564b9b2514ad001c9fadd81c378598eb3421b7 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Wed, 23 Oct 2024 14:42:23 -0700 Subject: [PATCH 32/45] put V2 stuff below the line --- Sources/XMTPiOS/Conversation.swift | 297 +++++++++++++++-------------- 1 file changed, 151 insertions(+), 146 deletions(-) diff --git a/Sources/XMTPiOS/Conversation.swift b/Sources/XMTPiOS/Conversation.swift index cffdc09c..14a14817 100644 --- a/Sources/XMTPiOS/Conversation.swift +++ b/Sources/XMTPiOS/Conversation.swift @@ -69,6 +69,149 @@ public enum Conversation: Sendable { } } + @discardableResult public func send(content: T, options: SendOptions? = nil, fallback _: String? = nil) async throws -> String { + switch self { + case let .v1(conversationV1): + return try await conversationV1.send(content: content, options: options) + case let .v2(conversationV2): + return try await conversationV2.send(content: content, options: options) + case let .group(group): + return try await group.send(content: content, options: options) + case let .dm(dm): + return try await dm.send(content: content, options: options) + } + } + + @discardableResult public func send(encodedContent: EncodedContent, options: SendOptions? = nil) async throws -> String { + switch self { + case let .v1(conversationV1): + return try await conversationV1.send(encodedContent: encodedContent, options: options) + case let .v2(conversationV2): + return try await conversationV2.send(encodedContent: encodedContent, options: options) + case let .group(group): + return try await group.send(content: encodedContent, options: options) + case let .dm(dm): + return try await dm.send(content: encodedContent, options: options) + } + } + + /// Send a message to the conversation + public func send(text: String, options: SendOptions? = nil) async throws -> String { + switch self { + case let .v1(conversationV1): + return try await conversationV1.send(content: text, options: options) + case let .v2(conversationV2): + return try await conversationV2.send(content: text, options: options) + case let .group(group): + return try await group.send(content: text, options: options) + case let .dm(dm): + return try await dm.send(content: text, options: options) + } + } + + public var clientAddress: String { + return client.address + } + + /// The topic identifier for this conversation + public var topic: String { + switch self { + case let .v1(conversation): + return conversation.topic.description + case let .v2(conversation): + return conversation.topic + case let .group(group): + return group.topic + case let .dm(dm): + return dm.topic + } + } + + /// Returns a stream you can iterate through to receive new messages in this conversation. + /// + /// > Note: All messages in the conversation are returned by this stream. If you want to filter out messages + /// by a sender, you can check the ``Client`` address against the message's ``peerAddress``. + public func streamMessages() -> AsyncThrowingStream { + switch self { + case let .v1(conversation): + return conversation.streamMessages() + case let .v2(conversation): + return conversation.streamMessages() + case let .group(group): + return group.streamMessages() + case let .dm(dm): + return dm.streamMessages() + } + } + + public func streamDecryptedMessages() -> AsyncThrowingStream { + switch self { + case let .v1(conversation): + return conversation.streamDecryptedMessages() + case let .v2(conversation): + return conversation.streamDecryptedMessages() + case let .group(group): + return group.streamDecryptedMessages() + case let .dm(dm): + return dm.streamDecryptedMessages() + } + } + + /// List messages in the conversation + public func messages(limit: Int? = nil, before: Date? = nil, after: Date? = nil, direction: PagingInfoSortDirection? = .descending) async throws -> [DecodedMessage] { + switch self { + case let .v1(conversationV1): + return try await conversationV1.messages(limit: limit, before: before, after: after, direction: direction) + case let .v2(conversationV2): + return try await conversationV2.messages(limit: limit, before: before, after: after, direction: direction) + case let .group(group): + return try await group.messages(before: before, after: after, limit: limit, direction: direction) + case let .dm(dm): + return try await dm.messages(before: before, after: after, limit: limit, direction: direction) + } + } + + public func decryptedMessages(limit: Int? = nil, before: Date? = nil, after: Date? = nil, direction: PagingInfoSortDirection? = .descending) async throws -> [DecryptedMessage] { + switch self { + case let .v1(conversationV1): + return try await conversationV1.decryptedMessages(limit: limit, before: before, after: after, direction: direction) + case let .v2(conversationV2): + return try await conversationV2.decryptedMessages(limit: limit, before: before, after: after, direction: direction) + case let .group(group): + return try await group.decryptedMessages(before: before, after: after, limit: limit, direction: direction) + case let .dm(dm): + return try await dm.decryptedMessages(before: before, after: after, limit: limit, direction: direction) + } + } + + public var consentProof: ConsentProofPayload? { + switch self { + case .v1(_): + return nil + case let .v2(conversationV2): + return conversationV2.consentProof + case .group(_): + return nil + case let .dm(dm): + return nil + } + } + + var client: Client { + switch self { + case let .v1(conversationV1): + return conversationV1.client + case let .v2(conversationV2): + return conversationV2.client + case let .group(group): + return group.client + case let .dm(dm): + return dm.client + } + } + + // ------- V1 V2 to be deprecated ------ + public func encodedContainer() throws -> ConversationContainer { switch self { case let .v1(conversationV1): @@ -199,18 +342,18 @@ public enum Conversation: Sendable { } } - public func prepareMessage(encodedContent: EncodedContent, options: SendOptions? = nil) async throws -> PreparedMessage { - switch self { - case let .v1(conversationV1): - return try await conversationV1.prepareMessage(encodedContent: encodedContent, options: options) - case let .v2(conversationV2): - return try await conversationV2.prepareMessage(encodedContent: encodedContent, options: options) + public func prepareMessage(encodedContent: EncodedContent, options: SendOptions? = nil) async throws -> PreparedMessage { + switch self { + case let .v1(conversationV1): + return try await conversationV1.prepareMessage(encodedContent: encodedContent, options: options) + case let .v2(conversationV2): + return try await conversationV2.prepareMessage(encodedContent: encodedContent, options: options) case .group(_): throw ConversationError.v3NotSupported("prepareMessage use prepareMessageV3 instead") case .dm(_): throw ConversationError.v3NotSupported("prepareMessage use prepareMessageV3 instead") - } - } + } + } public func prepareMessage(content: T, options: SendOptions? = nil) async throws -> PreparedMessage { switch self { @@ -240,63 +383,6 @@ public enum Conversation: Sendable { } } - @discardableResult public func send(content: T, options: SendOptions? = nil, fallback _: String? = nil) async throws -> String { - switch self { - case let .v1(conversationV1): - return try await conversationV1.send(content: content, options: options) - case let .v2(conversationV2): - return try await conversationV2.send(content: content, options: options) - case let .group(group): - return try await group.send(content: content, options: options) - case let .dm(dm): - return try await dm.send(content: content, options: options) - } - } - - @discardableResult public func send(encodedContent: EncodedContent, options: SendOptions? = nil) async throws -> String { - switch self { - case let .v1(conversationV1): - return try await conversationV1.send(encodedContent: encodedContent, options: options) - case let .v2(conversationV2): - return try await conversationV2.send(encodedContent: encodedContent, options: options) - case let .group(group): - return try await group.send(content: encodedContent, options: options) - case let .dm(dm): - return try await dm.send(content: encodedContent, options: options) - } - } - - /// Send a message to the conversation - public func send(text: String, options: SendOptions? = nil) async throws -> String { - switch self { - case let .v1(conversationV1): - return try await conversationV1.send(content: text, options: options) - case let .v2(conversationV2): - return try await conversationV2.send(content: text, options: options) - case let .group(group): - return try await group.send(content: text, options: options) - case let .dm(dm): - return try await dm.send(content: text, options: options) - } - } - - public var clientAddress: String { - return client.address - } - - /// The topic identifier for this conversation - public var topic: String { - switch self { - case let .v1(conversation): - return conversation.topic.description - case let .v2(conversation): - return conversation.topic - case let .group(group): - return group.topic - case let .dm(dm): - return dm.topic - } - } public func streamEphemeral() throws -> AsyncThrowingStream? { switch self { @@ -311,88 +397,7 @@ public enum Conversation: Sendable { } } - /// Returns a stream you can iterate through to receive new messages in this conversation. - /// - /// > Note: All messages in the conversation are returned by this stream. If you want to filter out messages - /// by a sender, you can check the ``Client`` address against the message's ``peerAddress``. - public func streamMessages() -> AsyncThrowingStream { - switch self { - case let .v1(conversation): - return conversation.streamMessages() - case let .v2(conversation): - return conversation.streamMessages() - case let .group(group): - return group.streamMessages() - case let .dm(dm): - return dm.streamMessages() - } - } - - public func streamDecryptedMessages() -> AsyncThrowingStream { - switch self { - case let .v1(conversation): - return conversation.streamDecryptedMessages() - case let .v2(conversation): - return conversation.streamDecryptedMessages() - case let .group(group): - return group.streamDecryptedMessages() - case let .dm(dm): - return dm.streamDecryptedMessages() - } - } - - /// List messages in the conversation - public func messages(limit: Int? = nil, before: Date? = nil, after: Date? = nil, direction: PagingInfoSortDirection? = .descending) async throws -> [DecodedMessage] { - switch self { - case let .v1(conversationV1): - return try await conversationV1.messages(limit: limit, before: before, after: after, direction: direction) - case let .v2(conversationV2): - return try await conversationV2.messages(limit: limit, before: before, after: after, direction: direction) - case let .group(group): - return try await group.messages(before: before, after: after, limit: limit, direction: direction) - case let .dm(dm): - return try await dm.messages(before: before, after: after, limit: limit, direction: direction) - } - } - - public func decryptedMessages(limit: Int? = nil, before: Date? = nil, after: Date? = nil, direction: PagingInfoSortDirection? = .descending) async throws -> [DecryptedMessage] { - switch self { - case let .v1(conversationV1): - return try await conversationV1.decryptedMessages(limit: limit, before: before, after: after, direction: direction) - case let .v2(conversationV2): - return try await conversationV2.decryptedMessages(limit: limit, before: before, after: after, direction: direction) - case let .group(group): - return try await group.decryptedMessages(before: before, after: after, limit: limit, direction: direction) - case let .dm(dm): - return try await dm.decryptedMessages(before: before, after: after, limit: limit, direction: direction) - } - } - - public var consentProof: ConsentProofPayload? { - switch self { - case .v1(_): - return nil - case let .v2(conversationV2): - return conversationV2.consentProof - case .group(_): - return nil - case let .dm(dm): - return nil - } - } - var client: Client { - switch self { - case let .v1(conversationV1): - return conversationV1.client - case let .v2(conversationV2): - return conversationV2.client - case let .group(group): - return group.client - case let .dm(dm): - return dm.client - } - } } extension Conversation: Hashable, Equatable { From 0087142c3cb49bddbe60c88ace4c46706e168194 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Wed, 23 Oct 2024 15:00:13 -0700 Subject: [PATCH 33/45] more common functions --- Sources/XMTPiOS/Conversation.swift | 93 +++++++++++++++++++++++++++++ Sources/XMTPiOS/Conversations.swift | 2 + Sources/XMTPiOS/Dm.swift | 9 +-- Sources/XMTPiOS/Group.swift | 9 +-- 4 files changed, 99 insertions(+), 14 deletions(-) diff --git a/Sources/XMTPiOS/Conversation.swift b/Sources/XMTPiOS/Conversation.swift index 14a14817..b3a4f3b1 100644 --- a/Sources/XMTPiOS/Conversation.swift +++ b/Sources/XMTPiOS/Conversation.swift @@ -29,6 +29,47 @@ public enum Conversation: Sendable { public enum Version { case v1, v2, group, dm } + + public var id: String { + get throws { + switch self { + case .v1(_): + throw ConversationError.v1NotSupported("id") + case .v2(_): + throw ConversationError.v2NotSupported("id") + case let .group(group): + return group.id + case let .dm(dm): + return dm.id + } + } + } + + public func isCreator() async throws -> Bool { + switch self { + case .v1(_): + throw ConversationError.v1NotSupported("isCreator") + case .v2(_): + throw ConversationError.v2NotSupported("isCreator") + case let .group(group): + return try group.isCreator() + case let .dm(dm): + return try dm.isCreator() + } + } + + public func members() async throws -> [Member] { + switch self { + case .v1(_): + throw ConversationError.v1NotSupported("members") + case .v2(_): + throw ConversationError.v2NotSupported("members") + case let .group(group): + return try await group.members + case let .dm(dm): + return try await dm.members + } + } public func consentState() async throws -> ConsentState { switch self { @@ -42,6 +83,58 @@ public enum Conversation: Sendable { return try dm.consentState() } } + + public func updateConsentState(state: ConsentState) async throws { + switch self { + case .v1(_): + throw ConversationError.v1NotSupported("updateConsentState use contact.allowAddresses instead") + case .v2(_): + throw ConversationError.v2NotSupported("updateConsentState use contact.allowAddresses instead") + case let .group(group): + try await group.updateConsentState(state: state) + case let .dm(dm): + try await dm.updateConsentState(state: state) + } + } + + public func sync() async throws { + switch self { + case .v1(_): + throw ConversationError.v1NotSupported("sync") + case .v2(_): + throw ConversationError.v2NotSupported("sync") + case let .group(group): + try await group.sync() + case let .dm(dm): + try await dm.sync() + } + } + + public func processMessage(envelopeBytes: Data) async throws -> MessageV3 { + switch self { + case .v1(_): + throw ConversationError.v1NotSupported("processMessage") + case .v2(_): + throw ConversationError.v2NotSupported("processMessage") + case let .group(group): + try await group.processMessage(envelopeBytes: envelopeBytes) + case let .dm(dm): + try await dm.processMessage(envelopeBytes: envelopeBytes) + } + } + + public func prepareMessageV3(content: T, options: SendOptions? = nil) async throws -> String { + switch self { + case .v1(_): + throw ConversationError.v1NotSupported("prepareMessageV3 use prepareMessage instead") + case .v2(_): + throw ConversationError.v2NotSupported("prepareMessageV3 use prepareMessage instead") + case let .group(group): + try await group.prepareMessage(content: content, options: options) + case let .dm(dm): + try await dm.prepareMessage(content: content, options: options) + } + } public var version: Version { switch self { diff --git a/Sources/XMTPiOS/Conversations.swift b/Sources/XMTPiOS/Conversations.swift index 405d3605..5b8579cc 100644 --- a/Sources/XMTPiOS/Conversations.swift +++ b/Sources/XMTPiOS/Conversations.swift @@ -741,6 +741,8 @@ public actor Conversations { } } } + + // ------- V1 V2 to be deprecated ------ private func makeConversation(from sealedInvitation: SealedInvitation) throws -> ConversationV2 { let unsealed = try sealedInvitation.v1.getInvitation(viewer: client.keys) diff --git a/Sources/XMTPiOS/Dm.swift b/Sources/XMTPiOS/Dm.swift index 7a956fa0..5ba2965c 100644 --- a/Sources/XMTPiOS/Dm.swift +++ b/Sources/XMTPiOS/Dm.swift @@ -87,14 +87,9 @@ public struct Dm: Identifiable, Equatable, Hashable { return try ffiConversation.consentState().fromFFI } - public func processMessage(envelopeBytes: Data) async throws -> DecodedMessage { + public func processMessage(envelopeBytes: Data) async throws -> MessageV3 { let message = try await ffiConversation.processStreamedConversationMessage(envelopeBytes: envelopeBytes) - return try MessageV3(client: client, ffiMessage: message).decode() - } - - public func processMessageDecrypted(envelopeBytes: Data) async throws -> DecryptedMessage { - let message = try await ffiConversation.processStreamedConversationMessage(envelopeBytes: envelopeBytes) - return try MessageV3(client: client, ffiMessage: message).decrypt() + return MessageV3(client: client, ffiMessage: message) } public func send(content: T, options: SendOptions? = nil) async throws -> String { diff --git a/Sources/XMTPiOS/Group.swift b/Sources/XMTPiOS/Group.swift index 5c57cf9a..3116356a 100644 --- a/Sources/XMTPiOS/Group.swift +++ b/Sources/XMTPiOS/Group.swift @@ -229,14 +229,9 @@ public struct Group: Identifiable, Equatable, Hashable { return try ffiGroup.consentState().fromFFI } - public func processMessage(envelopeBytes: Data) async throws -> DecodedMessage { + public func processMessage(envelopeBytes: Data) async throws -> MessageV3 { let message = try await ffiGroup.processStreamedConversationMessage(envelopeBytes: envelopeBytes) - return try MessageV3(client: client, ffiMessage: message).decode() - } - - public func processMessageDecrypted(envelopeBytes: Data) async throws -> DecryptedMessage { - let message = try await ffiGroup.processStreamedConversationMessage(envelopeBytes: envelopeBytes) - return try MessageV3(client: client, ffiMessage: message).decrypt() + return MessageV3(client: client, ffiMessage: message) } public func send(content: T, options: SendOptions? = nil) async throws -> String { From 6e7abe76de42197e3ce1902457775e1dfd76a9f6 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Wed, 23 Oct 2024 16:49:30 -0700 Subject: [PATCH 34/45] reorder the conversations class and add additional functionality --- Sources/XMTPiOS/Client.swift | 4 +- Sources/XMTPiOS/Conversations.swift | 607 ++++++++++++++++----------- Sources/XMTPiOS/Extensions/Ffi.swift | 6 +- 3 files changed, 372 insertions(+), 245 deletions(-) diff --git a/Sources/XMTPiOS/Client.swift b/Sources/XMTPiOS/Client.swift index cbb3d7c3..8a84ec04 100644 --- a/Sources/XMTPiOS/Client.swift +++ b/Sources/XMTPiOS/Client.swift @@ -700,9 +700,9 @@ public final class Client { } let conversation = try client.conversation(conversationId: conversationId.hexToData) return if (try conversation.groupMetadata().conversationType() == "dm") { - Conversation.dm(Dm(ffiConversation: conversation, client: self)) + Conversation.dm(conversation.dmFromFFI(client: self)) } else if (try conversation.groupMetadata().conversationType() == "group") { - Conversation.group(Group(ffiGroup: conversation, client: self)) + Conversation.group(conversation.groupFromFFI(client: self)) } else { nil } diff --git a/Sources/XMTPiOS/Conversations.swift b/Sources/XMTPiOS/Conversations.swift index 5b8579cc..a4d457d8 100644 --- a/Sources/XMTPiOS/Conversations.swift +++ b/Sources/XMTPiOS/Conversations.swift @@ -49,17 +49,19 @@ public enum GroupError: Error, CustomStringConvertible, LocalizedError { } } -final class GroupStreamCallback: FfiConversationCallback { - let client: Client - let callback: (Group) -> Void +public enum ConversationOrder { + case created_at, last_message +} - init(client: Client, callback: @escaping (Group) -> Void) { - self.client = client +final class ConversationStreamCallback: FfiConversationCallback { + let callback: (FfiConversation) -> Void + + init(callback: @escaping (FfiConversation) -> Void) { self.callback = callback } func onConversation(conversation: FfiConversation) { - self.callback(conversation.fromFFI(client: client)) + self.callback(conversation) } } @@ -119,7 +121,7 @@ public actor Conversations { try await v3Client.conversations().sync() } - public func syncAllGroups() async throws -> UInt32 { + public func syncAllConversations() async throws -> UInt32 { guard let v3Client = client.v3Client else { return 0 } @@ -140,19 +142,37 @@ public actor Conversations { if let limit { options.limit = Int64(limit) } - return try await v3Client.conversations().listGroups(opts: options).map { $0.fromFFI(client: client) } + return try await v3Client.conversations().listGroups(opts: options).map { $0.groupFromFFI(client: client) } + } + + public func listConversations(createdAfter: Date? = nil, createdBefore: Date? = nil, limit: Int? = nil, order: ConversationOrder? = nil, consentState: ConsentState? = nil) async throws -> [Group] { + // Todo: add ability to order and consent state + guard let v3Client = client.v3Client else { + return [] + } + var options = FfiListConversationsOptions(createdAfterNs: nil, createdBeforeNs: nil, limit: nil) + if let createdAfter { + options.createdAfterNs = Int64(createdAfter.millisecondsSinceEpoch) + } + if let createdBefore { + options.createdBeforeNs = Int64(createdBefore.millisecondsSinceEpoch) + } + if let limit { + options.limit = Int64(limit) + } + return try await v3Client.conversations().list(opts: options).map { $0.groupFromFFI(client: client) } } public func streamGroups() async throws -> AsyncThrowingStream { AsyncThrowingStream { continuation in let ffiStreamActor = FfiStreamActor() let task = Task { - let groupCallback = GroupStreamCallback(client: self.client) { group in + let groupCallback = ConversationStreamCallback() { group in guard !Task.isCancelled else { continuation.finish() return } - continuation.yield(group) + continuation.yield(group.groupFromFFI(client: self.client)) } guard let stream = await self.client.v3Client?.conversations().streamGroups(callback: groupCallback) else { continuation.finish(throwing: GroupError.streamingFailure) @@ -180,12 +200,12 @@ public actor Conversations { let ffiStreamActor = FfiStreamActor() let task = Task { let stream = await self.client.v3Client?.conversations().streamGroups( - callback: GroupStreamCallback(client: self.client) { group in + callback: ConversationStreamCallback() { group in guard !Task.isCancelled else { continuation.finish() return } - continuation.yield(Conversation.group(group)) + continuation.yield(Conversation.group(group.groupFromFFI(client: self.client))) } ) await ffiStreamActor.setFfiStream(stream) @@ -204,6 +224,65 @@ public actor Conversations { } } } + + private func streamConversations() -> AsyncThrowingStream { + AsyncThrowingStream { continuation in + let ffiStreamActor = FfiStreamActor() + let task = Task { + let stream = await self.client.v3Client?.conversations().stream( + callback: ConversationStreamCallback() { conversation in + guard !Task.isCancelled else { + continuation.finish() + return + } + do { + let conversationType = try conversation.groupMetadata().conversationType() + if conversationType == "dm" { + continuation.yield( + Conversation.dm(conversation.dmFromFFI(client: self.client)) + ) + } else if conversationType == "group" { + continuation.yield( + Conversation.group(conversation.groupFromFFI(client: self.client)) + ) + } + } catch { + // Do nothing if the conversation type is neither a group or dm + } + } + ) + await ffiStreamActor.setFfiStream(stream) + continuation.onTermination = { @Sendable reason in + Task { + await ffiStreamActor.endStream() + } + } + } + + continuation.onTermination = { @Sendable reason in + task.cancel() + Task { + await ffiStreamActor.endStream() + } + } + } + } + + public func findOrCreateDm(with peerAddress: String) async throws -> Dm { + guard let v3Client = client.v3Client else { + throw GroupError.alphaMLSNotEnabled + } + if peerAddress.lowercased() == client.address.lowercased() { + throw ConversationError.recipientIsSender + } + let canMessage = try await self.client.canMessageV3(address: peerAddress) + if !canMessage { + throw ConversationError.recipientNotOnNetwork + } + let dm = try await v3Client.conversations().createDm(accountAddress: peerAddress).dmFromFFI(client: client) + try await client.contacts.allow(addresses: [peerAddress]) + return dm + } public func newGroup(with addresses: [String], permissions: GroupPermissionPreconfiguration = .allMembers, @@ -283,153 +362,38 @@ public actor Conversations { groupDescription: description, groupPinnedFrameUrl: pinnedFrameUrl, customPermissionPolicySet: permissionPolicySet - )).fromFFI(client: client) + )).groupFromFFI(client: client) try await client.contacts.allowGroups(groupIds: [group.id]) return group } - - /// 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, - createdAtNs: data.createdNs - )) - } - Task { - await self.addConversation(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 - guard let apiClient = client.apiClient else { - throw ClientError.noV2Client("Error no V2 client initialized") - } - for batch in batches { - messages += try await apiClient.batchQuery(request: batch) - .responses.flatMap { res in - res.envelopes.compactMap { envelope in - let conversation = conversationsByTopic[envelope.contentTopic] - if conversation == nil { - print("discarding message, unknown conversation \(envelope)") - return nil - } - do { - return try conversation?.decode(envelope) - } catch { - print("discarding message, unable to decode \(envelope)") - return nil - } - } - } - } - return messages - } - - public func listBatchDecryptedMessages(topics: [String: Pagination?]) async throws -> [DecryptedMessage] { - 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: [DecryptedMessage] = [] - // TODO: consider using a task group here for parallel batch calls - guard let apiClient = client.apiClient else { - throw ClientError.noV2Client("Error no V2 client initialized") - } - for batch in batches { - messages += try await apiClient.batchQuery(request: batch) - .responses.flatMap { res in - res.envelopes.compactMap { envelope in - let conversation = conversationsByTopic[envelope.contentTopic] - if conversation == nil { - print("discarding message, unknown conversation \(envelope)") - return nil + + public func streamAllConversationMessages() -> AsyncThrowingStream { + AsyncThrowingStream { continuation in + let ffiStreamActor = FfiStreamActor() + let task = Task { + let stream = await self.client.v3Client?.conversations().streamAllMessages( + messageCallback: MessageCallback(client: self.client) { message in + guard !Task.isCancelled else { + continuation.finish() + Task { + await ffiStreamActor.endStream() // End the stream upon cancellation + } + return } do { - return try conversation?.decrypt(envelope) + continuation.yield(try MessageV3(client: self.client, ffiMessage: message).decode()) } catch { - print("discarding message, unable to decode \(envelope)") - return nil + print("Error onMessage \(error)") } } - } - } - return messages - } - - func streamAllV2Messages() -> AsyncThrowingStream { - AsyncThrowingStream { continuation in - let streamManager = StreamManager() - - Task { - var topics: [String] = [ - Topic.userInvite(client.address).description, - Topic.userIntro(client.address).description - ] - - for conversation in try await list() { - topics.append(conversation.topic) - } - - var subscriptionRequest = FfiV2SubscribeRequest(contentTopics: topics) + ) + await ffiStreamActor.setFfiStream(stream) + } - let subscriptionCallback = V2SubscriptionCallback { envelope in - Task { - do { - if let conversation = self.conversationsByTopic[envelope.contentTopic] { - let decoded = try conversation.decode(envelope) - continuation.yield(decoded) - } else if envelope.contentTopic.hasPrefix("/xmtp/0/invite-") { - let conversation = try self.fromInvite(envelope: envelope) - await self.addConversation(conversation) - topics.append(conversation.topic) - subscriptionRequest = FfiV2SubscribeRequest(contentTopics: topics) - try await streamManager.updateStream(with: subscriptionRequest) - } else if envelope.contentTopic.hasPrefix("/xmtp/0/intro-") { - let conversation = try self.fromIntro(envelope: envelope) - await self.addConversation(conversation) - let decoded = try conversation.decode(envelope) - continuation.yield(decoded) - topics.append(conversation.topic) - subscriptionRequest = FfiV2SubscribeRequest(contentTopics: topics) - try await streamManager.updateStream(with: subscriptionRequest) - } else { - print("huh \(envelope)") - } - } catch { - continuation.finish(throwing: error) - } - } - } - let newStream = try await client.subscribe2(request: subscriptionRequest, callback: subscriptionCallback) - streamManager.setStream(newStream) - - continuation.onTermination = { @Sendable reason in - Task { - try await streamManager.endStream() - } + continuation.onTermination = { _ in + task.cancel() + Task { + await ffiStreamActor.endStream() } } } @@ -565,93 +529,33 @@ public actor Conversations { } } + private func findExistingConversation(with peerAddress: String, conversationID: String?) throws -> Conversation? { + return try conversationsByTopic.first(where: { try $0.value.peerAddress == peerAddress && + (($0.value.conversationID ?? "") == (conversationID ?? "")) + })?.value + } + public func fromWelcome(envelopeBytes: Data) async throws -> Group? { + guard let v3Client = client.v3Client else { + return nil + } + let group = try await v3Client.conversations().processStreamedWelcomeMessage(envelopeBytes: envelopeBytes) + return Group(ffiGroup: group, client: client) + } - - func streamAllV2DecryptedMessages() -> AsyncThrowingStream { - AsyncThrowingStream { continuation in - let streamManager = StreamManager() - - Task { - var topics: [String] = [ - Topic.userInvite(client.address).description, - Topic.userIntro(client.address).description - ] - - for conversation in try await list() { - topics.append(conversation.topic) - } - - var subscriptionRequest = FfiV2SubscribeRequest(contentTopics: topics) - - let subscriptionCallback = V2SubscriptionCallback { envelope in - Task { - do { - if let conversation = self.conversationsByTopic[envelope.contentTopic] { - let decrypted = try conversation.decrypt(envelope) - continuation.yield(decrypted) - } else if envelope.contentTopic.hasPrefix("/xmtp/0/invite-") { - let conversation = try self.fromInvite(envelope: envelope) - await self.addConversation(conversation) - topics.append(conversation.topic) - subscriptionRequest = FfiV2SubscribeRequest(contentTopics: topics) - try await streamManager.updateStream(with: subscriptionRequest) - } else if envelope.contentTopic.hasPrefix("/xmtp/0/intro-") { - let conversation = try self.fromIntro(envelope: envelope) - await self.addConversation(conversation) - let decrypted = try conversation.decrypt(envelope) - continuation.yield(decrypted) - topics.append(conversation.topic) - subscriptionRequest = FfiV2SubscribeRequest(contentTopics: topics) - try await streamManager.updateStream(with: subscriptionRequest) - } else { - print("huh \(envelope)") - } - } catch { - continuation.finish(throwing: error) - } - } - } - let newStream = try await client.subscribe2(request: subscriptionRequest, callback: subscriptionCallback) - streamManager.setStream(newStream) - - continuation.onTermination = { @Sendable reason in - Task { - try await streamManager.endStream() - } - } - } - } - } - - public func fromInvite(envelope: Envelope) throws -> Conversation { - let sealedInvitation = try SealedInvitation(serializedData: envelope.message) - let unsealed = try sealedInvitation.v1.getInvitation(viewer: client.keys) - return try .v2(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?) throws -> Conversation? { - return try conversationsByTopic.first(where: { try $0.value.peerAddress == peerAddress && - (($0.value.conversationID ?? "") == (conversationID ?? "")) - })?.value - } - - public func fromWelcome(envelopeBytes: Data) async throws -> Group? { - guard let v3Client = client.v3Client else { - return nil - } - let group = try await v3Client.conversations().processStreamedWelcomeMessage(envelopeBytes: envelopeBytes) - return Group(ffiGroup: group, client: client) - } + public func conversationFromWelcome(envelopeBytes: Data) async throws -> Conversation? { + guard let v3Client = client.v3Client else { + return nil + } + let conversation = try await v3Client.conversations().processStreamedWelcomeMessage(envelopeBytes: envelopeBytes) + return if (try conversation.groupMetadata().conversationType() == "dm") { + Conversation.dm(conversation.dmFromFFI(client: client)) + } else if (try conversation.groupMetadata().conversationType() == "group") { + Conversation.group(conversation.groupFromFFI(client: client)) + } else { + nil + } + } public func newConversation(with peerAddress: String, context: InvitationV1.Context? = nil, consentProofPayload: ConsentProofPayload? = nil) async throws -> Conversation { if peerAddress.lowercased() == client.address.lowercased() { @@ -742,13 +646,6 @@ public actor Conversations { } } - // ------- V1 V2 to be deprecated ------ - - private func makeConversation(from sealedInvitation: SealedInvitation) throws -> ConversationV2 { - let unsealed = try sealedInvitation.v1.getInvitation(viewer: client.keys) - return try ConversationV2.create(client: client, invitation: unsealed, header: sealedInvitation.v1.header) - } - private func validateConsentSignature(signature: String, clientAddress: String, peerAddress: String, timestamp: UInt64) -> Bool { // timestamp should be in the past if timestamp > UInt64(Date().timeIntervalSince1970 * 1000) { @@ -870,6 +767,232 @@ public actor Conversations { } return hmacKeysResponse } + + // ------- V1 V2 to be deprecated ------ + + /// 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, + createdAtNs: data.createdNs + )) + } + Task { + await self.addConversation(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 + guard let apiClient = client.apiClient else { + throw ClientError.noV2Client("Error no V2 client initialized") + } + for batch in batches { + messages += try await apiClient.batchQuery(request: batch) + .responses.flatMap { res in + res.envelopes.compactMap { envelope in + let conversation = conversationsByTopic[envelope.contentTopic] + if conversation == nil { + print("discarding message, unknown conversation \(envelope)") + return nil + } + do { + return try conversation?.decode(envelope) + } catch { + print("discarding message, unable to decode \(envelope)") + return nil + } + } + } + } + return messages + } + + public func listBatchDecryptedMessages(topics: [String: Pagination?]) async throws -> [DecryptedMessage] { + 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: [DecryptedMessage] = [] + // TODO: consider using a task group here for parallel batch calls + guard let apiClient = client.apiClient else { + throw ClientError.noV2Client("Error no V2 client initialized") + } + for batch in batches { + messages += try await apiClient.batchQuery(request: batch) + .responses.flatMap { res in + res.envelopes.compactMap { envelope in + let conversation = conversationsByTopic[envelope.contentTopic] + if conversation == nil { + print("discarding message, unknown conversation \(envelope)") + return nil + } + do { + return try conversation?.decrypt(envelope) + } catch { + print("discarding message, unable to decode \(envelope)") + return nil + } + } + } + } + return messages + } + + private func makeConversation(from sealedInvitation: SealedInvitation) throws -> ConversationV2 { + let unsealed = try sealedInvitation.v1.getInvitation(viewer: client.keys) + return try ConversationV2.create(client: client, invitation: unsealed, header: sealedInvitation.v1.header) + } + + func streamAllV2Messages() -> AsyncThrowingStream { + AsyncThrowingStream { continuation in + let streamManager = StreamManager() + + Task { + var topics: [String] = [ + Topic.userInvite(client.address).description, + Topic.userIntro(client.address).description + ] + + for conversation in try await list() { + topics.append(conversation.topic) + } + + var subscriptionRequest = FfiV2SubscribeRequest(contentTopics: topics) + + let subscriptionCallback = V2SubscriptionCallback { envelope in + Task { + do { + if let conversation = self.conversationsByTopic[envelope.contentTopic] { + let decoded = try conversation.decode(envelope) + continuation.yield(decoded) + } else if envelope.contentTopic.hasPrefix("/xmtp/0/invite-") { + let conversation = try self.fromInvite(envelope: envelope) + await self.addConversation(conversation) + topics.append(conversation.topic) + subscriptionRequest = FfiV2SubscribeRequest(contentTopics: topics) + try await streamManager.updateStream(with: subscriptionRequest) + } else if envelope.contentTopic.hasPrefix("/xmtp/0/intro-") { + let conversation = try self.fromIntro(envelope: envelope) + await self.addConversation(conversation) + let decoded = try conversation.decode(envelope) + continuation.yield(decoded) + topics.append(conversation.topic) + subscriptionRequest = FfiV2SubscribeRequest(contentTopics: topics) + try await streamManager.updateStream(with: subscriptionRequest) + } else { + print("huh \(envelope)") + } + } catch { + continuation.finish(throwing: error) + } + } + } + let newStream = try await client.subscribe2(request: subscriptionRequest, callback: subscriptionCallback) + streamManager.setStream(newStream) + + continuation.onTermination = { @Sendable reason in + Task { + try await streamManager.endStream() + } + } + } + } + } + + func streamAllV2DecryptedMessages() -> AsyncThrowingStream { + AsyncThrowingStream { continuation in + let streamManager = StreamManager() + + Task { + var topics: [String] = [ + Topic.userInvite(client.address).description, + Topic.userIntro(client.address).description + ] + + for conversation in try await list() { + topics.append(conversation.topic) + } + + var subscriptionRequest = FfiV2SubscribeRequest(contentTopics: topics) + + let subscriptionCallback = V2SubscriptionCallback { envelope in + Task { + do { + if let conversation = self.conversationsByTopic[envelope.contentTopic] { + let decrypted = try conversation.decrypt(envelope) + continuation.yield(decrypted) + } else if envelope.contentTopic.hasPrefix("/xmtp/0/invite-") { + let conversation = try self.fromInvite(envelope: envelope) + await self.addConversation(conversation) + topics.append(conversation.topic) + subscriptionRequest = FfiV2SubscribeRequest(contentTopics: topics) + try await streamManager.updateStream(with: subscriptionRequest) + } else if envelope.contentTopic.hasPrefix("/xmtp/0/intro-") { + let conversation = try self.fromIntro(envelope: envelope) + await self.addConversation(conversation) + let decrypted = try conversation.decrypt(envelope) + continuation.yield(decrypted) + topics.append(conversation.topic) + subscriptionRequest = FfiV2SubscribeRequest(contentTopics: topics) + try await streamManager.updateStream(with: subscriptionRequest) + } else { + print("huh \(envelope)") + } + } catch { + continuation.finish(throwing: error) + } + } + } + let newStream = try await client.subscribe2(request: subscriptionRequest, callback: subscriptionCallback) + streamManager.setStream(newStream) + + continuation.onTermination = { @Sendable reason in + Task { + try await streamManager.endStream() + } + } + } + } + } + + public func fromInvite(envelope: Envelope) throws -> Conversation { + let sealedInvitation = try SealedInvitation(serializedData: envelope.message) + let unsealed = try sealedInvitation.v1.getInvitation(viewer: client.keys) + return try .v2(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 listIntroductionPeers(pagination: Pagination?) async throws -> [String: Date] { guard let apiClient = client.apiClient else { diff --git a/Sources/XMTPiOS/Extensions/Ffi.swift b/Sources/XMTPiOS/Extensions/Ffi.swift index 60e6787b..1289ac22 100644 --- a/Sources/XMTPiOS/Extensions/Ffi.swift +++ b/Sources/XMTPiOS/Extensions/Ffi.swift @@ -196,9 +196,13 @@ extension FfiV2SubscribeRequest { // MARK: Group extension FfiConversation { - func fromFFI(client: Client) -> Group { + func groupFromFFI(client: Client) -> Group { Group(ffiGroup: self, client: client) } + + func dmFromFFI(client: Client) -> Dm { + Dm(ffiConversation: self, client: client) + } } extension FfiConversationMember { From e445e131f79a0e487e60c4f761b0092222384173 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Wed, 23 Oct 2024 17:31:59 -0700 Subject: [PATCH 35/45] get everything compiling --- Sources/XMTPiOS/Client.swift | 14 +-- Sources/XMTPiOS/Conversations.swift | 160 +++++++++++++++++++++++-- Sources/XMTPiOS/Extensions/Ffi.swift | 8 ++ Tests/XMTPTests/GroupTests.swift | 5 +- Tests/XMTPTests/IntegrationTests.swift | 2 +- 5 files changed, 163 insertions(+), 26 deletions(-) diff --git a/Sources/XMTPiOS/Client.swift b/Sources/XMTPiOS/Client.swift index 8a84ec04..78279175 100644 --- a/Sources/XMTPiOS/Client.swift +++ b/Sources/XMTPiOS/Client.swift @@ -699,13 +699,7 @@ public final class Client { throw ClientError.noV3Client("Error no V3 client initialized") } let conversation = try client.conversation(conversationId: conversationId.hexToData) - return if (try conversation.groupMetadata().conversationType() == "dm") { - Conversation.dm(conversation.dmFromFFI(client: self)) - } else if (try conversation.groupMetadata().conversationType() == "group") { - Conversation.group(conversation.groupFromFFI(client: self)) - } else { - nil - } + return try conversation.toConversation(client: self) } public func findConversationByTopic(topic: String) throws -> Conversation? { @@ -718,11 +712,7 @@ public final class Client { if let match = regex.firstMatch(in: topic, options: [], range: range) { let conversationId = (topic as NSString).substring(with: match.range(at: 1)) let conversation = try client.conversation(conversationId: conversationId.hexToData) - if (try conversation.groupMetadata().conversationType() == "dm") { - return Conversation.dm(Dm(ffiConversation: conversation, client: self)) - } else if (try conversation.groupMetadata().conversationType() == "group") { - return Conversation.group(Group(ffiGroup: conversation, client: self)) - } + return try conversation.toConversation(client: self) } } return nil diff --git a/Sources/XMTPiOS/Conversations.swift b/Sources/XMTPiOS/Conversations.swift index a4d457d8..19f7376b 100644 --- a/Sources/XMTPiOS/Conversations.swift +++ b/Sources/XMTPiOS/Conversations.swift @@ -50,7 +50,7 @@ public enum GroupError: Error, CustomStringConvertible, LocalizedError { } public enum ConversationOrder { - case created_at, last_message + case createdAt, lastMessage } final class ConversationStreamCallback: FfiConversationCallback { @@ -121,6 +121,13 @@ public actor Conversations { try await v3Client.conversations().sync() } + public func syncAllGroups() async throws -> UInt32 { + guard let v3Client = client.v3Client else { + return 0 + } + return try await v3Client.conversations().syncAllConversations() + } + public func syncAllConversations() async throws -> UInt32 { guard let v3Client = client.v3Client else { return 0 @@ -145,7 +152,30 @@ public actor Conversations { return try await v3Client.conversations().listGroups(opts: options).map { $0.groupFromFFI(client: client) } } - public func listConversations(createdAfter: Date? = nil, createdBefore: Date? = nil, limit: Int? = nil, order: ConversationOrder? = nil, consentState: ConsentState? = nil) async throws -> [Group] { + public func dms(createdAfter: Date? = nil, createdBefore: Date? = nil, limit: Int? = nil) async throws -> [Dm] { + if (client.hasV2Client) { + throw ConversationError.v2NotSupported("Only supported with V3 only clients use newConversation instead") + } + guard let v3Client = client.v3Client else { + return [] + } + var options = FfiListConversationsOptions(createdAfterNs: nil, createdBeforeNs: nil, limit: nil) + if let createdAfter { + options.createdAfterNs = Int64(createdAfter.millisecondsSinceEpoch) + } + if let createdBefore { + options.createdBeforeNs = Int64(createdBefore.millisecondsSinceEpoch) + } + if let limit { + options.limit = Int64(limit) + } + return try await v3Client.conversations().listDms(opts: options).map { $0.dmFromFFI(client: client) } + } + + public func listConversations(createdAfter: Date? = nil, createdBefore: Date? = nil, limit: Int? = nil, order: ConversationOrder = .createdAt, consentState: ConsentState? = nil) async throws -> [Conversation] { + if (client.hasV2Client) { + throw ConversationError.v2NotSupported("Only supported with V3 only clients use list instead") + } // Todo: add ability to order and consent state guard let v3Client = client.v3Client else { return [] @@ -160,7 +190,48 @@ public actor Conversations { if let limit { options.limit = Int64(limit) } - return try await v3Client.conversations().list(opts: options).map { $0.groupFromFFI(client: client) } + let ffiConversations = try await v3Client.conversations().list(opts: options) + + let filteredConversations = try filterByConsentState(ffiConversations, consentState: consentState) + let sortedConversations = try sortConversations(filteredConversations, order: order) + + return try sortedConversations.map { try $0.toConversation(client: client) } + } + + private func sortConversations( + _ conversations: [FfiConversation], + order: ConversationOrder + ) throws -> [FfiConversation] { + switch order { + case .lastMessage: + let conversationWithTimestamp: [(FfiConversation, Int64?)] = try conversations.map { conversation in + let message = try conversation.findMessages( + opts: FfiListMessagesOptions( + sentBeforeNs: nil, + sentAfterNs: nil, + limit: 1, + deliveryStatus: nil, + direction: .descending + ) + ).first + return (conversation, message?.sentAtNs) + } + + let sortedTuples = conversationWithTimestamp.sorted { (lhs, rhs) in + (lhs.1 ?? 0) > (rhs.1 ?? 0) + } + return sortedTuples.map { $0.0 } + case .createdAt: + return conversations + } + } + + private func filterByConsentState( + _ conversations: [FfiConversation], + consentState: ConsentState? + ) throws -> [FfiConversation] { + guard let state = consentState else { return conversations } + return try conversations.filter { try $0.consentState() == state.toFFI } } public func streamGroups() async throws -> AsyncThrowingStream { @@ -227,6 +298,10 @@ public actor Conversations { private func streamConversations() -> AsyncThrowingStream { AsyncThrowingStream { continuation in + if (client.hasV2Client) { + continuation.finish(throwing: ConversationError.v2NotSupported("Only supported with V3 only clients use stream instead")) + return + } let ffiStreamActor = FfiStreamActor() let task = Task { let stream = await self.client.v3Client?.conversations().stream( @@ -269,6 +344,10 @@ public actor Conversations { } public func findOrCreateDm(with peerAddress: String) async throws -> Dm { + if (client.hasV2Client) { + throw ConversationError.v2NotSupported("Only supported with V3 only clients use newConversation instead") + } + guard let v3Client = client.v3Client else { throw GroupError.alphaMLSNotEnabled } @@ -369,6 +448,10 @@ public actor Conversations { public func streamAllConversationMessages() -> AsyncThrowingStream { AsyncThrowingStream { continuation in + if (client.hasV2Client) { + continuation.finish(throwing: ConversationError.v2NotSupported("Only supported with V3 clients. Use streamAllMessages instead.")) + return + } let ffiStreamActor = FfiStreamActor() let task = Task { let stream = await self.client.v3Client?.conversations().streamAllMessages( @@ -398,6 +481,43 @@ public actor Conversations { } } } + + public func streamAllDecryptedConversationMessages() -> AsyncThrowingStream { + AsyncThrowingStream { continuation in + if (client.hasV2Client) { + continuation.finish(throwing: ConversationError.v2NotSupported("Only supported with V3 clients. Use streamAllMessages instead.")) + return + } + let ffiStreamActor = FfiStreamActor() + let task = Task { + let stream = await self.client.v3Client?.conversations().streamAllMessages( + messageCallback: MessageCallback(client: self.client) { message in + guard !Task.isCancelled else { + continuation.finish() + Task { + await ffiStreamActor.endStream() // End the stream upon cancellation + } + return + } + do { + continuation.yield(try MessageV3(client: self.client, ffiMessage: message).decrypt()) + } catch { + print("Error onMessage \(error)") + } + } + ) + await ffiStreamActor.setFfiStream(stream) + } + + continuation.onTermination = { _ in + task.cancel() + Task { + await ffiStreamActor.endStream() + } + } + } + } + public func streamAllGroupMessages() -> AsyncThrowingStream { AsyncThrowingStream { continuation in @@ -548,13 +668,7 @@ public actor Conversations { return nil } let conversation = try await v3Client.conversations().processStreamedWelcomeMessage(envelopeBytes: envelopeBytes) - return if (try conversation.groupMetadata().conversationType() == "dm") { - Conversation.dm(conversation.dmFromFFI(client: client)) - } else if (try conversation.groupMetadata().conversationType() == "group") { - Conversation.group(conversation.groupFromFFI(client: client)) - } else { - nil - } + return try conversation.toConversation(client: client) } public func newConversation(with peerAddress: String, context: InvitationV1.Context? = nil, consentProofPayload: ConsentProofPayload? = nil) async throws -> Conversation { @@ -772,7 +886,10 @@ public actor Conversations { /// Import a previously seen conversation. /// See Conversation.toTopicData() - public func importTopicData(data: Xmtp_KeystoreApi_V1_TopicMap.TopicData) -> Conversation { + public func importTopicData(data: Xmtp_KeystoreApi_V1_TopicMap.TopicData) throws -> Conversation { + if (!client.hasV2Client) { + throw ConversationError.v3NotSupported("importTopicData only supported with V2 clients") + } let conversation: Conversation if !data.hasInvitation { let sentAt = Date(timeIntervalSince1970: TimeInterval(data.createdNs / 1_000_000_000)) @@ -794,6 +911,9 @@ public actor Conversations { } public func listBatchMessages(topics: [String: Pagination?]) async throws -> [DecodedMessage] { + if (!client.hasV2Client) { + throw ConversationError.v3NotSupported("listBatchMessages only supported with V2 clients. Use listConversations order lastMessage") + } let requests = topics.map { topic, page in makeQueryRequest(topic: topic, pagination: page) } @@ -828,6 +948,9 @@ public actor Conversations { } public func listBatchDecryptedMessages(topics: [String: Pagination?]) async throws -> [DecryptedMessage] { + if (!client.hasV2Client) { + throw ConversationError.v3NotSupported("listBatchMessages only supported with V2 clients. Use listConversations order lastMessage") + } let requests = topics.map { topic, page in makeQueryRequest(topic: topic, pagination: page) } @@ -868,6 +991,10 @@ public actor Conversations { func streamAllV2Messages() -> AsyncThrowingStream { AsyncThrowingStream { continuation in + if (!client.hasV2Client) { + continuation.finish(throwing: ConversationError.v3NotSupported("Only supported with V2 clients. Use streamAllConversationMessages instead.")) + return + } let streamManager = StreamManager() Task { @@ -925,7 +1052,10 @@ public actor Conversations { func streamAllV2DecryptedMessages() -> AsyncThrowingStream { AsyncThrowingStream { continuation in let streamManager = StreamManager() - + if (!client.hasV2Client) { + continuation.finish(throwing: ConversationError.v3NotSupported("Only supported with V2 clients. Use streamAllDecryptedConversationMessages instead.")) + return + } Task { var topics: [String] = [ Topic.userInvite(client.address).description, @@ -979,12 +1109,18 @@ public actor Conversations { } public func fromInvite(envelope: Envelope) throws -> Conversation { + if (!client.hasV2Client) { + throw ConversationError.v3NotSupported("fromIntro only supported with V2 clients use fromWelcome instead") + } let sealedInvitation = try SealedInvitation(serializedData: envelope.message) let unsealed = try sealedInvitation.v1.getInvitation(viewer: client.keys) return try .v2(ConversationV2.create(client: client, invitation: unsealed, header: sealedInvitation.v1.header)) } public func fromIntro(envelope: Envelope) throws -> Conversation { + if (!client.hasV2Client) { + throw ConversationError.v3NotSupported("fromIntro only supported with V2 clients use fromWelcome instead") + } let messageV1 = try MessageV1.fromBytes(envelope.message) let senderAddress = try messageV1.header.sender.walletAddress let recipientAddress = try messageV1.header.recipient.walletAddress diff --git a/Sources/XMTPiOS/Extensions/Ffi.swift b/Sources/XMTPiOS/Extensions/Ffi.swift index 1289ac22..71212fc4 100644 --- a/Sources/XMTPiOS/Extensions/Ffi.swift +++ b/Sources/XMTPiOS/Extensions/Ffi.swift @@ -203,6 +203,14 @@ extension FfiConversation { func dmFromFFI(client: Client) -> Dm { Dm(ffiConversation: self, client: client) } + + func toConversation(client: Client) throws -> Conversation { + if (try groupMetadata().conversationType() == "dm") { + Conversation.dm(self.dmFromFFI(client: client)) + } else { + Conversation.group(self.groupFromFFI(client: client)) + } + } } extension FfiConversationMember { diff --git a/Tests/XMTPTests/GroupTests.swift b/Tests/XMTPTests/GroupTests.swift index ba43bdbd..cfdeb760 100644 --- a/Tests/XMTPTests/GroupTests.swift +++ b/Tests/XMTPTests/GroupTests.swift @@ -760,7 +760,10 @@ class GroupTests: XCTestCase { return case .group(let group): bobGroup = group - } + case .dm(_): + XCTFail("failed converting conversation to group") + return + } groupName = try bobGroup.groupName() XCTAssertEqual(groupName, "Start Name") diff --git a/Tests/XMTPTests/IntegrationTests.swift b/Tests/XMTPTests/IntegrationTests.swift index 8cccdd17..835e374a 100644 --- a/Tests/XMTPTests/IntegrationTests.swift +++ b/Tests/XMTPTests/IntegrationTests.swift @@ -154,7 +154,7 @@ final class IntegrationTests: XCTestCase { options: opt ) // And it uses the saved topic data for the conversation - let aliceConvo2 = await alice2.conversations.importTopicData( + let aliceConvo2 = try await alice2.conversations.importTopicData( data: try Xmtp_KeystoreApi_V1_TopicMap.TopicData(serializedData: topicData)) XCTAssertEqual("example.com/alice-bob-1", aliceConvo2.conversationID) From e8f2d8c87a57dedbc636ac9fc2d92cee15aa5a34 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Wed, 23 Oct 2024 18:20:32 -0700 Subject: [PATCH 36/45] write a bunch of dm tests --- Sources/XMTPiOS/Client.swift | 3 + Sources/XMTPiOS/Dm.swift | 7 +- Tests/XMTPTests/DmTests.swift | 205 ++++++++++++++++++++++++++++++++++ 3 files changed, 213 insertions(+), 2 deletions(-) create mode 100644 Tests/XMTPTests/DmTests.swift diff --git a/Sources/XMTPiOS/Client.swift b/Sources/XMTPiOS/Client.swift index 78279175..c2444019 100644 --- a/Sources/XMTPiOS/Client.swift +++ b/Sources/XMTPiOS/Client.swift @@ -15,6 +15,7 @@ public enum ClientError: Error, CustomStringConvertible, LocalizedError { case creationError(String) case noV3Client(String) case noV2Client(String) + case missingInboxId public var description: String { switch self { @@ -24,6 +25,8 @@ public enum ClientError: Error, CustomStringConvertible, LocalizedError { return "ClientError.noV3Client: \(err)" case .noV2Client(let err): return "ClientError.noV2Client: \(err)" + case .missingInboxId: + return "ClientError.missingInboxId" } } diff --git a/Sources/XMTPiOS/Dm.swift b/Sources/XMTPiOS/Dm.swift index 5ba2965c..d788710f 100644 --- a/Sources/XMTPiOS/Dm.swift +++ b/Sources/XMTPiOS/Dm.swift @@ -57,13 +57,16 @@ public struct Dm: Identifiable, Equatable, Hashable { } } - public var peerInboxIds: [String] { + public var peerInboxId: String { get async throws { var ids = try await members.map(\.inboxId) if let index = ids.firstIndex(of: client.inboxID) { ids.remove(at: index) } - return ids + guard let inboxId = ids.first else { + throw ClientError.missingInboxId + } + return inboxId } } diff --git a/Tests/XMTPTests/DmTests.swift b/Tests/XMTPTests/DmTests.swift new file mode 100644 index 00000000..443352b4 --- /dev/null +++ b/Tests/XMTPTests/DmTests.swift @@ -0,0 +1,205 @@ +// +// DmTests.swift +// XMTPiOS +// +// Created by Naomi Plasterer on 10/23/24. +// + +import CryptoKit +import XCTest +@testable import XMTPiOS +import LibXMTP +import XMTPTestHelpers + +@available(iOS 16, *) +class DmTests: XCTestCase { + struct LocalFixtures { + var alix: PrivateKey! + var bo: PrivateKey! + var caro: PrivateKey! + var alixClient: Client! + var boClient: Client! + var caroClient: Client! + } + + func localFixtures() async throws -> LocalFixtures { + let key = try Crypto.secureRandomBytes(count: 32) + let alix = try PrivateKey.generate() + let alixClient = try await Client.createV3( + account: alix, + options: .init( + api: .init(env: .local, isSecure: false), + codecs: [GroupUpdatedCodec()], + enableV3: true, + encryptionKey: key + ) + ) + let bo = try PrivateKey.generate() + let boClient = try await Client.createV3( + account: bo, + options: .init( + api: .init(env: .local, isSecure: false), + codecs: [GroupUpdatedCodec()], + enableV3: true, + encryptionKey: key + ) + ) + let caro = try PrivateKey.generate() + let caroClient = try await Client.createV3( + account: caro, + options: .init( + api: .init(env: .local, isSecure: false), + codecs: [GroupUpdatedCodec()], + enableV3: true, + encryptionKey: key + ) + ) + + return .init( + alix: alix, + bo: bo, + caro: caro, + alixClient: alixClient, + boClient: boClient, + caroClient: caroClient + ) + } + + func testCanCreateADm() async throws { + let fixtures = try await localFixtures() + + let convo1 = try await fixtures.boClient.conversations.findOrCreateDm(with: fixtures.alix.walletAddress) + try await fixtures.alixClient.conversations.sync() + let sameConvo1 = try await fixtures.alixClient.conversations.findOrCreateDm(with: fixtures.bo.walletAddress) + XCTAssertEqual(convo1.id, sameConvo1.id) + } + + func testCanListDmMembers() async throws { + let fixtures = try await localFixtures() + + let dm = try await fixtures.boClient.conversations.findOrCreateDm(with: fixtures.alix.walletAddress) + var members = try await dm.members + XCTAssertEqual(members.count, 2) + + let peer = try await dm.peerInboxId + XCTAssertEqual(peer, fixtures.alixClient.inboxID) + } + + func testCannotStartGroupWithSelf() async throws { + let fixtures = try await localFixtures() + + await assertThrowsAsyncError( + try await fixtures.alixClient.conversations.findOrCreateDm(with: fixtures.alix.address) + ) + } + + func testCannotStartGroupWithNonRegisteredIdentity() async throws { + let fixtures = try await localFixtures() + let nonRegistered = try PrivateKey.generate() + + await assertThrowsAsyncError( + try await fixtures.alixClient.conversations.findOrCreateDm(with: nonRegistered.address) + ) + } + + func testDmStartsWithAllowedState() async throws { + let fixtures = try await localFixtures() + + let dm = try await fixtures.boClient.conversations.findOrCreateDm(with: fixtures.alix.walletAddress) + _ = try await dm.send(content: "howdy") + _ = try await dm.send(content: "gm") + try await dm.sync() + + let isAllowed = try await fixtures.boClient.contacts.isGroupAllowed(groupId: dm.id) + let dmState = try await fixtures.boClient.contacts.consentList.groupState(groupId: dm.id) + XCTAssertTrue(isAllowed) + XCTAssertEqual(dmState, .allowed) + XCTAssertEqual(try dm.consentState(), .allowed) + } + + func testCanSendMessageToDm() async throws { + let fixtures = try await localFixtures() + + let dm = try await fixtures.boClient.conversations.findOrCreateDm(with: fixtures.alix.walletAddress) + _ = try await dm.send(content: "howdy") + let messageId = try await dm.send(content: "gm") + try await dm.sync() + + let firstMessage = try await dm.messages().first! + XCTAssertEqual(firstMessage.body, "gm") + XCTAssertEqual(firstMessage.id, messageId) + XCTAssertEqual(firstMessage.deliveryStatus, .published) + let messages = try await dm.messages() + XCTAssertEqual(messages.count, 3) + + try await fixtures.alixClient.conversations.sync() + let sameDm = try await fixtures.alixClient.conversations.dms().last! + try await sameDm.sync() + + let sameMessages = try await sameDm.messages() + XCTAssertEqual(sameMessages.count, 2) + XCTAssertEqual(sameMessages.first!.body, "gm") + } + + func testCanStreamDmMessages() async throws { + let fixtures = try await localFixtures() + + let dm = try await fixtures.boClient.conversations.findOrCreateDm(with: fixtures.alix.walletAddress) + try await fixtures.alixClient.conversations.sync() + + let expectation1 = XCTestExpectation(description: "got a message") + expectation1.expectedFulfillmentCount = 1 + + Task(priority: .userInitiated) { + for try await _ in dm.streamMessages() { + expectation1.fulfill() + } + } + + _ = try await dm.send(content: "hi") + + await fulfillment(of: [expectation1], timeout: 3) + } + + func testCanStreamAllDecryptedDmMessages() async throws { + let fixtures = try await localFixtures() + + let dm = try await fixtures.boClient.conversations.findOrCreateDm(with: fixtures.alix.walletAddress) + try await fixtures.alixClient.conversations.sync() + + let expectation1 = XCTestExpectation(description: "got a message") + expectation1.expectedFulfillmentCount = 2 + + Task(priority: .userInitiated) { + for try await _ in await fixtures.alixClient.conversations.streamAllConversationMessages() { + expectation1.fulfill() + } + } + + _ = try await dm.send(content: "hi") + let caroDm = try await fixtures.caroClient.conversations.findOrCreateDm(with: fixtures.alixClient.address) + _ = try await caroDm.send(content: "hi") + + await fulfillment(of: [expectation1], timeout: 3) + } + + func testDmConsent() async throws { + let fixtures = try await localFixtures() + + let dm = try await fixtures.boClient.conversations.findOrCreateDm(with: fixtures.alix.walletAddress) + + let isGroup = try await fixtures.boClient.contacts.isGroupAllowed(groupId: dm.id) + XCTAssertTrue(isGroup) + XCTAssertEqual(try dm.consentState(), .allowed) + + try await fixtures.boClient.contacts.denyGroups(groupIds: [dm.id]) + let isDenied = try await fixtures.boClient.contacts.isGroupDenied(groupId: dm.id) + XCTAssertTrue(isDenied) + XCTAssertEqual(try dm.consentState(), .denied) + + try await dm.updateConsentState(state: .allowed) + let isAllowed = try await fixtures.boClient.contacts.isGroupAllowed(groupId: dm.id) + XCTAssertTrue(isAllowed) + XCTAssertEqual(try dm.consentState(), .allowed) + } +} From b8be6d2e5817e4cda148483f9ed3bb8fee493d8d Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Wed, 23 Oct 2024 18:34:20 -0700 Subject: [PATCH 37/45] more tests --- Tests/XMTPTests/V3ClientTests.swift | 94 +++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/Tests/XMTPTests/V3ClientTests.swift b/Tests/XMTPTests/V3ClientTests.swift index 4b0015ff..49c203da 100644 --- a/Tests/XMTPTests/V3ClientTests.swift +++ b/Tests/XMTPTests/V3ClientTests.swift @@ -70,6 +70,100 @@ class V3ClientTests: XCTestCase { try await fixtures.boV3Client.conversations.newGroup(with: [fixtures.alixV2.address]) ) } + + func testCanCreateDm() async throws { + let fixtures = try await localFixtures() + + let dm = try await fixtures.boV3Client.conversations.findOrCreateDm(with: fixtures.caroV2V3.walletAddress) + let members = try await dm.members + XCTAssertEqual(members.count, 2) + + let sameDm = try await fixtures.boV3Client.findDm(address: fixtures.caroV2V3.walletAddress) + XCTAssertEqual(sameDm?.id, dm.id) + + try await fixtures.caroV2V3Client.conversations.sync() + let caroDm = try await fixtures.caroV2V3Client.findDm(address: fixtures.boV3Client.address) + XCTAssertEqual(caroDm?.id, dm.id) + + await assertThrowsAsyncError( + try await fixtures.boV3Client.conversations.findOrCreateDm(with: fixtures.alixV2.walletAddress) + ) + } + + func testCanFindConversationByTopic() async throws { + let fixtures = try await localFixtures() + + let group = try await fixtures.boV3Client.conversations.newGroup(with: [fixtures.caroV2V3.walletAddress]) + let dm = try await fixtures.boV3Client.conversations.findOrCreateDm(with: fixtures.caroV2V3.walletAddress) + + let sameDm = try fixtures.boV3Client.findConversationByTopic(topic: dm.topic) + let sameGroup = try fixtures.boV3Client.findConversationByTopic(topic: group.topic) + + XCTAssertEqual(group.id, try sameGroup?.id) + XCTAssertEqual(dm.id, try sameDm?.id) + } + + func testCanListConversations() async throws { + let fixtures = try await localFixtures() + + let dm = try await fixtures.boV3Client.conversations.findOrCreateDm(with: fixtures.caroV2V3.walletAddress) + let group = try await fixtures.boV3Client.conversations.newGroup(with: [fixtures.caroV2V3.walletAddress]) + + let convoCount = try await fixtures.boV3Client.conversations.listConversations().count + let dmCount = try await fixtures.boV3Client.conversations.dms().count + let groupCount = try await fixtures.boV3Client.conversations.groups().count + XCTAssertEqual(convoCount, 2) + XCTAssertEqual(dmCount, 1) + XCTAssertEqual(groupCount, 1) + + try await fixtures.caroV2V3Client.conversations.sync() + let convoCount2 = try await fixtures.caroV2V3Client.conversations.list(includeGroups: true).count + let groupCount2 = try await fixtures.caroV2V3Client.conversations.groups().count + XCTAssertEqual(convoCount2, 1) + XCTAssertEqual(groupCount2, 1) + } + + func testCanListConversationsFiltered() async throws { + let fixtures = try await localFixtures() + + let dm = try await fixtures.boV3Client.conversations.findOrCreateDm(with: fixtures.caroV2V3.walletAddress) + let group = try await fixtures.boV3Client.conversations.newGroup(with: [fixtures.caroV2V3.walletAddress]) + + let convoCount = try await fixtures.boV3Client.conversations.listConversations().count + let convoCountConsent = try await fixtures.boV3Client.conversations.listConversations(consentState: .allowed).count + + XCTAssertEqual(convoCount, 2) + XCTAssertEqual(convoCountConsent, 2) + + try await group.updateConsentState(state: .denied) + + let convoCountAllowed = try await fixtures.boV3Client.conversations.listConversations(consentState: .allowed).count + let convoCountDenied = try await fixtures.boV3Client.conversations.listConversations(consentState: .allowed).count + + XCTAssertEqual(convoCountAllowed, 1) + XCTAssertEqual(convoCountDenied, 1) + } + + func testCanListConversationsOrder() async throws { + let fixtures = try await localFixtures() + + let dm = try await fixtures.boV3Client.conversations.findOrCreateDm(with: fixtures.caroV2V3.walletAddress) + let group1 = try await fixtures.boV3Client.conversations.newGroup(with: [fixtures.caroV2V3.walletAddress]) + let group2 = try await fixtures.boV3Client.conversations.newGroup(with: [fixtures.caroV2V3.walletAddress]) + + _ = try await dm.send(content: "Howdy") + _ = try await group2.send(content: "Howdy") + _ = try await fixtures.boV3Client.conversations.syncAllConversations() + + let conversations = try await fixtures.boV3Client.conversations.listConversations() + let conversationsOrdered = try await fixtures.boV3Client.conversations.listConversations(order: .lastMessage) + + XCTAssertEqual(conversations.count, 3) + XCTAssertEqual(conversationsOrdered.count, 3) + + XCTAssertEqual(try conversations.map { try $0.id }, [dm.id, group1.id, group2.id]) + XCTAssertEqual(try conversationsOrdered.map { try $0.id }, [group2.id, dm.id, group1.id]) + } func testsCanSendMessages() async throws { let fixtures = try await localFixtures() From 585010d8392be40038656afa5eb54f2e3a2b5e4d Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Thu, 24 Oct 2024 12:02:46 -0700 Subject: [PATCH 38/45] beefing up all the tests --- Sources/XMTPiOS/Conversations.swift | 2 +- Tests/XMTPTests/GroupTests.swift | 58 +++++++++++++-------- Tests/XMTPTests/V3ClientTests.swift | 80 +++++++++++++++++++++++++++++ XMTP.podspec | 2 +- 4 files changed, 120 insertions(+), 22 deletions(-) diff --git a/Sources/XMTPiOS/Conversations.swift b/Sources/XMTPiOS/Conversations.swift index 19f7376b..bbca3a91 100644 --- a/Sources/XMTPiOS/Conversations.swift +++ b/Sources/XMTPiOS/Conversations.swift @@ -296,7 +296,7 @@ public actor Conversations { } } - private func streamConversations() -> AsyncThrowingStream { + public func streamConversations() -> AsyncThrowingStream { AsyncThrowingStream { continuation in if (client.hasV2Client) { continuation.finish(throwing: ConversationError.v2NotSupported("Only supported with V3 only clients use stream instead")) diff --git a/Tests/XMTPTests/GroupTests.swift b/Tests/XMTPTests/GroupTests.swift index cfdeb760..c23f2872 100644 --- a/Tests/XMTPTests/GroupTests.swift +++ b/Tests/XMTPTests/GroupTests.swift @@ -39,51 +39,52 @@ class GroupTests: XCTestCase { var alice: PrivateKey! var bob: PrivateKey! var fred: PrivateKey! + var davonV3: PrivateKey! var aliceClient: Client! var bobClient: Client! var fredClient: Client! + var davonV3Client: Client! } func localFixtures() async throws -> LocalFixtures { let key = try Crypto.secureRandomBytes(count: 32) + let options = ClientOptions.init( + api: .init(env: .local, isSecure: false), + codecs: [GroupUpdatedCodec()], + enableV3: true, + encryptionKey: key + ) let alice = try PrivateKey.generate() let aliceClient = try await Client.create( account: alice, - options: .init( - api: .init(env: .local, isSecure: false), - codecs: [GroupUpdatedCodec()], - enableV3: true, - encryptionKey: key - ) + options: options ) let bob = try PrivateKey.generate() let bobClient = try await Client.create( account: bob, - options: .init( - api: .init(env: .local, isSecure: false), - codecs: [GroupUpdatedCodec()], - enableV3: true, - encryptionKey: key - ) + options: options ) let fred = try PrivateKey.generate() let fredClient = try await Client.create( account: fred, - options: .init( - api: .init(env: .local, isSecure: false), - codecs: [GroupUpdatedCodec()], - enableV3: true, - encryptionKey: key - ) + options: options + ) + + let davonV3 = try PrivateKey.generate() + let davonV3Client = try await Client.createV3( + account: davonV3, + options: options ) return .init( alice: alice, bob: bob, fred: fred, + davonV3: davonV3, aliceClient: aliceClient, bobClient: bobClient, - fredClient: fredClient + fredClient: fredClient, + davonV3Client: davonV3Client ) } @@ -195,7 +196,10 @@ class GroupTests: XCTestCase { func testCanListGroups() async throws { let fixtures = try await localFixtures() _ = try await fixtures.aliceClient.conversations.newGroup(with: [fixtures.bob.address]) - + _ = try await fixtures.davonV3Client.conversations.findOrCreateDm(with: fixtures.bob.walletAddress) + _ = try await fixtures.davonV3Client.conversations.findOrCreateDm(with: fixtures.alice.walletAddress) + + try await fixtures.aliceClient.conversations.sync() let aliceGroupCount = try await fixtures.aliceClient.conversations.groups().count try await fixtures.bobClient.conversations.sync() @@ -209,6 +213,8 @@ class GroupTests: XCTestCase { let fixtures = try await localFixtures() _ = try await fixtures.aliceClient.conversations.newGroup(with: [fixtures.bob.address]) _ = try await fixtures.aliceClient.conversations.newConversation(with: fixtures.bob.address) + _ = try await fixtures.davonV3Client.conversations.findOrCreateDm(with: fixtures.bob.walletAddress) + _ = try await fixtures.davonV3Client.conversations.findOrCreateDm(with: fixtures.alice.walletAddress) let aliceGroupCount = try await fixtures.aliceClient.conversations.list(includeGroups: true).count @@ -557,6 +563,7 @@ class GroupTests: XCTestCase { } _ = try await fixtures.bobClient.conversations.newGroup(with: [fixtures.alice.address]) + _ = try await fixtures.davonV3Client.conversations.findOrCreateDm(with: fixtures.alice.address) await fulfillment(of: [expectation1], timeout: 3) } @@ -575,6 +582,7 @@ class GroupTests: XCTestCase { _ = try await fixtures.bobClient.conversations.newGroup(with: [fixtures.alice.address]) _ = try await fixtures.bobClient.conversations.newConversation(with: fixtures.alice.address) + _ = try await fixtures.davonV3Client.conversations.findOrCreateDm(with: fixtures.alice.address) await fulfillment(of: [expectation1], timeout: 3) } @@ -659,6 +667,8 @@ class GroupTests: XCTestCase { expectation1.expectedFulfillmentCount = 2 let convo = try await fixtures.bobClient.conversations.newConversation(with: fixtures.alice.address) let group = try await fixtures.bobClient.conversations.newGroup(with: [fixtures.alice.address]) + let dm = try await fixtures.davonV3Client.conversations.findOrCreateDm(with: fixtures.alice.address) + try await fixtures.aliceClient.conversations.sync() Task(priority: .userInitiated) { for try await _ in try await fixtures.aliceClient.conversations.streamAllMessages(includeGroups: true) { @@ -668,6 +678,7 @@ class GroupTests: XCTestCase { _ = try await group.send(content: "hi") _ = try await convo.send(content: "hi") + _ = try await dm.send(content: "hi") await fulfillment(of: [expectation1], timeout: 3) } @@ -680,6 +691,7 @@ class GroupTests: XCTestCase { expectation1.expectedFulfillmentCount = 2 let convo = try await fixtures.bobClient.conversations.newConversation(with: fixtures.alice.address) let group = try await fixtures.bobClient.conversations.newGroup(with: [fixtures.alice.address]) + let dm = try await fixtures.davonV3Client.conversations.findOrCreateDm(with: fixtures.alice.address) try await fixtures.aliceClient.conversations.sync() Task(priority: .userInitiated) { for try await _ in await fixtures.aliceClient.conversations.streamAllDecryptedMessages(includeGroups: true) { @@ -690,6 +702,7 @@ class GroupTests: XCTestCase { _ = try await group.send(content: "hi") _ = try await group.send(content: membershipChange, options: SendOptions(contentType: ContentTypeGroupUpdated)) _ = try await convo.send(content: "hi") + _ = try await dm.send(content: "hi") await fulfillment(of: [expectation1], timeout: 3) } @@ -700,6 +713,7 @@ class GroupTests: XCTestCase { let expectation1 = XCTestExpectation(description: "got a conversation") let group = try await fixtures.bobClient.conversations.newGroup(with: [fixtures.alice.address]) + let dm = try await fixtures.davonV3Client.conversations.findOrCreateDm(with: fixtures.alice.address) try await fixtures.aliceClient.conversations.sync() Task(priority: .userInitiated) { for try await _ in await fixtures.aliceClient.conversations.streamAllGroupMessages() { @@ -708,6 +722,7 @@ class GroupTests: XCTestCase { } _ = try await group.send(content: "hi") + _ = try await dm.send(content: "hi") await fulfillment(of: [expectation1], timeout: 3) } @@ -717,6 +732,8 @@ class GroupTests: XCTestCase { let expectation1 = XCTestExpectation(description: "got a conversation") let group = try await fixtures.bobClient.conversations.newGroup(with: [fixtures.alice.address]) + let dm = try await fixtures.davonV3Client.conversations.findOrCreateDm(with: fixtures.alice.address) + try await fixtures.aliceClient.conversations.sync() Task(priority: .userInitiated) { for try await _ in await fixtures.aliceClient.conversations.streamAllGroupDecryptedMessages() { @@ -725,6 +742,7 @@ class GroupTests: XCTestCase { } _ = try await group.send(content: "hi") + _ = try await dm.send(content: "hi") await fulfillment(of: [expectation1], timeout: 3) } diff --git a/Tests/XMTPTests/V3ClientTests.swift b/Tests/XMTPTests/V3ClientTests.swift index 49c203da..78bc22db 100644 --- a/Tests/XMTPTests/V3ClientTests.swift +++ b/Tests/XMTPTests/V3ClientTests.swift @@ -188,6 +188,29 @@ class V3ClientTests: XCTestCase { XCTAssertEqual(sameGroupMessages?.first?.body, "gm") } + func testsCanSendMessagesToDm() async throws { + let fixtures = try await localFixtures() + let dm = try await fixtures.boV3Client.conversations.findOrCreateDm(with: fixtures.caroV2V3.address) + try await dm.send(content: "howdy") + let messageId = try await dm.send(content: "gm") + try await dm.sync() + + let dmMessages = try await dm.messages() + XCTAssertEqual(dmMessages.first?.body, "gm") + XCTAssertEqual(dmMessages.first?.id, messageId) + XCTAssertEqual(dmMessages.first?.deliveryStatus, .published) + XCTAssertEqual(dmMessages.count, 3) + + + try await fixtures.caroV2V3Client.conversations.sync() + let sameDm = try await fixtures.caroV2V3Client.findDm(address: fixtures.boV3Client.address) + try await sameDm?.sync() + + let sameDmMessages = try await sameDm?.messages() + XCTAssertEqual(sameDmMessages?.count, 2) + XCTAssertEqual(sameDmMessages?.first?.body, "gm") + } + func testGroupConsent() async throws { let fixtures = try await localFixtures() let group = try await fixtures.boV3Client.conversations.newGroup(with: [fixtures.caroV2V3.address]) @@ -243,7 +266,64 @@ class V3ClientTests: XCTestCase { XCTAssert(isAddressAllowed) XCTAssert(!isAddressDenied) } + + func testCanStreamAllMessagesFromV3Users() async throws { + let fixtures = try await localFixtures() + + let expectation1 = XCTestExpectation(description: "got a conversation") + expectation1.expectedFulfillmentCount = 2 + let convo = try await fixtures.boV3Client.conversations.findOrCreateDm(with: fixtures.caroV2V3.address) + let group = try await fixtures.caroV2V3Client.conversations.newGroup(with: [fixtures.boV3.address]) + try await fixtures.caroV2V3Client.conversations.sync() + Task(priority: .userInitiated) { + for try await _ in await fixtures.boV3Client.conversations.streamAllConversationMessages() { + expectation1.fulfill() + } + } + + _ = try await group.send(content: "hi") + _ = try await convo.send(content: "hi") + + await fulfillment(of: [expectation1], timeout: 3) + } + + func testCanStreamAllDecryptedMessagesFromV3Users() async throws { + let fixtures = try await localFixtures() + + let expectation1 = XCTestExpectation(description: "got a conversation") + expectation1.expectedFulfillmentCount = 2 + let convo = try await fixtures.boV3Client.conversations.findOrCreateDm(with: fixtures.caroV2V3.address) + let group = try await fixtures.caroV2V3Client.conversations.newGroup(with: [fixtures.boV3.address]) + try await fixtures.boV3Client.conversations.sync() + Task(priority: .userInitiated) { + for try await _ in await fixtures.boV3Client.conversations.streamAllDecryptedConversationMessages() { + expectation1.fulfill() + } + } + + _ = try await group.send(content: "hi") + _ = try await convo.send(content: "hi") + + await fulfillment(of: [expectation1], timeout: 3) + } + + func testCanStreamGroupsAndConversationsFromV3Users() async throws { + let fixtures = try await localFixtures() + let expectation1 = XCTestExpectation(description: "got a conversation") + expectation1.expectedFulfillmentCount = 2 + + Task(priority: .userInitiated) { + for try await _ in await fixtures.boV3Client.conversations.streamConversations() { + expectation1.fulfill() + } + } + + _ = try await fixtures.caroV2V3Client.conversations.newGroup(with: [fixtures.boV3.address]) + _ = try await fixtures.boV3Client.conversations.findOrCreateDm(with: fixtures.caroV2V3.address) + + await fulfillment(of: [expectation1], timeout: 3) + } func testCanStreamAllMessagesFromV2andV3Users() async throws { let fixtures = try await localFixtures() diff --git a/XMTP.podspec b/XMTP.podspec index e3f0e969..505cf860 100644 --- a/XMTP.podspec +++ b/XMTP.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |spec| # spec.name = "XMTP" - spec.version = "0.15.2" + spec.version = "0.16.0" spec.summary = "XMTP SDK Cocoapod" # This description is used to generate tags and improve search results. From 4f84a16229a7b69492c90f9e5552780a364b55a9 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Thu, 24 Oct 2024 12:29:55 -0700 Subject: [PATCH 39/45] fix up some tests --- Sources/XMTPiOS/Client.swift | 41 +++++++++++++++++++---------- Sources/XMTPiOS/Conversations.swift | 14 ++++++++-- 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/Sources/XMTPiOS/Client.swift b/Sources/XMTPiOS/Client.swift index c2444019..19ba7106 100644 --- a/Sources/XMTPiOS/Client.swift +++ b/Sources/XMTPiOS/Client.swift @@ -200,7 +200,7 @@ public final class Client { } public static func createV3(account: SigningKey, options: ClientOptions) async throws -> Client { - let accountAddress = account.address + let accountAddress = account.address.lowercased() let inboxId = try await getOrCreateInboxId(options: options, address: accountAddress) return try await initializeClient( @@ -212,10 +212,11 @@ public final class Client { } public static func buildV3(address: String, options: ClientOptions) async throws -> Client { - let inboxId = try await getOrCreateInboxId(options: options, address: address) + let accountAddress = address.lowercased() + let inboxId = try await getOrCreateInboxId(options: options, address: accountAddress) return try await initializeClient( - accountAddress: address, + accountAddress: accountAddress, options: options, signingKey: nil, inboxId: inboxId @@ -701,22 +702,30 @@ public final class Client { guard let client = v3Client else { throw ClientError.noV3Client("Error no V3 client initialized") } - let conversation = try client.conversation(conversationId: conversationId.hexToData) - return try conversation.toConversation(client: self) + do { + let conversation = try client.conversation(conversationId: conversationId.hexToData) + return try conversation.toConversation(client: self) + } catch { + return nil + } } public func findConversationByTopic(topic: String) throws -> Conversation? { guard let client = v3Client else { throw ClientError.noV3Client("Error no V3 client initialized") } - let regexPattern = #"/xmtp/mls/1/g-(.*?)/proto"# - if let regex = try? NSRegularExpression(pattern: regexPattern) { - let range = NSRange(location: 0, length: topic.utf16.count) - if let match = regex.firstMatch(in: topic, options: [], range: range) { - let conversationId = (topic as NSString).substring(with: match.range(at: 1)) - let conversation = try client.conversation(conversationId: conversationId.hexToData) - return try conversation.toConversation(client: self) + do { + let regexPattern = #"/xmtp/mls/1/g-(.*?)/proto"# + if let regex = try? NSRegularExpression(pattern: regexPattern) { + let range = NSRange(location: 0, length: topic.utf16.count) + if let match = regex.firstMatch(in: topic, options: [], range: range) { + let conversationId = (topic as NSString).substring(with: match.range(at: 1)) + let conversation = try client.conversation(conversationId: conversationId.hexToData) + return try conversation.toConversation(client: self) + } } + } catch { + return nil } return nil } @@ -728,8 +737,12 @@ public final class Client { guard let inboxId = try await inboxIdFromAddress(address: address) else { throw ClientError.creationError("No inboxId present") } - let conversation = try client.dmConversation(targetInboxId: inboxId) - return Dm(ffiConversation: conversation, client: self) + do { + let conversation = try client.dmConversation(targetInboxId: inboxId) + return Dm(ffiConversation: conversation, client: self) + } catch { + return nil + } } public func findMessage(messageId: String) throws -> MessageV3? { diff --git a/Sources/XMTPiOS/Conversations.swift b/Sources/XMTPiOS/Conversations.swift index bbca3a91..f554b688 100644 --- a/Sources/XMTPiOS/Conversations.swift +++ b/Sources/XMTPiOS/Conversations.swift @@ -358,9 +358,19 @@ public actor Conversations { if !canMessage { throw ConversationError.recipientNotOnNetwork } - let dm = try await v3Client.conversations().createDm(accountAddress: peerAddress).dmFromFFI(client: client) + try await client.contacts.allow(addresses: [peerAddress]) - return dm + + if let existingDm = try await client.findDm(address: peerAddress) { + return existingDm + } + + let newDm = try await v3Client.conversations() + .createDm(accountAddress: peerAddress.lowercased()) + .dmFromFFI(client: client) + + try await client.contacts.allow(addresses: [peerAddress]) + return newDm } public func newGroup(with addresses: [String], From f0948bac437255b0f4036117357252b8f23e49e4 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Thu, 24 Oct 2024 12:32:51 -0700 Subject: [PATCH 40/45] Update Sources/XMTPiOS/Conversations.swift Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- Sources/XMTPiOS/Conversations.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/XMTPiOS/Conversations.swift b/Sources/XMTPiOS/Conversations.swift index f554b688..fb0e399b 100644 --- a/Sources/XMTPiOS/Conversations.swift +++ b/Sources/XMTPiOS/Conversations.swift @@ -128,7 +128,7 @@ public actor Conversations { return try await v3Client.conversations().syncAllConversations() } - public func syncAllConversations() async throws -> UInt32 { + public func syncAllConversations() async throws -> UInt32 { guard let v3Client = client.v3Client else { return 0 } From 307c5a4a23e8788e3c062d5c60fd69797e5e6143 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Thu, 24 Oct 2024 12:34:05 -0700 Subject: [PATCH 41/45] Update Tests/XMTPTests/V3ClientTests.swift Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- Tests/XMTPTests/V3ClientTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/XMTPTests/V3ClientTests.swift b/Tests/XMTPTests/V3ClientTests.swift index 78bc22db..a6a8110d 100644 --- a/Tests/XMTPTests/V3ClientTests.swift +++ b/Tests/XMTPTests/V3ClientTests.swift @@ -138,7 +138,7 @@ class V3ClientTests: XCTestCase { try await group.updateConsentState(state: .denied) let convoCountAllowed = try await fixtures.boV3Client.conversations.listConversations(consentState: .allowed).count - let convoCountDenied = try await fixtures.boV3Client.conversations.listConversations(consentState: .allowed).count + let convoCountDenied = try await fixtures.boV3Client.conversations.listConversations(consentState: .denied).count XCTAssertEqual(convoCountAllowed, 1) XCTAssertEqual(convoCountDenied, 1) From ae5adf6b7c14420436e6e75de1570e74f1b236ce Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Thu, 24 Oct 2024 12:35:40 -0700 Subject: [PATCH 42/45] Update Sources/XMTPiOS/Conversation.swift Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- Sources/XMTPiOS/Conversation.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/XMTPiOS/Conversation.swift b/Sources/XMTPiOS/Conversation.swift index b3a4f3b1..f58a409b 100644 --- a/Sources/XMTPiOS/Conversation.swift +++ b/Sources/XMTPiOS/Conversation.swift @@ -130,9 +130,9 @@ public enum Conversation: Sendable { case .v2(_): throw ConversationError.v2NotSupported("prepareMessageV3 use prepareMessage instead") case let .group(group): - try await group.prepareMessage(content: content, options: options) + return try await group.prepareMessage(content: content, options: options) case let .dm(dm): - try await dm.prepareMessage(content: content, options: options) + return try await dm.prepareMessage(content: content, options: options) } } From 1d00582d57515be9ea023e5c14ddc5ad8b6efdf1 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Thu, 24 Oct 2024 12:36:31 -0700 Subject: [PATCH 43/45] Update Tests/XMTPTests/GroupTests.swift Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- Tests/XMTPTests/GroupTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/XMTPTests/GroupTests.swift b/Tests/XMTPTests/GroupTests.swift index c23f2872..80945344 100644 --- a/Tests/XMTPTests/GroupTests.swift +++ b/Tests/XMTPTests/GroupTests.swift @@ -196,8 +196,8 @@ class GroupTests: XCTestCase { func testCanListGroups() async throws { let fixtures = try await localFixtures() _ = try await fixtures.aliceClient.conversations.newGroup(with: [fixtures.bob.address]) - _ = try await fixtures.davonV3Client.conversations.findOrCreateDm(with: fixtures.bob.walletAddress) - _ = try await fixtures.davonV3Client.conversations.findOrCreateDm(with: fixtures.alice.walletAddress) + _ = try await fixtures.davonV3Client.conversations.findOrCreateDm(with: fixtures.bob.address) + _ = try await fixtures.davonV3Client.conversations.findOrCreateDm(with: fixtures.alice.address) try await fixtures.aliceClient.conversations.sync() let aliceGroupCount = try await fixtures.aliceClient.conversations.groups().count From d1d70d6b63309a5e87811e488bba332ea1efb942 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Thu, 24 Oct 2024 13:21:17 -0700 Subject: [PATCH 44/45] add return statements --- Sources/XMTPiOS/Conversation.swift | 4 ++-- Sources/XMTPiOS/Extensions/Ffi.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/XMTPiOS/Conversation.swift b/Sources/XMTPiOS/Conversation.swift index f58a409b..0509b924 100644 --- a/Sources/XMTPiOS/Conversation.swift +++ b/Sources/XMTPiOS/Conversation.swift @@ -117,9 +117,9 @@ public enum Conversation: Sendable { case .v2(_): throw ConversationError.v2NotSupported("processMessage") case let .group(group): - try await group.processMessage(envelopeBytes: envelopeBytes) + return try await group.processMessage(envelopeBytes: envelopeBytes) case let .dm(dm): - try await dm.processMessage(envelopeBytes: envelopeBytes) + return try await dm.processMessage(envelopeBytes: envelopeBytes) } } diff --git a/Sources/XMTPiOS/Extensions/Ffi.swift b/Sources/XMTPiOS/Extensions/Ffi.swift index 71212fc4..9da6402c 100644 --- a/Sources/XMTPiOS/Extensions/Ffi.swift +++ b/Sources/XMTPiOS/Extensions/Ffi.swift @@ -206,9 +206,9 @@ extension FfiConversation { func toConversation(client: Client) throws -> Conversation { if (try groupMetadata().conversationType() == "dm") { - Conversation.dm(self.dmFromFFI(client: client)) + return Conversation.dm(self.dmFromFFI(client: client)) } else { - Conversation.group(self.groupFromFFI(client: client)) + return Conversation.group(self.groupFromFFI(client: client)) } } } From ca81bb60536ff16dd50283ed72466a373bd991f2 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Thu, 24 Oct 2024 13:25:09 -0700 Subject: [PATCH 45/45] get all the tests passing --- Tests/XMTPTests/V3ClientTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/XMTPTests/V3ClientTests.swift b/Tests/XMTPTests/V3ClientTests.swift index a6a8110d..97a3c333 100644 --- a/Tests/XMTPTests/V3ClientTests.swift +++ b/Tests/XMTPTests/V3ClientTests.swift @@ -274,7 +274,7 @@ class V3ClientTests: XCTestCase { expectation1.expectedFulfillmentCount = 2 let convo = try await fixtures.boV3Client.conversations.findOrCreateDm(with: fixtures.caroV2V3.address) let group = try await fixtures.caroV2V3Client.conversations.newGroup(with: [fixtures.boV3.address]) - try await fixtures.caroV2V3Client.conversations.sync() + try await fixtures.boV3Client.conversations.sync() Task(priority: .userInitiated) { for try await _ in await fixtures.boV3Client.conversations.streamAllConversationMessages() { expectation1.fulfill()