diff --git a/example/src/main/java/org/xmtp/android/example/MainViewModel.kt b/example/src/main/java/org/xmtp/android/example/MainViewModel.kt index 7e0d30a2d..ffaa068ac 100644 --- a/example/src/main/java/org/xmtp/android/example/MainViewModel.kt +++ b/example/src/main/java/org/xmtp/android/example/MainViewModel.kt @@ -86,7 +86,7 @@ class MainViewModel : ViewModel() { MainListItem.Footer( id = "footer", ClientManager.client.address, - ClientManager.client.apiClient.environment.name + ClientManager.client.environment.name ) ) _uiState.value = UiState.Success(listItems) diff --git a/example/src/main/java/org/xmtp/android/example/connect/ConnectWalletViewModel.kt b/example/src/main/java/org/xmtp/android/example/connect/ConnectWalletViewModel.kt index 56903b37a..b773b5e88 100644 --- a/example/src/main/java/org/xmtp/android/example/connect/ConnectWalletViewModel.kt +++ b/example/src/main/java/org/xmtp/android/example/connect/ConnectWalletViewModel.kt @@ -90,7 +90,7 @@ class ConnectWalletViewModel(application: Application) : AndroidViewModel(applic Client.register(codec = GroupUpdatedCodec()) _uiState.value = ConnectUiState.Success( wallet.address, - PrivateKeyBundleV1Builder.encodeData(client.privateKeyBundleV1) + PrivateKeyBundleV1Builder.encodeData(client.v1keys) ) } catch (e: XMTPException) { _uiState.value = ConnectUiState.Error(e.message.orEmpty()) @@ -115,7 +115,7 @@ class ConnectWalletViewModel(application: Application) : AndroidViewModel(applic Client.register(codec = GroupUpdatedCodec()) _uiState.value = ConnectUiState.Success( wallet.address, - PrivateKeyBundleV1Builder.encodeData(client.privateKeyBundleV1) + PrivateKeyBundleV1Builder.encodeData(client.v1keys) ) } catch (e: Exception) { _uiState.value = ConnectUiState.Error(e.message.orEmpty()) diff --git a/library/src/androidTest/java/org/xmtp/android/library/ClientTest.kt b/library/src/androidTest/java/org/xmtp/android/library/ClientTest.kt index 27c77a294..7adc83892 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/ClientTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/ClientTest.kt @@ -29,8 +29,8 @@ class ClientTest { fun testHasPrivateKeyBundleV1() { val fakeWallet = PrivateKeyBuilder() val client = runBlocking { Client().create(account = fakeWallet) } - assertEquals(1, client.privateKeyBundleV1.preKeysList?.size) - val preKey = client.privateKeyBundleV1.preKeysList?.get(0) + assertEquals(1, client.v1keys.preKeysList?.size) + val preKey = client.v1keys.preKeysList?.get(0) assert(preKey?.publicKey?.hasSignature() ?: false) } @@ -56,12 +56,12 @@ class ClientTest { val clientFromV1Bundle = runBlocking { Client().buildFromBundle(bundle) } assertEquals(client.address, clientFromV1Bundle.address) assertEquals( - client.privateKeyBundleV1.identityKey, - clientFromV1Bundle.privateKeyBundleV1.identityKey, + client.v1keys.identityKey, + clientFromV1Bundle.v1keys.identityKey, ) assertEquals( - client.privateKeyBundleV1.preKeysList, - clientFromV1Bundle.privateKeyBundleV1.preKeysList, + client.v1keys.preKeysList, + clientFromV1Bundle.v1keys.preKeysList, ) } @@ -73,12 +73,12 @@ class ClientTest { val clientFromV1Bundle = runBlocking { Client().buildFromV1Bundle(bundleV1) } assertEquals(client.address, clientFromV1Bundle.address) assertEquals( - client.privateKeyBundleV1.identityKey, - clientFromV1Bundle.privateKeyBundleV1.identityKey, + client.v1keys.identityKey, + clientFromV1Bundle.v1keys.identityKey, ) assertEquals( - client.privateKeyBundleV1.preKeysList, - clientFromV1Bundle.privateKeyBundleV1.preKeysList, + client.v1keys.preKeysList, + clientFromV1Bundle.v1keys.preKeysList, ) } @@ -107,8 +107,8 @@ class ClientTest { } assertEquals(client.address, clientFromV1Bundle.address) assertEquals( - client.privateKeyBundleV1.identityKey, - clientFromV1Bundle.privateKeyBundleV1.identityKey, + client.v1keys.identityKey, + clientFromV1Bundle.v1keys.identityKey, ) runBlocking { @@ -146,6 +146,31 @@ class ClientTest { assertEquals(inboxId, client.inboxId) } + @Test + fun testCreatesAV3OnlyClient() { + val key = SecureRandom().generateSeed(32) + val context = InstrumentationRegistry.getInstrumentation().targetContext + val fakeWallet = PrivateKeyBuilder() + val options = ClientOptions( + ClientOptions.Api(XMTPEnvironment.LOCAL, false), + enableV3 = true, + appContext = context, + dbEncryptionKey = key + ) + val inboxId = runBlocking { Client.getOrCreateInboxId(options, fakeWallet.address) } + val client = runBlocking { + Client().createOrBuild( + account = fakeWallet, + options = options + ) + } + runBlocking { + client.canMessageV3(listOf(client.address))[client.address]?.let { assert(it) } + } + assert(client.installationId.isNotEmpty()) + assertEquals(inboxId, client.inboxId) + } + @Test fun testCanDeleteDatabase() { val key = SecureRandom().generateSeed(32) diff --git a/library/src/androidTest/java/org/xmtp/android/library/ConversationTest.kt b/library/src/androidTest/java/org/xmtp/android/library/ConversationTest.kt index 06752cf41..c520796b7 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/ConversationTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/ConversationTest.kt @@ -844,31 +844,31 @@ class ConversationTest { assertTrue("Bob should be allowed from conversation 2", isBobAllowed2) } + } - @Test - @Ignore("TODO: Fix Flaky Test") - fun testCanHaveImplicitConsentOnMessageSend() { - runBlocking { - val bobConversation = bobClient.conversations.newConversation(alice.walletAddress, null) - Thread.sleep(1000) - val isAllowed = bobConversation.consentState() == ConsentState.ALLOWED + @Test + @Ignore("TODO: Fix Flaky Test") + fun testCanHaveImplicitConsentOnMessageSend() { + runBlocking { + val bobConversation = bobClient.conversations.newConversation(alice.walletAddress, null) + Thread.sleep(1000) + val isAllowed = bobConversation.consentState() == ConsentState.ALLOWED - // Conversations you start should start as allowed - assertTrue("Bob convo should be allowed", isAllowed) + // Conversations you start should start as allowed + assertTrue("Bob convo should be allowed", isAllowed) - val aliceConversation = aliceClient.conversations.list()[0] - val isUnknown = aliceConversation.consentState() == ConsentState.UNKNOWN + val aliceConversation = aliceClient.conversations.list()[0] + val isUnknown = aliceConversation.consentState() == ConsentState.UNKNOWN - // Conversations you receive should start as unknown - assertTrue("Alice convo should be unknown", isUnknown) + // Conversations you receive should start as unknown + assertTrue("Alice convo should be unknown", isUnknown) - aliceConversation.send(content = "hey bob") - aliceClient.contacts.refreshConsentList() - val isNowAllowed = aliceConversation.consentState() == ConsentState.ALLOWED + aliceConversation.send(content = "hey bob") + aliceClient.contacts.refreshConsentList() + val isNowAllowed = aliceConversation.consentState() == ConsentState.ALLOWED - // Conversations you send a message to get marked as allowed - assertTrue("Should now be allowed", isNowAllowed) - } + // Conversations you send a message to get marked as allowed + assertTrue("Should now be allowed", isNowAllowed) } } diff --git a/library/src/androidTest/java/org/xmtp/android/library/ConversationsTest.kt b/library/src/androidTest/java/org/xmtp/android/library/ConversationsTest.kt index 15f2bae4b..3ee56c5e2 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/ConversationsTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/ConversationsTest.kt @@ -63,7 +63,7 @@ class ConversationsTest { val newWallet = PrivateKeyBuilder() val newClient = runBlocking { Client().create(account = newWallet) } val message = MessageV1Builder.buildEncode( - sender = newClient.privateKeyBundleV1, + sender = newClient.v1keys, recipient = fixtures.aliceClient.v1keys.toPublicKeyBundle(), message = TextCodec().encode(content = "hello").toByteArray(), timestamp = created @@ -160,6 +160,7 @@ class ConversationsTest { } @Test + @Ignore("TODO: Fix Flaky Test") fun testStreamTimeOutsAllMessages() { val boConversation = runBlocking { boClient.conversations.newConversation(alixClient.address) } diff --git a/library/src/androidTest/java/org/xmtp/android/library/LocalInstrumentedTest.kt b/library/src/androidTest/java/org/xmtp/android/library/LocalInstrumentedTest.kt index e16937119..c636fde56 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/LocalInstrumentedTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/LocalInstrumentedTest.kt @@ -352,7 +352,7 @@ class LocalInstrumentedTest { private fun publishLegacyContact(client: Client) { val contactBundle = Contact.ContactBundle.newBuilder().also { it.v1 = it.v1.toBuilder().apply { - keyBundle = client.privateKeyBundleV1.toPublicKeyBundle() + keyBundle = client.v1keys.toPublicKeyBundle() }.build() }.build() val envelope = Envelope.newBuilder().also { diff --git a/library/src/androidTest/java/org/xmtp/android/library/TestHelpers.kt b/library/src/androidTest/java/org/xmtp/android/library/TestHelpers.kt index 9e5a770ff..2542e543a 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/TestHelpers.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/TestHelpers.kt @@ -42,31 +42,30 @@ class FakeWallet : SigningKey { } data class Fixtures( - val aliceAccount: PrivateKeyBuilder, - val bobAccount: PrivateKeyBuilder, - val caroAccount: PrivateKeyBuilder, val clientOptions: ClientOptions? = ClientOptions( ClientOptions.Api(XMTPEnvironment.LOCAL, isSecure = false) ), ) { + val aliceAccount = PrivateKeyBuilder() + val bobAccount = PrivateKeyBuilder() + val caroAccount = PrivateKeyBuilder() + var alice: PrivateKey = aliceAccount.getPrivateKey() - var aliceClient: Client = runBlocking { Client().create(account = aliceAccount, options = clientOptions) } + var aliceClient: Client = + runBlocking { Client().create(account = aliceAccount, options = clientOptions) } + var bob: PrivateKey = bobAccount.getPrivateKey() - var bobClient: Client = runBlocking { Client().create(account = bobAccount, options = clientOptions) } - var caro: PrivateKey = caroAccount.getPrivateKey() - var caroClient: Client = runBlocking { Client().create(account = caroAccount, options = clientOptions) } + var bobClient: Client = + runBlocking { Client().create(account = bobAccount, options = clientOptions) } - constructor(clientOptions: ClientOptions?) : this( - aliceAccount = PrivateKeyBuilder(), - bobAccount = PrivateKeyBuilder(), - caroAccount = PrivateKeyBuilder(), - clientOptions = clientOptions - ) + var caro: PrivateKey = caroAccount.getPrivateKey() + var caroClient: Client = + runBlocking { Client().create(account = caroAccount, options = clientOptions) } fun publishLegacyContact(client: Client) { val contactBundle = ContactBundle.newBuilder().also { builder -> builder.v1 = builder.v1.toBuilder().also { - it.keyBundle = client.privateKeyBundleV1.toPublicKeyBundle() + it.keyBundle = client.v1keys.toPublicKeyBundle() }.build() }.build() val envelope = Envelope.newBuilder().apply { diff --git a/library/src/androidTest/java/org/xmtp/android/library/V3ClientTest.kt b/library/src/androidTest/java/org/xmtp/android/library/V3ClientTest.kt new file mode 100644 index 000000000..8b63f59a2 --- /dev/null +++ b/library/src/androidTest/java/org/xmtp/android/library/V3ClientTest.kt @@ -0,0 +1,212 @@ +package org.xmtp.android.library + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.junit.Assert +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.xmtp.android.library.messages.MessageDeliveryStatus +import org.xmtp.android.library.messages.PrivateKey +import org.xmtp.android.library.messages.PrivateKeyBuilder +import org.xmtp.android.library.messages.walletAddress +import java.security.SecureRandom + +@RunWith(AndroidJUnit4::class) +class V3ClientTest { + private lateinit var alixV2Wallet: PrivateKeyBuilder + private lateinit var boV3Wallet: PrivateKeyBuilder + private lateinit var alixV2: PrivateKey + private lateinit var alixV2Client: Client + private lateinit var boV3: PrivateKey + private lateinit var boV3Client: Client + private lateinit var caroV2V3Wallet: PrivateKeyBuilder + private lateinit var caroV2V3: PrivateKey + private lateinit var caroV2V3Client: Client + + @Before + fun setUp() { + val key = SecureRandom().generateSeed(32) + val context = InstrumentationRegistry.getInstrumentation().targetContext + + // Pure V2 + alixV2Wallet = PrivateKeyBuilder() + alixV2 = alixV2Wallet.getPrivateKey() + alixV2Client = runBlocking { + Client().create( + account = alixV2Wallet, + options = ClientOptions( + ClientOptions.Api(XMTPEnvironment.LOCAL, isSecure = false) + ) + ) + } + + // Pure V3 + boV3Wallet = PrivateKeyBuilder() + boV3 = boV3Wallet.getPrivateKey() + boV3Client = runBlocking { + Client().createOrBuild( + account = boV3Wallet, + options = ClientOptions( + ClientOptions.Api(XMTPEnvironment.LOCAL, false), + enableV3 = true, + appContext = context, + dbEncryptionKey = key + ) + ) + } + + // Both V3 & V2 + caroV2V3Wallet = PrivateKeyBuilder() + caroV2V3 = caroV2V3Wallet.getPrivateKey() + caroV2V3Client = + runBlocking { + Client().create( + account = caroV2V3Wallet, + options = ClientOptions( + ClientOptions.Api(XMTPEnvironment.LOCAL, false), + enableV3 = true, + appContext = context, + dbEncryptionKey = key + ) + ) + } + } + + @Test + fun testsCanCreateGroup() { + val group = runBlocking { boV3Client.conversations.newGroup(listOf(caroV2V3.walletAddress)) } + assertEquals( + group.members().map { it.inboxId }.sorted(), + listOf(caroV2V3Client.inboxId, boV3Client.inboxId).sorted() + ) + + Assert.assertThrows("Recipient not on network", XMTPException::class.java) { + runBlocking { boV3Client.conversations.newGroup(listOf(alixV2.walletAddress)) } + } + } + + @Test + fun testsCanSendMessages() { + val group = runBlocking { boV3Client.conversations.newGroup(listOf(caroV2V3.walletAddress)) } + runBlocking { group.send("howdy") } + val messageId = runBlocking { group.send("gm") } + runBlocking { group.sync() } + assertEquals(group.messages().first().body, "gm") + assertEquals(group.messages().first().id, messageId) + assertEquals(group.messages().first().deliveryStatus, MessageDeliveryStatus.PUBLISHED) + assertEquals(group.messages().size, 3) + + runBlocking { caroV2V3Client.conversations.syncGroups() } + val sameGroup = runBlocking { caroV2V3Client.conversations.listGroups().last() } + runBlocking { sameGroup.sync() } + assertEquals(sameGroup.messages().size, 2) + assertEquals(sameGroup.messages().first().body, "gm") + } + + @Test + fun testGroupConsent() { + runBlocking { + val group = boV3Client.conversations.newGroup(listOf(caroV2V3.walletAddress)) + assert(boV3Client.contacts.isGroupAllowed(group.id)) + assertEquals(group.consentState(), ConsentState.ALLOWED) + + boV3Client.contacts.denyGroups(listOf(group.id)) + assert(boV3Client.contacts.isGroupDenied(group.id)) + assertEquals(group.consentState(), ConsentState.DENIED) + + group.updateConsentState(ConsentState.ALLOWED) + assert(boV3Client.contacts.isGroupAllowed(group.id)) + assertEquals(group.consentState(), ConsentState.ALLOWED) + } + } + + @Test + fun testCanAllowAndDenyInboxId() { + runBlocking { + val boGroup = boV3Client.conversations.newGroup(listOf(caroV2V3.walletAddress)) + assert(!boV3Client.contacts.isInboxAllowed(caroV2V3Client.inboxId)) + assert(!boV3Client.contacts.isInboxDenied(caroV2V3Client.inboxId)) + + boV3Client.contacts.allowInboxes(listOf(caroV2V3Client.inboxId)) + var caroMember = boGroup.members().firstOrNull { it.inboxId == caroV2V3Client.inboxId } + assertEquals(caroMember!!.consentState, ConsentState.ALLOWED) + + assert(boV3Client.contacts.isInboxAllowed(caroV2V3Client.inboxId)) + assert(!boV3Client.contacts.isInboxDenied(caroV2V3Client.inboxId)) + assert(boV3Client.contacts.isAllowed(caroV2V3Client.address)) + assert(!boV3Client.contacts.isDenied(caroV2V3Client.address)) + + boV3Client.contacts.denyInboxes(listOf(caroV2V3Client.inboxId)) + caroMember = boGroup.members().firstOrNull { it.inboxId == caroV2V3Client.inboxId } + assertEquals(caroMember!!.consentState, ConsentState.DENIED) + + assert(!boV3Client.contacts.isInboxAllowed(caroV2V3Client.inboxId)) + assert(boV3Client.contacts.isInboxDenied(caroV2V3Client.inboxId)) + + // Cannot check inboxId for alix because they do not have an inboxID as V2 only client. + boV3Client.contacts.allow(listOf(alixV2Client.address)) + assert(boV3Client.contacts.isAllowed(alixV2Client.address)) + assert(!boV3Client.contacts.isDenied(alixV2Client.address)) + } + } + + @Test + fun testCanStreamAllMessagesFromV2andV3Users() { + val group = runBlocking { boV3Client.conversations.newGroup(listOf(caroV2V3.walletAddress)) } + val conversation = + runBlocking { alixV2Client.conversations.newConversation(caroV2V3.walletAddress) } + runBlocking { caroV2V3Client.conversations.syncGroups() } + + val allMessages = mutableListOf() + + val job = CoroutineScope(Dispatchers.IO).launch { + try { + caroV2V3Client.conversations.streamAllMessages(includeGroups = true) + .collect { message -> + allMessages.add(message) + } + } catch (e: Exception) { + } + } + Thread.sleep(1000) + runBlocking { + group.send("hi") + conversation.send("hi") + } + Thread.sleep(1000) + assertEquals(2, allMessages.size) + job.cancel() + } + + @Test + fun testCanStreamGroupsAndConversationsFromV2andV3Users() { + val allMessages = mutableListOf() + + val job = CoroutineScope(Dispatchers.IO).launch { + try { + caroV2V3Client.conversations.streamAll() + .collect { message -> + allMessages.add(message.topic) + } + } catch (e: Exception) { + } + } + Thread.sleep(1000) + + runBlocking { + alixV2Client.conversations.newConversation(caroV2V3.walletAddress) + Thread.sleep(1000) + boV3Client.conversations.newGroup(listOf(caroV2V3.walletAddress)) + } + + Thread.sleep(2000) + assertEquals(2, allMessages.size) + job.cancel() + } +} diff --git a/library/src/main/java/org/xmtp/android/library/Client.kt b/library/src/main/java/org/xmtp/android/library/Client.kt index cc483309a..f45146a68 100644 --- a/library/src/main/java/org/xmtp/android/library/Client.kt +++ b/library/src/main/java/org/xmtp/android/library/Client.kt @@ -83,10 +83,10 @@ data class ClientOptions( class Client() { lateinit var address: String - lateinit var privateKeyBundleV1: PrivateKeyBundleV1 - lateinit var apiClient: ApiClient lateinit var contacts: Contacts lateinit var conversations: Conversations + var privateKeyBundleV1: PrivateKeyBundleV1? = null + var apiClient: ApiClient? = null var logger: XMTPLogger = XMTPLogger() val libXMTPVersion: String = getVersionInfo() var installationId: String = "" @@ -94,6 +94,7 @@ class Client() { var dbPath: String = "" lateinit var inboxId: String var hasV2Client: Boolean = true + lateinit var environment: XMTPEnvironment companion object { private const val TAG = "Client" @@ -197,6 +198,28 @@ class Client() { this.dbPath = dbPath this.installationId = installationId this.inboxId = inboxId + this.hasV2Client = true + this.environment = apiClient.environment + } + + constructor( + address: String, + libXMTPClient: FfiXmtpClient, + dbPath: String, + installationId: String, + inboxId: String, + environment: XMTPEnvironment, + ) : this() { + this.address = address + this.contacts = Contacts(client = this) + this.v3Client = libXMTPClient + this.conversations = + Conversations(client = this, libXMTPConversations = libXMTPClient.conversations()) + this.dbPath = dbPath + this.installationId = installationId + this.inboxId = inboxId + this.hasV2Client = false + this.environment = environment } suspend fun buildFrom( @@ -266,6 +289,39 @@ class Client() { } } + // This is a V3 only feature + suspend fun createOrBuild( + account: SigningKey, + options: ClientOptions, + ): Client { + this.hasV2Client = false + val inboxId = getOrCreateInboxId(options, account.address) + + return try { + val (libXMTPClient, dbPath) = ffiXmtpClient( + options, + account, + options.appContext, + null, + account.address, + inboxId + ) + + libXMTPClient?.let { client -> + Client( + account.address, + client, + dbPath, + client.installationId().toHex(), + client.inboxId(), + options.api.env + ) + } ?: throw XMTPException("Error creating V3 client: libXMTPClient is null") + } catch (e: Exception) { + throw XMTPException("Error creating V3 client: ${e.message}", e) + } + } + suspend fun buildFromBundle( bundle: PrivateKeyBundle, options: ClientOptions? = null, @@ -318,7 +374,7 @@ class Client() { options: ClientOptions, account: SigningKey?, appContext: Context?, - privateKeyBundleV1: PrivateKeyBundleV1, + privateKeyBundleV1: PrivateKeyBundleV1?, address: String, inboxId: String, ): Pair { @@ -349,7 +405,7 @@ class Client() { accountAddress = accountAddress, inboxId = inboxId, nonce = 0.toULong(), - legacySignedPrivateKeyProto = privateKeyBundleV1.toV2().identityKey.toByteArray(), + legacySignedPrivateKeyProto = privateKeyBundleV1?.toV2()?.identityKey?.toByteArray(), historySyncUrl = options.historySyncUrl ) } else { @@ -437,7 +493,7 @@ class Client() { if (legacy) { val contactBundle = ContactBundle.newBuilder().also { it.v1 = it.v1.toBuilder().also { v1Builder -> - v1Builder.keyBundle = privateKeyBundleV1.toPublicKeyBundle() + v1Builder.keyBundle = v1keys.toPublicKeyBundle() }.build() }.build() @@ -477,11 +533,13 @@ class Client() { } suspend fun query(topic: Topic, pagination: Pagination? = null): QueryResponse { - return apiClient.queryTopic(topic = topic, pagination = pagination) + val client = apiClient ?: throw XMTPException("V2 only function") + return client.queryTopic(topic = topic, pagination = pagination) } suspend fun batchQuery(requests: List): BatchQueryResponse { - return apiClient.batchQuery(requests) + val client = apiClient ?: throw XMTPException("V2 only function") + return client.batchQuery(requests) } suspend fun subscribe( @@ -495,7 +553,8 @@ class Client() { request: FfiV2SubscribeRequest, callback: FfiV2SubscriptionCallback, ): FfiV2Subscription { - return apiClient.subscribe(request, callback) + val client = apiClient ?: throw XMTPException("V2 only function") + return client.subscribe(request, callback) } suspend fun fetchConversation( @@ -509,37 +568,34 @@ class Client() { } fun findGroup(groupId: String): Group? { - v3Client?.let { - try { - return Group(this, it.group(groupId.hexToByteArray())) - } catch (e: Exception) { - return null - } + val client = v3Client ?: throw XMTPException("Error no V3 client initialized") + try { + return Group(this, client.group(groupId.hexToByteArray())) + } catch (e: Exception) { + return null } - throw XMTPException("Error no V3 client initialized") } fun findMessage(messageId: String): MessageV3? { - v3Client?.let { - try { - return MessageV3(this, it.message(messageId.hexToByteArray())) - } catch (e: Exception) { - return null - } + val client = v3Client ?: throw XMTPException("Error no V3 client initialized") + return try { + MessageV3(this, client.message(messageId.hexToByteArray())) + } catch (e: Exception) { + null } - throw XMTPException("Error no V3 client initialized") } suspend fun publish(envelopes: List) { + val client = apiClient ?: throw XMTPException("V2 only function") val authorized = AuthorizedIdentity( address = address, - authorized = privateKeyBundleV1.identityKey.publicKey, - identity = privateKeyBundleV1.identityKey, + authorized = v1keys.identityKey.publicKey, + identity = v1keys.identityKey, ) val authToken = authorized.createAuthToken() - apiClient.setAuthToken(authToken) + client.setAuthToken(authToken) - apiClient.publish(envelopes = envelopes) + client.publish(envelopes = envelopes) } suspend fun ensureUserContactPublished() { @@ -618,17 +674,13 @@ class Client() { } suspend fun canMessageV3(addresses: List): Map { - v3Client?.let { - return it.canMessage(addresses) - } - throw XMTPException("Error no V3 client initialized") + return v3Client?.canMessage(addresses) + ?: throw XMTPException("Error no V3 client initialized") } suspend fun inboxIdFromAddress(address: String): String? { - v3Client?.let { - return it.findInboxId(address.lowercase()) - } - throw XMTPException("Error no V3 client initialized") + return v3Client?.findInboxId(address.lowercase()) + ?: throw XMTPException("Error no V3 client initialized") } fun deleteLocalDatabase() { @@ -652,29 +704,25 @@ class Client() { } suspend fun revokeAllOtherInstallations(signingKey: SigningKey) { - if (v3Client == null) throw XMTPException("Error no V3 client initialized") - v3Client?.let { client -> - val signatureRequest = client.revokeAllOtherInstallations() - signingKey.sign(signatureRequest.signatureText())?.let { - signatureRequest.addEcdsaSignature(it.rawData) - client.applySignatureRequest(signatureRequest) - } + val client = v3Client ?: throw XMTPException("Error no V3 client initialized") + val signatureRequest = client.revokeAllOtherInstallations() + signingKey.sign(signatureRequest.signatureText())?.let { + signatureRequest.addEcdsaSignature(it.rawData) + client.applySignatureRequest(signatureRequest) } } suspend fun inboxState(refreshFromNetwork: Boolean): InboxState { - v3Client?.let { - return InboxState(it.inboxState(refreshFromNetwork)) - } - throw XMTPException("Error no V3 client initialized") + val client = v3Client ?: throw XMTPException("Error no V3 client initialized") + return InboxState(client.inboxState(refreshFromNetwork)) } val privateKeyBundle: PrivateKeyBundle - get() = PrivateKeyBundleBuilder.buildFromV1Key(privateKeyBundleV1) + get() = PrivateKeyBundleBuilder.buildFromV1Key(v1keys) val v1keys: PrivateKeyBundleV1 - get() = privateKeyBundleV1 + get() = privateKeyBundleV1 ?: throw XMTPException("V2 only function") val keys: PrivateKeyBundleV2 - get() = privateKeyBundleV1.toV2() + get() = v1keys.toV2() } diff --git a/library/src/main/java/org/xmtp/android/library/Contacts.kt b/library/src/main/java/org/xmtp/android/library/Contacts.kt index dfe00e215..7584312ed 100644 --- a/library/src/main/java/org/xmtp/android/library/Contacts.kt +++ b/library/src/main/java/org/xmtp/android/library/Contacts.kt @@ -98,67 +98,67 @@ class ConsentList( val entries: MutableMap = mutableMapOf(), ) { private var lastFetched: Date? = null - private val publicKey = - client.privateKeyBundleV1.identityKey.publicKey.secp256K1Uncompressed.bytes - private val privateKey = client.privateKeyBundleV1.identityKey.secp256K1.bytes - - private val identifier: String = - uniffi.xmtpv3.generatePrivatePreferencesTopicIdentifier( - privateKey.toByteArray(), - ) @OptIn(ExperimentalUnsignedTypes::class) suspend fun load(): List { - val newDate = Date() - val envelopes = - client.apiClient.envelopes( - Topic.preferenceList(identifier).description, - Pagination( - after = lastFetched, - direction = MessageApiOuterClass.SortDirection.SORT_DIRECTION_ASCENDING, - limit = 500 - ), - ) - - lastFetched = newDate - val preferences: MutableList = mutableListOf() - for (envelope in envelopes) { - val payload = - uniffi.xmtpv3.userPreferencesDecrypt( - publicKey.toByteArray(), + if (client.hasV2Client) { + val newDate = Date() + val publicKey = + client.v1keys.identityKey.publicKey.secp256K1Uncompressed.bytes + val privateKey = client.v1keys.identityKey.secp256K1.bytes + val identifier: String = + uniffi.xmtpv3.generatePrivatePreferencesTopicIdentifier( privateKey.toByteArray(), - envelope.message.toByteArray(), ) - - preferences.add( - PrivatePreferencesAction.parseFrom( - payload.toUByteArray().toByteArray(), + val envelopes = + client.apiClient!!.envelopes( + Topic.preferenceList(identifier).description, + Pagination( + after = lastFetched, + direction = MessageApiOuterClass.SortDirection.SORT_DIRECTION_ASCENDING, + limit = 500 + ), ) - ) - } - preferences.iterator().forEach { preference -> - preference.allowAddress?.walletAddressesList?.forEach { address -> - allow(address) - } - preference.denyAddress?.walletAddressesList?.forEach { address -> - deny(address) - } - preference.allowGroup?.groupIdsList?.forEach { groupId -> - allowGroup(groupId) - } - preference.denyGroup?.groupIdsList?.forEach { groupId -> - denyGroup(groupId) + lastFetched = newDate + val preferences: MutableList = mutableListOf() + for (envelope in envelopes) { + val payload = + uniffi.xmtpv3.userPreferencesDecrypt( + publicKey.toByteArray(), + privateKey.toByteArray(), + envelope.message.toByteArray(), + ) + + preferences.add( + PrivatePreferencesAction.parseFrom( + payload.toUByteArray().toByteArray(), + ) + ) } - preference.allowInboxId?.inboxIdsList?.forEach { inboxId -> - allowInboxId(inboxId) - } - preference.denyInboxId?.inboxIdsList?.forEach { inboxId -> - denyInboxId(inboxId) + preferences.iterator().forEach { preference -> + preference.allowAddress?.walletAddressesList?.forEach { address -> + allow(address) + } + preference.denyAddress?.walletAddressesList?.forEach { address -> + deny(address) + } + preference.allowGroup?.groupIdsList?.forEach { groupId -> + allowGroup(groupId) + } + preference.denyGroup?.groupIdsList?.forEach { groupId -> + denyGroup(groupId) + } + + preference.allowInboxId?.inboxIdsList?.forEach { inboxId -> + allowInboxId(inboxId) + } + preference.denyInboxId?.inboxIdsList?.forEach { inboxId -> + denyInboxId(inboxId) + } } } - return entries.values.toList() } @@ -204,6 +204,14 @@ class ConsentList( } }.build() + val publicKey = + client.v1keys.identityKey.publicKey.secp256K1Uncompressed.bytes + val privateKey = client.v1keys.identityKey.secp256K1.bytes + val identifier: String = + uniffi.xmtpv3.generatePrivatePreferencesTopicIdentifier( + privateKey.toByteArray(), + ) + val message = uniffi.xmtpv3.userPreferencesEncrypt( publicKey.toByteArray(), diff --git a/library/src/main/java/org/xmtp/android/library/ConversationV1.kt b/library/src/main/java/org/xmtp/android/library/ConversationV1.kt index 3ce2b7a1a..2d031b5fe 100644 --- a/library/src/main/java/org/xmtp/android/library/ConversationV1.kt +++ b/library/src/main/java/org/xmtp/android/library/ConversationV1.kt @@ -76,7 +76,8 @@ data class ConversationV1( ): List { val pagination = Pagination(limit = limit, before = before, after = after, direction = direction) - val result = client.apiClient.envelopes(topic = topic.description, pagination = pagination) + val apiClient = client.apiClient ?: throw XMTPException("V2 only function") + val result = apiClient.envelopes(topic = topic.description, pagination = pagination) return result.mapNotNull { envelope -> decodeOrNull(envelope = envelope) @@ -105,8 +106,9 @@ data class ConversationV1( val pagination = Pagination(limit = limit, before = before, after = after, direction = direction) + val apiClient = client.apiClient ?: throw XMTPException("V2 only function") val envelopes = - client.apiClient.envelopes( + apiClient.envelopes( topic = Topic.directMessageV1(client.address, peerAddress).description, pagination = pagination, ) @@ -238,7 +240,7 @@ data class ConversationV1( } val date = Date() val message = MessageV1Builder.buildEncode( - sender = client.privateKeyBundleV1, + sender = client.v1keys, recipient = recipient, message = encodedContent.toByteArray(), timestamp = date, diff --git a/library/src/main/java/org/xmtp/android/library/ConversationV2.kt b/library/src/main/java/org/xmtp/android/library/ConversationV2.kt index e021226ef..981934d8f 100644 --- a/library/src/main/java/org/xmtp/android/library/ConversationV2.kt +++ b/library/src/main/java/org/xmtp/android/library/ConversationV2.kt @@ -85,8 +85,9 @@ data class ConversationV2( ): List { val pagination = Pagination(limit = limit, before = before, after = after, direction = direction) + val apiClient = client.apiClient ?: throw XMTPException("V2 only function") val result = - client.apiClient.envelopes( + apiClient.envelopes( topic = topic, pagination = pagination, ) @@ -117,7 +118,8 @@ data class ConversationV2( ): List { val pagination = Pagination(limit = limit, before = before, after = after, direction = direction) - val envelopes = client.apiClient.envelopes(topic, pagination) + val apiClient = client.apiClient ?: throw XMTPException("V2 only function") + val envelopes = apiClient.envelopes(topic, pagination) return envelopes.map { envelope -> decrypt(envelope) diff --git a/library/src/main/java/org/xmtp/android/library/Conversations.kt b/library/src/main/java/org/xmtp/android/library/Conversations.kt index 6a46810b9..b213f8d1c 100644 --- a/library/src/main/java/org/xmtp/android/library/Conversations.kt +++ b/library/src/main/java/org/xmtp/android/library/Conversations.kt @@ -136,7 +136,6 @@ data class Conversations( groupImageUrlSquare: String = "", groupDescription: String = "", groupPinnedFrameUrl: String = "", - ): Group { return newGroupInternal( accountAddresses, @@ -156,7 +155,7 @@ data class Conversations( groupImageUrlSquare: String, groupDescription: String, groupPinnedFrameUrl: String, - permissionsPolicySet: FfiPermissionPolicySet? + permissionsPolicySet: FfiPermissionPolicySet?, ): Group { if (accountAddresses.size == 1 && accountAddresses.first().lowercase() == client.address.lowercase() @@ -435,7 +434,8 @@ data class Conversations( } private suspend fun listIntroductionPeers(pagination: Pagination? = null): Map { - val envelopes = client.apiClient.queryTopic( + val apiClient = client.apiClient ?: throw XMTPException("V2 only function") + val envelopes = apiClient.queryTopic( topic = Topic.userIntro(client.address), pagination = pagination, ).envelopesList @@ -475,8 +475,9 @@ data class Conversations( * @return List of [SealedInvitation] that are inside of the range specified by [pagination] */ private suspend fun listInvitations(pagination: Pagination? = null): List { + val apiClient = client.apiClient ?: throw XMTPException("V2 only function") val envelopes = - client.apiClient.envelopes(Topic.userInvite(client.address).description, pagination) + apiClient.envelopes(Topic.userInvite(client.address).description, pagination) return envelopes.map { envelope -> SealedInvitation.parseFrom(envelope.message) } diff --git a/library/src/main/java/org/xmtp/android/library/messages/MessageV2.kt b/library/src/main/java/org/xmtp/android/library/messages/MessageV2.kt index 00fd92f1b..c8af884aa 100644 --- a/library/src/main/java/org/xmtp/android/library/messages/MessageV2.kt +++ b/library/src/main/java/org/xmtp/android/library/messages/MessageV2.kt @@ -148,7 +148,7 @@ class MessageV2Builder(val senderHmac: ByteArray? = null, val shouldPush: Boolea val digest = Hash.sha256(headerBytes + payload) val preKey = client.keys.preKeysList?.get(0) val signature = preKey?.sign(digest) - val bundle = client.privateKeyBundleV1.toV2().getPublicKeyBundle() + val bundle = client.v1keys.toV2().getPublicKeyBundle() val signedContent = SignedContentBuilder.builderFromPayload(payload, bundle, signature) val signedBytes = signedContent.toByteArray() val ciphertext = Crypto.encrypt(keyMaterial, signedBytes, additionalData = headerBytes)