Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'np/v3-only-client' of https://github.com/xmtp/xmtp-ios
Browse files Browse the repository at this point in the history
…into np/consent-v3
nplasterer committed Sep 20, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
2 parents 5b5fbf0 + f909726 commit 56eafba
Showing 14 changed files with 423 additions and 168 deletions.
2 changes: 1 addition & 1 deletion Sources/XMTPTestHelpers/TestHelpers.swift
Original file line number Diff line number Diff line change
@@ -88,7 +88,7 @@ public struct Fixtures {

public func publishLegacyContact(client: Client) async throws {
var contactBundle = ContactBundle()
contactBundle.v1.keyBundle = client.privateKeyBundleV1.toPublicKeyBundle()
contactBundle.v1.keyBundle = try client.v1keys.toPublicKeyBundle()

var envelope = Envelope()
envelope.contentTopic = Topic.contact(client.address).description
113 changes: 92 additions & 21 deletions Sources/XMTPiOS/Client.swift
Original file line number Diff line number Diff line change
@@ -14,13 +14,16 @@ public typealias PreEventCallback = () async throws -> Void
public enum ClientError: Error, CustomStringConvertible, LocalizedError {
case creationError(String)
case noV3Client(String)
case noV2Client(String)

public var description: String {
switch self {
case .creationError(let err):
return "ClientError.creationError: \(err)"
case .noV3Client(let err):
return "ClientError.noV3Client: \(err)"
case .noV2Client(let err):
return "ClientError.noV2Client: \(err)"
}
}

@@ -111,13 +114,15 @@ public struct ClientOptions {
public final class Client {
/// The wallet address of the ``SigningKey`` used to create this Client.
public let address: String
let privateKeyBundleV1: PrivateKeyBundleV1
let apiClient: ApiClient
let v3Client: LibXMTP.FfiXmtpClient?
var privateKeyBundleV1: PrivateKeyBundleV1? = nil
var apiClient: ApiClient? = nil
public let v3Client: LibXMTP.FfiXmtpClient?
public let libXMTPVersion: String = getVersionInfo()
public let dbPath: String
public let installationID: String
public let inboxID: String
public var hasV2Client: Bool = true


/// Access ``Conversations`` for this Client.
public lazy var conversations: Conversations = .init(client: self)
@@ -126,9 +131,7 @@ public final class Client {
public lazy var contacts: Contacts = .init(client: self)

/// The XMTP environment which specifies which network this Client is connected to.
public var environment: XMTPEnvironment {
apiClient.environment
}
public lazy var environment: XMTPEnvironment = apiClient?.environment ?? .dev

var codecRegistry = CodecRegistry()

@@ -157,11 +160,44 @@ public final class Client {
throw ClientError.creationError(detailedErrorMessage)
}
}

// 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)

let (libxmtpClient, dbPath) = try await initV3Client(
accountAddress: account.address,
options: options,
privateKeyBundleV1: nil,
signingKey: account,
inboxId: inboxId
)
guard let v3Client = libxmtpClient else {
throw ClientError.noV3Client("Error no V3 client initialized")
}

let client = try Client(
address: account.address,
v3Client: v3Client,
dbPath: dbPath,
installationID: v3Client.installationId().toHex,
inboxID: v3Client.inboxId(),
environment: options.api.env
)

let conversations = client.conversations
let contacts = client.contacts

for codec in (options.codecs) {
client.register(codec: codec)
}
return client
}

static func initV3Client(
accountAddress: String,
options: ClientOptions?,
privateKeyBundleV1: PrivateKeyBundleV1,
privateKeyBundleV1: PrivateKeyBundleV1?,
signingKey: SigningKey?,
inboxId: String
) async throws -> (FfiXmtpClient?, String) {
@@ -202,7 +238,7 @@ public final class Client {
inboxId: inboxId,
accountAddress: address,
nonce: 0,
legacySignedPrivateKeyProto: try privateKeyBundleV1.toV2().identityKey.serializedData(),
legacySignedPrivateKeyProto: try privateKeyBundleV1?.toV2().identityKey.serializedData(),
historySyncUrl: options?.historySyncUrl
)

@@ -377,22 +413,45 @@ public final class Client {
self.dbPath = dbPath
self.installationID = installationID
self.inboxID = inboxID
self.hasV2Client = true
self.environment = apiClient.environment
}

init(address: String, v3Client: LibXMTP.FfiXmtpClient, dbPath: String, installationID: String, inboxID: String, environment: XMTPEnvironment) throws {
self.address = address
self.v3Client = v3Client
self.dbPath = dbPath
self.installationID = installationID
self.inboxID = inboxID
self.hasV2Client = false
self.environment = environment
}

public var privateKeyBundle: PrivateKeyBundle {
PrivateKeyBundle(v1: privateKeyBundleV1)
get throws {
try PrivateKeyBundle(v1: v1keys)
}
}

public var publicKeyBundle: SignedPublicKeyBundle {
privateKeyBundleV1.toV2().getPublicKeyBundle()
get throws {
try v1keys.toV2().getPublicKeyBundle()
}
}

public var v1keys: PrivateKeyBundleV1 {
privateKeyBundleV1
get throws {
guard let keys = privateKeyBundleV1 else {
throw ClientError.noV2Client("Error no V2 client initialized")
}
return keys
}
}

public var keys: PrivateKeyBundleV2 {
privateKeyBundleV1.toV2()
get throws {
try v1keys.toV2()
}
}

public func canMessage(_ peerAddress: String) async throws -> Bool {
@@ -472,7 +531,7 @@ public final class Client {
func ensureUserContactPublished() async throws {
if let contact = try await getUserContact(peerAddress: address),
case .v2 = contact.version,
keys.getPublicKeyBundle().equals(contact.v2.keyBundle)
try keys.getPublicKeyBundle().equals(contact.v2.keyBundle)
{
return
}
@@ -485,7 +544,7 @@ public final class Client {

if legacy {
var contactBundle = ContactBundle()
contactBundle.v1.keyBundle = privateKeyBundleV1.toPublicKeyBundle()
contactBundle.v1.keyBundle = try v1keys.toPublicKeyBundle()

var envelope = Envelope()
envelope.contentTopic = Topic.contact(address).description
@@ -496,7 +555,7 @@ public final class Client {
}

var contactBundle = ContactBundle()
contactBundle.v2.keyBundle = keys.getPublicKeyBundle()
contactBundle.v2.keyBundle = try keys.getPublicKeyBundle()
contactBundle.v2.keyBundle.identityKey.signature.ensureWalletSignature()

var envelope = Envelope()
@@ -509,23 +568,32 @@ public final class Client {
}

public func query(topic: Topic, pagination: Pagination? = nil) async throws -> QueryResponse {
return try await apiClient.query(
guard let client = apiClient else {
throw ClientError.noV2Client("Error no V2 client initialized")
}
return try await client.query(
topic: topic,
pagination: pagination
)
}

public func batchQuery(request: BatchQueryRequest) async throws -> BatchQueryResponse {
return try await apiClient.batchQuery(request: request)
guard let client = apiClient else {
throw ClientError.noV2Client("Error no V2 client initialized")
}
return try await client.batchQuery(request: request)
}

public func publish(envelopes: [Envelope]) async throws {
let authorized = AuthorizedIdentity(address: address, authorized: privateKeyBundleV1.identityKey.publicKey, identity: privateKeyBundleV1.identityKey)
guard let client = apiClient else {
throw ClientError.noV2Client("Error no V2 client initialized")
}
let authorized = try AuthorizedIdentity(address: address, authorized: v1keys.identityKey.publicKey, identity: v1keys.identityKey)
let authToken = try await authorized.createAuthToken()

apiClient.setAuthToken(authToken)
client.setAuthToken(authToken)

try await apiClient.publish(envelopes: envelopes)
try await client.publish(envelopes: envelopes)
}

public func subscribe(
@@ -539,7 +607,10 @@ public final class Client {
request: FfiV2SubscribeRequest,
callback: FfiV2SubscriptionCallback
) async throws -> FfiV2Subscription {
return try await apiClient.subscribe(request: request, callback: callback)
guard let client = apiClient else {
throw ClientError.noV2Client("Error no V2 client initialized")
}
return try await client.subscribe(request: request, callback: callback)
}

public func deleteLocalDatabase() throws {
193 changes: 99 additions & 94 deletions Sources/XMTPiOS/Contacts.swift
Original file line number Diff line number Diff line change
@@ -57,120 +57,125 @@ public actor EntriesManager {

public class ConsentList {
public let entriesManager = EntriesManager()
var publicKey: Data
var privateKey: Data
var identifier: String?
var lastFetched: Date?
var client: Client

init(client: Client) {
self.client = client
privateKey = client.privateKeyBundleV1.identityKey.secp256K1.bytes
publicKey = client.privateKeyBundleV1.identityKey.publicKey.secp256K1Uncompressed.bytes
identifier = try? LibXMTP.generatePrivatePreferencesTopicIdentifier(privateKey: privateKey)
}

func load() async throws -> [ConsentListEntry] {
guard let identifier = identifier else {
throw ContactError.invalidIdentifier
}
let newDate = Date()

let pagination = Pagination(
limit: 500,
after: lastFetched,
direction: .ascending
)
let envelopes = try await client.apiClient.envelopes(topic: Topic.preferenceList(identifier).description, pagination: pagination)
lastFetched = newDate

var preferences: [PrivatePreferencesAction] = []

for envelope in envelopes {
let payload = try LibXMTP.userPreferencesDecrypt(publicKey: publicKey, privateKey: privateKey, message: envelope.message)

try preferences.append(PrivatePreferencesAction(serializedData: Data(payload)))
}
for preference in preferences {
for address in preference.allowAddress.walletAddresses {
_ = await allow(address: address)
}

for address in preference.denyAddress.walletAddresses {
_ = await deny(address: address)
}

for groupId in preference.allowGroup.groupIds {
_ = await allowGroup(groupId: groupId)
}

for groupId in preference.denyGroup.groupIds {
_ = await denyGroup(groupId: groupId)
if (client.hasV2Client) {
let privateKey = try client.v1keys.identityKey.secp256K1.bytes
let publicKey = try client.v1keys.identityKey.publicKey.secp256K1Uncompressed.bytes
let identifier = try? LibXMTP.generatePrivatePreferencesTopicIdentifier(privateKey: privateKey)

guard let identifier = identifier else {
throw ContactError.invalidIdentifier
}
let newDate = Date()

let pagination = Pagination(
limit: 500,
after: lastFetched,
direction: .ascending
)
let envelopes = try await client.apiClient!.envelopes(topic: Topic.preferenceList(identifier).description, pagination: pagination)
lastFetched = newDate

for inboxId in preference.allowInboxID.inboxIds {
_ = await allowInboxId(inboxId: inboxId)
var preferences: [PrivatePreferencesAction] = []

for envelope in envelopes {
let payload = try LibXMTP.userPreferencesDecrypt(publicKey: publicKey, privateKey: privateKey, message: envelope.message)

try preferences.append(PrivatePreferencesAction(serializedData: Data(payload)))
}

for inboxId in preference.denyInboxID.inboxIds {
_ = await denyInboxId(inboxId: inboxId)
for preference in preferences {
for address in preference.allowAddress.walletAddresses {
_ = await allow(address: address)
}

for address in preference.denyAddress.walletAddresses {
_ = await deny(address: address)
}

for groupId in preference.allowGroup.groupIds {
_ = await allowGroup(groupId: groupId)
}

for groupId in preference.denyGroup.groupIds {
_ = await denyGroup(groupId: groupId)
}

for inboxId in preference.allowInboxID.inboxIds {
_ = await allowInboxId(inboxId: inboxId)
}

for inboxId in preference.denyInboxID.inboxIds {
_ = await denyInboxId(inboxId: inboxId)
}
}
}

return await Array(entriesManager.map.values)
}

func publish(entries: [ConsentListEntry]) async throws {
guard let identifier = identifier else {
throw ContactError.invalidIdentifier
}
var payload = PrivatePreferencesAction()

for entry in entries {
switch entry.entryType {
case .address:
switch entry.consentType {
case .allowed:
payload.allowAddress.walletAddresses.append(entry.value)
case .denied:
payload.denyAddress.walletAddresses.append(entry.value)
case .unknown:
payload.messageType = nil
}
case .group_id:
switch entry.consentType {
case .allowed:
payload.allowGroup.groupIds.append(entry.value)
case .denied:
payload.denyGroup.groupIds.append(entry.value)
case .unknown:
payload.messageType = nil
}
case .inbox_id:
switch entry.consentType {
case .allowed:
payload.allowInboxID.inboxIds.append(entry.value)
case .denied:
payload.denyInboxID.inboxIds.append(entry.value)
case .unknown:
payload.messageType = nil
if (client.hasV2Client) {
let privateKey = try client.v1keys.identityKey.secp256K1.bytes
let publicKey = try client.v1keys.identityKey.publicKey.secp256K1Uncompressed.bytes
let identifier = try? LibXMTP.generatePrivatePreferencesTopicIdentifier(privateKey: privateKey)
guard let identifier = identifier else {
throw ContactError.invalidIdentifier
}
var payload = PrivatePreferencesAction()

for entry in entries {
switch entry.entryType {
case .address:
switch entry.consentType {
case .allowed:
payload.allowAddress.walletAddresses.append(entry.value)
case .denied:
payload.denyAddress.walletAddresses.append(entry.value)
case .unknown:
payload.messageType = nil
}
case .group_id:
switch entry.consentType {
case .allowed:
payload.allowGroup.groupIds.append(entry.value)
case .denied:
payload.denyGroup.groupIds.append(entry.value)
case .unknown:
payload.messageType = nil
}
case .inbox_id:
switch entry.consentType {
case .allowed:
payload.allowInboxID.inboxIds.append(entry.value)
case .denied:
payload.denyInboxID.inboxIds.append(entry.value)
case .unknown:
payload.messageType = nil
}
}
}

let message = try LibXMTP.userPreferencesEncrypt(
publicKey: publicKey,
privateKey: privateKey,
message: payload.serializedData()
)

let envelope = Envelope(
topic: Topic.preferenceList(identifier),
timestamp: Date(),
message: Data(message)
)

try await client.publish(envelopes: [envelope])
}
}

let message = try LibXMTP.userPreferencesEncrypt(
publicKey: publicKey,
privateKey: privateKey,
message: payload.serializedData()
)

let envelope = Envelope(
topic: Topic.preferenceList(identifier),
timestamp: Date(),
message: Data(message)
)

try await client.publish(envelopes: [envelope])
}

func allow(address: String) async -> ConsentListEntry {
15 changes: 10 additions & 5 deletions Sources/XMTPiOS/ConversationV1.swift
Original file line number Diff line number Diff line change
@@ -52,7 +52,7 @@ public struct ConversationV1 {
let date = sentAt

let message = try MessageV1.encode(
sender: client.privateKeyBundleV1,
sender: client.v1keys,
recipient: recipient,
message: try encodedContent.serializedData(),
timestamp: date
@@ -217,8 +217,10 @@ public struct ConversationV1 {

func decryptedMessages(limit: Int? = nil, before: Date? = nil, after: Date? = nil, direction: PagingInfoSortDirection? = .descending) async throws -> [DecryptedMessage] {
let pagination = Pagination(limit: limit, before: before, after: after, direction: direction)

let envelopes = try await client.apiClient.envelopes(
guard let apiClient = client.apiClient else {
throw ClientError.noV2Client("Error no V2 client initialized")
}
let envelopes = try await apiClient.envelopes(
topic: Topic.directMessageV1(client.address, peerAddress).description,
pagination: pagination
)
@@ -229,7 +231,10 @@ public struct ConversationV1 {
func messages(limit: Int? = nil, before: Date? = nil, after: Date? = nil, direction: PagingInfoSortDirection? = .descending) async throws -> [DecodedMessage] {
let pagination = Pagination(limit: limit, before: before, after: after, direction: direction)

let envelopes = try await client.apiClient.envelopes(
guard let apiClient = client.apiClient else {
throw ClientError.noV2Client("Error no V2 client initialized")
}
let envelopes = try await apiClient.envelopes(
topic: Topic.directMessageV1(client.address, peerAddress).description,
pagination: pagination
)
@@ -246,7 +251,7 @@ public struct ConversationV1 {

func decrypt(envelope: Envelope) throws -> DecryptedMessage {
let message = try Message(serializedData: envelope.message)
let decrypted = try message.v1.decrypt(with: client.privateKeyBundleV1)
let decrypted = try message.v1.decrypt(with: client.v1keys)

let encodedMessage = try EncodedContent(serializedData: decrypted)
let header = try message.v1.header
12 changes: 9 additions & 3 deletions Sources/XMTPiOS/ConversationV2.swift
Original file line number Diff line number Diff line change
@@ -37,7 +37,7 @@ public struct ConversationV2 {
private var header: SealedInvitationHeaderV1

static func create(client: Client, invitation: InvitationV1, header: SealedInvitationHeaderV1) throws -> ConversationV2 {
let myKeys = client.keys.getPublicKeyBundle()
let myKeys = try client.keys.getPublicKeyBundle()

let peer = try myKeys.walletAddress == (try header.sender.walletAddress) ? header.recipient : header.sender
let peerAddress = try peer.walletAddress
@@ -133,7 +133,10 @@ public struct ConversationV2 {

func messages(limit: Int? = nil, before: Date? = nil, after: Date? = nil, direction: PagingInfoSortDirection? = .descending) async throws -> [DecodedMessage] {
let pagination = Pagination(limit: limit, before: before, after: after, direction: direction)
let envelopes = try await client.apiClient.envelopes(topic: topic.description, pagination: pagination)
guard let apiClient = client.apiClient else {
throw ClientError.noV2Client("Error no V2 client initialized")
}
let envelopes = try await apiClient.envelopes(topic: topic.description, pagination: pagination)

return envelopes.compactMap { envelope in
do {
@@ -146,8 +149,11 @@ public struct ConversationV2 {
}

func decryptedMessages(limit: Int? = nil, before: Date? = nil, after: Date? = nil, direction: PagingInfoSortDirection? = .descending) async throws -> [DecryptedMessage] {
guard let apiClient = client.apiClient else {
throw ClientError.noV2Client("Error no V2 client initialized")
}
let pagination = Pagination(limit: limit, before: before, after: after, direction: direction)
let envelopes = try await client.apiClient.envelopes(topic: topic.description, pagination: pagination)
let envelopes = try await apiClient.envelopes(topic: topic.description, pagination: pagination)

return try envelopes.map { envelope in
try decrypt(envelope: envelope)
22 changes: 17 additions & 5 deletions Sources/XMTPiOS/Conversations.swift
Original file line number Diff line number Diff line change
@@ -296,8 +296,11 @@ public actor Conversations {
.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 client.apiClient.batchQuery(request: batch)
messages += try await apiClient.batchQuery(request: batch)
.responses.flatMap { res in
res.envelopes.compactMap { envelope in
let conversation = conversationsByTopic[envelope.contentTopic]
@@ -327,8 +330,11 @@ public actor Conversations {
.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 client.apiClient.batchQuery(request: batch)
messages += try await apiClient.batchQuery(request: batch)
.responses.flatMap { res in
res.envelopes.compactMap { envelope in
let conversation = conversationsByTopic[envelope.contentTopic]
@@ -833,15 +839,18 @@ public actor Conversations {
}

private func listIntroductionPeers(pagination: Pagination?) async throws -> [String: Date] {
let envelopes = try await client.apiClient.query(
guard let apiClient = client.apiClient else {
throw ClientError.noV2Client("Error no V2 client initialized")
}
let envelopes = try await apiClient.query(
topic: .userIntro(client.address),
pagination: pagination
).envelopes
let messages = envelopes.compactMap { envelope in
do {
let message = try MessageV1.fromBytes(envelope.message)
// Attempt to decrypt, just to make sure we can
_ = try message.decrypt(with: client.privateKeyBundleV1)
_ = try message.decrypt(with: client.v1keys)
return message
} catch {
return nil
@@ -867,7 +876,10 @@ public actor Conversations {
}

private func listInvitations(pagination: Pagination?) async throws -> [SealedInvitation] {
var envelopes = try await client.apiClient.envelopes(
guard let apiClient = client.apiClient else {
throw ClientError.noV2Client("Error no V2 client initialized")
}
var envelopes = try await apiClient.envelopes(
topic: Topic.userInvite(client.address).description,
pagination: pagination
)
4 changes: 2 additions & 2 deletions Sources/XMTPiOS/Frames/FramesClient.swift
Original file line number Diff line number Diff line change
@@ -62,14 +62,14 @@ public class FramesClient {
}

private func signDigest(digest: Data) async throws -> Signature {
let key = self.xmtpClient.keys.identityKey
let key = try self.xmtpClient.keys.identityKey
let privateKey = try PrivateKey(key)
let signature = try await privateKey.sign(Data(digest))
return signature
}

private func getPublicKeyBundle() async throws -> PublicKeyBundle {
let bundleBytes = self.xmtpClient.publicKeyBundle;
let bundleBytes = try self.xmtpClient.publicKeyBundle;
return try PublicKeyBundle(bundleBytes);
}

4 changes: 2 additions & 2 deletions Sources/XMTPiOS/Messages/MessageV2.swift
Original file line number Diff line number Diff line change
@@ -88,10 +88,10 @@ extension MessageV2 {
let headerBytes = try header.serializedData()

let digest = SHA256.hash(data: headerBytes + payload)
let preKey = client.keys.preKeys[0]
let preKey = try client.keys.preKeys[0]
let signature = try await preKey.sign(Data(digest))

let bundle = client.privateKeyBundleV1.toV2().getPublicKeyBundle()
let bundle = try client.v1keys.toV2().getPublicKeyBundle()

let signedContent = SignedContent(payload: payload, sender: bundle, signature: signature)
let signedBytes = try signedContent.serializedData()
69 changes: 45 additions & 24 deletions Tests/XMTPTests/ClientTests.swift
Original file line number Diff line number Diff line change
@@ -15,9 +15,9 @@ import XMTPTestHelpers
@available(iOS 15, *)
class ClientTests: XCTestCase {
func testTakesAWallet() async throws {
try TestConfig.skip(because: "run manually against dev")
let opts = ClientOptions(api: ClientOptions.Api(env: .local, isSecure: false))
let fakeWallet = try PrivateKey.generate()
_ = try await Client.create(account: fakeWallet)
_ = try await Client.create(account: fakeWallet, options: opts)
}

func testPassingSavedKeysWithNoSignerWithMLSErrors() async throws {
@@ -50,7 +50,7 @@ class ClientTests: XCTestCase {
)
)

let keys = client.privateKeyBundle
let keys = try client.privateKeyBundle
let otherClient = try await Client.from(
bundle: keys,
options: .init(
@@ -204,57 +204,59 @@ class ClientTests: XCTestCase {
let fakeWallet = try PrivateKey.generate()
let client = try await Client.create(account: fakeWallet, options: opts)

XCTAssertEqual(1, client.privateKeyBundleV1.preKeys.count)
XCTAssertEqual(1, try client.v1keys.preKeys.count)

let preKey = client.privateKeyBundleV1.preKeys[0]
let preKey = try client.v1keys.preKeys[0]

XCTAssert(preKey.publicKey.hasSignature, "prekey not signed")
}

func testCanBeCreatedWithBundle() async throws {
try TestConfig.skip(because: "run manually against dev")
let opts = ClientOptions(api: ClientOptions.Api(env: .local, isSecure: false))
let fakeWallet = try PrivateKey.generate()
let client = try await Client.create(account: fakeWallet)
let client = try await Client.create(account: fakeWallet, options: opts)

let bundle = client.privateKeyBundle
let clientFromV1Bundle = try await Client.from(bundle: bundle)
let bundle = try client.privateKeyBundle
let clientFromV1Bundle = try await Client.from(bundle: bundle, options: opts)

XCTAssertEqual(client.address, clientFromV1Bundle.address)
XCTAssertEqual(client.privateKeyBundleV1.identityKey, clientFromV1Bundle.privateKeyBundleV1.identityKey)
XCTAssertEqual(client.privateKeyBundleV1.preKeys, clientFromV1Bundle.privateKeyBundleV1.preKeys)
XCTAssertEqual(try client.v1keys.identityKey, try clientFromV1Bundle.v1keys.identityKey)
XCTAssertEqual(try client.v1keys.preKeys, try clientFromV1Bundle.v1keys.preKeys)
}

func testCanBeCreatedWithV1Bundle() async throws {
try TestConfig.skip(because: "run manually against dev")
let opts = ClientOptions(api: ClientOptions.Api(env: .local, isSecure: false))
let fakeWallet = try PrivateKey.generate()
let client = try await Client.create(account: fakeWallet)
let client = try await Client.create(account: fakeWallet, options: opts)

let bundleV1 = client.v1keys
let clientFromV1Bundle = try await Client.from(v1Bundle: bundleV1)
let bundleV1 = try client.v1keys
let clientFromV1Bundle = try await Client.from(v1Bundle: bundleV1, options: opts)

XCTAssertEqual(client.address, clientFromV1Bundle.address)
XCTAssertEqual(client.privateKeyBundleV1.identityKey, clientFromV1Bundle.privateKeyBundleV1.identityKey)
XCTAssertEqual(client.privateKeyBundleV1.preKeys, clientFromV1Bundle.privateKeyBundleV1.preKeys)
XCTAssertEqual(try client.v1keys.identityKey, try clientFromV1Bundle.v1keys.identityKey)
XCTAssertEqual(try client.v1keys.preKeys, try clientFromV1Bundle.v1keys.preKeys)
}

func testCanAccessPublicKeyBundle() async throws {
let opts = ClientOptions(api: ClientOptions.Api(env: .local, isSecure: false))
let fakeWallet = try PrivateKey.generate()
let client = try await Client.create(account: fakeWallet)
let client = try await Client.create(account: fakeWallet, options: opts)

let publicKeyBundle = client.keys.getPublicKeyBundle()
XCTAssertEqual(publicKeyBundle, client.publicKeyBundle)
let publicKeyBundle = try client.keys.getPublicKeyBundle()
XCTAssertEqual(publicKeyBundle, try client.publicKeyBundle)
}

func testCanSignWithPrivateIdentityKey() async throws {
let opts = ClientOptions(api: ClientOptions.Api(env: .local, isSecure: false))
let fakeWallet = try PrivateKey.generate()
let client = try await Client.create(account: fakeWallet)
let client = try await Client.create(account: fakeWallet, options: opts)

let digest = Util.keccak256(Data("hello world".utf8))
let signature = try await client.keys.identityKey.sign(digest)

let recovered = try KeyUtilx.recoverPublicKeyKeccak256(from: signature.rawData, message: Data("hello world".utf8))

XCTAssertEqual(recovered, client.keys.identityKey.publicKey.secp256K1Uncompressed.bytes)
let bytes = try client.keys.identityKey.publicKey.secp256K1Uncompressed.bytes
XCTAssertEqual(recovered, bytes)
}

func testPreEnableIdentityCallback() async throws {
@@ -331,7 +333,7 @@ class ClientTests: XCTestCase {
)
)

let keys = client.privateKeyBundle
let keys = try client.privateKeyBundle
let bundleClient = try await Client.from(
bundle: keys,
options: .init(
@@ -456,6 +458,25 @@ class ClientTests: XCTestCase {
XCTAssertEqual(inboxId, alixClient.inboxID)
}

func testCreatesAPureV3Client() async throws {
let key = try Crypto.secureRandomBytes(count: 32)
let alix = try PrivateKey.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.createOrBuild(
account: alix,
options: options
)

XCTAssertEqual(inboxId, alixClient.inboxID)
}

func testRevokesAllOtherInstallations() async throws {
let key = try Crypto.secureRandomBytes(count: 32)
let alix = try PrivateKey.generate()
10 changes: 5 additions & 5 deletions Tests/XMTPTests/ConversationTests.swift
Original file line number Diff line number Diff line change
@@ -97,7 +97,7 @@ class ConversationTests: XCTestCase {
}

func testCanStreamConversationsV2() async throws {
let options = ClientOptions(api: ClientOptions.Api(env: .dev, isSecure: true))
let options = ClientOptions(api: ClientOptions.Api(env: .local, isSecure: false))
let wallet = try PrivateKey.generate()
let client = try await Client.create(account: wallet, options: options)

@@ -145,7 +145,7 @@ class ConversationTests: XCTestCase {

func publishLegacyContact(client: Client) async throws {
var contactBundle = ContactBundle()
contactBundle.v1.keyBundle = client.privateKeyBundleV1.toPublicKeyBundle()
contactBundle.v1.keyBundle = try client.v1keys.toPublicKeyBundle()

var envelope = Envelope()
envelope.contentTopic = Topic.contact(client.address).description
@@ -215,10 +215,10 @@ class ConversationTests: XCTestCase {
let headerBytes = try header.serializedData()

let digest = SHA256.hash(data: headerBytes + tamperedPayload)
let preKey = aliceClient.keys.preKeys[0]
let preKey = try aliceClient.keys.preKeys[0]
let signature = try await preKey.sign(Data(digest))

let bundle = aliceClient.privateKeyBundleV1.toV2().getPublicKeyBundle()
let bundle = try aliceClient.v1keys.toV2().getPublicKeyBundle()

let signedContent = SignedContent(payload: originalPayload, sender: bundle, signature: signature)
let signedBytes = try signedContent.serializedData()
@@ -375,7 +375,7 @@ class ConversationTests: XCTestCase {
try await bobConversation.send(content: "Hello")

// Now we send some garbage and expect it to be properly ignored.
try await bobClient.apiClient.publish(envelopes: [
try await bobClient.apiClient!.publish(envelopes: [
Envelope(
topic: bobConversation.topic,
timestamp: Date(),
2 changes: 1 addition & 1 deletion Tests/XMTPTests/ConversationsTest.swift
Original file line number Diff line number Diff line change
@@ -21,7 +21,7 @@ class ConversationsTests: XCTestCase {
let created = Date()

let message = try MessageV1.encode(
sender: fixtures.bobClient.privateKeyBundleV1,
sender: try fixtures.bobClient.v1keys,
recipient: fixtures.aliceClient.v1keys.toPublicKeyBundle(),
message: try TextCodec().encode(content: "hello", client: client).serializedData(),
timestamp: created
8 changes: 4 additions & 4 deletions Tests/XMTPTests/IntegrationTests.swift
Original file line number Diff line number Diff line change
@@ -52,7 +52,7 @@ final class IntegrationTests: XCTestCase {
try await delayToPropagate()
let contact = try await alice.getUserContact(peerAddress: alice.address)

XCTAssertEqual(contact!.v2.keyBundle.identityKey.secp256K1Uncompressed, alice.privateKeyBundleV1.identityKey.publicKey.secp256K1Uncompressed)
XCTAssertEqual(contact!.v2.keyBundle.identityKey.secp256K1Uncompressed, try alice.v1keys.identityKey.publicKey.secp256K1Uncompressed)
XCTAssert(contact!.v2.keyBundle.identityKey.hasSignature == true, "no signature")
XCTAssert(contact!.v2.keyBundle.preKey.hasSignature == true, "pre key not signed")

@@ -474,7 +474,7 @@ final class IntegrationTests: XCTestCase {
key.publicKey.secp256K1Uncompressed.bytes = Data(try LibXMTP.publicKeyFromPrivateKeyK256(privateKeyBytes: keyBytes))

let client = try await XMTPiOS.Client.create(account: key)
XCTAssertEqual(client.apiClient.environment, .dev)
XCTAssertEqual(client.environment, .dev)

let conversations = try await client.conversations.list()
XCTAssertEqual(1, conversations.count)
@@ -542,7 +542,7 @@ final class IntegrationTests: XCTestCase {
key.publicKey.secp256K1Uncompressed.bytes = Data(try LibXMTP.publicKeyFromPrivateKeyK256(privateKeyBytes: keyBytes))

let client = try await XMTPiOS.Client.create(account: key)
XCTAssertEqual(client.apiClient.environment, .dev)
XCTAssertEqual(client.environment, .dev)

let convo = try await client.conversations.list()[0]
let message = try await convo.messages()[0]
@@ -565,7 +565,7 @@ final class IntegrationTests: XCTestCase {
key.publicKey.secp256K1Uncompressed.bytes = Data(try LibXMTP.publicKeyFromPrivateKeyK256(privateKeyBytes: keyBytes))

let client = try await XMTPiOS.Client.create(account: key)
XCTAssertEqual(client.apiClient.environment, .dev)
XCTAssertEqual(client.environment, .dev)

let convo = try await client.conversations.list()[0]
let message = try await convo.messages().last!
135 changes: 135 additions & 0 deletions Tests/XMTPTests/V3ClientTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
//
// File.swift
//
//
// Created by Naomi Plasterer on 9/19/24.
//

import CryptoKit
import XCTest
@testable import XMTPiOS
import LibXMTP
import XMTPTestHelpers

@available(iOS 16, *)
class V3ClientTests: XCTestCase {
// Use these fixtures to talk to the local node
struct LocalFixtures {
var alixV2: PrivateKey!
var boV3: PrivateKey!
var caroV2V3: PrivateKey!
var alixV2Client: Client!
var boV3Client: Client!
var caroV2V3Client: Client!
}

func localFixtures() async throws -> LocalFixtures {
let key = try Crypto.secureRandomBytes(count: 32)
let alixV2 = try PrivateKey.generate()
let alixV2Client = try await Client.create(
account: alixV2,
options: .init(
api: .init(env: .local, isSecure: false)
)
)
let boV3 = try PrivateKey.generate()
let boV3Client = try await Client.createOrBuild(
account: boV3,
options: .init(
api: .init(env: .local, isSecure: false),
enableV3: true,
encryptionKey: key
)
)
let caroV2V3 = try PrivateKey.generate()
let caroV2V3Client = try await Client.create(
account: caroV2V3,
options: .init(
api: .init(env: .local, isSecure: false),
enableV3: true,
encryptionKey: key
)
)

return .init(
alixV2: alixV2,
boV3: boV3,
caroV2V3: caroV2V3,
alixV2Client: alixV2Client,
boV3Client: boV3Client,
caroV2V3Client: caroV2V3Client
)
}

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()
XCTAssertEqual([fixtures.caroV2V3Client.inboxID, fixtures.boV3Client.inboxID].sorted(), members)

await assertThrowsAsyncError(
try await fixtures.boV3Client.conversations.newGroup(with: [fixtures.alixV2.address])
)
}

func testsCanSendMessages() async throws {
let fixtures = try await localFixtures()
let group = try await fixtures.boV3Client.conversations.newGroup(with: [fixtures.caroV2V3.address])
try await group.send(content: "howdy")
let messageId = try await group.send(content: "gm")
try await group.sync()

let groupMessages = try await group.messages()
XCTAssertEqual(groupMessages.first?.body, "gm")
XCTAssertEqual(groupMessages.first?.id, messageId)
XCTAssertEqual(groupMessages.first?.deliveryStatus, .published)
XCTAssertEqual(groupMessages.count, 3)


try await fixtures.caroV2V3Client.conversations.sync()
let sameGroup = try await fixtures.caroV2V3Client.conversations.groups().last
try await sameGroup?.sync()

let sameGroupMessages = try await sameGroup?.messages()
XCTAssertEqual(sameGroupMessages?.count, 2)
XCTAssertEqual(sameGroupMessages?.first?.body, "gm")
}

func testCanStreamAllMessagesFromV2andV3Users() async throws {
let fixtures = try await localFixtures()

let expectation1 = XCTestExpectation(description: "got a conversation")
expectation1.expectedFulfillmentCount = 2
let convo = try await fixtures.alixV2Client.conversations.newConversation(with: fixtures.caroV2V3.address)
let group = try await fixtures.boV3Client.conversations.newGroup(with: [fixtures.caroV2V3.address])
try await fixtures.caroV2V3Client.conversations.sync()
Task(priority: .userInitiated) {
for try await _ in await fixtures.caroV2V3Client.conversations.streamAllMessages(includeGroups: true) {
expectation1.fulfill()
}
}

_ = try await group.send(content: "hi")
_ = try await convo.send(content: "hi")

await fulfillment(of: [expectation1], timeout: 3)
}

func testCanStreamGroupsAndConversationsFromV2andV3Users() async throws {
let fixtures = try await localFixtures()

let expectation1 = XCTestExpectation(description: "got a conversation")
expectation1.expectedFulfillmentCount = 2

Task(priority: .userInitiated) {
for try await _ in await fixtures.caroV2V3Client.conversations.streamAll() {
expectation1.fulfill()
}
}

_ = try await fixtures.boV3Client.conversations.newGroup(with: [fixtures.caroV2V3.address])
_ = try await fixtures.alixV2Client.conversations.newConversation(with: fixtures.caroV2V3.address)

await fulfillment(of: [expectation1], timeout: 3)
}
}
2 changes: 1 addition & 1 deletion XMTP.podspec
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@ Pod::Spec.new do |spec|
#

spec.name = "XMTP"
spec.version = "0.14.13"
spec.version = "0.14.14"
spec.summary = "XMTP SDK Cocoapod"

# This description is used to generate tags and improve search results.

0 comments on commit 56eafba

Please sign in to comment.