diff --git a/android/build.gradle b/android/build.gradle index d91a38c8c..b5fc17af2 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -98,7 +98,7 @@ repositories { dependencies { implementation project(':expo-modules-core') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}" - implementation "org.xmtp:android:0.15.12" + implementation "org.xmtp:android:0.16.0" implementation 'com.google.code.gson:gson:2.10.1' implementation 'com.facebook.react:react-native:0.71.3' implementation "com.daveanthonythomas.moshipack:moshipack:1.0.1" diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt index ffe23e466..1b0104bd9 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -47,6 +47,7 @@ import org.xmtp.android.library.PreEventCallback import org.xmtp.android.library.PreparedMessage import org.xmtp.android.library.SendOptions import org.xmtp.android.library.SigningKey +import org.xmtp.android.library.WalletType import org.xmtp.android.library.XMTPEnvironment import org.xmtp.android.library.XMTPException import org.xmtp.android.library.codecs.Attachment @@ -55,6 +56,7 @@ import org.xmtp.android.library.codecs.EncodedContent import org.xmtp.android.library.codecs.EncryptedEncodedContent import org.xmtp.android.library.codecs.RemoteAttachment import org.xmtp.android.library.codecs.decoded +import org.xmtp.android.library.hexToByteArray import org.xmtp.android.library.messages.EnvelopeBuilder import org.xmtp.android.library.messages.InvitationV1ContextBuilder import org.xmtp.android.library.messages.MessageDeliveryStatus @@ -69,7 +71,6 @@ import org.xmtp.proto.message.api.v1.MessageApiOuterClass import org.xmtp.proto.message.contents.Invitation.ConsentProofPayload import org.xmtp.proto.message.contents.PrivateKeyOuterClass import uniffi.xmtpv3.org.xmtp.android.library.libxmtp.GroupPermissionPreconfiguration -import uniffi.xmtpv3.org.xmtp.android.library.libxmtp.InboxState import uniffi.xmtpv3.org.xmtp.android.library.libxmtp.PermissionOption import java.io.BufferedReader import java.io.File @@ -80,8 +81,16 @@ import kotlin.coroutines.Continuation import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException -class ReactNativeSigner(var module: XMTPModule, override var address: String) : SigningKey { + +class ReactNativeSigner( + var module: XMTPModule, + override var address: String, + override var type: WalletType = WalletType.EOA, + override var chainId: Long? = null, + override var blockNumber: Long? = null, +) : SigningKey { private val continuations: MutableMap> = mutableMapOf() + private val scwContinuations: MutableMap> = mutableMapOf() fun handle(id: String, signature: String) { val continuation = continuations[id] ?: return @@ -101,6 +110,20 @@ class ReactNativeSigner(var module: XMTPModule, override var address: String) : continuations.remove(id) } + fun handleSCW(id: String, signature: String) { + val continuation = scwContinuations[id] ?: return + continuation.resume(signature.hexToByteArray()) + scwContinuations.remove(id) + } + + override suspend fun signSCW(message: String): ByteArray { + val request = SignatureRequest(message = message) + module.sendEvent("sign", mapOf("id" to request.id, "message" to request.message)) + return suspendCancellableCoroutine { continuation -> + scwContinuations[request.id] = continuation + } + } + override suspend fun sign(data: ByteArray): Signature { val request = SignatureRequest(message = String(data, Charsets.UTF_8)) module.sendEvent("sign", mapOf("id" to request.id, "message" to request.message)) @@ -330,6 +353,11 @@ class XMTPModule : Module() { signer?.handle(id = requestID, signature = signature) } + Function("receiveSCWSignature") { requestID: String, signature: String -> + logV("receiveSCWSignature") + signer?.handleSCW(id = requestID, signature = signature) + } + // Generate a random wallet and set the client to that AsyncFunction("createRandom") Coroutine { hasCreateIdentityCallback: Boolean?, hasEnableIdentityCallback: Boolean?, hasPreAuthenticateToInboxCallback: Boolean?, dbEncryptionKey: List?, authParams: String -> withContext(Dispatchers.IO) { @@ -375,10 +403,17 @@ class XMTPModule : Module() { } } - AsyncFunction("createOrBuild") Coroutine { address: String, hasCreateIdentityCallback: Boolean?, hasEnableIdentityCallback: Boolean?, hasAuthInboxCallback: Boolean?, dbEncryptionKey: List?, authParams: String -> + AsyncFunction("createV3") Coroutine { address: String, hasCreateIdentityCallback: Boolean?, hasEnableIdentityCallback: Boolean?, hasAuthInboxCallback: Boolean?, dbEncryptionKey: List?, authParams: String -> withContext(Dispatchers.IO) { - logV("createOrBuild") - val reactSigner = ReactNativeSigner(module = this@XMTPModule, address = address) + logV("createV3") + val authOptions = AuthParamsWrapper.authParamsFromJson(authParams) + val reactSigner = ReactNativeSigner( + module = this@XMTPModule, + address = address, + type = authOptions.walletType, + chainId = authOptions.chainId, + blockNumber = authOptions.blockNumber + ) signer = reactSigner val options = clientOptions( dbEncryptionKey, @@ -387,7 +422,7 @@ class XMTPModule : Module() { hasEnableIdentityCallback, hasAuthInboxCallback, ) - val client = Client().createOrBuild(account = reactSigner, options = options) + val client = Client().createV3(account = reactSigner, options = options) clients[client.inboxId] = client ContentJson.Companion signer = null @@ -395,6 +430,21 @@ class XMTPModule : Module() { } } + AsyncFunction("buildV3") Coroutine { address: String, dbEncryptionKey: List?, authParams: String -> + withContext(Dispatchers.IO) { + logV("buildV3") + val authOptions = AuthParamsWrapper.authParamsFromJson(authParams) + val options = clientOptions( + dbEncryptionKey, + authParams, + ) + val client = Client().buildV3(address = address, options = options) + ContentJson.Companion + clients[client.inboxId] = client + ClientWrapper.encodeToObj(client) + } + } + AsyncFunction("createRandomV3") Coroutine { hasCreateIdentityCallback: Boolean?, hasEnableIdentityCallback: Boolean?, hasPreAuthenticateToInboxCallback: Boolean?, dbEncryptionKey: List?, authParams: String -> withContext(Dispatchers.IO) { logV("createRandomV3") @@ -406,7 +456,7 @@ class XMTPModule : Module() { hasEnableIdentityCallback, hasPreAuthenticateToInboxCallback, ) - val randomClient = Client().createOrBuild(account = privateKey, options = options) + val randomClient = Client().createV3(account = privateKey, options = options) ContentJson.Companion clients[randomClient.inboxId] = randomClient @@ -635,7 +685,7 @@ class XMTPModule : Module() { val sortedGroupList = if (order == ConversationOrder.LAST_MESSAGE) { client.conversations.listGroups() .sortedByDescending { group -> - group.decryptedMessages().firstOrNull()?.sentAt + group.decryptedMessages(limit = 1).firstOrNull()?.sentAt } .let { groups -> if (limit != null && limit > 0) groups.take(limit) else groups diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/AuthParamsWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/AuthParamsWrapper.kt index 99146a715..a1a461cea 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/AuthParamsWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/AuthParamsWrapper.kt @@ -1,6 +1,7 @@ package expo.modules.xmtpreactnativesdk.wrappers import com.google.gson.JsonParser +import org.xmtp.android.library.WalletType class AuthParamsWrapper( val environment: String, @@ -8,6 +9,9 @@ class AuthParamsWrapper( val enableV3: Boolean = false, val dbDirectory: String?, val historySyncUrl: String?, + val walletType: WalletType = WalletType.EOA, + val chainId: Long?, + val blockNumber: Long?, ) { companion object { fun authParamsFromJson(authParams: String): AuthParamsWrapper { @@ -17,8 +21,18 @@ class AuthParamsWrapper( if (jsonOptions.has("appVersion")) jsonOptions.get("appVersion").asString else null, if (jsonOptions.has("enableV3")) jsonOptions.get("enableV3").asBoolean else false, if (jsonOptions.has("dbDirectory")) jsonOptions.get("dbDirectory").asString else null, - if (jsonOptions.has("historySyncUrl")) jsonOptions.get("historySyncUrl").asString else null - ) + if (jsonOptions.has("historySyncUrl")) jsonOptions.get("historySyncUrl").asString else null, + if (jsonOptions.has("walletType")) { + when (jsonOptions.get("walletType").asString) { + "SCW" -> WalletType.SCW + else -> WalletType.EOA + } + } else { + WalletType.EOA + }, + if (jsonOptions.has("chainId")) jsonOptions.get("chainId").asLong else null, + if (jsonOptions.has("blockNumber")) jsonOptions.get("blockNumber").asLong else null, + ) } } } diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt index 5fdb142af..b75c58c4f 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt @@ -37,7 +37,7 @@ class GroupWrapper { put("consentState", consentStateToString(group.consentState())) } if (groupParams.lastMessage) { - val lastMessage = group.decryptedMessages().firstOrNull() + val lastMessage = group.decryptedMessages(limit = 1).firstOrNull() if (lastMessage != null) { put("lastMessage", DecodedMessageWrapper.encode(lastMessage)) } diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index c66401c3c..ee02ff527 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -56,7 +56,7 @@ PODS: - hermes-engine/Pre-built (= 0.71.14) - hermes-engine/Pre-built (0.71.14) - libevent (2.1.12) - - LibXMTP (0.5.9-beta0) + - LibXMTP (0.5.10) - Logging (1.0.0) - MessagePacker (0.4.7) - MMKV (2.0.0): @@ -449,16 +449,16 @@ PODS: - GenericJSON (~> 2.0) - Logging (~> 1.0.0) - secp256k1.swift (~> 0.1) - - XMTP (0.15.0): + - XMTP (0.15.2): - Connect-Swift (= 0.12.0) - GzipSwift - - LibXMTP (= 0.5.9-beta0) + - LibXMTP (= 0.5.10) - web3.swift - XMTPReactNative (0.1.0): - ExpoModulesCore - MessagePacker - secp256k1.swift - - XMTP (= 0.15.0) + - XMTP (= 0.15.2) - Yoga (1.14.0) DEPENDENCIES: @@ -711,7 +711,7 @@ SPEC CHECKSUMS: GzipSwift: 893f3e48e597a1a4f62fafcb6514220fcf8287fa hermes-engine: d7cc127932c89c53374452d6f93473f1970d8e88 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 - LibXMTP: 5a38722a68a9469be2e711857a5e7d9dd3aa8a61 + LibXMTP: 3b64b0b1e4157ff73c37cde60fe943f89e6f8693 Logging: 9ef4ecb546ad3169398d5a723bc9bea1c46bef26 MessagePacker: ab2fe250e86ea7aedd1a9ee47a37083edd41fd02 MMKV: f7d1d5945c8765f97f39c3d121f353d46735d801 @@ -763,8 +763,8 @@ SPEC CHECKSUMS: secp256k1.swift: a7e7a214f6db6ce5db32cc6b2b45e5c4dd633634 SwiftProtobuf: 407a385e97fd206c4fbe880cc84123989167e0d1 web3.swift: 2263d1e12e121b2c42ffb63a5a7beb1acaf33959 - XMTP: 09faa347569b092005997364f7fe787ccc33f3d5 - XMTPReactNative: 6404c11e6dd11820742d4af899daeea389fc442f + XMTP: 7d47e6bc507db66dd01116ce2b4ed04dd3560a4f + XMTPReactNative: 1a946cd697598fb4bc560a637094e63c4d553df3 Yoga: e71803b4c1fff832ccf9b92541e00f9b873119b9 PODFILE CHECKSUM: 0e6fe50018f34e575d38dc6a1fdf1f99c9596cdd diff --git a/example/src/tests/v3OnlyTests.ts b/example/src/tests/v3OnlyTests.ts index f673dfbea..8f3a03469 100644 --- a/example/src/tests/v3OnlyTests.ts +++ b/example/src/tests/v3OnlyTests.ts @@ -39,8 +39,20 @@ test('can make a V3 only client', async () => { client.inboxId === inboxId, `inboxIds should match but were ${client.inboxId} and ${inboxId}` ) - const canMessageV3 = await client.canGroupMessage([client.address]) + const client2 = await Client.buildV3(client.address, { + env: 'local', + appVersion: 'Testing/0.0.0', + enableV3: true, + dbEncryptionKey: keyBytes, + }) + + assert( + client.inboxId === client2.inboxId, + `inboxIds should match but were ${client.inboxId} and ${client2.inboxId}` + ) + + const canMessageV3 = await client.canGroupMessage([client.address]) assert( canMessageV3[client.address.toLowerCase()] === true, `canMessageV3 should be true` @@ -51,7 +63,6 @@ test('can make a V3 only client', async () => { } catch (error) { return true } - throw new Error('should throw error when hitting V2 api') }) diff --git a/ios/ReactNativeSigner.swift b/ios/ReactNativeSigner.swift index 018238867..e147c12a9 100644 --- a/ios/ReactNativeSigner.swift +++ b/ios/ReactNativeSigner.swift @@ -14,11 +14,18 @@ class ReactNativeSigner: NSObject, XMTP.SigningKey { var module: XMTPModule var address: String + var type: WalletType + var chainId: Int64? + var blockNumber: Int64? var continuations: [String: CheckedContinuation] = [:] + var scwContinuations: [String: CheckedContinuation] = [:] - init(module: XMTPModule, address: String) { + init(module: XMTPModule, address: String, walletType: WalletType = WalletType.EOA, chainId: Int64? = nil, blockNumber: Int64? = nil) { self.module = module self.address = address + self.type = walletType + self.chainId = chainId + self.blockNumber = blockNumber } func handle(id: String, signature: String) throws { @@ -40,6 +47,28 @@ class ReactNativeSigner: NSObject, XMTP.SigningKey { continuation.resume(returning: signature) continuations.removeValue(forKey: id) } + + func handleSCW(id: String, signature: String) throws { + guard let continuation = scwContinuations[id] else { + return + } + + continuation.resume(returning: signature.hexToData) + scwContinuations.removeValue(forKey: id) + } + + func signSCW(message: String) async throws -> Data { + let request = SignatureRequest(message: message) + + module.sendEvent("sign", [ + "id": request.id, + "message": request.message, + ]) + + return try await withCheckedThrowingContinuation { continuation in + scwContinuations[request.id] = continuation + } + } func sign(_ data: Data) async throws -> XMTP.Signature { let request = SignatureRequest(message: String(data: data, encoding: .utf8)!) diff --git a/ios/Wrappers/AuthParamsWrapper.swift b/ios/Wrappers/AuthParamsWrapper.swift index 763071ee8..e5fed3096 100644 --- a/ios/Wrappers/AuthParamsWrapper.swift +++ b/ios/Wrappers/AuthParamsWrapper.swift @@ -14,19 +14,25 @@ struct AuthParamsWrapper { let enableV3: Bool let dbDirectory: String? let historySyncUrl: String? - - init(environment: String, appVersion: String?, enableV3: Bool, dbDirectory: String?, historySyncUrl: String?) { + let walletType: WalletType + let chainId: Int64? + let blockNumber: Int64? + + init(environment: String, appVersion: String?, enableV3: Bool, dbDirectory: String?, historySyncUrl: String?, walletType: WalletType, chainId: Int64?, blockNumber: Int64?) { self.environment = environment self.appVersion = appVersion self.enableV3 = enableV3 self.dbDirectory = dbDirectory self.historySyncUrl = historySyncUrl + self.walletType = walletType + self.chainId = chainId + self.blockNumber = blockNumber } static func authParamsFromJson(_ authParams: String) -> AuthParamsWrapper { guard let data = authParams.data(using: .utf8), let jsonOptions = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { - return AuthParamsWrapper(environment: "dev", appVersion: nil, enableV3: false, dbDirectory: nil, historySyncUrl: nil) + return AuthParamsWrapper(environment: "dev", appVersion: nil, enableV3: false, dbDirectory: nil, historySyncUrl: nil, walletType: WalletType.EOA, chainId: nil, blockNumber: nil) } let environment = jsonOptions["environment"] as? String ?? "dev" @@ -34,13 +40,28 @@ struct AuthParamsWrapper { let enableV3 = jsonOptions["enableV3"] as? Bool ?? false let dbDirectory = jsonOptions["dbDirectory"] as? String let historySyncUrl = jsonOptions["historySyncUrl"] as? String + let walletTypeString = jsonOptions["walletType"] as? String ?? "EOA" + let chainId = jsonOptions["chainId"] as? Int64 + let blockNumber = jsonOptions["blockNumber"] as? Int64 + + let walletType = { switch walletTypeString { + case "SCW": + return WalletType.SCW + default: + return WalletType.EOA + } + }() + return AuthParamsWrapper( environment: environment, appVersion: appVersion, enableV3: enableV3, dbDirectory: dbDirectory, - historySyncUrl: historySyncUrl + historySyncUrl: historySyncUrl, + walletType: walletType, + chainId: chainId, + blockNumber: blockNumber ) } } diff --git a/ios/Wrappers/GroupWrapper.swift b/ios/Wrappers/GroupWrapper.swift index f00f73aeb..928fdd6d5 100644 --- a/ios/Wrappers/GroupWrapper.swift +++ b/ios/Wrappers/GroupWrapper.swift @@ -48,7 +48,7 @@ struct GroupWrapper { result["consentState"] = ConsentWrapper.consentStateToString(state: try group.consentState()) } if groupParams.lastMessage { - if let lastMessage = try await group.decryptedMessages().first { + if let lastMessage = try await group.decryptedMessages(limit: 1).first { result["lastMessage"] = try DecodedMessageWrapper.encode(lastMessage, client: client) } } diff --git a/ios/XMTPModule.swift b/ios/XMTPModule.swift index 90d278617..5ec9831e9 100644 --- a/ios/XMTPModule.swift +++ b/ios/XMTPModule.swift @@ -223,6 +223,10 @@ public class XMTPModule: Module { Function("receiveSignature") { (requestID: String, signature: String) in try signer?.handle(id: requestID, signature: signature) } + + Function("receiveSCWSignature") { (requestID: String, signature: String) in + try signer?.handleSCW(id: requestID, signature: signature) + } // Generate a random wallet and set the client to that AsyncFunction("createRandom") { (hasCreateIdentityCallback: Bool?, hasEnableIdentityCallback: Bool?, hasAuthenticateToInboxCallback: Bool?, dbEncryptionKey: [UInt8]?, authParams: String) -> [String: String] in @@ -311,14 +315,15 @@ public class XMTPModule: Module { dbDirectory: authOptions.dbDirectory, historySyncUrl: authOptions.historySyncUrl ) - let client = try await Client.createOrBuild(account: privateKey, options: options) + let client = try await Client.createV3(account: privateKey, options: options) await clientsManager.updateClient(key: client.inboxID, client: client) return try ClientWrapper.encodeToObj(client) } - AsyncFunction("createOrBuild") { (address: String, hasCreateIdentityCallback: Bool?, hasEnableIdentityCallback: Bool?, hasAuthenticateToInboxCallback: Bool?, dbEncryptionKey: [UInt8]?, authParams: String) in - let signer = ReactNativeSigner(module: self, address: address) + AsyncFunction("createV3") { (address: String, hasCreateIdentityCallback: Bool?, hasEnableIdentityCallback: Bool?, hasAuthenticateToInboxCallback: Bool?, dbEncryptionKey: [UInt8]?, authParams: String) in + let authOptions = AuthParamsWrapper.authParamsFromJson(authParams) + let signer = ReactNativeSigner(module: self, address: address, walletType: authOptions.walletType, chainId: authOptions.chainId, blockNumber: authOptions.blockNumber) self.signer = signer if(hasCreateIdentityCallback ?? false) { self.preCreateIdentityCallbackDeferred = DispatchSemaphore(value: 0) @@ -333,7 +338,6 @@ public class XMTPModule: Module { let preEnableIdentityCallback: PreEventCallback? = hasEnableIdentityCallback ?? false ? self.preEnableIdentityCallback : nil let preAuthenticateToInboxCallback: PreEventCallback? = hasAuthenticateToInboxCallback ?? false ? self.preAuthenticateToInboxCallback : nil let encryptionKeyData = dbEncryptionKey == nil ? nil : Data(dbEncryptionKey!) - let authOptions = AuthParamsWrapper.authParamsFromJson(authParams) let options = self.createClientConfig( env: authOptions.environment, @@ -346,11 +350,31 @@ public class XMTPModule: Module { dbDirectory: authOptions.dbDirectory, historySyncUrl: authOptions.historySyncUrl ) - let client = try await XMTP.Client.createOrBuild(account: signer, options: options) + let client = try await XMTP.Client.createV3(account: signer, options: options) await self.clientsManager.updateClient(key: client.inboxID, client: client) self.signer = nil self.sendEvent("authedV3", try ClientWrapper.encodeToObj(client)) } + + AsyncFunction("buildV3") { (address: String, dbEncryptionKey: [UInt8]?, authParams: String) -> [String: String] in + let authOptions = AuthParamsWrapper.authParamsFromJson(authParams) + let encryptionKeyData = dbEncryptionKey == nil ? nil : Data(dbEncryptionKey!) + + let options = self.createClientConfig( + env: authOptions.environment, + appVersion: authOptions.appVersion, + preEnableIdentityCallback: preEnableIdentityCallback, + preCreateIdentityCallback: preCreateIdentityCallback, + preAuthenticateToInboxCallback: preAuthenticateToInboxCallback, + enableV3: authOptions.enableV3, + dbEncryptionKey: encryptionKeyData, + dbDirectory: authOptions.dbDirectory, + historySyncUrl: authOptions.historySyncUrl + ) + let client = try await XMTP.Client.buildV3(address: address, options: options) + await clientsManager.updateClient(key: client.inboxID, client: client) + return try ClientWrapper.encodeToObj(client) + } // Remove a client from memory for a given inboxId AsyncFunction("dropClient") { (inboxId: String) in @@ -547,7 +571,7 @@ public class XMTPModule: Module { var groupsWithMessages: [(Group, Date)] = [] for group in groups { do { - let firstMessage = try await group.decryptedMessages().first + let firstMessage = try await group.decryptedMessages(limit: 1).first let sentAt = firstMessage?.sentAt ?? Date.distantPast groupsWithMessages.append((group, sentAt)) } catch { diff --git a/ios/XMTPReactNative.podspec b/ios/XMTPReactNative.podspec index c8a48e722..fd780cac2 100644 --- a/ios/XMTPReactNative.podspec +++ b/ios/XMTPReactNative.podspec @@ -26,5 +26,5 @@ Pod::Spec.new do |s| s.source_files = "**/*.{h,m,swift}" s.dependency 'secp256k1.swift' s.dependency "MessagePacker" - s.dependency "XMTP", "= 0.15.0" + s.dependency "XMTP", "= 0.15.2" end diff --git a/src/index.ts b/src/index.ts index a1b67c66d..73177c589 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ import { Client } from '.' import { ConversationContext } from './XMTP.types' import XMTPModule from './XMTPModule' import { InboxId } from './lib/Client' +import { WalletType } from './lib/Signer' import { ConsentListEntry, ConsentState } from './lib/ConsentListEntry' import { ContentCodec, @@ -121,6 +122,10 @@ export async function receiveSignature(requestID: string, signature: string) { return await XMTPModule.receiveSignature(requestID, signature) } +export async function receiveSCWSignature(requestID: string, signature: string) { + return await XMTPModule.receiveSCWSignature(requestID, signature) +} + export async function createRandom( environment: 'local' | 'dev' | 'production', appVersion?: string | undefined, @@ -210,7 +215,7 @@ export async function createRandomV3( ) } -export async function createOrBuild( +export async function createV3( address: string, environment: 'local' | 'dev' | 'production', appVersion?: string | undefined, @@ -220,7 +225,10 @@ export async function createOrBuild( enableV3?: boolean | undefined, dbEncryptionKey?: Uint8Array | undefined, dbDirectory?: string | undefined, - historySyncUrl?: string | undefined + historySyncUrl?: string | undefined, + walletType?: WalletType | undefined, + chainId?: number | undefined, + blockNumber?: number | undefined ) { const encryptionKey = dbEncryptionKey ? Array.from(dbEncryptionKey) @@ -232,8 +240,11 @@ export async function createOrBuild( enableV3, dbDirectory, historySyncUrl, + walletType, + chainId, + blockNumber, } - return await XMTPModule.createOrBuild( + return await XMTPModule.createV3( address, hasCreateIdentityCallback, hasEnableIdentityCallback, @@ -243,6 +254,33 @@ export async function createOrBuild( ) } +export async function buildV3( + address: string, + environment: 'local' | 'dev' | 'production', + appVersion?: string | undefined, + enableV3?: boolean | undefined, + dbEncryptionKey?: Uint8Array | undefined, + dbDirectory?: string | undefined, + historySyncUrl?: string | undefined +) { + const encryptionKey = dbEncryptionKey + ? Array.from(dbEncryptionKey) + : undefined + + const authParams: AuthParams = { + environment, + appVersion, + enableV3, + dbDirectory, + historySyncUrl, + } + return await XMTPModule.buildV3( + address, + encryptionKey, + JSON.stringify(authParams) + ) +} + export async function dropClient(inboxId: string) { return await XMTPModule.dropClient(inboxId) } @@ -1288,6 +1326,9 @@ interface AuthParams { enableV3?: boolean dbDirectory?: string historySyncUrl?: string + walletType?: string + chainId?: number + blockNumber?: number } interface CreateGroupParams { diff --git a/src/lib/Client.ts b/src/lib/Client.ts index c9b9a95df..4df46aac7 100644 --- a/src/lib/Client.ts +++ b/src/lib/Client.ts @@ -304,7 +304,7 @@ export class Client< * * See {@link https://xmtp.org/docs/build/authentication#create-a-client | XMTP Docs} for more information. */ - static async createOrBuild< + static async createV3< ContentCodecs extends DefaultContentTypes = DefaultContentTypes, >( wallet: Signer | WalletClient | null, @@ -331,17 +331,24 @@ export class Client< const request: { id: string; message: string } = message try { const signatureString = await signer.signMessage(request.message) - const eSig = splitSignature(signatureString) - const r = hexToBytes(eSig.r) - const s = hexToBytes(eSig.s) - const sigBytes = new Uint8Array(65) - sigBytes.set(r) - sigBytes.set(s, r.length) - sigBytes[64] = eSig.recoveryParam - - const signature = Buffer.from(sigBytes).toString('base64') - - await XMTPModule.receiveSignature(request.id, signature) + if (signer.walletType() === 'SCW') { + await XMTPModule.receiveSCWSignature( + request.id, + signatureString + ) + } else { + const eSig = splitSignature(signatureString) + const r = hexToBytes(eSig.r) + const s = hexToBytes(eSig.s) + const sigBytes = new Uint8Array(65) + sigBytes.set(r) + sigBytes.set(s, r.length) + sigBytes[64] = eSig.recoveryParam + + const signature = Buffer.from(sigBytes).toString('base64') + + await XMTPModule.receiveSignature(request.id, signature) + } } catch (e) { const errorMessage = 'ERROR in create. User rejected signature' console.info(errorMessage, e) @@ -379,7 +386,7 @@ export class Client< ) } ) - await XMTPModule.createOrBuild( + await XMTPModule.createV3( await signer.getAddress(), options.env, options.appVersion, @@ -389,7 +396,10 @@ export class Client< Boolean(options.enableV3), options.dbEncryptionKey, options.dbDirectory, - options.historySyncUrl + options.historySyncUrl, + signer.walletType(), + signer.getChainId(), + signer.getBlockNumber() ) })().catch((error) => { this.removeAllSubscriptions( @@ -402,6 +412,47 @@ export class Client< }) } + /** + * Builds a V3 ONLY instance of the Client class using the provided address and chainId if SCW. + * + * @param {string} address - The address of the account to build + * @param {Partial} opts - Configuration options for the Client. Must include an encryption key. + * @returns {Promise} A Promise that resolves to a new V3 ONLY Client instance. + * + * See {@link https://xmtp.org/docs/build/authentication#create-a-client | XMTP Docs} for more information. + */ + static async buildV3< + ContentCodecs extends DefaultContentTypes = DefaultContentTypes, + >( + address: string, + options: ClientOptions & { codecs?: ContentCodecs } + ): Promise> { + options.enableV3 = true + if ( + options.dbEncryptionKey === undefined || + options.dbEncryptionKey.length !== 32 + ) { + throw new Error('Must pass an encryption key that is exactly 32 bytes.') + } + const client = await XMTPModule.buildV3( + address, + options.env, + options.appVersion, + Boolean(options.enableV3), + options.dbEncryptionKey, + options.dbDirectory, + options.historySyncUrl + ) + + return new Client( + client['address'], + client['inboxId'], + client['installationId'], + client['dbPath'], + options.codecs || [] + ) + } + /** * Drop the client from memory. Use when you want to remove the client from memory and are done with it. */ diff --git a/src/lib/Signer.ts b/src/lib/Signer.ts index 3ef6a7c3f..efc994d27 100644 --- a/src/lib/Signer.ts +++ b/src/lib/Signer.ts @@ -1,7 +1,12 @@ import type { WalletClient } from 'viem' +export type WalletType = 'EOA' | 'SCW' + export interface Signer { getAddress: () => Promise + getChainId: () => number | undefined + getBlockNumber: () => number | undefined + walletType: () => WalletType | undefined signMessage: (message: string) => Promise } @@ -37,5 +42,8 @@ export function convertWalletClientToSigner( message: typeof message === 'string' ? message : { raw: message }, account, }), + getChainId: () => undefined, + getBlockNumber: () => undefined, + walletType: () => undefined, } }