Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Pull v3 client into its own PR #233

Merged
merged 5 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,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.3.0"),
.package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.0.0"),
.package(url: "https://github.com/xmtp/libxmtp-swift", revision: "92274fe"),
.package(url: "https://github.com/xmtp/libxmtp-swift", revision: "503086d"),
// .package(path: "../libxmtp-swift")
],
targets: [
Expand Down
122 changes: 107 additions & 15 deletions Sources/XMTPiOS/Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ public struct ClientOptions {
/// Specify which XMTP network to connect to. Defaults to ``.dev``
public var env: XMTPEnvironment = .dev

/// Optional: Specify self-reported version e.g. XMTPInbox/v1.0.0.
/// Specify whether the API client should use TLS security. In general this should only be false when using the `.local` environment.
public var isSecure: Bool = true

/// Specify whether the API client should use TLS security. In general this should only be false when using the `.local` environment.
/// /// Optional: Specify self-reported version e.g. XMTPInbox/v1.0.0.
public var appVersion: String?

public init(env: XMTPEnvironment = .dev, isSecure: Bool = true, appVersion: String? = nil) {
Expand All @@ -44,11 +44,23 @@ public struct ClientOptions {
/// `preCreateIdentityCallback` will be called immediately before a Create Identity wallet signature is requested from the user.
public var preCreateIdentityCallback: PreEventCallback?

public init(api: Api = Api(), codecs: [any ContentCodec] = [], preEnableIdentityCallback: PreEventCallback? = nil, preCreateIdentityCallback: PreEventCallback? = nil) {
public var mlsAlpha = false
public var mlsEncryptionKey: Data?

public init(
api: Api = Api(),
codecs: [any ContentCodec] = [],
preEnableIdentityCallback: PreEventCallback? = nil,
preCreateIdentityCallback: PreEventCallback? = nil,
mlsAlpha: Bool = false,
mlsEncryptionKey: Data? = nil
) {
self.api = api
self.codecs = codecs
self.preEnableIdentityCallback = preEnableIdentityCallback
self.preCreateIdentityCallback = preCreateIdentityCallback
self.mlsAlpha = mlsAlpha
self.mlsEncryptionKey = mlsEncryptionKey
}
}

Expand All @@ -60,11 +72,12 @@ public struct ClientOptions {
/// 2. To sign a random salt used to encrypt the key bundle in storage. This happens every time the client is started, including the very first time).
///
/// > Important: The client connects to the XMTP `dev` environment by default. Use ``ClientOptions`` to change this and other parameters of the network connection.
public final class Client: Sendable {
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?

/// Access ``Conversations`` for this Client.
public lazy var conversations: Conversations = .init(client: self)
Expand Down Expand Up @@ -95,28 +108,80 @@ public final class Client: Sendable {
)
return try await create(account: account, apiClient: apiClient, options: options)
} catch {
throw ClientError.creationError(error.localizedDescription)
throw ClientError.creationError("\(error)")
}
}

static func initV3Client(
address: String,
options: ClientOptions?,
source: LegacyIdentitySource,
privateKeyBundleV1: PrivateKeyBundleV1,
signingKey: SigningKey?
) async throws -> FfiXmtpClient? {
if options?.mlsAlpha == true, options?.api.env == .local {
let dbURL = URL.documentsDirectory.appendingPathComponent("xmtp-\(options?.api.env.rawValue ?? "")-\(address).db3")
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't know how rawValue works but in Android this would return the actual value so 10.2.2.2 or whatever. I was thinking this would return just enum name like local, dev, or prod. Atleast thats how we do it in android. 🤷‍♀️ Not a huge deal tho.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ya I'm ok with it just returning hostnames.

let v3Client = try await LibXMTP.createClient(
logger: XMTPLogger(),
host: GRPCApiClient.envToUrl(env: options?.api.env ?? .local),
isSecure: (options?.api.env ?? .local) != .local,
db: dbURL.path,
encryptionKey: options?.mlsEncryptionKey,
accountAddress: address,
legacyIdentitySource: source,
legacySignedPrivateKeyProto: try privateKeyBundleV1.toV2().identityKey.serializedData()
)

if let textToSign = v3Client.textToSign() {
guard let signingKey else {
throw ClientError.creationError("No v3 keys found, you must pass a SigningKey in order to enable alpha MLS features")
}

let signature = try await signingKey.sign(message: textToSign)
try await v3Client.registerIdentity(recoverableWalletSignature: signature.rawData)
} else {
try await v3Client.registerIdentity(recoverableWalletSignature: nil)
}

return v3Client
} else {
return nil
}
}

static func create(account: SigningKey, apiClient: ApiClient, options: ClientOptions? = nil) async throws -> Client {
let privateKeyBundleV1 = try await loadOrCreateKeys(for: account, apiClient: apiClient, options: options)
let (privateKeyBundleV1, source) = try await loadOrCreateKeys(for: account, apiClient: apiClient, options: options)

let v3Client = try await initV3Client(
address: account.address,
options: options,
source: source,
privateKeyBundleV1: privateKeyBundleV1,
signingKey: account
)

let client = try Client(address: account.address, privateKeyBundleV1: privateKeyBundleV1, apiClient: apiClient)
if let textToSign = v3Client?.textToSign() {
let signature = try await account.sign(message: textToSign).rawData
try await v3Client?.registerIdentity(recoverableWalletSignature: signature)
}

let client = try Client(address: account.address, privateKeyBundleV1: privateKeyBundleV1, apiClient: apiClient, v3Client: v3Client)
try await client.ensureUserContactPublished()

for codec in (options?.codecs ?? []) {
client.register(codec: codec)
}
Comment on lines +171 to +173
Copy link
Contributor

Choose a reason for hiding this comment

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

Same about the codec code? Was this meant to be committed here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was actually not working before.


return client
}

static func loadOrCreateKeys(for account: SigningKey, apiClient: ApiClient, options: ClientOptions? = nil) async throws -> PrivateKeyBundleV1 {
// swiftlint:disable no_optional_try
static func loadOrCreateKeys(for account: SigningKey, apiClient: ApiClient, options: ClientOptions? = nil) async throws -> (PrivateKeyBundleV1, LegacyIdentitySource) {
if let keys = try await loadPrivateKeys(for: account, apiClient: apiClient, options: options) {
// swiftlint:enable no_optional_try
print("loading existing private keys.")
#if DEBUG
print("Loaded existing private keys.")
#endif
return keys
return (keys, .network)
} else {
#if DEBUG
print("No existing keys found, creating new bundle.")
Expand All @@ -133,7 +198,7 @@ public final class Client: Sendable {
Envelope(topic: .userPrivateStoreKeyBundle(account.address), timestamp: Date(), message: encryptedKeys.serializedData()),
])

return keys
return (keys, .keyGenerator)
}
}

Expand All @@ -155,12 +220,24 @@ public final class Client: Sendable {
return nil
}

public func canMessageV3(address: String) async throws -> Bool {
guard let v3Client else {
return false
}

return try await v3Client.canMessage(accountAddresses: [address]) == [true]
}

public static func from(bundle: PrivateKeyBundle, options: ClientOptions? = nil) async throws -> Client {
return try await from(v1Bundle: bundle.v1, options: options)
}

/// Create a Client from saved v1 key bundle.
public static func from(v1Bundle: PrivateKeyBundleV1, options: ClientOptions? = nil) async throws -> Client {
public static func from(
v1Bundle: PrivateKeyBundleV1,
options: ClientOptions? = nil,
signingKey: SigningKey? = nil
) async throws -> Client {
let address = try v1Bundle.identityKey.publicKey.recoverWalletSignerPublicKey().walletAddress

let options = options ?? ClientOptions()
Expand All @@ -172,13 +249,28 @@ public final class Client: Sendable {
rustClient: client
)

return try Client(address: address, privateKeyBundleV1: v1Bundle, apiClient: apiClient)
let v3Client = try await initV3Client(
address: address,
options: options,
source: .static,
privateKeyBundleV1: v1Bundle,
signingKey: nil
)

let result = try Client(address: address, privateKeyBundleV1: v1Bundle, apiClient: apiClient, v3Client: v3Client)

for codec in options.codecs {
result.register(codec: codec)
}
Comment on lines +262 to +264
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason you added this here? Feels like if we didn't have it before than it doesn't make sense to add it now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It was busted before.


return result
}

init(address: String, privateKeyBundleV1: PrivateKeyBundleV1, apiClient: ApiClient) throws {
init(address: String, privateKeyBundleV1: PrivateKeyBundleV1, apiClient: ApiClient, v3Client: LibXMTP.FfiXmtpClient?) throws {
self.address = address
self.privateKeyBundleV1 = privateKeyBundleV1
self.apiClient = apiClient
self.v3Client = v3Client
}

public var privateKeyBundle: PrivateKeyBundle {
Expand Down
24 changes: 24 additions & 0 deletions Sources/XMTPiOS/Extensions/URL.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// URL.swift
//
//
// Created by Pat Nakajima on 2/1/24.
//

import Foundation

extension URL {
static var documentsDirectory: URL {
// swiftlint:disable no_optional_try
guard let documentsDirectory = try? FileManager.default.url(
for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: false
) else {
fatalError("No documents directory")
}

return documentsDirectory
}
}
29 changes: 29 additions & 0 deletions Sources/XMTPiOS/XMTPLogger.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// Logger.swift
//
//
// Created by Pat Nakajima on 8/28/23.
//

import Foundation
import LibXMTP
import os

class XMTPLogger: FfiLogger {
let logger = Logger()

func log(level: UInt32, levelLabel: String, message: String) {
switch level {
case 1:
logger.error("libxmtp[\(levelLabel)] - \(message)")
case 2, 3:
logger.info("libxmtp[\(levelLabel)] - \(message)")
case 4:
logger.debug("libxmtp[\(levelLabel)] - \(message)")
case 5:
logger.trace("libxmtp[\(levelLabel)] - \(message)")
default:
print("libxmtp[\(levelLabel)] - \(message)")
}
}
}
74 changes: 74 additions & 0 deletions Tests/XMTPTests/ClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,80 @@ class ClientTests: XCTestCase {
_ = try await Client.create(account: fakeWallet)
}

func testPassingSavedKeysWithNoSignerWithMLSErrors() async throws {
try TestConfig.skipIfNotRunningLocalNodeTests()

let bo = try PrivateKey.generate()

do {
let client = try await Client.create(
account: bo,
options: .init(
api: .init(env: .local, isSecure: false),
mlsAlpha: true
)
)
} catch {
XCTAssert(error.localizedDescription.contains("no keys"))
}
}

func testPassingSavedKeysWithMLS() async throws {
try TestConfig.skipIfNotRunningLocalNodeTests()

let bo = try PrivateKey.generate()
let client = try await Client.create(
account: bo,
options: .init(
api: .init(env: .local, isSecure: false),
mlsAlpha: true
)
)

let keys = client.privateKeyBundle
let otherClient = try await Client.from(
bundle: keys,
options: .init(
api: .init(env: .local, isSecure: false),
// Should not need to pass the signer again
mlsAlpha: true
)
)

XCTAssertEqual(client.address, otherClient.address)
}

func testPassingMLSEncryptionKey() async throws {
try TestConfig.skipIfNotRunningLocalNodeTests()

let bo = try PrivateKey.generate()
let key = try Crypto.secureRandomBytes(count: 32)

_ = try await Client.create(
account: bo,
options: .init(
api: .init(env: .local, isSecure: false),
mlsAlpha: true,
mlsEncryptionKey: key
)
)

do {
_ = try await Client.create(
account: bo,
options: .init(
api: .init(env: .local, isSecure: false),
mlsAlpha: true,
mlsEncryptionKey: nil // No key should error
)
)

XCTFail("did not throw")
} catch {
XCTAssert(true)
}
}

func testCanMessage() async throws {
let fixtures = await fixtures()
let notOnNetwork = try PrivateKey.generate()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/xmtp/libxmtp-swift",
"state" : {
"branch" : "92274fe",
"revision" : "92274fe0dde1fc7f8f716ebcffa3d252813be56d"
"branch" : "503086d",
"revision" : "503086d91ba6c3420aca118e4812d62a004d17ee"
}
},
{
Expand Down
Loading