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

Smart Contract Wallets #403

Draft
wants to merge 33 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
ede8122
update package
nplasterer Sep 13, 2024
70c3a41
Merge branch 'main' of https://github.com/xmtp/xmtp-ios
nplasterer Sep 13, 2024
c72c33c
Merge branch 'main' of https://github.com/xmtp/xmtp-ios
nplasterer Sep 20, 2024
2f9cca8
add chain id and SCW check
nplasterer Sep 20, 2024
4e43786
add implementation
nplasterer Sep 20, 2024
ede8c29
fix a little formatting
nplasterer Sep 21, 2024
fd8ddc1
change defaults
nplasterer Sep 21, 2024
bc9fb62
Merge branch 'main' of https://github.com/xmtp/xmtp-ios
nplasterer Sep 22, 2024
65a4aaa
make a test release pod
nplasterer Sep 24, 2024
a366503
bump the latest libxmtp
nplasterer Sep 24, 2024
a46302b
fix up all the async tests
nplasterer Sep 24, 2024
16e2e6d
add installation timestamps and async members
nplasterer Sep 26, 2024
35f8053
Merge branch 'main' of https://github.com/xmtp/xmtp-ios into np/smart…
nplasterer Sep 26, 2024
f02d489
fix up the tests and bump the pod
nplasterer Sep 26, 2024
0e8ab5b
Merge branch 'np/stream-groups-logging' of https://github.com/xmtp/xm…
nplasterer Sep 26, 2024
e246103
bump to the next version
nplasterer Sep 26, 2024
f19b9b7
Merge branch 'main' of https://github.com/xmtp/xmtp-ios into np/smart…
nplasterer Sep 26, 2024
ef8dea9
bad merge
nplasterer Sep 26, 2024
03d4215
Merge branch 'main' of https://github.com/xmtp/xmtp-ios into np/smart…
nplasterer Oct 2, 2024
67a88fc
update the package
nplasterer Oct 2, 2024
929ac84
fix up bad merge
nplasterer Oct 2, 2024
a2d472a
Merge branch 'main' of https://github.com/xmtp/xmtp-ios into np/smart…
nplasterer Oct 9, 2024
63b79e1
make block number optional
nplasterer Oct 9, 2024
402ee51
add a test to reproduce the scw error
nplasterer Oct 9, 2024
d1f509d
update to latest libxmtp
nplasterer Oct 11, 2024
abe6af8
update the signers
nplasterer Oct 18, 2024
2cd20ce
update to the latest libxmtp functions
nplasterer Oct 18, 2024
2812b8a
fix the linter
nplasterer Oct 18, 2024
b03101f
get on a working version
nplasterer Oct 19, 2024
263bf64
check the chain id
nplasterer Oct 19, 2024
14b0cff
chain id is optional
nplasterer Oct 19, 2024
b986370
fix the lint issue
nplasterer Oct 20, 2024
87b8b42
tag
nplasterer Oct 20, 2024
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 @@ -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.
Expand Down
35 changes: 35 additions & 0 deletions Sources/XMTPTestHelpers/TestHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#if canImport(XCTest)
import Combine
import CryptoKit
import XCTest
@testable import XMTPiOS
import LibXMTP
Expand Down Expand Up @@ -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!
Expand Down
72 changes: 57 additions & 15 deletions Sources/XMTPiOS/Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -161,39 +161,69 @@ 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,
inboxID: v3Client.inboxId(),
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?,
Expand Down Expand Up @@ -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.")
}
Expand All @@ -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)")
Expand Down Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/XMTPiOS/Contacts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
14 changes: 7 additions & 7 deletions Sources/XMTPiOS/Conversations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
Expand Down Expand Up @@ -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] {
Expand All @@ -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<Group, Error> {
Expand All @@ -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
}
Expand All @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
6 changes: 3 additions & 3 deletions Sources/XMTPiOS/Extensions/Ffi.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
}
Expand Down
8 changes: 4 additions & 4 deletions Sources/XMTPiOS/Group.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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()
}

Expand Down Expand Up @@ -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()
}

Expand Down
4 changes: 2 additions & 2 deletions Sources/XMTPiOS/Mls/Member.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
36 changes: 36 additions & 0 deletions Sources/XMTPiOS/SigningKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,40 @@ 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

/// 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)
Expand All @@ -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."])
}
}
Loading
Loading