Skip to content

Commit

Permalink
Pull v3 client into its own PR
Browse files Browse the repository at this point in the history
  • Loading branch information
nakajima committed Feb 5, 2024
1 parent e8bb502 commit 70a61ee
Show file tree
Hide file tree
Showing 7 changed files with 188 additions and 19 deletions.
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
2 changes: 1 addition & 1 deletion Sources/XMTPTestHelpers/TestHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public struct TestConfig {
}

static public func skipIfNotRunningLocalNodeTests() throws {
try XCTSkipIf(!TEST_SERVER_ENABLED, "requires local node")
// try XCTSkipIf(!TEST_SERVER_ENABLED, "requires local node")
}

static public func skip(because: String) throws {
Expand Down
114 changes: 99 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 enableAlphaMLS: Bool = false
public var mlsEncryptionKey: Data?

public init(
api: Api = Api(),
codecs: [any ContentCodec] = [],
preEnableIdentityCallback: PreEventCallback? = nil,
preCreateIdentityCallback: PreEventCallback? = nil,
enableAlphaMLS: Bool = false,
mlsEncryptionKey: Data? = nil
) {
self.api = api
self.codecs = codecs
self.preEnableIdentityCallback = preEnableIdentityCallback
self.preCreateIdentityCallback = preCreateIdentityCallback
self.enableAlphaMLS = enableAlphaMLS
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,64 @@ 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) async throws -> FfiXmtpClient? {
let v3Client: FfiXmtpClient?

if options?.enableAlphaMLS == true && options?.api.env == .local {
let dbURL = URL.documentsDirectory.appendingPathComponent("xmtp-\(options?.api.env.rawValue ?? "")-\(address).db3")
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()
)
} else {
v3Client = nil
}

return v3Client
}

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
)

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

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 +182,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 +204,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 +233,36 @@ 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
)

if let v3Client, 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)
}

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

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

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 @@
//
// File.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)")
}
}
}
32 changes: 32 additions & 0 deletions Tests/XMTPTests/ClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,38 @@ class ClientTests: XCTestCase {
_ = try await Client.create(account: fakeWallet)
}

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),
enableAlphaMLS: true,
mlsEncryptionKey: key
)
)

do {
_ = try await Client.create(
account: bo,
options: .init(
api: .init(env: .local, isSecure: false),
enableAlphaMLS: 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

0 comments on commit 70a61ee

Please sign in to comment.