diff --git a/Package.swift b/Package.swift index f83b6b7b..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-beta1"), + .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/Sources/XMTPTestHelpers/TestHelpers.swift b/Sources/XMTPTestHelpers/TestHelpers.swift index d9a43f26..41a8fa52 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,40 @@ 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 signSCW(message: String) async throws -> Data { + // swiftlint:disable force_unwrapping + let digest = SHA256.hash(data: message.data(using: .utf8)!) + // swiftlint:enable force_unwrapping + return Data(digest) + } +} + @available(iOS 15, *) public struct Fixtures { public var alice: PrivateKey! diff --git a/Sources/XMTPiOS/Client.swift b/Sources/XMTPiOS/Client.swift index e0da501d..010602d2 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) - + 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,42 @@ 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 = 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( + accountAddress: accountAddress, + options: options, + signingKey: account, + inboxId: inboxId + ) + } + + 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) + + return try await initializeClient( + accountAddress: accountAddress, + options: options, + signingKey: nil, + inboxId: inboxId + ) + } + static func initV3Client( accountAddress: String, options: ClientOptions?, @@ -224,7 +254,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,8 +276,20 @@ public final class Client { if let signatureRequest = v3Client.signatureRequest() { 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 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(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) } catch { throw ClientError.creationError("Failed to sign the message: \(error.localizedDescription)") @@ -651,7 +693,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 7eb93eab..5c720edf 100644 --- a/Sources/XMTPiOS/SigningKey.swift +++ b/Sources/XMTPiOS/SigningKey.swift @@ -19,6 +19,15 @@ 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 "1" + var chainId: Int64? { get } + + /// The blockNumber of the chain for example "1" + var blockNumber: Int64? { get } /// Sign the data and return a secp256k1 compact recoverable signature. func sign(_ data: Data) async throws -> Signature @@ -26,9 +35,24 @@ 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 { + public var isSmartContractWallet: Bool { + return false + } + + public var chainId: Int64? { + return nil + } + + public var blockNumber: Int64? { + return nil + } + func createIdentity(_ identity: PrivateKey, preCreateIdentityCallback: PreEventCallback? = nil) async throws -> AuthorizedIdentity { var slimKey = PublicKey() slimKey.timestamp = UInt64(Date().millisecondsSinceEpoch) @@ -50,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 40b2495e..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 { diff --git a/Tests/XMTPTests/V3ClientTests.swift b/Tests/XMTPTests/V3ClientTests.swift index 4cb7d59e..91a70fd8 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 { @@ -33,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), @@ -51,6 +52,7 @@ class V3ClientTests: XCTestCase { ) ) + return .init( alixV2: alixV2, boV3: boV3, diff --git a/XMTP.podspec b/XMTP.podspec index 76198185..ecec0f30 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.2-alpha0" 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-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 321e8126..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" : "f07383efdee7bed94120dd73efa4ff1c4dcead3c", - "version" : "0.5.9-beta1" + "revision" : "dad96a78a12fdeeb22e5fea5c2e39d073084b0d7", + "version" : "0.5.9-beta4" } }, {