From ed5b11a059d8db86fb8368d93b8eb3577b2e9ffb Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Sun, 3 Nov 2024 10:53:25 -0800 Subject: [PATCH 1/2] remove decrypted messages and only use decoded --- .../org/xmtp/android/library/CodecTest.kt | 8 - .../java/org/xmtp/android/library/DmTest.kt | 53 +----- .../org/xmtp/android/library/GroupTest.kt | 97 +--------- .../org/xmtp/android/library/ReactionTest.kt | 2 - .../library/SmartContractWalletTest.kt | 4 +- .../org/xmtp/android/library/V3ClientTest.kt | 33 +--- .../java/org/xmtp/android/library/Client.kt | 16 +- .../org/xmtp/android/library/Conversation.kt | 44 +---- .../org/xmtp/android/library/Conversations.kt | 66 +------ .../xmtp/android/library/DecodedMessage.kt | 2 +- .../main/java/org/xmtp/android/library/Dm.kt | 59 +----- .../java/org/xmtp/android/library/Group.kt | 59 +----- .../libxmtp/{MessageV3.kt => Message.kt} | 27 +-- .../library/messages/DecryptedMessage.kt | 13 -- .../xmtp/android/library/messages/Message.kt | 33 ---- .../library/messages/MessageHeaderV1.kt | 19 -- .../library/messages/MessageHeaderv2.kt | 16 -- .../android/library/messages/MessageV1.kt | 84 --------- .../android/library/messages/MessageV2.kt | 174 ------------------ 19 files changed, 36 insertions(+), 773 deletions(-) rename library/src/main/java/org/xmtp/android/library/libxmtp/{MessageV3.kt => Message.kt} (74%) delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/DecryptedMessage.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/Message.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/MessageHeaderV1.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/MessageHeaderv2.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/MessageV1.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/MessageV2.kt diff --git a/library/src/androidTest/java/org/xmtp/android/library/CodecTest.kt b/library/src/androidTest/java/org/xmtp/android/library/CodecTest.kt index d15ba7603..1f65c045e 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/CodecTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/CodecTest.kt @@ -4,21 +4,13 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.protobuf.kotlin.toByteStringUtf8 import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith -import org.xmtp.android.library.Crypto.Companion.verifyHmacSignature import org.xmtp.android.library.codecs.ContentCodec import org.xmtp.android.library.codecs.ContentTypeId import org.xmtp.android.library.codecs.ContentTypeIdBuilder import org.xmtp.android.library.codecs.EncodedContent -import org.xmtp.android.library.codecs.TextCodec -import org.xmtp.android.library.messages.InvitationV1ContextBuilder -import org.xmtp.android.library.messages.MessageV2Builder -import org.xmtp.android.library.messages.PrivateKeyBuilder import org.xmtp.android.library.messages.walletAddress -import java.time.Instant data class NumberCodec( override var contentType: ContentTypeId = ContentTypeIdBuilder.builderFromAuthorityId( diff --git a/library/src/androidTest/java/org/xmtp/android/library/DmTest.kt b/library/src/androidTest/java/org/xmtp/android/library/DmTest.kt index 4c64ba832..71a388b93 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/DmTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/DmTest.kt @@ -17,8 +17,7 @@ import org.xmtp.android.library.codecs.Reaction import org.xmtp.android.library.codecs.ReactionAction import org.xmtp.android.library.codecs.ReactionCodec import org.xmtp.android.library.codecs.ReactionSchema -import org.xmtp.android.library.messages.DecryptedMessage -import org.xmtp.android.library.messages.MessageDeliveryStatus +import org.xmtp.android.library.libxmtp.Message.* import org.xmtp.android.library.messages.PrivateKey import org.xmtp.android.library.messages.PrivateKeyBuilder import org.xmtp.android.library.messages.walletAddress @@ -277,56 +276,6 @@ class DmTest { job.cancel() } - @Test - fun testCanStreamDecryptedDmMessages() = kotlinx.coroutines.test.runTest { - val dm = boClient.conversations.findOrCreateDm(alix.walletAddress) - alixClient.conversations.syncConversations() - val alixDm = alixClient.findDm(bo.walletAddress) - dm.streamDecryptedMessages().test { - alixDm?.send("hi") - assertEquals("hi", awaitItem().encodedContent.content.toStringUtf8()) - alixDm?.send("hi again") - assertEquals("hi again", awaitItem().encodedContent.content.toStringUtf8()) - } - } - - @Test - fun testCanStreamAllDecryptedDmMessages() { - val dm = runBlocking { boClient.conversations.findOrCreateDm(alix.walletAddress) } - runBlocking { alixClient.conversations.syncConversations() } - - val allMessages = mutableListOf() - - val job = CoroutineScope(Dispatchers.IO).launch { - try { - alixClient.conversations.streamAllDecryptedMessages().collect { message -> - allMessages.add(message) - } - } catch (e: Exception) { - } - } - Thread.sleep(2500) - - for (i in 0 until 2) { - runBlocking { dm.send(text = "Message $i") } - Thread.sleep(100) - } - assertEquals(2, allMessages.size) - - val caroDm = - runBlocking { caroClient.conversations.findOrCreateDm(alixClient.address) } - Thread.sleep(2500) - - for (i in 0 until 2) { - runBlocking { caroDm.send(text = "Message $i") } - Thread.sleep(100) - } - - assertEquals(4, allMessages.size) - - job.cancel() - } - @Test fun testCanStreamConversations() = kotlinx.coroutines.test.runTest { boClient.conversations.stream().test { diff --git a/library/src/androidTest/java/org/xmtp/android/library/GroupTest.kt b/library/src/androidTest/java/org/xmtp/android/library/GroupTest.kt index 6309a758d..b5b0f63c3 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/GroupTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/GroupTest.kt @@ -19,8 +19,7 @@ import org.xmtp.android.library.codecs.Reaction import org.xmtp.android.library.codecs.ReactionAction import org.xmtp.android.library.codecs.ReactionCodec import org.xmtp.android.library.codecs.ReactionSchema -import org.xmtp.android.library.messages.DecryptedMessage -import org.xmtp.android.library.messages.MessageDeliveryStatus +import org.xmtp.android.library.libxmtp.Message.* import org.xmtp.android.library.messages.PrivateKey import org.xmtp.android.library.messages.PrivateKeyBuilder import org.xmtp.android.library.messages.walletAddress @@ -336,13 +335,13 @@ class GroupTest { fun testMessageTimeIsCorrect() { val alixGroup = runBlocking { alixClient.conversations.newGroup(listOf(boClient.address)) } runBlocking { alixGroup.send("Hello") } - assertEquals(alixGroup.decryptedMessages().size, 2) + assertEquals(alixGroup.messages().size, 2) runBlocking { alixGroup.sync() } - val message2 = alixGroup.decryptedMessages().last() + val message2 = alixGroup.messages().last() runBlocking { alixGroup.sync() } - val message3 = alixGroup.decryptedMessages().last() + val message3 = alixGroup.messages().last() assertEquals(message3.id, message2.id) - assertEquals(message3.sentAt.time, message2.sentAt.time) + assertEquals(message3.sent.time, message2.sent.time) } @Test @@ -665,92 +664,6 @@ class GroupTest { job.cancel() } - @Test - fun testCanStreamDecryptedGroupMessages() = kotlinx.coroutines.test.runTest { - val group = boClient.conversations.newGroup(listOf(alix.walletAddress)) - alixClient.conversations.syncConversations() - val alixGroup = alixClient.conversations.listGroups().first() - group.streamDecryptedMessages().test { - alixGroup.send("hi") - assertEquals("hi", awaitItem().encodedContent.content.toStringUtf8()) - alixGroup.send("hi again") - assertEquals("hi again", awaitItem().encodedContent.content.toStringUtf8()) - } - } - - @Test - fun testCanStreamAllDecryptedGroupMessages() { - val group = runBlocking { caroClient.conversations.newGroup(listOf(alix.walletAddress)) } - val dm = runBlocking { davonV3Client.conversations.findOrCreateDm(alix.walletAddress) } - runBlocking { alixClient.conversations.syncConversations() } - - val allMessages = mutableListOf() - - val job = CoroutineScope(Dispatchers.IO).launch { - try { - alixClient.conversations.streamAllDecryptedMessages().collect { message -> - allMessages.add(message) - } - } catch (e: Exception) { - } - } - Thread.sleep(2500) - - runBlocking { dm.send("Should not stream") } - for (i in 0 until 2) { - runBlocking { group.send(text = "Message $i") } - Thread.sleep(100) - } - assertEquals(2, allMessages.size) - - val caroGroup = - runBlocking { caroClient.conversations.newGroup(listOf(alixClient.address)) } - Thread.sleep(2500) - - for (i in 0 until 2) { - runBlocking { caroGroup.send(text = "Message $i") } - Thread.sleep(100) - } - - assertEquals(4, allMessages.size) - - job.cancel() - } - - @Test - fun testCanStreamAllDecryptedMessages() { - val group = runBlocking { caroClient.conversations.newGroup(listOf(alix.walletAddress)) } - val dm = runBlocking { davonV3Client.conversations.findOrCreateDm(alix.walletAddress) } - val conversation = - runBlocking { boClient.conversations.newConversation(alix.walletAddress) } - runBlocking { alixClient.conversations.syncConversations() } - - val allMessages = mutableListOf() - - val job = CoroutineScope(Dispatchers.IO).launch { - try { - alixClient.conversations.streamAllDecryptedMessages() - .collect { message -> - allMessages.add(message) - } - } catch (e: Exception) { - } - } - Thread.sleep(2500) - - runBlocking { - dm.send("should not stream") - group.send("hi") - conversation.send("hi") - } - - Thread.sleep(1000) - - assertEquals(2, allMessages.size) - - job.cancel() - } - @Test fun testCanStreamGroups() = kotlinx.coroutines.test.runTest { boClient.conversations.stream().test { diff --git a/library/src/androidTest/java/org/xmtp/android/library/ReactionTest.kt b/library/src/androidTest/java/org/xmtp/android/library/ReactionTest.kt index 1d7451ae0..4ccd65486 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/ReactionTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/ReactionTest.kt @@ -4,7 +4,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.protobuf.kotlin.toByteStringUtf8 import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.xmtp.android.library.codecs.ContentTypeReaction @@ -13,7 +12,6 @@ import org.xmtp.android.library.codecs.Reaction import org.xmtp.android.library.codecs.ReactionAction import org.xmtp.android.library.codecs.ReactionCodec import org.xmtp.android.library.codecs.ReactionSchema -import org.xmtp.android.library.messages.MessageV2Builder import org.xmtp.android.library.messages.walletAddress @RunWith(AndroidJUnit4::class) diff --git a/library/src/androidTest/java/org/xmtp/android/library/SmartContractWalletTest.kt b/library/src/androidTest/java/org/xmtp/android/library/SmartContractWalletTest.kt index cb71d3d6f..15ebf5436 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/SmartContractWalletTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/SmartContractWalletTest.kt @@ -10,7 +10,7 @@ import org.junit.Assert.assertEquals import org.junit.BeforeClass import org.junit.Test import org.junit.runner.RunWith -import org.xmtp.android.library.messages.MessageDeliveryStatus +import org.xmtp.android.library.libxmtp.Message import org.xmtp.android.library.messages.PrivateKey import org.xmtp.android.library.messages.PrivateKeyBuilder import org.xmtp.android.library.messages.walletAddress @@ -130,7 +130,7 @@ class SmartContractWalletTest { runBlocking { boGroup.sync() } assertEquals(boGroup.messages().first().body, "gm") assertEquals(boGroup.messages().first().id, messageId) - assertEquals(boGroup.messages().first().deliveryStatus, MessageDeliveryStatus.PUBLISHED) + assertEquals(boGroup.messages().first().deliveryStatus, Message.MessageDeliveryStatus.PUBLISHED) assertEquals(boGroup.messages().size, 3) runBlocking { davonSCWClient.conversations.syncConversations() } diff --git a/library/src/androidTest/java/org/xmtp/android/library/V3ClientTest.kt b/library/src/androidTest/java/org/xmtp/android/library/V3ClientTest.kt index d2f6f49e2..17e0051b6 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/V3ClientTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/V3ClientTest.kt @@ -11,8 +11,8 @@ import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.xmtp.android.library.messages.DecryptedMessage -import org.xmtp.android.library.messages.MessageDeliveryStatus +import org.xmtp.android.library.libxmtp.Message +import org.xmtp.android.library.libxmtp.Message.* import org.xmtp.android.library.messages.PrivateKey import org.xmtp.android.library.messages.PrivateKeyBuilder import org.xmtp.android.library.messages.walletAddress @@ -313,35 +313,6 @@ class V3ClientTest { job.cancel() } - @Test - fun testCanStreamAllDecryptedMessagesFromV3Users() { - val group = - runBlocking { caroV2V3Client.conversations.newGroup(listOf(boV3.walletAddress)) } - val conversation = - runBlocking { boV3Client.conversations.findOrCreateDm(caroV2V3.walletAddress) } - runBlocking { boV3Client.conversations.syncConversations() } - - val allMessages = mutableListOf() - - val job = CoroutineScope(Dispatchers.IO).launch { - try { - boV3Client.conversations.streamAllDecryptedMessages() - .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 testCanStreamGroupsAndConversationsFromV3Users() { val allMessages = mutableListOf() 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 f91ba3920..7510e5d13 100644 --- a/library/src/main/java/org/xmtp/android/library/Client.kt +++ b/library/src/main/java/org/xmtp/android/library/Client.kt @@ -1,28 +1,23 @@ package org.xmtp.android.library import android.content.Context -import android.os.Build import android.util.Log -import com.google.crypto.tink.subtle.Base64 -import com.google.gson.GsonBuilder import kotlinx.coroutines.runBlocking import org.web3j.crypto.Keys import org.web3j.crypto.Keys.toChecksumAddress import org.xmtp.android.library.codecs.ContentCodec import org.xmtp.android.library.codecs.TextCodec -import org.xmtp.android.library.libxmtp.MessageV3 +import org.xmtp.android.library.libxmtp.Message import org.xmtp.android.library.libxmtp.XMTPLogger import org.xmtp.android.library.messages.ContactBundle import org.xmtp.android.library.messages.EncryptedPrivateKeyBundle import org.xmtp.android.library.messages.Envelope import org.xmtp.android.library.messages.EnvelopeBuilder -import org.xmtp.android.library.messages.InvitationV1ContextBuilder import org.xmtp.android.library.messages.Pagination import org.xmtp.android.library.messages.PrivateKeyBundle import org.xmtp.android.library.messages.PrivateKeyBundleBuilder import org.xmtp.android.library.messages.PrivateKeyBundleV1 import org.xmtp.android.library.messages.PrivateKeyBundleV2 -import org.xmtp.android.library.messages.SealedInvitationHeaderV1 import org.xmtp.android.library.messages.Topic import org.xmtp.android.library.messages.decrypted import org.xmtp.android.library.messages.encrypted @@ -48,12 +43,7 @@ import uniffi.xmtpv3.getInboxIdForAddress import uniffi.xmtpv3.getVersionInfo import uniffi.xmtpv3.org.xmtp.android.library.libxmtp.InboxState import java.io.File -import java.nio.charset.StandardCharsets -import java.text.SimpleDateFormat -import java.time.Instant import java.util.Date -import java.util.Locale -import java.util.TimeZone typealias PublishResponse = org.xmtp.proto.message.api.v1.MessageApiOuterClass.PublishResponse typealias QueryResponse = org.xmtp.proto.message.api.v1.MessageApiOuterClass.QueryResponse @@ -640,10 +630,10 @@ class Client() { } } - fun findMessage(messageId: String): MessageV3? { + fun findMessage(messageId: String): Message? { val client = v3Client ?: throw XMTPException("Error no V3 client initialized") return try { - MessageV3(this, client.message(messageId.hexToByteArray())) + Message(this, client.message(messageId.hexToByteArray())) } catch (e: Exception) { null } diff --git a/library/src/main/java/org/xmtp/android/library/Conversation.kt b/library/src/main/java/org/xmtp/android/library/Conversation.kt index 26c6ca0e5..81319ac6f 100644 --- a/library/src/main/java/org/xmtp/android/library/Conversation.kt +++ b/library/src/main/java/org/xmtp/android/library/Conversation.kt @@ -1,20 +1,12 @@ package org.xmtp.android.library -import android.util.Log -import com.google.protobuf.kotlin.toByteString import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.runBlocking import org.xmtp.android.library.codecs.EncodedContent import org.xmtp.android.library.libxmtp.Member -import org.xmtp.android.library.libxmtp.MessageV3 -import org.xmtp.android.library.messages.DecryptedMessage -import org.xmtp.android.library.messages.Envelope +import org.xmtp.android.library.libxmtp.Message import org.xmtp.android.library.messages.PagingInfoSortDirection -import org.xmtp.proto.keystore.api.v1.Keystore.TopicMap.TopicData import org.xmtp.proto.message.api.v1.MessageApiOuterClass -import org.xmtp.proto.message.contents.Invitation import org.xmtp.proto.message.contents.Invitation.ConsentProofPayload -import org.xmtp.proto.message.contents.Invitation.InvitationV1.Aes256gcmHkdfsha256 import java.util.Date /** @@ -138,7 +130,7 @@ sealed class Conversation { * If [limit] is specified then results are pulled in pages of that size. * If [direction] is specified then that will control the sort order of te messages. */ - suspend fun messages( + fun messages( limit: Int? = null, before: Date? = null, after: Date? = null, @@ -158,35 +150,14 @@ sealed class Conversation { } } - suspend fun decryptedMessages( - limit: Int? = null, - before: Date? = null, - after: Date? = null, - direction: PagingInfoSortDirection = MessageApiOuterClass.SortDirection.SORT_DIRECTION_DESCENDING, - ): List { - return when (this) { - is Group -> group.decryptedMessages(limit, before, after, direction) - is Dm -> dm.decryptedMessages(limit, before, after, direction) - } - } - - fun decrypt( - message: MessageV3, - ): DecryptedMessage { - return when (this) { - is Group -> message.decrypt() - is Dm -> message.decrypt() - } - } - - fun decode(message: MessageV3): DecodedMessage { + fun decode(message: Message): DecodedMessage { return when (this) { is Group -> message.decode() is Dm -> message.decode() } } - suspend fun processMessage(envelopeBytes: ByteArray): MessageV3 { + suspend fun processMessage(envelopeBytes: ByteArray): Message { return when (this) { is Group -> group.processMessage(envelopeBytes) is Dm -> dm.processMessage(envelopeBytes) @@ -220,11 +191,4 @@ sealed class Conversation { is Dm -> dm.streamMessages() } } - - fun streamDecryptedMessages(): Flow { - return when (this) { - is Group -> group.streamDecryptedMessages() - is Dm -> dm.streamDecryptedMessages() - } - } } 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 2c65f379e..af73066c2 100644 --- a/library/src/main/java/org/xmtp/android/library/Conversations.kt +++ b/library/src/main/java/org/xmtp/android/library/Conversations.kt @@ -1,48 +1,18 @@ package org.xmtp.android.library import android.util.Log -import com.google.protobuf.kotlin.toByteString import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.merge -import kotlinx.coroutines.launch import org.xmtp.android.library.ConsentState.Companion.toFfiConsentState -import org.xmtp.android.library.GRPCApiClient.Companion.makeQueryRequest -import org.xmtp.android.library.Util.Companion.envelopeFromFFi -import org.xmtp.android.library.libxmtp.MessageV3 -import org.xmtp.android.library.messages.DecryptedMessage -import org.xmtp.android.library.messages.Envelope -import org.xmtp.android.library.messages.EnvelopeBuilder -import org.xmtp.android.library.messages.InvitationV1 -import org.xmtp.android.library.messages.MessageV1Builder -import org.xmtp.android.library.messages.Pagination -import org.xmtp.android.library.messages.SealedInvitation -import org.xmtp.android.library.messages.SealedInvitationBuilder -import org.xmtp.android.library.messages.SignedPublicKeyBundle -import org.xmtp.android.library.messages.Topic -import org.xmtp.android.library.messages.createDeterministic -import org.xmtp.android.library.messages.decrypt -import org.xmtp.android.library.messages.getInvitation -import org.xmtp.android.library.messages.header -import org.xmtp.android.library.messages.involves -import org.xmtp.android.library.messages.recipientAddress -import org.xmtp.android.library.messages.senderAddress -import org.xmtp.android.library.messages.sentAt -import org.xmtp.android.library.messages.toSignedPublicKeyBundle -import org.xmtp.android.library.messages.walletAddress +import org.xmtp.android.library.libxmtp.Message import org.xmtp.proto.keystore.api.v1.Keystore -import org.xmtp.proto.keystore.api.v1.Keystore.GetConversationHmacKeysResponse.HmacKeyData -import org.xmtp.proto.keystore.api.v1.Keystore.GetConversationHmacKeysResponse.HmacKeys -import org.xmtp.proto.keystore.api.v1.Keystore.TopicMap.TopicData -import org.xmtp.proto.message.contents.Contact import org.xmtp.proto.message.contents.Invitation import uniffi.xmtpv3.FfiConversation import uniffi.xmtpv3.FfiConversationCallback import uniffi.xmtpv3.FfiConversations import uniffi.xmtpv3.FfiCreateGroupOptions import uniffi.xmtpv3.FfiDirection -import uniffi.xmtpv3.FfiEnvelope import uniffi.xmtpv3.FfiGroupPermissionsOptions import uniffi.xmtpv3.FfiListConversationsOptions import uniffi.xmtpv3.FfiListMessagesOptions @@ -50,10 +20,6 @@ import uniffi.xmtpv3.FfiMessage import uniffi.xmtpv3.FfiMessageCallback import uniffi.xmtpv3.FfiPermissionPolicySet import uniffi.xmtpv3.FfiSubscribeException -import uniffi.xmtpv3.FfiV2SubscribeRequest -import uniffi.xmtpv3.FfiV2Subscription -import uniffi.xmtpv3.FfiV2SubscriptionCallback -import uniffi.xmtpv3.NoPointer import uniffi.xmtpv3.org.xmtp.android.library.libxmtp.GroupPermissionPreconfiguration import uniffi.xmtpv3.org.xmtp.android.library.libxmtp.PermissionPolicySet import java.util.Date @@ -379,7 +345,7 @@ data class Conversations( val messageCallback = object : FfiMessageCallback { override fun onMessage(message: FfiMessage) { val conversation = client.findConversation(message.convoId.toHex()) - val decodedMessage = MessageV3(client, message).decodeOrNull() + val decodedMessage = Message(client, message).decodeOrNull() when (conversation?.version) { Conversation.Version.DM -> { decodedMessage?.let { trySend(it) } @@ -400,32 +366,4 @@ data class Conversations( awaitClose { stream.end() } } - - fun streamAllDecryptedMessages(): Flow = callbackFlow { - val messageCallback = object : FfiMessageCallback { - override fun onMessage(message: FfiMessage) { - val conversation = client.findConversation(message.convoId.toHex()) - val decryptedMessage = MessageV3(client, message).decryptOrNull() - - when (conversation?.version) { - Conversation.Version.DM -> { - decryptedMessage?.let { trySend(it) } - } - - else -> { - decryptedMessage?.let { trySend(it) } - } - } - } - - override fun onError(error: FfiSubscribeException) { - Log.e("XMTP all message stream", error.message.toString()) - } - } - - val stream = libXMTPConversations?.streamAllMessages(messageCallback) - ?: throw XMTPException("Client does not support Groups") - - awaitClose { stream.end() } - } } \ No newline at end of file diff --git a/library/src/main/java/org/xmtp/android/library/DecodedMessage.kt b/library/src/main/java/org/xmtp/android/library/DecodedMessage.kt index eab903b1a..6b0e85043 100644 --- a/library/src/main/java/org/xmtp/android/library/DecodedMessage.kt +++ b/library/src/main/java/org/xmtp/android/library/DecodedMessage.kt @@ -2,7 +2,7 @@ package org.xmtp.android.library import org.xmtp.android.library.codecs.TextCodec import org.xmtp.android.library.codecs.decoded -import org.xmtp.android.library.messages.MessageDeliveryStatus +import org.xmtp.android.library.libxmtp.Message.* import org.xmtp.proto.message.contents.Content import java.util.Date diff --git a/library/src/main/java/org/xmtp/android/library/Dm.kt b/library/src/main/java/org/xmtp/android/library/Dm.kt index 64ace0606..8676da65b 100644 --- a/library/src/main/java/org/xmtp/android/library/Dm.kt +++ b/library/src/main/java/org/xmtp/android/library/Dm.kt @@ -8,9 +8,8 @@ import org.xmtp.android.library.codecs.ContentCodec import org.xmtp.android.library.codecs.EncodedContent import org.xmtp.android.library.codecs.compress import org.xmtp.android.library.libxmtp.Member -import org.xmtp.android.library.libxmtp.MessageV3 -import org.xmtp.android.library.messages.DecryptedMessage -import org.xmtp.android.library.messages.MessageDeliveryStatus +import org.xmtp.android.library.libxmtp.Message +import org.xmtp.android.library.libxmtp.Message.* import org.xmtp.android.library.messages.PagingInfoSortDirection import org.xmtp.android.library.messages.Topic import org.xmtp.proto.message.api.v1.MessageApiOuterClass.SortDirection @@ -125,41 +124,13 @@ class Dm(val client: Client, private val libXMTPGroup: FfiConversation) { } ) ).mapNotNull { - MessageV3(client, it).decodeOrNull() + Message(client, it).decodeOrNull() } } - fun decryptedMessages( - limit: Int? = null, - before: Date? = null, - after: Date? = null, - direction: PagingInfoSortDirection = SortDirection.SORT_DIRECTION_DESCENDING, - deliveryStatus: MessageDeliveryStatus = MessageDeliveryStatus.ALL, - ): List { - return libXMTPGroup.findMessages( - opts = FfiListMessagesOptions( - sentBeforeNs = before?.time?.nanoseconds?.toLong(DurationUnit.NANOSECONDS), - sentAfterNs = after?.time?.nanoseconds?.toLong(DurationUnit.NANOSECONDS), - limit = limit?.toLong(), - deliveryStatus = when (deliveryStatus) { - MessageDeliveryStatus.PUBLISHED -> FfiDeliveryStatus.PUBLISHED - MessageDeliveryStatus.UNPUBLISHED -> FfiDeliveryStatus.UNPUBLISHED - MessageDeliveryStatus.FAILED -> FfiDeliveryStatus.FAILED - else -> null - }, - direction = when (direction) { - SortDirection.SORT_DIRECTION_ASCENDING -> FfiDirection.ASCENDING - else -> FfiDirection.DESCENDING - } - ) - ).mapNotNull { - MessageV3(client, it).decryptOrNull() - } - } - - suspend fun processMessage(envelopeBytes: ByteArray): MessageV3 { + suspend fun processMessage(envelopeBytes: ByteArray): Message { val message = libXMTPGroup.processStreamedConversationMessage(envelopeBytes) - return MessageV3(client, message) + return Message(client, message) } fun creatorInboxId(): String { @@ -177,7 +148,7 @@ class Dm(val client: Client, private val libXMTPGroup: FfiConversation) { fun streamMessages(): Flow = callbackFlow { val messageCallback = object : FfiMessageCallback { override fun onMessage(message: FfiMessage) { - val decodedMessage = MessageV3(client, message).decodeOrNull() + val decodedMessage = Message(client, message).decodeOrNull() decodedMessage?.let { trySend(it) } @@ -192,24 +163,6 @@ class Dm(val client: Client, private val libXMTPGroup: FfiConversation) { awaitClose { stream.end() } } - fun streamDecryptedMessages(): Flow = callbackFlow { - val messageCallback = object : FfiMessageCallback { - override fun onMessage(message: FfiMessage) { - val decryptedMessage = MessageV3(client, message).decryptOrNull() - decryptedMessage?.let { - trySend(it) - } - } - - override fun onError(error: FfiSubscribeException) { - Log.e("XMTP Dm stream", error.message.toString()) - } - } - - val stream = libXMTPGroup.stream(messageCallback) - awaitClose { stream.end() } - } - suspend fun updateConsentState(state: ConsentState) { if (client.hasV2Client) { when (state) { diff --git a/library/src/main/java/org/xmtp/android/library/Group.kt b/library/src/main/java/org/xmtp/android/library/Group.kt index eca6771d9..f0fe94747 100644 --- a/library/src/main/java/org/xmtp/android/library/Group.kt +++ b/library/src/main/java/org/xmtp/android/library/Group.kt @@ -8,9 +8,8 @@ import org.xmtp.android.library.codecs.ContentCodec import org.xmtp.android.library.codecs.EncodedContent import org.xmtp.android.library.codecs.compress import org.xmtp.android.library.libxmtp.Member -import org.xmtp.android.library.libxmtp.MessageV3 -import org.xmtp.android.library.messages.DecryptedMessage -import org.xmtp.android.library.messages.MessageDeliveryStatus +import org.xmtp.android.library.libxmtp.Message +import org.xmtp.android.library.libxmtp.Message.* import org.xmtp.android.library.messages.PagingInfoSortDirection import org.xmtp.android.library.messages.Topic import org.xmtp.proto.message.api.v1.MessageApiOuterClass.SortDirection.SORT_DIRECTION_ASCENDING @@ -143,41 +142,13 @@ class Group(val client: Client, private val libXMTPGroup: FfiConversation) { } ) ).mapNotNull { - MessageV3(client, it).decodeOrNull() + Message(client, it).decodeOrNull() } } - fun decryptedMessages( - limit: Int? = null, - before: Date? = null, - after: Date? = null, - direction: PagingInfoSortDirection = SORT_DIRECTION_DESCENDING, - deliveryStatus: MessageDeliveryStatus = MessageDeliveryStatus.ALL, - ): List { - return libXMTPGroup.findMessages( - opts = FfiListMessagesOptions( - sentBeforeNs = before?.time?.nanoseconds?.toLong(DurationUnit.NANOSECONDS), - sentAfterNs = after?.time?.nanoseconds?.toLong(DurationUnit.NANOSECONDS), - limit = limit?.toLong(), - deliveryStatus = when (deliveryStatus) { - MessageDeliveryStatus.PUBLISHED -> FfiDeliveryStatus.PUBLISHED - MessageDeliveryStatus.UNPUBLISHED -> FfiDeliveryStatus.UNPUBLISHED - MessageDeliveryStatus.FAILED -> FfiDeliveryStatus.FAILED - else -> null - }, - direction = when (direction) { - SORT_DIRECTION_ASCENDING -> FfiDirection.ASCENDING - else -> FfiDirection.DESCENDING - } - ) - ).mapNotNull { - MessageV3(client, it).decryptOrNull() - } - } - - suspend fun processMessage(envelopeBytes: ByteArray): MessageV3 { + suspend fun processMessage(envelopeBytes: ByteArray): Message { val message = libXMTPGroup.processStreamedConversationMessage(envelopeBytes) - return MessageV3(client, message) + return Message(client, message) } suspend fun updateConsentState(state: ConsentState) { @@ -406,7 +377,7 @@ class Group(val client: Client, private val libXMTPGroup: FfiConversation) { fun streamMessages(): Flow = callbackFlow { val messageCallback = object : FfiMessageCallback { override fun onMessage(message: FfiMessage) { - val decodedMessage = MessageV3(client, message).decodeOrNull() + val decodedMessage = Message(client, message).decodeOrNull() decodedMessage?.let { trySend(it) } @@ -420,22 +391,4 @@ class Group(val client: Client, private val libXMTPGroup: FfiConversation) { val stream = libXMTPGroup.stream(messageCallback) awaitClose { stream.end() } } - - fun streamDecryptedMessages(): Flow = callbackFlow { - val messageCallback = object : FfiMessageCallback { - override fun onMessage(message: FfiMessage) { - val decryptedMessage = MessageV3(client, message).decryptOrNull() - decryptedMessage?.let { - trySend(it) - } - } - - override fun onError(error: FfiSubscribeException) { - Log.e("XMTP Group stream", error.message.toString()) - } - } - - val stream = libXMTPGroup.stream(messageCallback) - awaitClose { stream.end() } - } } diff --git a/library/src/main/java/org/xmtp/android/library/libxmtp/MessageV3.kt b/library/src/main/java/org/xmtp/android/library/libxmtp/Message.kt similarity index 74% rename from library/src/main/java/org/xmtp/android/library/libxmtp/MessageV3.kt rename to library/src/main/java/org/xmtp/android/library/libxmtp/Message.kt index 631fd2e5f..88116451c 100644 --- a/library/src/main/java/org/xmtp/android/library/libxmtp/MessageV3.kt +++ b/library/src/main/java/org/xmtp/android/library/libxmtp/Message.kt @@ -6,8 +6,6 @@ import org.xmtp.android.library.DecodedMessage import org.xmtp.android.library.XMTPException import org.xmtp.android.library.codecs.ContentTypeGroupUpdated import org.xmtp.android.library.codecs.EncodedContent -import org.xmtp.android.library.messages.DecryptedMessage -import org.xmtp.android.library.messages.MessageDeliveryStatus import org.xmtp.android.library.messages.Topic import org.xmtp.android.library.toHex import uniffi.xmtpv3.FfiDeliveryStatus @@ -15,7 +13,10 @@ import uniffi.xmtpv3.FfiConversationMessageKind import uniffi.xmtpv3.FfiMessage import java.util.Date -data class MessageV3(val client: Client, private val libXMTPMessage: FfiMessage) { +data class Message(val client: Client, private val libXMTPMessage: FfiMessage) { + enum class MessageDeliveryStatus { + ALL, PUBLISHED, UNPUBLISHED, FAILED + } val id: String get() = libXMTPMessage.id.toHex() @@ -64,24 +65,4 @@ data class MessageV3(val client: Client, private val libXMTPMessage: FfiMessage) null } } - - fun decryptOrNull(): DecryptedMessage? { - return try { - decrypt() - } catch (e: Exception) { - Log.d("MESSAGE_V3", "discarding message that failed to decrypt", e) - null - } - } - - fun decrypt(): DecryptedMessage { - return DecryptedMessage( - id = id, - topic = Topic.groupMessage(convoId).description, - encodedContent = decode().encodedContent, - senderAddress = senderInboxId, - sentAt = sentAt, - deliveryStatus = deliveryStatus - ) - } } diff --git a/library/src/main/java/org/xmtp/android/library/messages/DecryptedMessage.kt b/library/src/main/java/org/xmtp/android/library/messages/DecryptedMessage.kt deleted file mode 100644 index accd89459..000000000 --- a/library/src/main/java/org/xmtp/android/library/messages/DecryptedMessage.kt +++ /dev/null @@ -1,13 +0,0 @@ -package org.xmtp.android.library.messages - -import org.xmtp.android.library.codecs.EncodedContent -import java.util.Date - -data class DecryptedMessage( - var id: String, - var encodedContent: EncodedContent, - var senderAddress: String, - var sentAt: Date, - var topic: String = "", - var deliveryStatus: MessageDeliveryStatus = MessageDeliveryStatus.PUBLISHED -) diff --git a/library/src/main/java/org/xmtp/android/library/messages/Message.kt b/library/src/main/java/org/xmtp/android/library/messages/Message.kt deleted file mode 100644 index 2beb7c8ac..000000000 --- a/library/src/main/java/org/xmtp/android/library/messages/Message.kt +++ /dev/null @@ -1,33 +0,0 @@ -package org.xmtp.android.library.messages - -typealias Message = org.xmtp.proto.message.contents.MessageOuterClass.Message - -enum class MessageDeliveryStatus { - ALL, PUBLISHED, UNPUBLISHED, FAILED -} - -enum class MessageVersion(val rawValue: String) { - V1("v1"), - V2("v2"); - - companion object { - operator fun invoke(rawValue: String) = - MessageVersion.values().firstOrNull { it.rawValue == rawValue } - } -} - -class MessageBuilder { - companion object { - fun buildFromMessageV1(v1: MessageV1): Message { - return Message.newBuilder().also { - it.v1 = v1 - }.build() - } - - fun buildFromMessageV2(v2: MessageV2): Message { - return Message.newBuilder().also { - it.v2 = v2 - }.build() - } - } -} diff --git a/library/src/main/java/org/xmtp/android/library/messages/MessageHeaderV1.kt b/library/src/main/java/org/xmtp/android/library/messages/MessageHeaderV1.kt deleted file mode 100644 index 8090f8914..000000000 --- a/library/src/main/java/org/xmtp/android/library/messages/MessageHeaderV1.kt +++ /dev/null @@ -1,19 +0,0 @@ -package org.xmtp.android.library.messages - -typealias MessageHeaderV1 = org.xmtp.proto.message.contents.MessageOuterClass.MessageHeaderV1 - -class MessageHeaderV1Builder { - companion object { - fun buildFromPublicBundles( - sender: PublicKeyBundle, - recipient: PublicKeyBundle, - timestamp: Long - ): MessageHeaderV1 { - return MessageHeaderV1.newBuilder().also { - it.sender = sender - it.recipient = recipient - it.timestamp = timestamp - }.build() - } - } -} diff --git a/library/src/main/java/org/xmtp/android/library/messages/MessageHeaderv2.kt b/library/src/main/java/org/xmtp/android/library/messages/MessageHeaderv2.kt deleted file mode 100644 index 2e7c2b7e5..000000000 --- a/library/src/main/java/org/xmtp/android/library/messages/MessageHeaderv2.kt +++ /dev/null @@ -1,16 +0,0 @@ -package org.xmtp.android.library.messages - -import java.util.Date - -typealias MessageHeaderV2 = org.xmtp.proto.message.contents.MessageOuterClass.MessageHeaderV2 - -class MessageHeaderV2Builder { - companion object { - fun buildFromTopic(topic: String, created: Date): MessageHeaderV2 { - return MessageHeaderV2.newBuilder().also { - it.topic = topic - it.createdNs = (created.time * 1_000_000) - }.build() - } - } -} diff --git a/library/src/main/java/org/xmtp/android/library/messages/MessageV1.kt b/library/src/main/java/org/xmtp/android/library/messages/MessageV1.kt deleted file mode 100644 index 2b8d7594c..000000000 --- a/library/src/main/java/org/xmtp/android/library/messages/MessageV1.kt +++ /dev/null @@ -1,84 +0,0 @@ -package org.xmtp.android.library.messages - -import com.google.protobuf.kotlin.toByteString -import org.xmtp.android.library.CipherText -import org.xmtp.android.library.Crypto -import org.xmtp.android.library.XMTPException -import org.xmtp.proto.message.contents.MessageOuterClass -import java.util.Date - -typealias MessageV1 = org.xmtp.proto.message.contents.MessageOuterClass.MessageV1 - -class MessageV1Builder { - companion object { - fun buildEncode( - sender: PrivateKeyBundleV1, - recipient: PublicKeyBundle, - message: ByteArray, - timestamp: Date - ): MessageV1 { - val secret = sender.sharedSecret( - peer = recipient, - myPreKey = sender.preKeysList[0].publicKey, - isRecipient = false - ) - val header = MessageHeaderV1Builder.buildFromPublicBundles( - sender = sender.toPublicKeyBundle(), - recipient = recipient, - timestamp = timestamp.time - ) - val headerBytes = header.toByteArray() - val ciphertext = Crypto.encrypt(secret, message, additionalData = headerBytes) - return buildFromCipherText(headerBytes = headerBytes, ciphertext = ciphertext) - } - - fun buildFromBytes(bytes: ByteArray): MessageV1 { - val message = Message.parseFrom(bytes) - val headerBytes: ByteArray - val ciphertext: CipherText - when (message.versionCase) { - MessageOuterClass.Message.VersionCase.V1 -> { - headerBytes = message.v1.headerBytes.toByteArray() - ciphertext = message.v1.ciphertext - } - MessageOuterClass.Message.VersionCase.V2 -> { - headerBytes = message.v2.headerBytes.toByteArray() - ciphertext = message.v2.ciphertext - } - else -> throw XMTPException("Cannot decode from bytes") - } - return buildFromCipherText(headerBytes, ciphertext) - } - - fun buildFromCipherText(headerBytes: ByteArray, ciphertext: CipherText?): MessageV1 { - return MessageV1.newBuilder().also { - it.headerBytes = headerBytes.toByteString() - it.ciphertext = ciphertext - }.build() - } - } -} - -val MessageV1.header: MessageHeaderV1 - get() = MessageHeaderV1.parseFrom(headerBytes) - -val MessageV1.senderAddress: String - get() = header.sender.identityKey.recoverWalletSignerPublicKey().walletAddress - -val MessageV1.sentAt: Date get() = Date(header.timestamp) - -val MessageV1.recipientAddress: String - get() = header.recipient.identityKey.recoverWalletSignerPublicKey().walletAddress - -fun MessageV1.decrypt(viewer: PrivateKeyBundleV1?): ByteArray? { - val header = MessageHeaderV1.parseFrom(headerBytes) - val recipient = header.recipient - val sender = header.sender - val secret: ByteArray = if (viewer?.walletAddress == sender.walletAddress) { - viewer.sharedSecret(peer = recipient, myPreKey = sender.preKey, isRecipient = false) - } else { - viewer?.sharedSecret(peer = sender, myPreKey = recipient.preKey, isRecipient = true) - ?: byteArrayOf() - } - return Crypto.decrypt(secret, ciphertext, additionalData = headerBytes.toByteArray()) -} 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 deleted file mode 100644 index c8af884aa..000000000 --- a/library/src/main/java/org/xmtp/android/library/messages/MessageV2.kt +++ /dev/null @@ -1,174 +0,0 @@ -package org.xmtp.android.library.messages - -import com.google.protobuf.kotlin.toByteString -import com.google.protobuf.kotlin.toByteStringUtf8 -import org.web3j.crypto.ECDSASignature -import org.web3j.crypto.Hash -import org.web3j.crypto.Sign -import org.xmtp.android.library.CipherText -import org.xmtp.android.library.Client -import org.xmtp.android.library.Crypto -import org.xmtp.android.library.DecodedMessage -import org.xmtp.android.library.KeyUtil -import org.xmtp.android.library.XMTPException -import org.xmtp.android.library.codecs.ContentCodec -import org.xmtp.android.library.codecs.EncodedContent -import java.math.BigInteger -import java.util.Date - -typealias MessageV2 = org.xmtp.proto.message.contents.MessageOuterClass.MessageV2 - -class MessageV2Builder(val senderHmac: ByteArray? = null, val shouldPush: Boolean = false) { - lateinit var messageV2: MessageV2 - - companion object { - fun buildFromCipherText( - headerBytes: ByteArray, - ciphertext: CipherText?, - senderHmac: ByteArray, - shouldPush: Boolean, - ): MessageV2Builder { - val messageBuilder = MessageV2Builder(senderHmac = senderHmac, shouldPush = shouldPush) - messageBuilder.messageV2 = MessageV2.newBuilder().also { - it.headerBytes = headerBytes.toByteString() - it.ciphertext = ciphertext - it.shouldPush = shouldPush - it.senderHmac = senderHmac.toByteString() - }.build() - return messageBuilder - } - - fun buildDecode( - id: String, - topic: String, - message: MessageV2, - keyMaterial: ByteArray, - client: Client, - ): DecodedMessage { - try { - val decryptedMessage = buildDecrypt(id, topic, message, keyMaterial, client) - - return DecodedMessage( - id = id, - client = client, - topic = decryptedMessage.topic, - encodedContent = decryptedMessage.encodedContent, - senderAddress = decryptedMessage.senderAddress, - sent = decryptedMessage.sentAt, - ) - } catch (e: Exception) { - throw XMTPException("Error decoding message", e) - } - } - - fun buildDecrypt( - id: String, - topic: String, - message: MessageV2, - keyMaterial: ByteArray, - client: Client, - ): DecryptedMessage { - val decrypted = - Crypto.decrypt(keyMaterial, message.ciphertext, message.headerBytes.toByteArray()) - val signed = SignedContent.parseFrom(decrypted) - - if (!signed.sender.hasPreKey() || !signed.sender.hasIdentityKey()) { - throw XMTPException("missing sender pre-key or identity key") - } - - val senderPreKey = PublicKeyBuilder.buildFromSignedPublicKey(signed.sender.preKey) - val senderIdentityKey = - PublicKeyBuilder.buildFromSignedPublicKey(signed.sender.identityKey) - - if (!senderPreKey.signature.verify( - senderIdentityKey, - signed.sender.preKey.keyBytes.toByteArray(), - ) - ) { - throw XMTPException("pre-key not signed by identity key") - } - - // Verify content signature - val digest = - Hash.sha256(message.headerBytes.toByteArray() + signed.payload.toByteArray()) - - val signatureData = - KeyUtil.getSignatureData(signed.signature.rawData.toByteString().toByteArray()) - val publicKey = Sign.recoverFromSignature( - BigInteger(1, signatureData.v).toInt(), - ECDSASignature(BigInteger(1, signatureData.r), BigInteger(1, signatureData.s)), - digest, - ) - - val key = PublicKey.newBuilder().also { - it.secp256K1Uncompressed = it.secp256K1Uncompressed.toBuilder().also { keyBuilder -> - keyBuilder.bytes = - KeyUtil.addUncompressedByte(publicKey.toByteArray()).toByteString() - }.build() - }.build() - - if (key.walletAddress != (PublicKeyBuilder.buildFromSignedPublicKey(signed.sender.preKey).walletAddress)) { - throw XMTPException("Invalid signature") - } - - val encodedMessage = EncodedContent.parseFrom(signed.payload) - val header = MessageHeaderV2.parseFrom(message.headerBytes) - if (header.topic != topic) { - throw XMTPException("Topic mismatch") - } - - return DecryptedMessage( - id = id, - encodedContent = encodedMessage, - senderAddress = signed.sender.walletAddress, - sentAt = Date(header.createdNs / 1_000_000), - topic = topic, - ) - } - - private fun , T> shouldPush(codec: Codec, content: T?): Boolean { - if (content != null) { - return codec.shouldPush(content = content) - } else { - throw XMTPException("Codec invalid content") - } - } - - fun , T> buildEncode( - client: Client, - encodedContent: EncodedContent, - topic: String, - keyMaterial: ByteArray, - codec: Codec, - ): MessageV2Builder { - val payload = encodedContent.toByteArray() - val date = Date() - val header = MessageHeaderV2Builder.buildFromTopic(topic, date) - val headerBytes = header.toByteArray() - val digest = Hash.sha256(headerBytes + payload) - val preKey = client.keys.preKeysList?.get(0) - val signature = preKey?.sign(digest) - 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) - - val thirtyDayPeriodsSinceEpoch = - (Date().time / 1000 / 60 / 60 / 24 / 30).toInt() - val info = "$thirtyDayPeriodsSinceEpoch-${client.address}" - val infoEncoded = info.toByteStringUtf8().toByteArray() - val senderHmacGenerated = - Crypto.calculateMac( - Crypto.deriveKey(keyMaterial, ByteArray(0), infoEncoded), - headerBytes - ) - - return buildFromCipherText( - headerBytes, - ciphertext, - senderHmacGenerated, - shouldPush(codec = codec, content = codec.decode(encodedContent)), - ) - } - } -} From 79e275f45aef5676fa66b0eb3f4338003e9b8ae9 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Wed, 6 Nov 2024 17:31:16 -0800 Subject: [PATCH 2/2] [V3] Remove contacts and rename to preferences (#314) * remove contacts and rename to preferences * [V3] Remove the ability to create a V2 client (#315) * remove the ability to create a v2 client entirely * remove a bunch of classes no longer needed * [V3] Fix up all the tests (#316) * get all the tests compiling * fix up the wallet address * more clean up * get all the tests passing * make the ffiClient private so people dont call it directly * [V3] Update example app (#317) * update the example to be v3 only * remove force * remove some impots * update frames * clean up the code * rename and fix up the lint --- .../org/xmtp/android/example/ClientManager.kt | 15 +- .../org/xmtp/android/example/MainActivity.kt | 2 +- .../org/xmtp/android/example/MainViewModel.kt | 5 +- .../example/connect/ConnectWalletFragment.kt | 9 +- .../example/connect/ConnectWalletViewModel.kt | 9 +- .../ConversationDetailViewModel.kt | 10 +- .../conversation/ConversationViewHolder.kt | 9 +- .../NewConversationBottomSheet.kt | 2 +- .../conversation/NewGroupBottomSheet.kt | 2 +- .../PushNotificationsService.kt | 19 +- .../org/xmtp/android/example/utils/KeyUtil.kt | 4 +- library/build.gradle | 1 + .../xmtp/android/library/AttachmentTest.kt | 8 +- .../org/xmtp/android/library/ClientTest.kt | 235 +------ .../org/xmtp/android/library/CodecTest.kt | 8 +- .../org/xmtp/android/library/ContactsTest.kt | 59 -- .../xmtp/android/library/ConversationsTest.kt | 170 +++++ .../java/org/xmtp/android/library/DmTest.kt | 88 +-- .../org/xmtp/android/library/FramesTest.kt | 71 -- .../android/library/GroupPermissionsTest.kt | 22 +- .../org/xmtp/android/library/GroupTest.kt | 148 +++-- .../xmtp/android/library/GroupUpdatedTest.kt | 21 +- .../xmtp/android/library/InvitationTest.kt | 197 ------ .../org/xmtp/android/library/ReactionTest.kt | 8 +- .../xmtp/android/library/ReadReceiptTest.kt | 8 +- .../org/xmtp/android/library/ReplyTest.kt | 8 +- .../library/SmartContractWalletTest.kt | 112 +++- .../org/xmtp/android/library/TestHelpers.kt | 56 +- .../org/xmtp/android/library/V3ClientTest.kt | 396 ----------- .../org/xmtp/android/library/ApiClient.kt | 237 ------- .../android/library/AuthorizedIdentity.kt | 53 -- .../java/org/xmtp/android/library/Client.kt | 622 +++--------------- .../org/xmtp/android/library/Constants.kt | 5 - .../java/org/xmtp/android/library/Contacts.kt | 430 ------------ .../org/xmtp/android/library/Conversation.kt | 7 +- .../org/xmtp/android/library/Conversations.kt | 172 ++--- .../main/java/org/xmtp/android/library/Dm.kt | 16 +- .../java/org/xmtp/android/library/Group.kt | 15 +- .../xmtp/android/library/PreparedMessage.kt | 38 -- .../android/library/PrivatePreferences.kt | 136 ++++ .../org/xmtp/android/library/SigningKey.kt | 66 -- .../android/library/frames/FramesClient.kt | 94 --- .../xmtp/android/library/libxmtp/Message.kt | 5 + .../xmtp/android/library/messages/AuthData.kt | 18 - .../android/library/messages/ContactBundle.kt | 100 --- .../messages/EncryptedPrivateKeyBundle.kt | 29 - .../xmtp/android/library/messages/Envelope.kt | 26 - .../android/library/messages/InvitationV1.kt | 120 ---- .../android/library/messages/PagingInfo.kt | 28 - .../android/library/messages/PrivateKey.kt | 25 - .../library/messages/PrivateKeyBundle.kt | 50 -- .../library/messages/PrivateKeyBundleV1.kt | 104 --- .../library/messages/PrivateKeyBundleV2.kt | 72 -- .../android/library/messages/PublicKey.kt | 63 +- .../library/messages/PublicKeyBundle.kt | 20 - .../library/messages/SealedInvitation.kt | 50 -- .../messages/SealedInvitationHeaderV1.kt | 19 - .../library/messages/SealedInvitationV1.kt | 45 -- .../android/library/messages/Signature.kt | 20 - .../android/library/messages/SignedContent.kt | 21 - .../library/messages/SignedPrivateKey.kt | 33 - .../library/messages/SignedPublicKey.kt | 63 -- .../library/messages/SignedPublicKeyBundle.kt | 26 - .../xmtp/android/library/messages/Token.kt | 3 - .../xmtp/android/library/messages/Topic.kt | 42 -- .../library/messages/UnsignedPublicKey.kt | 29 - .../android/library/AuthenticationTest.kt | 47 -- .../org/xmtp/android/library/ContactTest.kt | 66 -- .../android/library/PreparedMessageTest.kt | 30 - .../android/library/PrivateKeyBundleTest.kt | 43 -- .../org/xmtp/android/library/TestHelpers.kt | 24 +- 71 files changed, 780 insertions(+), 4034 deletions(-) delete mode 100644 library/src/androidTest/java/org/xmtp/android/library/ContactsTest.kt create mode 100644 library/src/androidTest/java/org/xmtp/android/library/ConversationsTest.kt delete mode 100644 library/src/androidTest/java/org/xmtp/android/library/FramesTest.kt delete mode 100644 library/src/androidTest/java/org/xmtp/android/library/InvitationTest.kt delete mode 100644 library/src/androidTest/java/org/xmtp/android/library/V3ClientTest.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/ApiClient.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/AuthorizedIdentity.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/Constants.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/Contacts.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/PreparedMessage.kt create mode 100644 library/src/main/java/org/xmtp/android/library/PrivatePreferences.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/frames/FramesClient.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/AuthData.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/ContactBundle.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/EncryptedPrivateKeyBundle.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/Envelope.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/InvitationV1.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/PagingInfo.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/PrivateKeyBundle.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/PrivateKeyBundleV1.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/PrivateKeyBundleV2.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/PublicKeyBundle.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/SealedInvitation.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/SealedInvitationHeaderV1.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/SealedInvitationV1.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/SignedContent.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/SignedPrivateKey.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/SignedPublicKey.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/SignedPublicKeyBundle.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/Token.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/UnsignedPublicKey.kt delete mode 100644 library/src/test/java/org/xmtp/android/library/AuthenticationTest.kt delete mode 100644 library/src/test/java/org/xmtp/android/library/ContactTest.kt delete mode 100644 library/src/test/java/org/xmtp/android/library/PreparedMessageTest.kt delete mode 100644 library/src/test/java/org/xmtp/android/library/PrivateKeyBundleTest.kt diff --git a/example/src/main/java/org/xmtp/android/example/ClientManager.kt b/example/src/main/java/org/xmtp/android/example/ClientManager.kt index 19820ef88..725549d9d 100644 --- a/example/src/main/java/org/xmtp/android/example/ClientManager.kt +++ b/example/src/main/java/org/xmtp/android/example/ClientManager.kt @@ -12,7 +12,6 @@ import org.xmtp.android.library.Client import org.xmtp.android.library.ClientOptions import org.xmtp.android.library.XMTPEnvironment import org.xmtp.android.library.codecs.GroupUpdatedCodec -import org.xmtp.android.library.messages.PrivateKeyBundleV1Builder import org.xmtp.android.library.messages.walletAddress import java.security.SecureRandom @@ -20,11 +19,8 @@ object ClientManager { fun clientOptions(appContext: Context, address: String): ClientOptions { val keyUtil = KeyUtil(appContext) - var encryptionKey = keyUtil.retrieveKey(address) - if (encryptionKey == null || encryptionKey.isEmpty()) { - encryptionKey = SecureRandom().generateSeed(32) - keyUtil.storeKey(address, encryptionKey) - } + val encryptionKey = keyUtil.retrieveKey(address)?.takeUnless { it.isEmpty() } + ?: SecureRandom().generateSeed(32).also { keyUtil.storeKey(address, it) } return ClientOptions( api = ClientOptions.Api( @@ -32,7 +28,6 @@ object ClientManager { appVersion = "XMTPAndroidExample/v1.0.0", isSecure = true ), - enableV3 = true, appContext = appContext, dbEncryptionKey = encryptionKey ) @@ -51,14 +46,12 @@ object ClientManager { } @UiThread - fun createClient(encodedPrivateKeyData: String, appContext: Context) { + fun createClient(address: String, appContext: Context) { if (clientState.value is ClientState.Ready) return GlobalScope.launch(Dispatchers.IO) { try { - val v1Bundle = - PrivateKeyBundleV1Builder.fromEncodedData(data = encodedPrivateKeyData) _client = - Client().buildFrom(v1Bundle, clientOptions(appContext, v1Bundle.walletAddress)) + Client().build(address, clientOptions(appContext, address)) Client.register(codec = GroupUpdatedCodec()) _clientState.value = ClientState.Ready } catch (e: Exception) { diff --git a/example/src/main/java/org/xmtp/android/example/MainActivity.kt b/example/src/main/java/org/xmtp/android/example/MainActivity.kt index 239a36a89..2c05d2eb2 100644 --- a/example/src/main/java/org/xmtp/android/example/MainActivity.kt +++ b/example/src/main/java/org/xmtp/android/example/MainActivity.kt @@ -128,7 +128,7 @@ class MainActivity : AppCompatActivity(), ConversationDetailActivity.intent( this, topic = conversation.topic, - peerAddress = conversation.peerAddress + peerAddress = conversation.id ) ) } 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 ffaa068ac..ec57cb807 100644 --- a/example/src/main/java/org/xmtp/android/example/MainViewModel.kt +++ b/example/src/main/java/org/xmtp/android/example/MainViewModel.kt @@ -45,7 +45,7 @@ class MainViewModel : ViewModel() { viewModelScope.launch(Dispatchers.IO) { val listItems = mutableListOf() try { - val conversations = ClientManager.client.conversations.list(includeGroups = true) + val conversations = ClientManager.client.conversations.list() val hmacKeysResult = ClientManager.client.conversations.getHmacKeys() val subscriptions: MutableList = conversations.map { val hmacKeys = hmacKeysResult.hmacKeysMap @@ -61,7 +61,6 @@ class MainViewModel : ViewModel() { sub.addAllHmacKeys(result) } sub.topic = it.topic - sub.isSilent = it.version == Conversation.Version.V1 }.build() }.toMutableList() @@ -105,7 +104,7 @@ class MainViewModel : ViewModel() { val stream: StateFlow = stateFlow(viewModelScope, null) { subscriptionCount -> if (ClientManager.clientState.value is ClientManager.ClientState.Ready) { - ClientManager.client.conversations.streamAll() + ClientManager.client.conversations.stream() .flowWhileShared( subscriptionCount, SharingStarted.WhileSubscribed(1000L) diff --git a/example/src/main/java/org/xmtp/android/example/connect/ConnectWalletFragment.kt b/example/src/main/java/org/xmtp/android/example/connect/ConnectWalletFragment.kt index 711736eae..f3b2de2ff 100644 --- a/example/src/main/java/org/xmtp/android/example/connect/ConnectWalletFragment.kt +++ b/example/src/main/java/org/xmtp/android/example/connect/ConnectWalletFragment.kt @@ -81,10 +81,7 @@ class ConnectWalletFragment : Fragment() { when (uiState) { is ConnectWalletViewModel.ConnectUiState.Error -> showError(uiState.message) ConnectWalletViewModel.ConnectUiState.Loading -> showLoading() - is ConnectWalletViewModel.ConnectUiState.Success -> signIn( - uiState.address, - uiState.encodedKeyData - ) + is ConnectWalletViewModel.ConnectUiState.Success -> signIn(uiState.address) ConnectWalletViewModel.ConnectUiState.Unknown -> Unit } @@ -103,10 +100,10 @@ class ConnectWalletFragment : Fragment() { } } - private fun signIn(address: String, encodedKey: String) { + private fun signIn(address: String) { val accountManager = AccountManager.get(requireContext()) Account(address, resources.getString(R.string.account_type)).also { account -> - accountManager.addAccountExplicitly(account, encodedKey, null) + accountManager.addAccountExplicitly(account, address, null) } requireActivity().startActivity(Intent(requireActivity(), MainActivity::class.java)) requireActivity().finish() 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 b773b5e88..627947746 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 @@ -21,7 +21,6 @@ import org.xmtp.android.library.Client import org.xmtp.android.library.XMTPException import org.xmtp.android.library.codecs.GroupUpdatedCodec import org.xmtp.android.library.messages.PrivateKeyBuilder -import org.xmtp.android.library.messages.PrivateKeyBundleV1Builder class ConnectWalletViewModel(application: Application) : AndroidViewModel(application) { @@ -89,8 +88,7 @@ class ConnectWalletViewModel(application: Application) : AndroidViewModel(applic val client = Client().create(wallet, ClientManager.clientOptions(getApplication(), wallet.address)) Client.register(codec = GroupUpdatedCodec()) _uiState.value = ConnectUiState.Success( - wallet.address, - PrivateKeyBundleV1Builder.encodeData(client.v1keys) + wallet.address ) } catch (e: XMTPException) { _uiState.value = ConnectUiState.Error(e.message.orEmpty()) @@ -114,8 +112,7 @@ class ConnectWalletViewModel(application: Application) : AndroidViewModel(applic val client = Client().create(wallet, ClientManager.clientOptions(getApplication(), wallet.address)) Client.register(codec = GroupUpdatedCodec()) _uiState.value = ConnectUiState.Success( - wallet.address, - PrivateKeyBundleV1Builder.encodeData(client.v1keys) + wallet.address ) } catch (e: Exception) { _uiState.value = ConnectUiState.Error(e.message.orEmpty()) @@ -132,7 +129,7 @@ class ConnectWalletViewModel(application: Application) : AndroidViewModel(applic sealed class ConnectUiState { object Unknown : ConnectUiState() object Loading : ConnectUiState() - data class Success(val address: String, val encodedKeyData: String) : ConnectUiState() + data class Success(val address: String) : ConnectUiState() data class Error(val message: String) : ConnectUiState() } diff --git a/example/src/main/java/org/xmtp/android/example/conversation/ConversationDetailViewModel.kt b/example/src/main/java/org/xmtp/android/example/conversation/ConversationDetailViewModel.kt index a9444e979..98df784d8 100644 --- a/example/src/main/java/org/xmtp/android/example/conversation/ConversationDetailViewModel.kt +++ b/example/src/main/java/org/xmtp/android/example/conversation/ConversationDetailViewModel.kt @@ -51,10 +51,7 @@ class ConversationDetailViewModel(private val savedStateHandle: SavedStateHandle val listItems = mutableListOf() try { if (conversation == null) { - conversation = ClientManager.client.fetchConversation( - conversationTopic, - includeGroups = true - ) + conversation = ClientManager.client.findConversationByTopic(conversationTopic!!) } conversation?.let { if (conversation is Conversation.Group) { @@ -79,10 +76,7 @@ class ConversationDetailViewModel(private val savedStateHandle: SavedStateHandle if (conversation == null) { conversation = runBlocking { - ClientManager.client.fetchConversation( - conversationTopic, - includeGroups = false - ) + ClientManager.client.findConversationByTopic(conversationTopic!!) } } if (conversation != null) { diff --git a/example/src/main/java/org/xmtp/android/example/conversation/ConversationViewHolder.kt b/example/src/main/java/org/xmtp/android/example/conversation/ConversationViewHolder.kt index 34a1ab895..fa76a042e 100644 --- a/example/src/main/java/org/xmtp/android/example/conversation/ConversationViewHolder.kt +++ b/example/src/main/java/org/xmtp/android/example/conversation/ConversationViewHolder.kt @@ -27,14 +27,7 @@ class ConversationViewHolder( fun bind(item: MainViewModel.MainListItem.ConversationItem) { conversation = item.conversation - binding.peerAddress.text = if (item.conversation.peerAddress.contains(",")) { - val addresses = item.conversation.peerAddress.split(",") - addresses.joinToString(" & ") { - it.truncatedAddress() - } - } else { - item.conversation.peerAddress.truncatedAddress() - } + binding.peerAddress.text = item.conversation.id.truncatedAddress() val messageBody: String = if (item.mostRecentMessage?.content() is String) { item.mostRecentMessage.body.orEmpty() diff --git a/example/src/main/java/org/xmtp/android/example/conversation/NewConversationBottomSheet.kt b/example/src/main/java/org/xmtp/android/example/conversation/NewConversationBottomSheet.kt index 512ea892c..3aa293e22 100644 --- a/example/src/main/java/org/xmtp/android/example/conversation/NewConversationBottomSheet.kt +++ b/example/src/main/java/org/xmtp/android/example/conversation/NewConversationBottomSheet.kt @@ -81,7 +81,7 @@ class NewConversationBottomSheet : BottomSheetDialogFragment() { ConversationDetailActivity.intent( requireContext(), topic = uiState.conversation.topic, - peerAddress = uiState.conversation.peerAddress + peerAddress = uiState.conversation.id ) ) dismiss() diff --git a/example/src/main/java/org/xmtp/android/example/conversation/NewGroupBottomSheet.kt b/example/src/main/java/org/xmtp/android/example/conversation/NewGroupBottomSheet.kt index 300f00530..aad1710a9 100644 --- a/example/src/main/java/org/xmtp/android/example/conversation/NewGroupBottomSheet.kt +++ b/example/src/main/java/org/xmtp/android/example/conversation/NewGroupBottomSheet.kt @@ -97,7 +97,7 @@ class NewGroupBottomSheet : BottomSheetDialogFragment() { ConversationDetailActivity.intent( requireContext(), topic = uiState.conversation.topic, - peerAddress = uiState.conversation.peerAddress + peerAddress = uiState.conversation.id ) ) dismiss() diff --git a/example/src/main/java/org/xmtp/android/example/pushnotifications/PushNotificationsService.kt b/example/src/main/java/org/xmtp/android/example/pushnotifications/PushNotificationsService.kt index 7fb476c7f..1b11d33f6 100644 --- a/example/src/main/java/org/xmtp/android/example/pushnotifications/PushNotificationsService.kt +++ b/example/src/main/java/org/xmtp/android/example/pushnotifications/PushNotificationsService.kt @@ -20,11 +20,8 @@ import org.xmtp.android.example.R import org.xmtp.android.example.conversation.ConversationDetailActivity import org.xmtp.android.example.extension.truncatedAddress import org.xmtp.android.example.utils.KeyUtil -import org.xmtp.android.library.Conversation import org.xmtp.android.library.codecs.GroupUpdated -import org.xmtp.android.library.messages.EnvelopeBuilder import org.xmtp.android.library.messages.Topic -import java.util.Date class PushNotificationsService : FirebaseMessagingService() { @@ -70,14 +67,14 @@ class PushNotificationsService : FirebaseMessagingService() { ConversationDetailActivity.intent( this, topic = group.topic, - peerAddress = Conversation.Group(group).peerAddress + peerAddress = group.id ), (PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT) ) NotificationCompat.Builder(this, CHANNEL_ID) .setSmallIcon(R.drawable.ic_xmtp_white) - .setContentTitle(Conversation.Group(group).peerAddress.truncatedAddress()) + .setContentTitle(group.id.truncatedAddress()) .setContentText("New Group Chat") .setAutoCancel(true) .setColor(ContextCompat.getColor(this, R.color.black)) @@ -86,19 +83,15 @@ class PushNotificationsService : FirebaseMessagingService() { .setContentIntent(pendingIntent) } else { val conversation = - runBlocking { ClientManager.client.fetchConversation(topic, includeGroups = true) } + runBlocking { ClientManager.client.findConversationByTopic(topic) } if (conversation == null) { Log.e(TAG, topic) Log.e(TAG, "No keys or conversation persisted") return } - val decodedMessage = if (conversation is Conversation.Group) { - runBlocking { conversation.group.processMessage(encryptedMessageData).decode() } - } else { - val envelope = EnvelopeBuilder.buildFromString(topic, Date(), encryptedMessageData) - conversation.decode(envelope) - } - val peerAddress = conversation.peerAddress + val decodedMessage = + runBlocking { conversation.processMessage(encryptedMessageData).decode() } + val peerAddress = conversation.id val body: String = if (decodedMessage.content() is String) { decodedMessage.body diff --git a/example/src/main/java/org/xmtp/android/example/utils/KeyUtil.kt b/example/src/main/java/org/xmtp/android/example/utils/KeyUtil.kt index 88c065623..0061913d1 100644 --- a/example/src/main/java/org/xmtp/android/example/utils/KeyUtil.kt +++ b/example/src/main/java/org/xmtp/android/example/utils/KeyUtil.kt @@ -23,12 +23,12 @@ class KeyUtil(val context: Context) { return accountManager.getPassword(account) } - fun storeKey(address: String, key: ByteArray?) { + fun storeKey(address: String, dbEncryptionKey: ByteArray?) { val alias = "xmtp-dev-${address.lowercase()}" val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) val editor = prefs.edit() - editor.putString(alias, encodeToString(key, NO_WRAP)) + editor.putString(alias, encodeToString(dbEncryptionKey, NO_WRAP)) editor.apply() } diff --git a/library/build.gradle b/library/build.gradle index 886ffba89..bea9cc977 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -92,6 +92,7 @@ dependencies { api 'org.xmtp:proto-kotlin:3.71.0' testImplementation 'junit:junit:4.13.2' + testImplementation 'androidx.test:monitor:1.7.2' androidTestImplementation 'app.cash.turbine:turbine:1.1.0' androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0' androidTestImplementation 'androidx.test.ext:junit:1.1.5' diff --git a/library/src/androidTest/java/org/xmtp/android/library/AttachmentTest.kt b/library/src/androidTest/java/org/xmtp/android/library/AttachmentTest.kt index d42908f33..8dac7c006 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/AttachmentTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/AttachmentTest.kt @@ -24,9 +24,9 @@ class AttachmentTest { Client.register(codec = AttachmentCodec()) val fixtures = fixtures() - val aliceClient = fixtures.aliceClient + val aliceClient = fixtures.alixClient val aliceConversation = runBlocking { - aliceClient.conversations.newConversation(fixtures.bob.walletAddress) + aliceClient.conversations.newConversation(fixtures.bo.walletAddress) } runBlocking { @@ -36,8 +36,8 @@ class AttachmentTest { ) } val messages = runBlocking { aliceConversation.messages() } - assertEquals(messages.size, 1) - if (messages.size == 1) { + assertEquals(messages.size, 2) + if (messages.size == 2) { val content: Attachment? = messages[0].content() assertEquals("test.txt", content?.filename) assertEquals("text/plain", content?.mimeType) 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 15f221f67..3d512f8c5 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/ClientTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/ClientTest.kt @@ -9,9 +9,6 @@ import org.junit.Assert.fail import org.junit.Test import org.junit.runner.RunWith import org.xmtp.android.library.messages.PrivateKeyBuilder -import org.xmtp.android.library.messages.PrivateKeyBundleV1Builder -import org.xmtp.android.library.messages.generate -import org.xmtp.proto.message.contents.PrivateKeyOuterClass import uniffi.xmtpv3.GenericException import java.security.SecureRandom import java.util.concurrent.CompletableFuture @@ -19,77 +16,13 @@ import java.util.concurrent.TimeUnit @RunWith(AndroidJUnit4::class) class ClientTest { - @Test - fun testTakesAWallet() { - val fakeWallet = PrivateKeyBuilder() - runBlocking { Client().create(account = fakeWallet) } - } - - @Test - fun testHasPrivateKeyBundleV1() { - val fakeWallet = PrivateKeyBuilder() - val client = runBlocking { Client().create(account = fakeWallet) } - assertEquals(1, client.v1keys.preKeysList?.size) - val preKey = client.v1keys.preKeysList?.get(0) - assert(preKey?.publicKey?.hasSignature() ?: false) - } - - @Test - fun testSerialization() { - val wallet = PrivateKeyBuilder() - val v1 = - PrivateKeyOuterClass.PrivateKeyBundleV1.newBuilder().build().generate(wallet = wallet) - val encodedData = PrivateKeyBundleV1Builder.encodeData(v1) - val v1Copy = PrivateKeyBundleV1Builder.fromEncodedData(encodedData) - val client = runBlocking { Client().buildFrom(v1Copy) } - assertEquals( - wallet.address, - client.address, - ) - } - @Test fun testCanBeCreatedWithBundle() { - val fakeWallet = PrivateKeyBuilder() - val client = runBlocking { Client().create(account = fakeWallet) } - val bundle = client.privateKeyBundle - val clientFromV1Bundle = runBlocking { Client().buildFromBundle(bundle) } - assertEquals(client.address, clientFromV1Bundle.address) - assertEquals( - client.v1keys.identityKey, - clientFromV1Bundle.v1keys.identityKey, - ) - assertEquals( - client.v1keys.preKeysList, - clientFromV1Bundle.v1keys.preKeysList, - ) - } - - @Test - fun testCanBeCreatedWithV1Bundle() { - val fakeWallet = PrivateKeyBuilder() - val client = runBlocking { Client().create(account = fakeWallet) } - val bundleV1 = client.v1keys - val clientFromV1Bundle = runBlocking { Client().buildFromV1Bundle(bundleV1) } - assertEquals(client.address, clientFromV1Bundle.address) - assertEquals( - client.v1keys.identityKey, - clientFromV1Bundle.v1keys.identityKey, - ) - assertEquals( - client.v1keys.preKeysList, - clientFromV1Bundle.v1keys.preKeysList, - ) - } - - @Test - fun testV3CanBeCreatedWithBundle() { 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 ) @@ -98,37 +31,27 @@ class ClientTest { } runBlocking { - client.canMessageV3(listOf(client.address))[client.address]?.let { assert(it) } + client.canMessage(listOf(client.address))[client.address]?.let { assert(it) } } - val bundle = client.privateKeyBundle - val clientFromV1Bundle = runBlocking { - Client().buildFromBundle(bundle, options = options) + val fromBundle = runBlocking { + Client().build(fakeWallet.address, options = options) } - assertEquals(client.address, clientFromV1Bundle.address) - assertEquals( - client.v1keys.identityKey, - clientFromV1Bundle.v1keys.identityKey, - ) + assertEquals(client.address, fromBundle.address) + assertEquals(client.inboxId, fromBundle.inboxId) runBlocking { - clientFromV1Bundle.canMessageV3(listOf(client.address))[client.address]?.let { assert(it) } + fromBundle.canMessage(listOf(client.address))[client.address]?.let { assert(it) } } - - assertEquals( - client.address, - clientFromV1Bundle.address - ) } @Test - fun testCreatesAV3Client() { + fun testCreatesAClient() { 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 ) @@ -140,49 +63,12 @@ class ClientTest { ) } runBlocking { - client.canMessageV3(listOf(client.address))[client.address]?.let { assert(it) } + client.canMessage(listOf(client.address))[client.address]?.let { assert(it) } } assert(client.installationId.isNotEmpty()) 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().createV3( - account = fakeWallet, - options = options - ) - } - runBlocking { - client.canMessageV3(listOf(client.address))[client.address]?.let { assert(it) } - } - assert(client.installationId.isNotEmpty()) - assertEquals(inboxId, client.inboxId) - - val sameClient = runBlocking { - Client().buildV3( - address = fakeWallet.address, - options = options - ) - } - runBlocking { - client.canMessageV3(listOf(sameClient.address))[sameClient.address]?.let { assert(it) } - } - assert(sameClient.installationId.isNotEmpty()) - assertEquals(client.inboxId, sameClient.inboxId) - } - @Test fun testCanDeleteDatabase() { val key = SecureRandom().generateSeed(32) @@ -194,7 +80,6 @@ class ClientTest { account = fakeWallet, options = ClientOptions( ClientOptions.Api(XMTPEnvironment.LOCAL, false), - enableV3 = true, appContext = context, dbEncryptionKey = key ) @@ -205,7 +90,6 @@ class ClientTest { account = fakeWallet2, options = ClientOptions( ClientOptions.Api(XMTPEnvironment.LOCAL, false), - enableV3 = true, appContext = context, dbEncryptionKey = key ) @@ -226,7 +110,6 @@ class ClientTest { account = fakeWallet, options = ClientOptions( ClientOptions.Api(XMTPEnvironment.LOCAL, false), - enableV3 = true, appContext = context, dbEncryptionKey = key ) @@ -239,7 +122,7 @@ class ClientTest { } @Test - fun testCreatesAV3DevClient() { + fun testCreatesADevClient() { val key = SecureRandom().generateSeed(32) val context = InstrumentationRegistry.getInstrumentation().targetContext val fakeWallet = PrivateKeyBuilder() @@ -248,19 +131,18 @@ class ClientTest { account = fakeWallet, options = ClientOptions( ClientOptions.Api(XMTPEnvironment.DEV, true), - enableV3 = true, appContext = context, dbEncryptionKey = key ) ) } runBlocking { - client.canMessageV3(listOf(client.address))[client.address]?.let { assert(it) } + client.canMessage(listOf(client.address))[client.address]?.let { assert(it) } } } @Test - fun testCreatesAV3ProductionClient() { + fun testCreatesAProductionClient() { val key = SecureRandom().generateSeed(32) val context = InstrumentationRegistry.getInstrumentation().targetContext val fakeWallet = PrivateKeyBuilder() @@ -269,98 +151,13 @@ class ClientTest { account = fakeWallet, options = ClientOptions( ClientOptions.Api(XMTPEnvironment.PRODUCTION, true), - enableV3 = true, appContext = context, dbEncryptionKey = key ) ) } runBlocking { - client.canMessageV3(listOf(client.address))[client.address]?.let { assert(it) } - } - } - - @Test - fun testDoesNotCreateAV3Client() { - val fakeWallet = PrivateKeyBuilder() - val client = runBlocking { Client().create(account = fakeWallet) } - assertThrows("Error no V3 client initialized", XMTPException::class.java) { - runBlocking { - client.canMessageV3(listOf(client.address))[client.address]?.let { assert(!it) } - } - } - } - - @Test - fun testCanMessage() { - val fixtures = fixtures() - val notOnNetwork = PrivateKeyBuilder() - val canMessage = runBlocking { fixtures.aliceClient.canMessage(fixtures.bobClient.address) } - val cannotMessage = runBlocking { fixtures.aliceClient.canMessage(notOnNetwork.address) } - assert(canMessage) - assert(!cannotMessage) - } - - @Test - fun testPublicCanMessage() { - val aliceWallet = PrivateKeyBuilder() - val notOnNetwork = PrivateKeyBuilder() - val opts = ClientOptions(ClientOptions.Api(XMTPEnvironment.LOCAL, false)) - val aliceClient = runBlocking { - Client().create(aliceWallet, opts) - } - runBlocking { aliceClient.ensureUserContactPublished() } - - val canMessage = runBlocking { Client.canMessage(aliceWallet.address, opts) } - val cannotMessage = runBlocking { Client.canMessage(notOnNetwork.address, opts) } - - assert(canMessage) - assert(!cannotMessage) - } - - @Test - fun testPreEnableIdentityCallback() { - val fakeWallet = PrivateKeyBuilder() - val expectation = CompletableFuture() - - val preEnableIdentityCallback: suspend () -> Unit = { - expectation.complete(Unit) - } - - val opts = ClientOptions( - ClientOptions.Api(XMTPEnvironment.LOCAL, false), - preEnableIdentityCallback = preEnableIdentityCallback - ) - - try { - runBlocking { - Client().create(account = fakeWallet, options = opts) - } - expectation.get(5, TimeUnit.SECONDS) - } catch (e: Exception) { - fail("Error: $e") - } - } - - @Test - fun testPreCreateIdentityCallback() { - val fakeWallet = PrivateKeyBuilder() - val expectation = CompletableFuture() - - val preCreateIdentityCallback: suspend () -> Unit = { - expectation.complete(Unit) - } - - val opts = ClientOptions( - ClientOptions.Api(XMTPEnvironment.LOCAL, false), - preCreateIdentityCallback = preCreateIdentityCallback - ) - - try { - runBlocking { Client().create(account = fakeWallet, options = opts) } - expectation.get(5, TimeUnit.SECONDS) - } catch (e: Exception) { - fail("Error: $e") + client.canMessage(listOf(client.address))[client.address]?.let { assert(it) } } } @@ -378,7 +175,6 @@ class ClientTest { val opts = ClientOptions( ClientOptions.Api(XMTPEnvironment.LOCAL, false), preAuthenticateToInboxCallback = preAuthenticateToInboxCallback, - enableV3 = true, appContext = context, dbEncryptionKey = key ) @@ -402,7 +198,6 @@ class ClientTest { account = fakeWallet, options = ClientOptions( ClientOptions.Api(XMTPEnvironment.LOCAL, false), - enableV3 = true, appContext = context, dbEncryptionKey = key ) @@ -413,7 +208,6 @@ class ClientTest { account = fakeWallet2, options = ClientOptions( ClientOptions.Api(XMTPEnvironment.LOCAL, false), - enableV3 = true, appContext = context, dbEncryptionKey = key ) @@ -454,7 +248,6 @@ class ClientTest { account = alixWallet, options = ClientOptions( ClientOptions.Api(XMTPEnvironment.LOCAL, false), - enableV3 = true, appContext = context, dbEncryptionKey = key ) @@ -465,7 +258,6 @@ class ClientTest { account = boWallet, options = ClientOptions( ClientOptions.Api(XMTPEnvironment.LOCAL, false), - enableV3 = true, appContext = context, dbEncryptionKey = key ) @@ -487,7 +279,6 @@ class ClientTest { account = alixWallet, options = ClientOptions( ClientOptions.Api(XMTPEnvironment.LOCAL, false), - enableV3 = true, appContext = context, dbEncryptionKey = key ) @@ -499,7 +290,6 @@ class ClientTest { account = alixWallet, options = ClientOptions( ClientOptions.Api(XMTPEnvironment.LOCAL, false), - enableV3 = true, appContext = context, dbEncryptionKey = key ) @@ -513,7 +303,6 @@ class ClientTest { account = alixWallet, options = ClientOptions( ClientOptions.Api(XMTPEnvironment.LOCAL, false), - enableV3 = true, appContext = context, dbEncryptionKey = key ) diff --git a/library/src/androidTest/java/org/xmtp/android/library/CodecTest.kt b/library/src/androidTest/java/org/xmtp/android/library/CodecTest.kt index 1f65c045e..b25d160af 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/CodecTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/CodecTest.kt @@ -49,9 +49,9 @@ class CodecTest { fun testCanRoundTripWithCustomContentType() { Client.register(codec = NumberCodec()) val fixtures = fixtures() - val aliceClient = fixtures.aliceClient + val aliceClient = fixtures.alixClient val aliceConversation = runBlocking { - aliceClient.conversations.newConversation(fixtures.bob.walletAddress) + aliceClient.conversations.newConversation(fixtures.bo.walletAddress) } runBlocking { aliceConversation.send( @@ -60,8 +60,8 @@ class CodecTest { ) } val messages = runBlocking { aliceConversation.messages() } - assertEquals(messages.size, 1) - if (messages.size == 1) { + assertEquals(messages.size, 2) + if (messages.size == 2) { val content: Double? = messages[0].content() assertEquals(3.14, content) assertEquals("Error: This app does not support numbers.", messages[0].fallbackContent) diff --git a/library/src/androidTest/java/org/xmtp/android/library/ContactsTest.kt b/library/src/androidTest/java/org/xmtp/android/library/ContactsTest.kt deleted file mode 100644 index bceb66c4d..000000000 --- a/library/src/androidTest/java/org/xmtp/android/library/ContactsTest.kt +++ /dev/null @@ -1,59 +0,0 @@ -package org.xmtp.android.library - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import kotlinx.coroutines.runBlocking -import org.junit.Assert.assertEquals -import org.junit.Test -import org.junit.runner.RunWith -import org.xmtp.android.library.messages.walletAddress - -@RunWith(AndroidJUnit4::class) -class ContactsTest { - - @Test - fun testNormalizesAddresses() { - val fixtures = fixtures() - runBlocking { fixtures.bobClient.ensureUserContactPublished() } - val bobAddressLowerCased = fixtures.bobClient.address.lowercase() - val bobContact = fixtures.aliceClient.getUserContact(peerAddress = bobAddressLowerCased) - assert(bobContact != null) - } - - @Test - fun testCanFindContact() { - val fixtures = fixtures() - runBlocking { fixtures.bobClient.ensureUserContactPublished() } - val contactBundle = fixtures.aliceClient.contacts.find(fixtures.bob.walletAddress) - assertEquals(contactBundle?.walletAddress, fixtures.bob.walletAddress) - } - - @Test - fun testAllowAddress() { - val fixtures = fixtures() - - val contacts = fixtures.bobClient.contacts - var result = runBlocking { contacts.isAllowed(fixtures.alice.walletAddress) } - - assert(!result) - - runBlocking { contacts.allow(listOf(fixtures.alice.walletAddress)) } - - result = runBlocking { contacts.isAllowed(fixtures.alice.walletAddress) } - assert(result) - } - - @Test - fun testDenyAddress() { - val fixtures = fixtures() - - val contacts = fixtures.bobClient.contacts - var result = runBlocking { contacts.isAllowed(fixtures.alice.walletAddress) } - - assert(!result) - - runBlocking { contacts.deny(listOf(fixtures.alice.walletAddress)) } - - result = runBlocking { contacts.isDenied(fixtures.alice.walletAddress) } - assert(result) - } -} diff --git a/library/src/androidTest/java/org/xmtp/android/library/ConversationsTest.kt b/library/src/androidTest/java/org/xmtp/android/library/ConversationsTest.kt new file mode 100644 index 000000000..239b1a58c --- /dev/null +++ b/library/src/androidTest/java/org/xmtp/android/library/ConversationsTest.kt @@ -0,0 +1,170 @@ +package org.xmtp.android.library + +import androidx.test.ext.junit.runners.AndroidJUnit4 +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.libxmtp.Message.* +import org.xmtp.android.library.messages.PrivateKey +import org.xmtp.android.library.messages.PrivateKeyBuilder +import org.xmtp.android.library.messages.walletAddress + +@RunWith(AndroidJUnit4::class) +class ConversationsTest { + private lateinit var alixWallet: PrivateKeyBuilder + private lateinit var boWallet: PrivateKeyBuilder + private lateinit var alix: PrivateKey + private lateinit var alixClient: Client + private lateinit var bo: PrivateKey + private lateinit var boClient: Client + private lateinit var caroWallet: PrivateKeyBuilder + private lateinit var caro: PrivateKey + private lateinit var caroClient: Client + private lateinit var fixtures: Fixtures + + @Before + fun setUp() { + fixtures = fixtures() + alixWallet = fixtures.alixAccount + alix = fixtures.alix + boWallet = fixtures.boAccount + bo = fixtures.bo + caroWallet = fixtures.caroAccount + caro = fixtures.caro + + alixClient = fixtures.alixClient + boClient = fixtures.boClient + caroClient = fixtures.caroClient + } + + @Test + fun testsCanFindConversationByTopic() { + val group = + runBlocking { boClient.conversations.newGroup(listOf(caro.walletAddress)) } + val dm = runBlocking { boClient.conversations.findOrCreateDm(caro.walletAddress) } + + val sameDm = boClient.findConversationByTopic(dm.topic) + val sameGroup = boClient.findConversationByTopic(group.topic) + assertEquals(group.id, sameGroup?.id) + assertEquals(dm.id, sameDm?.id) + } + + @Test + fun testsCanListConversations() { + val dm = runBlocking { boClient.conversations.findOrCreateDm(caro.walletAddress) } + val group = + runBlocking { boClient.conversations.newGroup(listOf(caro.walletAddress)) } + assertEquals(runBlocking { boClient.conversations.list().size }, 2) + assertEquals(runBlocking { boClient.conversations.listDms().size }, 1) + assertEquals(runBlocking { boClient.conversations.listGroups().size }, 1) + + runBlocking { caroClient.conversations.syncConversations() } + assertEquals( + runBlocking { caroClient.conversations.list().size }, + 2 + ) + assertEquals(runBlocking { caroClient.conversations.listGroups().size }, 1) + } + + @Test + fun testsCanListConversationsFiltered() { + val dm = runBlocking { boClient.conversations.findOrCreateDm(caro.walletAddress) } + val group = + runBlocking { boClient.conversations.newGroup(listOf(caro.walletAddress)) } + assertEquals(runBlocking { boClient.conversations.list().size }, 2) + assertEquals( + runBlocking { boClient.conversations.list(consentState = ConsentState.ALLOWED).size }, + 2 + ) + runBlocking { group.updateConsentState(ConsentState.DENIED) } + assertEquals( + runBlocking { boClient.conversations.list(consentState = ConsentState.ALLOWED).size }, + 1 + ) + assertEquals( + runBlocking { boClient.conversations.list(consentState = ConsentState.DENIED).size }, + 1 + ) + assertEquals(runBlocking { boClient.conversations.list().size }, 2) + } + + @Test + fun testCanListConversationsOrder() { + val dm = runBlocking { boClient.conversations.findOrCreateDm(caro.walletAddress) } + val group1 = + runBlocking { boClient.conversations.newGroup(listOf(caro.walletAddress)) } + val group2 = + runBlocking { boClient.conversations.newGroup(listOf(caro.walletAddress)) } + runBlocking { dm.send("Howdy") } + runBlocking { group2.send("Howdy") } + runBlocking { boClient.conversations.syncAllConversations() } + val conversations = runBlocking { boClient.conversations.list() } + val conversationsOrdered = + runBlocking { boClient.conversations.list(order = Conversations.ConversationOrder.LAST_MESSAGE) } + assertEquals(conversations.size, 3) + assertEquals(conversationsOrdered.size, 3) + assertEquals(conversations.map { it.id }, listOf(dm.id, group1.id, group2.id)) + assertEquals(conversationsOrdered.map { it.id }, listOf(group2.id, dm.id, group1.id)) + } + + @Test + fun testCanStreamAllMessages() { + val group = + runBlocking { caroClient.conversations.newGroup(listOf(bo.walletAddress)) } + val conversation = + runBlocking { boClient.conversations.findOrCreateDm(caro.walletAddress) } + runBlocking { boClient.conversations.syncConversations() } + + val allMessages = mutableListOf() + + val job = CoroutineScope(Dispatchers.IO).launch { + try { + boClient.conversations.streamAllMessages() + .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 testCanStreamGroupsAndConversations() { + val allMessages = mutableListOf() + + val job = CoroutineScope(Dispatchers.IO).launch { + try { + boClient.conversations.stream() + .collect { message -> + allMessages.add(message.topic) + } + } catch (e: Exception) { + } + } + Thread.sleep(1000) + + runBlocking { + caroClient.conversations.newGroup(listOf(bo.walletAddress)) + Thread.sleep(1000) + boClient.conversations.findOrCreateDm(caro.walletAddress) + } + + Thread.sleep(2000) + assertEquals(2, allMessages.size) + job.cancel() + } +} diff --git a/library/src/androidTest/java/org/xmtp/android/library/DmTest.kt b/library/src/androidTest/java/org/xmtp/android/library/DmTest.kt index 71a388b93..33be0fbfc 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/DmTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/DmTest.kt @@ -1,7 +1,6 @@ package org.xmtp.android.library import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry import app.cash.turbine.test import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -17,11 +16,10 @@ import org.xmtp.android.library.codecs.Reaction import org.xmtp.android.library.codecs.ReactionAction import org.xmtp.android.library.codecs.ReactionCodec import org.xmtp.android.library.codecs.ReactionSchema -import org.xmtp.android.library.libxmtp.Message.* +import org.xmtp.android.library.libxmtp.Message.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 DmTest { @@ -37,48 +35,17 @@ class DmTest { @Before fun setUp() { - val key = SecureRandom().generateSeed(32) - val context = InstrumentationRegistry.getInstrumentation().targetContext - alixWallet = PrivateKeyBuilder() - alix = alixWallet.getPrivateKey() - alixClient = runBlocking { - Client().createV3( - account = alixWallet, - options = ClientOptions( - ClientOptions.Api(XMTPEnvironment.LOCAL, false), - enableV3 = true, - appContext = context, - dbEncryptionKey = key - ) - ) - } - boWallet = PrivateKeyBuilder() - bo = boWallet.getPrivateKey() - boClient = runBlocking { - Client().createV3( - account = boWallet, - options = ClientOptions( - ClientOptions.Api(XMTPEnvironment.LOCAL, false), - enableV3 = true, - appContext = context, - dbEncryptionKey = key - ) - ) - } - - caroWallet = PrivateKeyBuilder() - caro = caroWallet.getPrivateKey() - caroClient = runBlocking { - Client().createV3( - account = caroWallet, - options = ClientOptions( - ClientOptions.Api(XMTPEnvironment.LOCAL, false), - enableV3 = true, - appContext = context, - dbEncryptionKey = key - ) - ) - } + val fixtures = fixtures() + alixWallet = fixtures.alixAccount + alix = fixtures.alix + boWallet = fixtures.boAccount + bo = fixtures.bo + caroWallet = fixtures.caroAccount + caro = fixtures.caro + + alixClient = fixtures.alixClient + boClient = fixtures.boClient + caroClient = fixtures.caroClient } @Test @@ -127,7 +94,6 @@ class DmTest { fun testCannotCreateDmWithMemberNotOnV3() { val chuxAccount = PrivateKeyBuilder() val chux: PrivateKey = chuxAccount.getPrivateKey() - runBlocking { Client().create(account = chuxAccount) } assertThrows("Recipient not on network", XMTPException::class.java) { runBlocking { boClient.conversations.findOrCreateDm(chux.walletAddress) } @@ -148,8 +114,7 @@ class DmTest { dm.send("howdy") dm.send("gm") dm.sync() - assert(boClient.contacts.isGroupAllowed(dm.id)) - assertEquals(boClient.contacts.consentList.groupState(dm.id), ConsentState.ALLOWED) + assertEquals(boClient.preferences.consentList.conversationState(dm.id), ConsentState.ALLOWED) assertEquals(dm.consentState(), ConsentState.ALLOWED) } } @@ -293,15 +258,32 @@ class DmTest { runBlocking { val dm = boClient.conversations.findOrCreateDm(alix.walletAddress) - assert(boClient.contacts.isGroupAllowed(dm.id)) + assertEquals(boClient.preferences.consentList.conversationState(dm.id), ConsentState.ALLOWED) + assertEquals(dm.consentState(), ConsentState.ALLOWED) - boClient.contacts.denyGroups(listOf(dm.id)) - assert(boClient.contacts.isGroupDenied(dm.id)) + boClient.preferences.consentList.setConsentState( + listOf( + ConsentListEntry( + dm.id, + EntryType.CONVERSATION_ID, + ConsentState.DENIED + ) + ) + ) + assertEquals(boClient.preferences.consentList.conversationState(dm.id), ConsentState.DENIED) assertEquals(dm.consentState(), ConsentState.DENIED) - dm.updateConsentState(ConsentState.ALLOWED) - assert(boClient.contacts.isGroupAllowed(dm.id)) + boClient.preferences.consentList.setConsentState( + listOf( + ConsentListEntry( + dm.id, + EntryType.CONVERSATION_ID, + ConsentState.ALLOWED + ) + ) + ) + assertEquals(boClient.preferences.consentList.conversationState(dm.id), ConsentState.ALLOWED) assertEquals(dm.consentState(), ConsentState.ALLOWED) } } diff --git a/library/src/androidTest/java/org/xmtp/android/library/FramesTest.kt b/library/src/androidTest/java/org/xmtp/android/library/FramesTest.kt deleted file mode 100644 index db0bc52c8..000000000 --- a/library/src/androidTest/java/org/xmtp/android/library/FramesTest.kt +++ /dev/null @@ -1,71 +0,0 @@ -package org.xmtp.android.library - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import kotlinx.coroutines.runBlocking -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotNull -import org.junit.Test -import org.junit.runner.RunWith -import org.xmtp.android.library.frames.ConversationActionInputs -import org.xmtp.android.library.frames.DmActionInputs -import org.xmtp.android.library.frames.FrameActionInputs -import org.xmtp.android.library.frames.FramePostPayload -import org.xmtp.android.library.frames.FramesClient -import org.xmtp.android.library.frames.GetMetadataResponse -import java.net.HttpURLConnection -import java.net.URL - -@RunWith(AndroidJUnit4::class) -class FramesTest { - @Test - fun testFramesClient() { - val frameUrl = "https://fc-polls-five.vercel.app/polls/01032f47-e976-42ee-9e3d-3aac1324f4b8" - val fixtures = fixtures() - val aliceClient = fixtures.aliceClient - - val framesClient = FramesClient(xmtpClient = aliceClient) - val conversationTopic = "foo" - val participantAccountAddresses = listOf("alix", "bo") - val metadata: GetMetadataResponse - runBlocking { - metadata = framesClient.proxy.readMetadata(url = frameUrl) - } - - val dmInputs = DmActionInputs( - conversationTopic = conversationTopic, - participantAccountAddresses = participantAccountAddresses - ) - val conversationInputs = ConversationActionInputs.Dm(dmInputs) - val frameInputs = FrameActionInputs( - frameUrl = frameUrl, - buttonIndex = 1, - inputText = null, - state = null, - conversationInputs = conversationInputs - ) - val signedPayload: FramePostPayload - runBlocking { - signedPayload = framesClient.signFrameAction(inputs = frameInputs) - } - val postUrl = metadata.extractedTags["fc:frame:post_url"] - assertNotNull(postUrl) - val response: GetMetadataResponse - runBlocking { - response = framesClient.proxy.post(url = postUrl!!, payload = signedPayload) - } - - assertEquals(response.extractedTags["fc:frame"], "vNext") - - val imageUrl = response.extractedTags["fc:frame:image"] - assertNotNull(imageUrl) - - val mediaUrl = framesClient.proxy.mediaUrl(url = imageUrl!!) - - val url = URL(mediaUrl) - val connection = url.openConnection() as HttpURLConnection - connection.requestMethod = "GET" - val responseCode = connection.responseCode - assertEquals(responseCode, 200) - assertEquals(connection.contentType, "image/png") - } -} diff --git a/library/src/androidTest/java/org/xmtp/android/library/GroupPermissionsTest.kt b/library/src/androidTest/java/org/xmtp/android/library/GroupPermissionsTest.kt index 00dbcf707..c92cfa287 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/GroupPermissionsTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/GroupPermissionsTest.kt @@ -35,24 +35,16 @@ class GroupPermissionsTest { fun setUp() { val key = SecureRandom().generateSeed(32) val context = InstrumentationRegistry.getInstrumentation().targetContext - fixtures = - fixtures( - clientOptions = ClientOptions( - ClientOptions.Api(XMTPEnvironment.LOCAL, false), - enableV3 = true, - appContext = context, - dbEncryptionKey = key - ) - ) - alixWallet = fixtures.aliceAccount - alix = fixtures.alice - boWallet = fixtures.bobAccount - bo = fixtures.bob + fixtures = fixtures() + alixWallet = fixtures.alixAccount + alix = fixtures.alix + boWallet = fixtures.boAccount + bo = fixtures.bo caroWallet = fixtures.caroAccount caro = fixtures.caro - alixClient = fixtures.aliceClient - boClient = fixtures.bobClient + alixClient = fixtures.alixClient + boClient = fixtures.boClient caroClient = fixtures.caroClient } diff --git a/library/src/androidTest/java/org/xmtp/android/library/GroupTest.kt b/library/src/androidTest/java/org/xmtp/android/library/GroupTest.kt index b5b0f63c3..5e6f6ed8d 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/GroupTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/GroupTest.kt @@ -1,7 +1,6 @@ package org.xmtp.android.library import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry import app.cash.turbine.test import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -19,14 +18,13 @@ import org.xmtp.android.library.codecs.Reaction import org.xmtp.android.library.codecs.ReactionAction import org.xmtp.android.library.codecs.ReactionCodec import org.xmtp.android.library.codecs.ReactionSchema -import org.xmtp.android.library.libxmtp.Message.* +import org.xmtp.android.library.libxmtp.Message.MessageDeliveryStatus import org.xmtp.android.library.messages.PrivateKey import org.xmtp.android.library.messages.PrivateKeyBuilder import org.xmtp.android.library.messages.walletAddress import org.xmtp.proto.mls.message.contents.TranscriptMessages import uniffi.xmtpv3.org.xmtp.android.library.libxmtp.GroupPermissionPreconfiguration import uniffi.xmtpv3.org.xmtp.android.library.libxmtp.PermissionOption -import java.security.SecureRandom @RunWith(AndroidJUnit4::class) class GroupTest { @@ -39,39 +37,21 @@ class GroupTest { private lateinit var caroWallet: PrivateKeyBuilder private lateinit var caro: PrivateKey private lateinit var caroClient: Client - private lateinit var davonV3Wallet: PrivateKeyBuilder - private lateinit var davonV3: PrivateKey - private lateinit var davonV3Client: Client private lateinit var fixtures: Fixtures @Before fun setUp() { - val key = SecureRandom().generateSeed(32) - val context = InstrumentationRegistry.getInstrumentation().targetContext - val options = ClientOptions( - ClientOptions.Api(XMTPEnvironment.LOCAL, false), - enableV3 = true, - appContext = context, - dbEncryptionKey = key - ) - fixtures = - fixtures( - clientOptions = options - ) - alixWallet = fixtures.aliceAccount - alix = fixtures.alice - boWallet = fixtures.bobAccount - bo = fixtures.bob + fixtures = fixtures() + alixWallet = fixtures.alixAccount + alix = fixtures.alix + boWallet = fixtures.boAccount + bo = fixtures.bo caroWallet = fixtures.caroAccount caro = fixtures.caro - davonV3Wallet = PrivateKeyBuilder() - davonV3 = davonV3Wallet.getPrivateKey() - alixClient = fixtures.aliceClient - boClient = fixtures.bobClient + alixClient = fixtures.alixClient + boClient = fixtures.boClient caroClient = fixtures.caroClient - davonV3Client = - runBlocking { Client().createV3(account = davonV3Wallet, options = options) } } @Test @@ -130,9 +110,12 @@ class GroupTest { assert(alixGroup.id.isNotEmpty()) runBlocking { - assertEquals(boClient.contacts.consentList.groupState(boGroup.id), ConsentState.ALLOWED) assertEquals( - alixClient.contacts.consentList.groupState(alixGroup.id), + boClient.preferences.consentList.conversationState(boGroup.id), + ConsentState.ALLOWED + ) + assertEquals( + alixClient.preferences.consentList.conversationState(alixGroup.id), ConsentState.UNKNOWN ) } @@ -386,7 +369,6 @@ class GroupTest { runBlocking { boClient.conversations.newGroup(listOf(alix.walletAddress)) boClient.conversations.newGroup(listOf(caro.walletAddress)) - davonV3Client.conversations.findOrCreateDm(bo.walletAddress) boClient.conversations.syncConversations() } val groups = runBlocking { boClient.conversations.listGroups() } @@ -399,7 +381,6 @@ class GroupTest { boClient.conversations.newGroup(listOf(alix.walletAddress)) boClient.conversations.newGroup(listOf(caro.walletAddress)) boClient.conversations.newConversation(alix.walletAddress) - davonV3Client.conversations.findOrCreateDm(bo.walletAddress) boClient.conversations.syncConversations() } val convos = runBlocking { boClient.conversations.list() } @@ -410,7 +391,6 @@ class GroupTest { fun testCannotSendMessageToGroupMemberNotOnV3() { val chuxAccount = PrivateKeyBuilder() val chux: PrivateKey = chuxAccount.getPrivateKey() - runBlocking { Client().create(account = chuxAccount) } assertThrows("Recipient not on network", XMTPException::class.java) { runBlocking { boClient.conversations.newGroup(listOf(chux.walletAddress)) } @@ -437,8 +417,11 @@ class GroupTest { group.send("howdy") group.send("gm") group.sync() - assert(boClient.contacts.isGroupAllowed(group.id)) - assertEquals(boClient.contacts.consentList.groupState(group.id), ConsentState.ALLOWED) + assertEquals(group.consentState(), ConsentState.ALLOWED) + assertEquals( + boClient.preferences.consentList.conversationState(group.id), + ConsentState.ALLOWED + ) } } @@ -592,7 +575,6 @@ class GroupTest { @Test fun testCanStreamAllGroupMessages() { val group = runBlocking { caroClient.conversations.newGroup(listOf(alix.walletAddress)) } - val dm = runBlocking { davonV3Client.conversations.findOrCreateDm(alix.walletAddress) } runBlocking { alixClient.conversations.syncConversations() } val allMessages = mutableListOf() @@ -607,7 +589,6 @@ class GroupTest { } Thread.sleep(2500) - runBlocking { dm.send("should not stream") } for (i in 0 until 2) { runBlocking { group.send(text = "Message $i") @@ -633,7 +614,6 @@ class GroupTest { @Test fun testCanStreamAllMessages() { val group = runBlocking { caroClient.conversations.newGroup(listOf(alix.walletAddress)) } - val dm = runBlocking { davonV3Client.conversations.findOrCreateDm(alix.walletAddress) } val conversation = runBlocking { boClient.conversations.newConversation(alix.walletAddress) } runBlocking { alixClient.conversations.syncConversations() } @@ -654,7 +634,6 @@ class GroupTest { runBlocking { group.send("hi") conversation.send("hi") - dm.send("should not stream") } Thread.sleep(1000) @@ -673,9 +652,6 @@ class GroupTest { val group2 = caroClient.conversations.newGroup(listOf(bo.walletAddress)) assertEquals(group2.id, awaitItem().id) - davonV3Client.conversations.findOrCreateDm(bo.walletAddress) - expectNoEvents() - cancelAndConsumeRemainingEvents() } } @@ -695,7 +671,6 @@ class GroupTest { Thread.sleep(2500) runBlocking { - davonV3Client.conversations.findOrCreateDm(alix.walletAddress) alixClient.conversations.newConversation(bo.walletAddress) Thread.sleep(2500) caroClient.conversations.newGroup(listOf(alix.walletAddress)) @@ -718,15 +693,29 @@ class GroupTest { caro.walletAddress ) ) - assert(boClient.contacts.isGroupAllowed(group.id)) + assertEquals( + boClient.preferences.consentList.conversationState(group.id), + ConsentState.ALLOWED + ) assertEquals(group.consentState(), ConsentState.ALLOWED) - boClient.contacts.denyGroups(listOf(group.id)) - assert(boClient.contacts.isGroupDenied(group.id)) + boClient.preferences.consentList.setConsentState( + listOf( + ConsentListEntry( + group.id, + EntryType.CONVERSATION_ID, + ConsentState.DENIED + ) + ) + ) + assertEquals(boClient.preferences.consentList.conversationState(group.id), ConsentState.DENIED) assertEquals(group.consentState(), ConsentState.DENIED) group.updateConsentState(ConsentState.ALLOWED) - assert(boClient.contacts.isGroupAllowed(group.id)) + assertEquals( + boClient.preferences.consentList.conversationState(group.id), + ConsentState.ALLOWED + ) assertEquals(group.consentState(), ConsentState.ALLOWED) } } @@ -735,30 +724,63 @@ class GroupTest { fun testCanAllowAndDenyInboxId() { runBlocking { val boGroup = boClient.conversations.newGroup(listOf(alix.walletAddress)) - assert(!boClient.contacts.isInboxAllowed(alixClient.inboxId)) - assert(!boClient.contacts.isInboxDenied(alixClient.inboxId)) - - boClient.contacts.allowInboxes(listOf(alixClient.inboxId)) + assertEquals( + boClient.preferences.consentList.inboxIdState(alixClient.inboxId), + ConsentState.UNKNOWN + ) + boClient.preferences.consentList.setConsentState( + listOf( + ConsentListEntry( + alixClient.inboxId, + EntryType.INBOX_ID, + ConsentState.ALLOWED + ) + ) + ) var alixMember = boGroup.members().firstOrNull { it.inboxId == alixClient.inboxId } assertEquals(alixMember!!.consentState, ConsentState.ALLOWED) - assert(boClient.contacts.isInboxAllowed(alixClient.inboxId)) - assert(!boClient.contacts.isInboxDenied(alixClient.inboxId)) + assertEquals( + boClient.preferences.consentList.inboxIdState(alixClient.inboxId), + ConsentState.ALLOWED + ) - boClient.contacts.denyInboxes(listOf(alixClient.inboxId)) + boClient.preferences.consentList.setConsentState( + listOf( + ConsentListEntry( + alixClient.inboxId, + EntryType.INBOX_ID, + ConsentState.DENIED + ) + ) + ) alixMember = boGroup.members().firstOrNull { it.inboxId == alixClient.inboxId } assertEquals(alixMember!!.consentState, ConsentState.DENIED) - assert(!boClient.contacts.isInboxAllowed(alixClient.inboxId)) - assert(boClient.contacts.isInboxDenied(alixClient.inboxId)) + assertEquals( + boClient.preferences.consentList.inboxIdState(alixClient.inboxId), + ConsentState.DENIED + ) - boClient.contacts.allow(listOf(alixClient.address)) + boClient.preferences.consentList.setConsentState( + listOf( + ConsentListEntry( + alixClient.address, + EntryType.ADDRESS, + ConsentState.ALLOWED + ) + ) + ) alixMember = boGroup.members().firstOrNull { it.inboxId == alixClient.inboxId } assertEquals(alixMember!!.consentState, ConsentState.ALLOWED) - assert(boClient.contacts.isInboxAllowed(alixClient.inboxId)) - assert(!boClient.contacts.isInboxDenied(alixClient.inboxId)) - assert(boClient.contacts.isAllowed(alixClient.address)) - assert(!boClient.contacts.isDenied(alixClient.address)) + assertEquals( + boClient.preferences.consentList.inboxIdState(alixClient.inboxId), + ConsentState.ALLOWED + ) + assertEquals( + boClient.preferences.consentList.addressState(alixClient.address), + ConsentState.ALLOWED + ) } } @@ -809,9 +831,9 @@ class GroupTest { } runBlocking { alixClient.conversations.syncConversations() } val alixGroup: Group = alixClient.findGroup(boGroup.id)!! - runBlocking { assert(!alixClient.contacts.isGroupAllowed(boGroup.id)) } + runBlocking { assertEquals(alixGroup.consentState(), ConsentState.UNKNOWN) } val preparedMessageId = runBlocking { alixGroup.prepareMessage("Test text") } - runBlocking { assert(alixClient.contacts.isGroupAllowed(boGroup.id)) } + runBlocking { assertEquals(alixGroup.consentState(), ConsentState.ALLOWED) } assertEquals(alixGroup.messages().size, 1) assertEquals(alixGroup.messages(deliveryStatus = MessageDeliveryStatus.PUBLISHED).size, 0) assertEquals(alixGroup.messages(deliveryStatus = MessageDeliveryStatus.UNPUBLISHED).size, 1) diff --git a/library/src/androidTest/java/org/xmtp/android/library/GroupUpdatedTest.kt b/library/src/androidTest/java/org/xmtp/android/library/GroupUpdatedTest.kt index 2bedbeb63..dd2547eab 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/GroupUpdatedTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/GroupUpdatedTest.kt @@ -33,23 +33,16 @@ class GroupUpdatedTest { @Before fun setUp() { val key = SecureRandom().generateSeed(32) - fixtures = fixtures( - clientOptions = ClientOptions( - ClientOptions.Api(XMTPEnvironment.LOCAL, false), - enableV3 = true, - appContext = context, - dbEncryptionKey = key - ) - ) - alixWallet = fixtures.aliceAccount - alix = fixtures.alice - boWallet = fixtures.bobAccount - bo = fixtures.bob + fixtures = fixtures() + alixWallet = fixtures.alixAccount + alix = fixtures.alix + boWallet = fixtures.boAccount + bo = fixtures.bo caroWallet = fixtures.caroAccount caro = fixtures.caro - alixClient = fixtures.aliceClient - boClient = fixtures.bobClient + alixClient = fixtures.alixClient + boClient = fixtures.boClient caroClient = fixtures.caroClient } diff --git a/library/src/androidTest/java/org/xmtp/android/library/InvitationTest.kt b/library/src/androidTest/java/org/xmtp/android/library/InvitationTest.kt deleted file mode 100644 index 104a42da1..000000000 --- a/library/src/androidTest/java/org/xmtp/android/library/InvitationTest.kt +++ /dev/null @@ -1,197 +0,0 @@ -package org.xmtp.android.library - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.google.protobuf.kotlin.toByteString -import kotlinx.coroutines.runBlocking -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotEquals -import org.junit.Test -import org.junit.runner.RunWith -import org.web3j.utils.Numeric -import org.xmtp.android.library.messages.InvitationV1 -import org.xmtp.android.library.messages.InvitationV1ContextBuilder -import org.xmtp.android.library.messages.PrivateKey -import org.xmtp.android.library.messages.PrivateKeyBuilder -import org.xmtp.android.library.messages.PrivateKeyBundle -import org.xmtp.android.library.messages.PrivateKeyBundleV1 -import org.xmtp.android.library.messages.SealedInvitation -import org.xmtp.android.library.messages.SealedInvitationBuilder -import org.xmtp.android.library.messages.createDeterministic -import org.xmtp.android.library.messages.generate -import org.xmtp.android.library.messages.getInvitation -import org.xmtp.android.library.messages.getPublicKeyBundle -import org.xmtp.android.library.messages.header -import org.xmtp.android.library.messages.sharedSecret -import org.xmtp.android.library.messages.toPublicKeyBundle -import org.xmtp.android.library.messages.toV2 -import java.util.Date - -@RunWith(AndroidJUnit4::class) -class InvitationTest { - @Test - fun testExistingWallet() { - // Generated from JS script - val ints = arrayOf( - 31, 116, 198, 193, 189, 122, 19, 254, 191, 189, 211, 215, 255, 131, - 171, 239, 243, 33, 4, 62, 143, 86, 18, 195, 251, 61, 128, 90, 34, 126, 219, 236 - ) - val bytes = - ints.foldIndexed(ByteArray(ints.size)) { i, a, v -> a.apply { set(i, v.toByte()) } } - val key = PrivateKey.newBuilder().also { - it.secp256K1 = - it.secp256K1.toBuilder().also { builder -> builder.bytes = bytes.toByteString() } - .build() - it.publicKey = it.publicKey.toBuilder().also { builder -> - builder.secp256K1Uncompressed = - builder.secp256K1Uncompressed.toBuilder().also { keyBuilder -> - keyBuilder.bytes = - KeyUtil.addUncompressedByte(KeyUtil.getPublicKey(bytes)).toByteString() - }.build() - }.build() - }.build() - - val client = runBlocking { Client().create(account = PrivateKeyBuilder(key)) } - val conversations = runBlocking { client.conversations.list() } - assertEquals(1, conversations.size) - val message = runBlocking { conversations[0].messages().firstOrNull() } - assertEquals(message?.body, "hello") - } - - @Test - fun testGenerateSealedInvitation() { - val aliceWallet = FakeWallet.generate() - val bobWallet = FakeWallet.generate() - val alice = PrivateKeyBundleV1.newBuilder().build().generate(wallet = aliceWallet) - val bob = PrivateKeyBundleV1.newBuilder().build().generate(wallet = bobWallet) - val invitation = InvitationV1.newBuilder().build().createDeterministic( - sender = alice.toV2(), - recipient = bob.toV2().getPublicKeyBundle() - ) - val newInvitation = SealedInvitationBuilder.buildFromV1( - sender = alice.toV2(), - recipient = bob.toV2().getPublicKeyBundle(), - created = Date(), - invitation = invitation - ) - val deserialized = SealedInvitation.parseFrom(newInvitation.toByteArray()) - assert(!deserialized.v1.headerBytes.isEmpty) - assertEquals(newInvitation, deserialized) - val header = newInvitation.v1.header - // Ensure the headers haven't been mangled - assertEquals(header.sender, alice.toV2().getPublicKeyBundle()) - assertEquals(header.recipient, bob.toV2().getPublicKeyBundle()) - // Ensure alice can decrypt the invitation - val aliceInvite = newInvitation.v1.getInvitation(viewer = alice.toV2()) - assertEquals(aliceInvite.topic, invitation.topic) - assertEquals( - aliceInvite.aes256GcmHkdfSha256.keyMaterial, - invitation.aes256GcmHkdfSha256.keyMaterial - ) - // Ensure bob can decrypt the invitation - val bobInvite = newInvitation.v1.getInvitation(viewer = bob.toV2()) - assertEquals(bobInvite.topic, invitation.topic) - assertEquals( - bobInvite.aes256GcmHkdfSha256.keyMaterial, - invitation.aes256GcmHkdfSha256.keyMaterial - ) - } - - @Test - fun testDeterministicInvite() { - val aliceWallet = FakeWallet.generate() - val bobWallet = FakeWallet.generate() - val alice = PrivateKeyBundleV1.newBuilder().build().generate(wallet = aliceWallet) - val bob = PrivateKeyBundleV1.newBuilder().build().generate(wallet = bobWallet) - val makeInvite = { conversationId: String -> - InvitationV1.newBuilder().build().createDeterministic( - sender = alice.toV2(), - recipient = bob.toV2().getPublicKeyBundle(), - context = InvitationV1ContextBuilder.buildFromConversation(conversationId) - ) - } - // Repeatedly making the same invite should use the same topic/keys - val original = makeInvite("example.com/conversation-foo") - for (i in 1..10) { - val invite = makeInvite("example.com/conversation-foo") - assertEquals(original.topic, invite.topic) - } - // But when the conversationId changes then it use a new topic/keys - val invite = makeInvite("example.com/conversation-bar") - assertNotEquals(original.topic, invite.topic) - } - - @Test - fun testGeneratesKnownDeterministicTopic() { - // address = 0xF56d1F3b1290204441Cb3843C2Cac1C2f5AEd690 - val aliceKeyData = - Numeric.hexStringToByteArray("0x0a8a030ac20108c192a3f7923112220a2068d2eb2ef8c50c4916b42ce638c5610e44ff4eb3ecb098c9dacf032625c72f101a940108c192a3f7923112460a440a40fc9822283078c323c9319c45e60ab42c65f6e1744ed8c23c52728d456d33422824c98d307e8b1c86a26826578523ba15fe6f04a17fca176664ee8017ec8ba59310011a430a410498dc2315dd45d99f5e900a071e7b56142de344540f07fbc73a0f9a5d5df6b52eb85db06a3825988ab5e04746bc221fcdf5310a44d9523009546d4bfbfbb89cfb12c20108eb92a3f7923112220a20788be9da8e1a1a08b05f7cbf22d86980bc056b130c482fa5bd26ccb8d29b30451a940108eb92a3f7923112460a440a40a7afa25cb6f3fbb98f9e5cd92a1df1898452e0dfa1d7e5affe9eaf9b72dd14bc546d86c399768badf983f07fa7dd16eee8d793357ce6fccd676807d87bcc595510011a430a410422931e6295c3c93a5f6f5e729dc02e1754e916cb9be16d36dc163a300931f42a0cd5fde957d75c2068e1980c5f86843daf16aba8ae57e8160b8b9f0191def09e") - val aliceKeys = PrivateKeyBundle.parseFrom(aliceKeyData).v1.toV2() - - // address = 0x3De402A325323Bb97f00cE3ad5bFAc96A11F9A34 - val bobKeyData = - Numeric.hexStringToByteArray("0x0a88030ac001088cd68df7923112220a209057f8d813314a2aae74e6c4c30f909c1c496b6037ce32a12c613558a8e961681a9201088cd68df7923112440a420a40501ae9b4f75d5bb5bae3ca4ecfda4ede9edc5a9b7fc2d56dc7325b837957c23235cc3005b46bb9ef485f106404dcf71247097ed509635590f4b7987b833d03661a430a4104e61a7ae511567f4a2b5551221024b6932d6cdb8ecf3876ec64cf29be4291dd5428fc0301963cdf6939978846e2c35fd38fcb70c64296a929f166ef6e4e91045712c20108b8d68df7923112220a2027707399474d417bf6aae4baa3d73b285bf728353bc3e156b0e32461ebb48f8c1a940108b8d68df7923112460a440a40fb96fa38c3f013830abb61cf6b39776e0475eb1379c66013569c3d2daecdd48c7fbee945dcdbdc5717d1f4ffd342c4d3f1b7215912829751a94e3ae11007e0a110011a430a4104952b7158cfe819d92743a4132e2e3ae867d72f6a08292aebf471d0a7a2907f3e9947719033e20edc9ca9665874bd88c64c6b62c01928065f6069c5c80c699924") - val bobKeys = PrivateKeyBundle.parseFrom(bobKeyData).v1.toV2() - - val aliceInvite = InvitationV1.newBuilder().build().createDeterministic( - sender = aliceKeys, - recipient = bobKeys.getPublicKeyBundle(), - context = InvitationV1ContextBuilder.buildFromConversation("test") - ) - - assertEquals( - aliceInvite.topic, - "/xmtp/0/m-4b52be1e8567d72d0bc407debe2d3c7fca2ae93a47e58c3f9b5c5068aff80ec5/proto" - ) - - val bobInvite = InvitationV1.newBuilder().build().createDeterministic( - sender = bobKeys, - recipient = aliceKeys.getPublicKeyBundle(), - context = InvitationV1ContextBuilder.buildFromConversation("test") - ) - - assertEquals( - aliceInvite.topic, - "/xmtp/0/m-4b52be1e8567d72d0bc407debe2d3c7fca2ae93a47e58c3f9b5c5068aff80ec5/proto" - ) - - assertEquals( - bobInvite.topic, - "/xmtp/0/m-4b52be1e8567d72d0bc407debe2d3c7fca2ae93a47e58c3f9b5c5068aff80ec5/proto" - ) - } - - @Test - fun testCreatesDeterministicTopicsBidirectionally() { - val aliceWallet = FakeWallet.generate() - val bobWallet = FakeWallet.generate() - val alice = PrivateKeyBundleV1.newBuilder().build().generate(wallet = aliceWallet) - val bob = PrivateKeyBundleV1.newBuilder().build().generate(wallet = bobWallet) - - val aliceInvite = InvitationV1.newBuilder().build().createDeterministic( - sender = alice.toV2(), - recipient = bob.toV2().getPublicKeyBundle(), - context = null - ) - - val bobInvite = InvitationV1.newBuilder().build().createDeterministic( - sender = bob.toV2(), - recipient = alice.toV2().getPublicKeyBundle(), - context = null - ) - - val aliceSharedSecret = alice.sharedSecret( - bob.toPublicKeyBundle(), - alice.getPreKeys(0).publicKey, - false - ) - - val bobSharedSecret = bob.sharedSecret( - alice.toPublicKeyBundle(), bob.getPreKeys(0).publicKey, - true - ) - - assertEquals(aliceSharedSecret.contentToString(), bobSharedSecret.contentToString()) - - assertEquals(aliceInvite.topic, bobInvite.topic) - } -} diff --git a/library/src/androidTest/java/org/xmtp/android/library/ReactionTest.kt b/library/src/androidTest/java/org/xmtp/android/library/ReactionTest.kt index 4ccd65486..94f292961 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/ReactionTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/ReactionTest.kt @@ -66,9 +66,9 @@ class ReactionTest { Client.register(codec = ReactionCodec()) val fixtures = fixtures() - val aliceClient = fixtures.aliceClient + val aliceClient = fixtures.alixClient val aliceConversation = runBlocking { - aliceClient.conversations.newConversation(fixtures.bob.walletAddress) + aliceClient.conversations.newConversation(fixtures.bo.walletAddress) } runBlocking { aliceConversation.send(text = "hey alice 2 bob") } @@ -89,8 +89,8 @@ class ReactionTest { ) } val messages = runBlocking { aliceConversation.messages() } - assertEquals(messages.size, 2) - if (messages.size == 2) { + assertEquals(messages.size, 3) + if (messages.size == 3) { val content: Reaction? = messages.first().content() assertEquals("U+1F603", content?.content) assertEquals(messageToReact.id, content?.reference) diff --git a/library/src/androidTest/java/org/xmtp/android/library/ReadReceiptTest.kt b/library/src/androidTest/java/org/xmtp/android/library/ReadReceiptTest.kt index 8a3fc8df9..058506a2d 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/ReadReceiptTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/ReadReceiptTest.kt @@ -18,9 +18,9 @@ class ReadReceiptTest { Client.register(codec = ReadReceiptCodec()) val fixtures = fixtures() - val aliceClient = fixtures.aliceClient + val aliceClient = fixtures.alixClient val aliceConversation = runBlocking { - aliceClient.conversations.newConversation(fixtures.bob.walletAddress) + aliceClient.conversations.newConversation(fixtures.bo.walletAddress) } runBlocking { aliceConversation.send(text = "hey alice 2 bob") } @@ -34,8 +34,8 @@ class ReadReceiptTest { ) } val messages = runBlocking { aliceConversation.messages() } - assertEquals(messages.size, 2) - if (messages.size == 2) { + assertEquals(messages.size, 3) + if (messages.size == 3) { val contentType: String = messages.first().encodedContent.type.typeId assertEquals(contentType, "readReceipt") } diff --git a/library/src/androidTest/java/org/xmtp/android/library/ReplyTest.kt b/library/src/androidTest/java/org/xmtp/android/library/ReplyTest.kt index 75cb66675..413a9644a 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/ReplyTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/ReplyTest.kt @@ -19,9 +19,9 @@ class ReplyTest { Client.register(codec = ReplyCodec()) val fixtures = fixtures() - val aliceClient = fixtures.aliceClient + val aliceClient = fixtures.alixClient val aliceConversation = runBlocking { - aliceClient.conversations.newConversation(fixtures.bob.walletAddress) + aliceClient.conversations.newConversation(fixtures.bo.walletAddress) } runBlocking { aliceConversation.send(text = "hey alice 2 bob") } @@ -41,8 +41,8 @@ class ReplyTest { ) } val messages = runBlocking { aliceConversation.messages() } - assertEquals(messages.size, 2) - if (messages.size == 2) { + assertEquals(messages.size, 3) + if (messages.size == 3) { val content: Reply? = messages.first().content() assertEquals("Hello", content?.content) assertEquals(messageToReact.id, content?.reference) diff --git a/library/src/androidTest/java/org/xmtp/android/library/SmartContractWalletTest.kt b/library/src/androidTest/java/org/xmtp/android/library/SmartContractWalletTest.kt index 15ebf5436..27c46bb3c 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/SmartContractWalletTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/SmartContractWalletTest.kt @@ -39,7 +39,6 @@ class SmartContractWalletTest { val context = InstrumentationRegistry.getInstrumentation().targetContext options = ClientOptions( ClientOptions.Api(XMTPEnvironment.LOCAL, false), - enableV3 = true, appContext = context, dbEncryptionKey = key ) @@ -48,7 +47,7 @@ class SmartContractWalletTest { boV3Wallet = PrivateKeyBuilder() boV3 = boV3Wallet.getPrivateKey() boV3Client = runBlocking { - Client().createV3( + Client().create( account = boV3Wallet, options = options ) @@ -57,7 +56,7 @@ class SmartContractWalletTest { // SCW davonSCW = FakeSCWWallet.generate(ANVIL_TEST_PRIVATE_KEY_1) davonSCWClient = runBlocking { - Client().createV3( + Client().create( account = davonSCW, options = options ) @@ -66,7 +65,7 @@ class SmartContractWalletTest { // SCW eriSCW = FakeSCWWallet.generate(ANVIL_TEST_PRIVATE_KEY_2) eriSCWClient = runBlocking { - Client().createV3( + Client().create( account = eriSCW, options = options ) @@ -77,7 +76,7 @@ class SmartContractWalletTest { @Test fun testCanBuildASCW() { val davonSCWClient2 = runBlocking { - Client().buildV3( + Client().build( address = davonSCW.address, options = options ) @@ -130,7 +129,10 @@ class SmartContractWalletTest { runBlocking { boGroup.sync() } assertEquals(boGroup.messages().first().body, "gm") assertEquals(boGroup.messages().first().id, messageId) - assertEquals(boGroup.messages().first().deliveryStatus, Message.MessageDeliveryStatus.PUBLISHED) + assertEquals( + boGroup.messages().first().deliveryStatus, + Message.MessageDeliveryStatus.PUBLISHED + ) assertEquals(boGroup.messages().size, 3) runBlocking { davonSCWClient.conversations.syncConversations() } @@ -159,15 +161,32 @@ class SmartContractWalletTest { ) ) } - assert(davonSCWClient.contacts.isGroupAllowed(davonGroup.id)) + assertEquals( + davonSCWClient.preferences.consentList.conversationState(davonGroup.id), + ConsentState.ALLOWED + ) assertEquals(davonGroup.consentState(), ConsentState.ALLOWED) - davonSCWClient.contacts.denyGroups(listOf(davonGroup.id)) - assert(davonSCWClient.contacts.isGroupDenied(davonGroup.id)) + davonSCWClient.preferences.consentList.setConsentState( + listOf( + ConsentListEntry( + davonGroup.id, + EntryType.CONVERSATION_ID, + ConsentState.DENIED + ) + ) + ) + assertEquals( + davonSCWClient.preferences.consentList.conversationState(davonGroup.id), + ConsentState.DENIED + ) assertEquals(davonGroup.consentState(), ConsentState.DENIED) davonGroup.updateConsentState(ConsentState.ALLOWED) - assert(davonSCWClient.contacts.isGroupAllowed(davonGroup.id)) + assertEquals( + davonSCWClient.preferences.consentList.conversationState(davonGroup.id), + ConsentState.ALLOWED + ) assertEquals(davonGroup.consentState(), ConsentState.ALLOWED) } } @@ -183,30 +202,63 @@ class SmartContractWalletTest { ) ) } - assert(!davonSCWClient.contacts.isInboxAllowed(boV3Client.inboxId)) - assert(!davonSCWClient.contacts.isInboxDenied(boV3Client.inboxId)) - - davonSCWClient.contacts.allowInboxes(listOf(boV3Client.inboxId)) - var caroMember = davonGroup.members().firstOrNull { it.inboxId == boV3Client.inboxId } - assertEquals(caroMember!!.consentState, ConsentState.ALLOWED) + assertEquals( + davonSCWClient.preferences.consentList.inboxIdState(boV3Client.inboxId), + ConsentState.UNKNOWN + ) + davonSCWClient.preferences.consentList.setConsentState( + listOf( + ConsentListEntry( + boV3Client.inboxId, + EntryType.INBOX_ID, + ConsentState.ALLOWED + ) + ) + ) + var alixMember = davonGroup.members().firstOrNull { it.inboxId == boV3Client.inboxId } + assertEquals(alixMember!!.consentState, ConsentState.ALLOWED) - assert(davonSCWClient.contacts.isInboxAllowed(boV3Client.inboxId)) - assert(!davonSCWClient.contacts.isInboxDenied(boV3Client.inboxId)) - assert(davonSCWClient.contacts.isAllowed(boV3Client.address)) - assert(!davonSCWClient.contacts.isDenied(boV3Client.address)) + assertEquals( + davonSCWClient.preferences.consentList.inboxIdState(boV3Client.inboxId), + ConsentState.ALLOWED + ) - davonSCWClient.contacts.denyInboxes(listOf(boV3Client.inboxId)) - caroMember = davonGroup.members().firstOrNull { it.inboxId == boV3Client.inboxId } - assertEquals(caroMember!!.consentState, ConsentState.DENIED) + davonSCWClient.preferences.consentList.setConsentState( + listOf( + ConsentListEntry( + boV3Client.inboxId, + EntryType.INBOX_ID, + ConsentState.DENIED + ) + ) + ) + alixMember = davonGroup.members().firstOrNull { it.inboxId == boV3Client.inboxId } + assertEquals(alixMember!!.consentState, ConsentState.DENIED) - assert(!davonSCWClient.contacts.isInboxAllowed(boV3Client.inboxId)) - assert(davonSCWClient.contacts.isInboxDenied(boV3Client.inboxId)) + assertEquals( + davonSCWClient.preferences.consentList.inboxIdState(boV3Client.inboxId), + ConsentState.DENIED + ) - davonSCWClient.contacts.allow(listOf(eriSCWClient.address)) - assert(davonSCWClient.contacts.isAllowed(eriSCWClient.address)) - assert(!davonSCWClient.contacts.isDenied(eriSCWClient.address)) - assert(davonSCWClient.contacts.isInboxAllowed(eriSCWClient.inboxId)) - assert(!davonSCWClient.contacts.isInboxDenied(eriSCWClient.inboxId)) + davonSCWClient.preferences.consentList.setConsentState( + listOf( + ConsentListEntry( + eriSCWClient.address, + EntryType.ADDRESS, + ConsentState.ALLOWED + ) + ) + ) + alixMember = davonGroup.members().firstOrNull { it.inboxId == eriSCWClient.inboxId } + assertEquals(alixMember!!.consentState, ConsentState.ALLOWED) + assertEquals( + davonSCWClient.preferences.consentList.inboxIdState(eriSCWClient.inboxId), + ConsentState.ALLOWED + ) + assertEquals( + davonSCWClient.preferences.consentList.addressState(eriSCWClient.address), + ConsentState.ALLOWED + ) } } 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 2b68048b0..78fcfde49 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/TestHelpers.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/TestHelpers.kt @@ -1,5 +1,6 @@ package org.xmtp.android.library +import androidx.test.platform.app.InstrumentationRegistry import kotlinx.coroutines.runBlocking import org.web3j.abi.FunctionEncoder import org.web3j.abi.datatypes.DynamicBytes @@ -12,17 +13,13 @@ import org.web3j.tx.gas.DefaultGasProvider import org.web3j.utils.Numeric import org.xmtp.android.library.artifact.CoinbaseSmartWallet import org.xmtp.android.library.artifact.CoinbaseSmartWalletFactory -import org.xmtp.android.library.messages.ContactBundle -import org.xmtp.android.library.messages.Envelope import org.xmtp.android.library.messages.PrivateKey import org.xmtp.android.library.messages.PrivateKeyBuilder import org.xmtp.android.library.messages.Signature -import org.xmtp.android.library.messages.Topic import org.xmtp.android.library.messages.ethHash -import org.xmtp.android.library.messages.toPublicKeyBundle import org.xmtp.android.library.messages.walletAddress import java.math.BigInteger -import java.util.Date +import java.security.SecureRandom class FakeWallet : SigningKey { private var privateKey: PrivateKey @@ -143,43 +140,30 @@ class FakeSCWWallet : SigningKey { } } -data class Fixtures( - val clientOptions: ClientOptions? = ClientOptions( - ClientOptions.Api(XMTPEnvironment.LOCAL, isSecure = false) - ), -) { - val aliceAccount = PrivateKeyBuilder() - val bobAccount = PrivateKeyBuilder() +class Fixtures { + val key = SecureRandom().generateSeed(32) + val context = InstrumentationRegistry.getInstrumentation().targetContext + val clientOptions = ClientOptions( + ClientOptions.Api(XMTPEnvironment.LOCAL, isSecure = false), + dbEncryptionKey = key, + appContext = context, + ) + val alixAccount = PrivateKeyBuilder() + val boAccount = PrivateKeyBuilder() val caroAccount = PrivateKeyBuilder() - val davonV3Account = PrivateKeyBuilder() - var alice: PrivateKey = aliceAccount.getPrivateKey() - var aliceClient: Client = - runBlocking { Client().create(account = aliceAccount, options = clientOptions) } + var alix: PrivateKey = alixAccount.getPrivateKey() + var alixClient: Client = + runBlocking { Client().create(account = alixAccount, options = clientOptions) } - var bob: PrivateKey = bobAccount.getPrivateKey() - var bobClient: Client = - runBlocking { Client().create(account = bobAccount, options = clientOptions) } + var bo: PrivateKey = boAccount.getPrivateKey() + var boClient: Client = + runBlocking { Client().create(account = boAccount, options = 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.v1keys.toPublicKeyBundle() - }.build() - }.build() - val envelope = Envelope.newBuilder().apply { - contentTopic = Topic.contact(client.address).description - timestampNs = (Date().time * 1_000_000) - message = contactBundle.toByteString() - }.build() - - runBlocking { client.publish(envelopes = listOf(envelope)) } - } } -fun fixtures(clientOptions: ClientOptions? = null): Fixtures = - Fixtures(clientOptions) +fun fixtures(): Fixtures = + Fixtures() diff --git a/library/src/androidTest/java/org/xmtp/android/library/V3ClientTest.kt b/library/src/androidTest/java/org/xmtp/android/library/V3ClientTest.kt deleted file mode 100644 index 17e0051b6..000000000 --- a/library/src/androidTest/java/org/xmtp/android/library/V3ClientTest.kt +++ /dev/null @@ -1,396 +0,0 @@ -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.libxmtp.Message -import org.xmtp.android.library.libxmtp.Message.* -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().createV3( - 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( - runBlocking { 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 testsCanCreateDm() { - val dm = runBlocking { boV3Client.conversations.findOrCreateDm(caroV2V3.walletAddress) } - assertEquals( - runBlocking { dm.members().map { it.inboxId }.sorted() }, - listOf(caroV2V3Client.inboxId, boV3Client.inboxId).sorted() - ) - - val sameDm = runBlocking { boV3Client.findDm(caroV2V3.walletAddress) } - assertEquals(sameDm?.id, dm.id) - - runBlocking { caroV2V3Client.conversations.syncConversations() } - val caroDm = runBlocking { caroV2V3Client.findDm(boV3Client.address) } - assertEquals(caroDm?.id, dm.id) - - Assert.assertThrows("Recipient not on network", XMTPException::class.java) { - runBlocking { boV3Client.conversations.findOrCreateDm(alixV2.walletAddress) } - } - } - - @Test - fun testsCanFindConversationByTopic() { - val group = - runBlocking { boV3Client.conversations.newGroup(listOf(caroV2V3.walletAddress)) } - val dm = runBlocking { boV3Client.conversations.findOrCreateDm(caroV2V3.walletAddress) } - - val sameDm = boV3Client.findConversationByTopic(dm.topic) - val sameGroup = boV3Client.findConversationByTopic(group.topic) - assertEquals(group.id, sameGroup?.id) - assertEquals(dm.id, sameDm?.id) - } - - @Test - fun testsCanListConversations() { - val dm = runBlocking { boV3Client.conversations.findOrCreateDm(caroV2V3.walletAddress) } - val group = - runBlocking { boV3Client.conversations.newGroup(listOf(caroV2V3.walletAddress)) } - assertEquals(runBlocking { boV3Client.conversations.list().size }, 2) - assertEquals(runBlocking { boV3Client.conversations.listDms().size }, 1) - assertEquals(runBlocking { boV3Client.conversations.listGroups().size }, 1) - - runBlocking { caroV2V3Client.conversations.syncConversations() } - assertEquals( - runBlocking { caroV2V3Client.conversations.list().size }, - 1 - ) - assertEquals(runBlocking { caroV2V3Client.conversations.listGroups().size }, 1) - } - - @Test - fun testsCanListConversationsFiltered() { - val dm = runBlocking { boV3Client.conversations.findOrCreateDm(caroV2V3.walletAddress) } - val group = - runBlocking { boV3Client.conversations.newGroup(listOf(caroV2V3.walletAddress)) } - assertEquals(runBlocking { boV3Client.conversations.list().size }, 2) - assertEquals( - runBlocking { boV3Client.conversations.list(consentState = ConsentState.ALLOWED).size }, - 2 - ) - runBlocking { group.updateConsentState(ConsentState.DENIED) } - assertEquals( - runBlocking { boV3Client.conversations.list(consentState = ConsentState.ALLOWED).size }, - 1 - ) - assertEquals( - runBlocking { boV3Client.conversations.list(consentState = ConsentState.DENIED).size }, - 1 - ) - assertEquals(runBlocking { boV3Client.conversations.list().size }, 2) - } - - @Test - fun testCanListConversationsOrder() { - val dm = runBlocking { boV3Client.conversations.findOrCreateDm(caroV2V3.walletAddress) } - val group1 = - runBlocking { boV3Client.conversations.newGroup(listOf(caroV2V3.walletAddress)) } - val group2 = - runBlocking { boV3Client.conversations.newGroup(listOf(caroV2V3.walletAddress)) } - runBlocking { dm.send("Howdy") } - runBlocking { group2.send("Howdy") } - runBlocking { boV3Client.conversations.syncAllConversations() } - val conversations = runBlocking { boV3Client.conversations.list() } - val conversationsOrdered = - runBlocking { boV3Client.conversations.list(order = Conversations.ConversationOrder.LAST_MESSAGE) } - assertEquals(conversations.size, 3) - assertEquals(conversationsOrdered.size, 3) - assertEquals(conversations.map { it.id }, listOf(dm.id, group1.id, group2.id)) - assertEquals(conversationsOrdered.map { it.id }, listOf(group2.id, dm.id, group1.id)) - } - - @Test - fun testsCanSendMessagesToGroup() { - 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.syncConversations() } - val sameGroup = runBlocking { caroV2V3Client.conversations.listGroups().last() } - runBlocking { sameGroup.sync() } - assertEquals(sameGroup.messages().size, 2) - assertEquals(sameGroup.messages().first().body, "gm") - } - - @Test - fun testsCanSendMessagesToDm() { - var boDm = - runBlocking { boV3Client.conversations.findOrCreateDm(caroV2V3.walletAddress) } - runBlocking { boDm.send("howdy") } - var messageId = runBlocking { boDm.send("gm") } - var boDmMessage = runBlocking { boDm.messages() } - assertEquals(boDmMessage.first().body, "gm") - assertEquals(boDmMessage.first().id, messageId) - assertEquals(boDmMessage.first().deliveryStatus, MessageDeliveryStatus.PUBLISHED) - assertEquals(boDmMessage.size, 3) - - runBlocking { caroV2V3Client.conversations.syncConversations() } - val caroDm = runBlocking { caroV2V3Client.findDm(boV3.walletAddress) } - runBlocking { caroDm!!.sync() } - var caroDmMessage = runBlocking { caroDm!!.messages() } - assertEquals(caroDmMessage.size, 2) - assertEquals(caroDmMessage.first().body, "gm") - - runBlocking { caroDm!!.send("howdy") } - messageId = runBlocking { caroDm!!.send("gm") } - caroDmMessage = runBlocking { caroDm!!.messages() } - assertEquals(caroDmMessage.first().body, "gm") - assertEquals(caroDmMessage.first().id, messageId) - assertEquals(caroDmMessage.first().deliveryStatus, MessageDeliveryStatus.PUBLISHED) - assertEquals(caroDmMessage.size, 4) - - runBlocking { boV3Client.conversations.syncConversations() } - boDm = runBlocking { boV3Client.findDm(caroV2V3.walletAddress)!! } - runBlocking { boDm.sync() } - boDmMessage = runBlocking { boDm.messages() } - assertEquals(boDmMessage.size, 5) - assertEquals(boDmMessage.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 testCanStreamAllMessagesFromV3Users() { - val group = - runBlocking { caroV2V3Client.conversations.newGroup(listOf(boV3.walletAddress)) } - val conversation = - runBlocking { boV3Client.conversations.findOrCreateDm(caroV2V3.walletAddress) } - runBlocking { boV3Client.conversations.syncConversations() } - - val allMessages = mutableListOf() - - val job = CoroutineScope(Dispatchers.IO).launch { - try { - boV3Client.conversations.streamAllMessages() - .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 testCanStreamGroupsAndConversationsFromV3Users() { - val allMessages = mutableListOf() - - val job = CoroutineScope(Dispatchers.IO).launch { - try { - boV3Client.conversations.stream() - .collect { message -> - allMessages.add(message.topic) - } - } catch (e: Exception) { - } - } - Thread.sleep(1000) - - runBlocking { - caroV2V3Client.conversations.newGroup(listOf(boV3.walletAddress)) - Thread.sleep(1000) - boV3Client.conversations.findOrCreateDm(caroV2V3.walletAddress) - } - - Thread.sleep(2000) - assertEquals(2, allMessages.size) - job.cancel() - } - - @Test - fun testCanStreamAllMessagesFromV2andV3Users() { - val group = - runBlocking { boV3Client.conversations.newGroup(listOf(caroV2V3.walletAddress)) } - val conversation = - runBlocking { alixV2Client.conversations.newConversation(caroV2V3.walletAddress) } - runBlocking { caroV2V3Client.conversations.syncConversations() } - - val allMessages = mutableListOf() - - val job = CoroutineScope(Dispatchers.IO).launch { - try { - caroV2V3Client.conversations.streamAllMessages() - .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.stream() - .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/ApiClient.kt b/library/src/main/java/org/xmtp/android/library/ApiClient.kt deleted file mode 100644 index 7f24fc939..000000000 --- a/library/src/main/java/org/xmtp/android/library/ApiClient.kt +++ /dev/null @@ -1,237 +0,0 @@ -package org.xmtp.android.library - -import com.google.protobuf.kotlin.toByteString -import org.xmtp.android.library.Util.Companion.envelopeFromFFi -import org.xmtp.android.library.messages.Pagination -import org.xmtp.android.library.messages.Topic -import org.xmtp.proto.message.api.v1.MessageApiOuterClass.BatchQueryResponse -import org.xmtp.proto.message.api.v1.MessageApiOuterClass.Cursor -import org.xmtp.proto.message.api.v1.MessageApiOuterClass.Envelope -import org.xmtp.proto.message.api.v1.MessageApiOuterClass.PagingInfo -import org.xmtp.proto.message.api.v1.MessageApiOuterClass.QueryRequest -import org.xmtp.proto.message.api.v1.MessageApiOuterClass.QueryResponse -import org.xmtp.proto.message.api.v1.MessageApiOuterClass.SortDirection -import uniffi.xmtpv3.FfiCursor -import uniffi.xmtpv3.FfiEnvelope -import uniffi.xmtpv3.FfiPagingInfo -import uniffi.xmtpv3.FfiPublishRequest -import uniffi.xmtpv3.FfiSortDirection -import uniffi.xmtpv3.FfiV2ApiClient -import uniffi.xmtpv3.FfiV2BatchQueryRequest -import uniffi.xmtpv3.FfiV2BatchQueryResponse -import uniffi.xmtpv3.FfiV2QueryRequest -import uniffi.xmtpv3.FfiV2QueryResponse -import uniffi.xmtpv3.FfiV2SubscribeRequest -import uniffi.xmtpv3.FfiV2Subscription -import uniffi.xmtpv3.FfiV2SubscriptionCallback -import java.io.Closeable - -interface ApiClient { - val environment: XMTPEnvironment - fun setAuthToken(token: String) - suspend fun query( - topic: String, - pagination: Pagination? = null, - cursor: Cursor? = null, - ): QueryResponse - - suspend fun queryTopic(topic: Topic, pagination: Pagination? = null): QueryResponse - suspend fun batchQuery(requests: List): BatchQueryResponse - suspend fun envelopes(topic: String, pagination: Pagination? = null): List - suspend fun publish(envelopes: List) - suspend fun subscribe( - request: FfiV2SubscribeRequest, - callback: FfiV2SubscriptionCallback, - ): FfiV2Subscription -} - -data class GRPCApiClient( - override val environment: XMTPEnvironment, - val rustV2Client: FfiV2ApiClient, -) : - ApiClient, Closeable { - companion object { - - fun makeQueryRequest( - topic: String, - pagination: Pagination? = null, - cursor: Cursor? = null, - ): QueryRequest = - QueryRequest.newBuilder() - .addContentTopics(topic).also { - if (pagination != null) { - it.pagingInfo = pagination.pagingInfo - } - if (pagination?.before != null) { - it.endTimeNs = pagination.before.time * 1_000_000 - it.pagingInfo = it.pagingInfo.toBuilder().also { info -> - info.direction = pagination.direction - }.build() - } - if (pagination?.after != null) { - it.startTimeNs = pagination.after.time * 1_000_000 - it.pagingInfo = it.pagingInfo.toBuilder().also { info -> - info.direction = pagination.direction - }.build() - } - if (cursor != null) { - it.pagingInfo = it.pagingInfo.toBuilder().also { info -> - info.cursor = cursor - }.build() - } - }.build() - } - - private var authToken: String? = null - - override fun setAuthToken(token: String) { - authToken = token - } - - override suspend fun query( - topic: String, - pagination: Pagination?, - cursor: Cursor?, - ): QueryResponse { - val request = makeQueryRequest(topic, pagination, cursor) - return queryResponseFromFFi(rustV2Client.query(queryRequestToFFi(request))) - } - - /** - * This is a helper for paginating through a full query. - * It yields all the envelopes in the query using the paging info - * from the prior response to fetch the next page. - */ - override suspend fun envelopes( - topic: String, - pagination: Pagination?, - ): List { - var envelopes: MutableList = mutableListOf() - var hasNextPage = true - var cursor: Cursor? = null - while (hasNextPage) { - val response = - query(topic = topic, pagination = pagination, cursor = cursor) - envelopes.addAll(response.envelopesList) - cursor = response.pagingInfo.cursor - hasNextPage = response.envelopesList.isNotEmpty() && response.pagingInfo.hasCursor() - if (pagination?.limit != null && pagination.limit <= 100 && envelopes.size >= pagination.limit) { - envelopes = envelopes.take(pagination.limit).toMutableList() - break - } - } - - return envelopes - } - - override suspend fun queryTopic(topic: Topic, pagination: Pagination?): QueryResponse { - return query(topic.description, pagination) - } - - override suspend fun batchQuery( - requests: List, - ): BatchQueryResponse { - val batchRequest = requests.map { queryRequestToFFi(it) } - return batchQueryResponseFromFFi(rustV2Client.batchQuery(FfiV2BatchQueryRequest(requests = batchRequest))) - } - - override suspend fun publish(envelopes: List) { - val ffiEnvelopes = envelopes.map { envelopeToFFi(it) } - val request = FfiPublishRequest(envelopes = ffiEnvelopes) - - rustV2Client.publish(request = request, authToken = authToken ?: "") - } - - override suspend fun subscribe( - request: FfiV2SubscribeRequest, - callback: FfiV2SubscriptionCallback, - ): FfiV2Subscription { - return rustV2Client.subscribe(request, callback) - } - - override fun close() { - rustV2Client.close() - } - - private fun envelopeToFFi(envelope: Envelope): FfiEnvelope { - return FfiEnvelope( - contentTopic = envelope.contentTopic, - timestampNs = envelope.timestampNs.toULong(), - message = envelope.message.toByteArray() - ) - } - - private fun queryRequestToFFi(request: QueryRequest): FfiV2QueryRequest { - return FfiV2QueryRequest( - contentTopics = request.contentTopicsList, - startTimeNs = request.startTimeNs.toULong(), - endTimeNs = request.endTimeNs.toULong(), - pagingInfo = pagingInfoToFFi(request.pagingInfo) - ) - } - - private fun queryResponseFromFFi(response: FfiV2QueryResponse): QueryResponse { - return QueryResponse.newBuilder().also { queryResponse -> - queryResponse.addAllEnvelopes(response.envelopes.map { envelopeFromFFi(it) }) - response.pagingInfo?.let { - queryResponse.pagingInfo = pagingInfoFromFFi(it) - } - }.build() - } - - private fun batchQueryResponseFromFFi(response: FfiV2BatchQueryResponse): BatchQueryResponse { - return BatchQueryResponse.newBuilder().also { queryResponse -> - queryResponse.addAllResponses(response.responses.map { queryResponseFromFFi(it) }) - }.build() - } - - private fun pagingInfoFromFFi(info: FfiPagingInfo): PagingInfo { - return PagingInfo.newBuilder().also { - it.limit = info.limit.toInt() - info.cursor?.let { cursor -> - it.cursor = cursorFromFFi(cursor) - } - it.direction = directionFromFfi(info.direction) - }.build() - } - - private fun pagingInfoToFFi(info: PagingInfo): FfiPagingInfo { - return FfiPagingInfo( - limit = info.limit.toUInt(), - cursor = cursorToFFi(info.cursor), - direction = directionToFfi(info.direction) - ) - } - - private fun directionToFfi(direction: SortDirection): FfiSortDirection { - return when (direction) { - SortDirection.SORT_DIRECTION_ASCENDING -> FfiSortDirection.ASCENDING - SortDirection.SORT_DIRECTION_DESCENDING -> FfiSortDirection.DESCENDING - else -> FfiSortDirection.UNSPECIFIED - } - } - - private fun directionFromFfi(direction: FfiSortDirection): SortDirection { - return when (direction) { - FfiSortDirection.ASCENDING -> SortDirection.SORT_DIRECTION_ASCENDING - FfiSortDirection.DESCENDING -> SortDirection.SORT_DIRECTION_DESCENDING - else -> SortDirection.SORT_DIRECTION_UNSPECIFIED - } - } - - private fun cursorToFFi(cursor: Cursor): FfiCursor { - return FfiCursor( - digest = cursor.index.digest.toByteArray(), - senderTimeNs = cursor.index.senderTimeNs.toULong() - ) - } - - private fun cursorFromFFi(cursor: FfiCursor): Cursor { - return Cursor.newBuilder().also { - it.index = it.index.toBuilder().also { index -> - index.digest = cursor.digest.toByteString() - index.senderTimeNs = cursor.senderTimeNs.toLong() - }.build() - }.build() - } -} diff --git a/library/src/main/java/org/xmtp/android/library/AuthorizedIdentity.kt b/library/src/main/java/org/xmtp/android/library/AuthorizedIdentity.kt deleted file mode 100644 index 22c259936..000000000 --- a/library/src/main/java/org/xmtp/android/library/AuthorizedIdentity.kt +++ /dev/null @@ -1,53 +0,0 @@ -package org.xmtp.android.library - -import android.util.Base64 -import com.google.crypto.tink.subtle.Base64.encodeToString -import kotlinx.coroutines.runBlocking -import org.xmtp.android.library.messages.AuthDataBuilder -import org.xmtp.android.library.messages.PrivateKey -import org.xmtp.android.library.messages.PrivateKeyBuilder -import org.xmtp.android.library.messages.PrivateKeyBundle -import org.xmtp.android.library.messages.PrivateKeyBundleV1 -import org.xmtp.android.library.messages.PublicKey -import org.xmtp.android.library.messages.Token -import org.xmtp.android.library.messages.walletAddress -import org.xmtp.proto.message.contents.PrivateKeyOuterClass - -data class AuthorizedIdentity( - var address: String, - var authorized: PublicKey, - var identity: PrivateKey, -) { - - constructor(privateKeyBundleV1: PrivateKeyBundleV1) : this( - privateKeyBundleV1.identityKey.walletAddress, - privateKeyBundleV1.identityKey.publicKey, - privateKeyBundleV1.identityKey, - ) - - fun createAuthToken(): String { - val authData = AuthDataBuilder.buildFromWalletAddress(walletAddress = address) - val signature = runBlocking { - PrivateKeyBuilder(identity).sign(Util.keccak256(authData.toByteArray())) - } - - val token = Token.newBuilder().also { - it.identityKey = authorized - it.authDataBytes = authData.toByteString() - it.authDataSignature = signature - }.build().toByteArray() - return encodeToString(token, Base64.NO_WRAP) - } - - val toBundle: PrivateKeyBundle - get() { - return PrivateKeyOuterClass.PrivateKeyBundle.newBuilder().also { - it.v1 = it.v1.toBuilder().also { v1Builder -> - v1Builder.identityKey = identity - v1Builder.identityKey = v1Builder.identityKey.toBuilder().also { idKeyBuilder -> - idKeyBuilder.publicKey = authorized - }.build() - }.build() - }.build() - } -} 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 7510e5d13..55d6e2508 100644 --- a/library/src/main/java/org/xmtp/android/library/Client.kt +++ b/library/src/main/java/org/xmtp/android/library/Client.kt @@ -1,68 +1,33 @@ package org.xmtp.android.library import android.content.Context -import android.util.Log import kotlinx.coroutines.runBlocking -import org.web3j.crypto.Keys -import org.web3j.crypto.Keys.toChecksumAddress import org.xmtp.android.library.codecs.ContentCodec import org.xmtp.android.library.codecs.TextCodec import org.xmtp.android.library.libxmtp.Message import org.xmtp.android.library.libxmtp.XMTPLogger -import org.xmtp.android.library.messages.ContactBundle -import org.xmtp.android.library.messages.EncryptedPrivateKeyBundle -import org.xmtp.android.library.messages.Envelope -import org.xmtp.android.library.messages.EnvelopeBuilder -import org.xmtp.android.library.messages.Pagination -import org.xmtp.android.library.messages.PrivateKeyBundle -import org.xmtp.android.library.messages.PrivateKeyBundleBuilder -import org.xmtp.android.library.messages.PrivateKeyBundleV1 -import org.xmtp.android.library.messages.PrivateKeyBundleV2 -import org.xmtp.android.library.messages.Topic -import org.xmtp.android.library.messages.decrypted -import org.xmtp.android.library.messages.encrypted -import org.xmtp.android.library.messages.ensureWalletSignature -import org.xmtp.android.library.messages.generate -import org.xmtp.android.library.messages.getPublicKeyBundle import org.xmtp.android.library.messages.rawData -import org.xmtp.android.library.messages.recoverWalletSignerPublicKey -import org.xmtp.android.library.messages.toPublicKeyBundle -import org.xmtp.android.library.messages.toV2 -import org.xmtp.android.library.messages.walletAddress -import org.xmtp.proto.message.api.v1.MessageApiOuterClass -import org.xmtp.proto.message.api.v1.MessageApiOuterClass.BatchQueryResponse -import org.xmtp.proto.message.api.v1.MessageApiOuterClass.QueryRequest -import uniffi.xmtpv3.FfiV2SubscribeRequest -import uniffi.xmtpv3.FfiV2Subscription -import uniffi.xmtpv3.FfiV2SubscriptionCallback import uniffi.xmtpv3.FfiXmtpClient import uniffi.xmtpv3.createClient -import uniffi.xmtpv3.createV2Client import uniffi.xmtpv3.generateInboxId import uniffi.xmtpv3.getInboxIdForAddress import uniffi.xmtpv3.getVersionInfo import uniffi.xmtpv3.org.xmtp.android.library.libxmtp.InboxState import java.io.File -import java.util.Date -typealias PublishResponse = org.xmtp.proto.message.api.v1.MessageApiOuterClass.PublishResponse -typealias QueryResponse = org.xmtp.proto.message.api.v1.MessageApiOuterClass.QueryResponse typealias PreEventCallback = suspend () -> Unit data class ClientOptions( val api: Api = Api(), - val preCreateIdentityCallback: PreEventCallback? = null, - val preEnableIdentityCallback: PreEventCallback? = null, val preAuthenticateToInboxCallback: PreEventCallback? = null, - val appContext: Context? = null, - val enableV3: Boolean = false, - val dbDirectory: String? = null, - val dbEncryptionKey: ByteArray? = null, + val appContext: Context, + val dbEncryptionKey: ByteArray, val historySyncUrl: String = when (api.env) { XMTPEnvironment.PRODUCTION -> "https://message-history.production.ephemera.network/" XMTPEnvironment.LOCAL -> "http://0.0.0.0:5558" else -> "https://message-history.dev.ephemera.network/" }, + val dbDirectory: String? = null, ) { data class Api( val env: XMTPEnvironment = XMTPEnvironment.DEV, @@ -73,18 +38,15 @@ data class ClientOptions( class Client() { lateinit var address: String - lateinit var contacts: Contacts + lateinit var inboxId: String + lateinit var installationId: String + lateinit var preferences: PrivatePreferences lateinit var conversations: Conversations - var privateKeyBundleV1: PrivateKeyBundleV1? = null - var apiClient: ApiClient? = null + lateinit var environment: XMTPEnvironment + lateinit var dbPath: String var logger: XMTPLogger = XMTPLogger() val libXMTPVersion: String = getVersionInfo() - var installationId: String = "" - var v3Client: FfiXmtpClient? = null - var dbPath: String = "" - lateinit var inboxId: String - var hasV2Client: Boolean = true - lateinit var environment: XMTPEnvironment + private lateinit var ffiClient: FfiXmtpClient companion object { private const val TAG = "Client" @@ -111,85 +73,6 @@ class Client() { fun register(codec: ContentCodec<*>) { codecRegistry.register(codec = codec) } - - /** - * Use the {@param api} to fetch any stored keys belonging to {@param address}. - * - * The user will need to be prompted to sign to decrypt each bundle. - */ - suspend fun authCheck(api: ApiClient, address: String): List { - val topic = Topic.userPrivateStoreKeyBundle(toChecksumAddress(address)) - val res = api.queryTopic(topic) - return res.envelopesList.mapNotNull { - try { - EncryptedPrivateKeyBundle.parseFrom(it.message) - } catch (e: Exception) { - Log.e(TAG, "discarding malformed private key bundle: ${e.message}", e) - null - } - } - } - - /** - * Use the {@param api} to save the {@param encryptedKeys} for {@param address}. - * - * The {@param keys} are used to authorize the publish request. - */ - suspend fun authSave( - api: ApiClient, - v1Key: PrivateKeyBundleV1, - encryptedKeys: EncryptedPrivateKeyBundle, - ) { - val authorizedIdentity = AuthorizedIdentity(v1Key) - authorizedIdentity.address = v1Key.walletAddress - val authToken = authorizedIdentity.createAuthToken() - api.setAuthToken(authToken) - api.publish( - envelopes = listOf( - EnvelopeBuilder.buildFromTopic( - topic = Topic.userPrivateStoreKeyBundle(v1Key.walletAddress), - timestamp = Date(), - message = encryptedKeys.toByteArray(), - ), - ), - ) - } - - suspend fun canMessage(peerAddress: String, options: ClientOptions? = null): Boolean { - val clientOptions = options ?: ClientOptions() - val v2Client = - createV2Client( - host = clientOptions.api.env.getUrl(), - isSecure = clientOptions.api.isSecure - ) - clientOptions.api.appVersion?.let { v2Client.setAppVersion(it) } - val api = GRPCApiClient(environment = clientOptions.api.env, rustV2Client = v2Client) - val topics = api.queryTopic(Topic.contact(peerAddress)).envelopesList - return topics.isNotEmpty() - } - } - - constructor( - address: String, - privateKeyBundleV1: PrivateKeyBundleV1, - apiClient: ApiClient, - libXMTPClient: FfiXmtpClient? = null, - dbPath: String = "", - installationId: String = "", - inboxId: String, - ) : this() { - this.address = address - this.privateKeyBundleV1 = privateKeyBundleV1 - this.apiClient = apiClient - 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 = true - this.environment = apiClient.environment } constructor( @@ -201,400 +84,138 @@ class Client() { environment: XMTPEnvironment, ) : this() { this.address = address - this.contacts = Contacts(client = this) - this.v3Client = libXMTPClient + this.preferences = PrivatePreferences(client = this, ffiClient = libXMTPClient) + this.ffiClient = libXMTPClient this.conversations = - Conversations(client = this, libXMTPConversations = libXMTPClient.conversations()) + Conversations(client = this, ffiConversations = libXMTPClient.conversations()) this.dbPath = dbPath this.installationId = installationId this.inboxId = inboxId - this.hasV2Client = false this.environment = environment } - suspend fun buildFrom( - bundle: PrivateKeyBundleV1, - options: ClientOptions? = null, - account: SigningKey? = null, - ): Client { - return buildFromV1Bundle(bundle, options, account) - } - - suspend fun create( - account: SigningKey, - options: ClientOptions? = null, - ): Client { - val clientOptions = options ?: ClientOptions() - val v2Client = - createV2Client( - host = clientOptions.api.env.getUrl(), - isSecure = clientOptions.api.isSecure - ) - clientOptions.api.appVersion?.let { v2Client.setAppVersion(it) } - val apiClient = GRPCApiClient(environment = clientOptions.api.env, rustV2Client = v2Client) - return create( - account = account, - apiClient = apiClient, - options = options, - ) - } - - suspend fun create( - account: SigningKey, - apiClient: ApiClient, - options: ClientOptions? = null, - ): Client { - val clientOptions = options ?: ClientOptions() - try { - val privateKeyBundleV1 = loadOrCreateKeys( - account, - apiClient, - clientOptions - ) - val inboxId = getOrCreateInboxId(clientOptions, account.address) - val (libXMTPClient, dbPath) = - ffiXmtpClient( - clientOptions, - account, - clientOptions.appContext, - privateKeyBundleV1, - account.address, - inboxId - ) - - val client = - Client( - account.address, - privateKeyBundleV1, - apiClient, - libXMTPClient, - dbPath, - libXMTPClient?.installationId()?.toHex() ?: "", - libXMTPClient?.inboxId() ?: inboxId - ) - client.ensureUserContactPublished() - return client - } catch (e: java.lang.Exception) { - throw XMTPException("Error creating client ${e.message}", e) - } - } - private suspend fun initializeV3Client( - accountAddress: String, + address: String, clientOptions: ClientOptions, signingKey: SigningKey? = null, ): Client { + val accountAddress = address.lowercase() val inboxId = getOrCreateInboxId(clientOptions, accountAddress) - val (libXMTPClient, dbPath) = ffiXmtpClient( + val (ffiClient, dbPath) = createFfiClient( + accountAddress, + inboxId, clientOptions, signingKey, clientOptions.appContext, - null, - accountAddress, - inboxId ) - libXMTPClient?.let { client -> - return Client( - accountAddress, - client, - dbPath, - client.installationId().toHex(), - client.inboxId(), - clientOptions.api.env - ) - } ?: throw XMTPException("Error creating V3 client: libXMTPClient is null") + return Client( + accountAddress, + ffiClient, + dbPath, + ffiClient.installationId().toHex(), + ffiClient.inboxId(), + clientOptions.api.env + ) } // Function to create a V3 client with a signing key - suspend fun createV3( + suspend fun create( account: SigningKey, - options: ClientOptions? = null, + options: ClientOptions, ): Client { - this.hasV2Client = false - val clientOptions = options ?: ClientOptions(enableV3 = true) - val accountAddress = account.address.lowercase() return try { - initializeV3Client(accountAddress, clientOptions, account) + initializeV3Client(account.address, options, account) } catch (e: Exception) { throw XMTPException("Error creating V3 client: ${e.message}", e) } } - // Function to build a V3 client without a signing key (using only address (& chainId for SCW)) - suspend fun buildV3( + // Function to build a V3 client from a address + suspend fun build( address: String, - options: ClientOptions? = null, + options: ClientOptions, ): Client { - this.hasV2Client = false - val clientOptions = options ?: ClientOptions(enableV3 = true) - val accountAddress = address.lowercase() return try { - initializeV3Client(accountAddress, clientOptions) + initializeV3Client(address, options) } catch (e: Exception) { throw XMTPException("Error creating V3 client: ${e.message}", e) } } - suspend fun buildFromBundle( - bundle: PrivateKeyBundle, - options: ClientOptions? = null, - account: SigningKey? = null, - ): Client = - buildFromV1Bundle(v1Bundle = bundle.v1, account = account, options = options) - - suspend fun buildFromV1Bundle( - v1Bundle: PrivateKeyBundleV1, - options: ClientOptions? = null, - account: SigningKey? = null, - ): Client { - val address = v1Bundle.identityKey.publicKey.recoverWalletSignerPublicKey().walletAddress - val newOptions = options ?: ClientOptions() - val v2Client = - createV2Client( - host = newOptions.api.env.getUrl(), - isSecure = newOptions.api.isSecure - ) - newOptions.api.appVersion?.let { v2Client.setAppVersion(it) } - val apiClient = GRPCApiClient(environment = newOptions.api.env, rustV2Client = v2Client) - val inboxId = getOrCreateInboxId(newOptions, address) - val (v3Client, dbPath) = if (isV3Enabled(options)) { - ffiXmtpClient( - newOptions, - account, - options?.appContext, - v1Bundle, - address, - inboxId - ) - } else Pair(null, "") - - return Client( - address = address, - privateKeyBundleV1 = v1Bundle, - apiClient = apiClient, - libXMTPClient = v3Client, - dbPath = dbPath, - installationId = v3Client?.installationId()?.toHex() ?: "", - inboxId = v3Client?.inboxId() ?: inboxId - ) - } - - private fun isV3Enabled(options: ClientOptions?): Boolean { - return (options != null && options.enableV3 && options.appContext != null) - } - - private suspend fun ffiXmtpClient( - options: ClientOptions, - account: SigningKey?, - appContext: Context?, - privateKeyBundleV1: PrivateKeyBundleV1?, - address: String, + private suspend fun createFfiClient( + accountAddress: String, inboxId: String, - ): Pair { - var dbPath = "" - val accountAddress = address.lowercase() - val v3Client: FfiXmtpClient? = - if (isV3Enabled(options)) { - val alias = "xmtp-${options.api.env}-$inboxId" - - val mlsDbDirectory = options.dbDirectory - val directoryFile = if (mlsDbDirectory != null) { - File(mlsDbDirectory) - } else { - File(appContext?.filesDir?.absolutePath, "xmtp_db") - } - directoryFile.mkdir() - dbPath = directoryFile.absolutePath + "/$alias.db3" - - val encryptionKey = options.dbEncryptionKey - ?: throw XMTPException("No encryption key passed for the database. Please store and provide a secure encryption key.") - - createClient( - logger = logger, - host = options.api.env.getUrl(), - isSecure = options.api.isSecure, - db = dbPath, - encryptionKey = encryptionKey, - accountAddress = accountAddress, - inboxId = inboxId, - nonce = 0.toULong(), - legacySignedPrivateKeyProto = privateKeyBundleV1?.toV2()?.identityKey?.toByteArray(), - historySyncUrl = options.historySyncUrl - ) - } else { - null - } - - if (v3Client != null) { - options.preAuthenticateToInboxCallback?.let { - runBlocking { - it.invoke() - } - } - v3Client.signatureRequest()?.let { signatureRequest -> - if (account != null) { - if (account.type == WalletType.SCW) { - val chainId = account.chainId ?: throw XMTPException("ChainId is required for smart contract wallets") - signatureRequest.addScwSignature( - account.signSCW(signatureRequest.signatureText()), - account.address.lowercase(), - chainId.toULong(), - account.blockNumber?.toULong() - ) - } else { - account.sign(signatureRequest.signatureText())?.let { - signatureRequest.addEcdsaSignature(it.rawData) - } - } - - v3Client.registerIdentity(signatureRequest) - } else { - throw XMTPException("No signer passed but signer was required.") - } - } - } - Log.i(TAG, "LibXMTP $libXMTPVersion") - return Pair(v3Client, dbPath) - } - - /** - * This authenticates using [account] acquired from network storage - * encrypted using the [wallet]. - * - * e.g. this might be called the first time a user logs in from a new device. - * The next time they launch the app they can [buildFromV1Key]. - * - * If there are stored keys then this asks the [wallet] to - * [encrypted] so that we can decrypt the stored [keys]. - * - * If there are no stored keys then this generates a new identityKey - * and asks the [wallet] to both [createIdentity] and enable Identity Saving - * so we can then store it encrypted for the next time. - */ - private suspend fun loadOrCreateKeys( - account: SigningKey, - apiClient: ApiClient, - options: ClientOptions? = null, - ): PrivateKeyBundleV1 { - val keys = loadPrivateKeys(account, apiClient, options) - return if (keys != null) { - keys + options: ClientOptions, + signingKey: SigningKey?, + appContext: Context, + ): Pair { + val alias = "xmtp-${options.api.env}-$inboxId" + + val mlsDbDirectory = options.dbDirectory + val directoryFile = if (mlsDbDirectory != null) { + File(mlsDbDirectory) } else { - val v1Keys = PrivateKeyBundleV1.newBuilder().build().generate(account, options) - val keyBundle = PrivateKeyBundleBuilder.buildFromV1Key(v1Keys) - val encryptedKeys = keyBundle.encrypted(account, options?.preEnableIdentityCallback) - authSave(apiClient, keyBundle.v1, encryptedKeys) - v1Keys + File(appContext.filesDir.absolutePath, "xmtp_db") } - } + directoryFile.mkdir() + dbPath = directoryFile.absolutePath + "/$alias.db3" + + val ffiClient = createClient( + logger = logger, + host = options.api.env.getUrl(), + isSecure = options.api.isSecure, + db = dbPath, + encryptionKey = options.dbEncryptionKey, + accountAddress = accountAddress, + inboxId = inboxId, + nonce = 0.toULong(), + legacySignedPrivateKeyProto = null, + historySyncUrl = options.historySyncUrl + ) - /** - * This authenticates with [keys] directly received. - * e.g. this might be called on subsequent app launches once we - * have already stored the keys from a previous session. - */ - private suspend fun loadPrivateKeys( - account: SigningKey, - apiClient: ApiClient, - options: ClientOptions? = null, - ): PrivateKeyBundleV1? { - val encryptedBundles = authCheck(apiClient, account.address) - for (encryptedBundle in encryptedBundles) { - try { - val bundle = - encryptedBundle.decrypted(account, options?.preEnableIdentityCallback) - return bundle.v1 - } catch (e: Throwable) { - print("Error decoding encrypted private key bundle: $e") - continue + options.preAuthenticateToInboxCallback?.let { + runBlocking { + it.invoke() } } - return null - } + ffiClient.signatureRequest()?.let { signatureRequest -> + if (signingKey != null) { + if (signingKey.type == WalletType.SCW) { + val chainId = signingKey.chainId + ?: throw XMTPException("ChainId is required for smart contract wallets") + signatureRequest.addScwSignature( + signingKey.signSCW(signatureRequest.signatureText()), + signingKey.address.lowercase(), + chainId.toULong(), + signingKey.blockNumber?.toULong() + ) + } else { + signingKey.sign(signatureRequest.signatureText())?.let { + signatureRequest.addEcdsaSignature(it.rawData) + } + } - suspend fun publishUserContact(legacy: Boolean = false) { - val envelopes: MutableList = mutableListOf() - if (legacy) { - val contactBundle = ContactBundle.newBuilder().also { - it.v1 = it.v1.toBuilder().also { v1Builder -> - v1Builder.keyBundle = v1keys.toPublicKeyBundle() - }.build() - }.build() - - val envelope = MessageApiOuterClass.Envelope.newBuilder().apply { - contentTopic = Topic.contact(address).description - timestampNs = Date().time * 1_000_000 - message = contactBundle.toByteString() - }.build() - - envelopes.add(envelope) + ffiClient.registerIdentity(signatureRequest) + } else { + throw XMTPException("No signer passed but signer was required.") + } } - val contactBundle = ContactBundle.newBuilder().also { - it.v2 = it.v2.toBuilder().also { v2Builder -> - v2Builder.keyBundle = keys.getPublicKeyBundle() - }.build() - it.v2 = it.v2.toBuilder().also { v2Builder -> - v2Builder.keyBundle = v2Builder.keyBundle.toBuilder().also { keyBuilder -> - keyBuilder.identityKey = - keyBuilder.identityKey.toBuilder().also { idBuilder -> - idBuilder.signature = - it.v2.keyBundle.identityKey.signature.ensureWalletSignature() - }.build() - }.build() - }.build() - }.build() - val envelope = MessageApiOuterClass.Envelope.newBuilder().apply { - contentTopic = Topic.contact(address).description - timestampNs = Date().time * 1_000_000 - message = contactBundle.toByteString() - }.build() - envelopes.add(envelope) - publish(envelopes = envelopes) - } - - fun getUserContact(peerAddress: String): ContactBundle? { - return contacts.find(Keys.toChecksumAddress(peerAddress)) - } - - suspend fun query(topic: Topic, pagination: Pagination? = null): QueryResponse { - val client = apiClient ?: throw XMTPException("V2 only function") - return client.queryTopic(topic = topic, pagination = pagination) - } - - suspend fun batchQuery(requests: List): BatchQueryResponse { - val client = apiClient ?: throw XMTPException("V2 only function") - return client.batchQuery(requests) - } - suspend fun subscribe( - topics: List, - callback: FfiV2SubscriptionCallback, - ): FfiV2Subscription { - return subscribe2(FfiV2SubscribeRequest(topics), callback) - } - - suspend fun subscribe2( - request: FfiV2SubscribeRequest, - callback: FfiV2SubscriptionCallback, - ): FfiV2Subscription { - val client = apiClient ?: throw XMTPException("V2 only function") - return client.subscribe(request, callback) + return Pair(ffiClient, dbPath) } fun findGroup(groupId: String): Group? { - val client = v3Client ?: throw XMTPException("Error no V3 client initialized") try { - return Group(this, client.conversation(groupId.hexToByteArray())) + return Group(this, ffiClient.conversation(groupId.hexToByteArray())) } catch (e: Exception) { return null } } fun findConversation(conversationId: String): Conversation? { - val client = v3Client ?: throw XMTPException("Error no V3 client initialized") - val conversation = client.conversation(conversationId.hexToByteArray()) + val conversation = ffiClient.conversation(conversationId.hexToByteArray()) return if (conversation.groupMetadata().conversationType() == "dm") { Conversation.Dm(Dm(this, conversation)) } else if (conversation.groupMetadata().conversationType() == "group") { @@ -605,11 +226,10 @@ class Client() { } fun findConversationByTopic(topic: String): Conversation? { - val client = v3Client ?: throw XMTPException("Error no V3 client initialized") val regex = """/xmtp/mls/1/g-(.*?)/proto""".toRegex() val matchResult = regex.find(topic) val conversationId = matchResult?.groupValues?.get(1) ?: "" - val conversation = client.conversation(conversationId.hexToByteArray()) + val conversation = ffiClient.conversation(conversationId.hexToByteArray()) return if (conversation.groupMetadata().conversationType() == "dm") { Conversation.Dm(Dm(this, conversation)) } else if (conversation.groupMetadata().conversationType() == "group") { @@ -620,66 +240,29 @@ class Client() { } suspend fun findDm(address: String): Dm? { - val client = v3Client ?: throw XMTPException("Error no V3 client initialized") val inboxId = inboxIdFromAddress(address.lowercase()) ?: throw XMTPException("No inboxId present") try { - return Dm(this, client.dmConversation(inboxId)) + return Dm(this, ffiClient.dmConversation(inboxId)) } catch (e: Exception) { return null } } fun findMessage(messageId: String): Message? { - val client = v3Client ?: throw XMTPException("Error no V3 client initialized") return try { - Message(this, client.message(messageId.hexToByteArray())) + Message(this, ffiClient.message(messageId.hexToByteArray())) } catch (e: Exception) { null } } - suspend fun publish(envelopes: List) { - val client = apiClient ?: throw XMTPException("V2 only function") - val authorized = AuthorizedIdentity( - address = address, - authorized = v1keys.identityKey.publicKey, - identity = v1keys.identityKey, - ) - val authToken = authorized.createAuthToken() - client.setAuthToken(authToken) - - client.publish(envelopes = envelopes) - } - - suspend fun ensureUserContactPublished() { - val contact = getUserContact(peerAddress = address) - if (contact != null && keys.getPublicKeyBundle() == contact.v2.keyBundle) { - return - } - - publishUserContact(legacy = true) - } - - /** - * Whether or not we can send messages to [address]. - * @param peerAddress is the address of the client that you want to send messages - * - * @return false when [peerAddress] has never signed up for XMTP - * or when the message is addressed to the sender (no self-messaging). - */ - suspend fun canMessage(peerAddress: String): Boolean { - return query(Topic.contact(peerAddress)).envelopesList.size > 0 - } - - suspend fun canMessageV3(addresses: List): Map { - return v3Client?.canMessage(addresses) - ?: throw XMTPException("Error no V3 client initialized") + suspend fun canMessage(addresses: List): Map { + return ffiClient.canMessage(addresses) } suspend fun inboxIdFromAddress(address: String): String? { - return v3Client?.findInboxId(address.lowercase()) - ?: throw XMTPException("Error no V3 client initialized") + return ffiClient.findInboxId(address.lowercase()) } fun deleteLocalDatabase() { @@ -691,37 +274,26 @@ class Client() { message = "This function is delicate and should be used with caution. App will error if database not properly reconnected. See: reconnectLocalDatabase()", ) fun dropLocalDatabaseConnection() { - v3Client?.releaseDbConnection() + ffiClient.releaseDbConnection() } suspend fun reconnectLocalDatabase() { - v3Client?.dbReconnect() ?: throw XMTPException("Error no V3 client initialized") + ffiClient.dbReconnect() } suspend fun requestMessageHistorySync() { - v3Client?.requestHistorySync() ?: throw XMTPException("Error no V3 client initialized") + ffiClient.requestHistorySync() } suspend fun revokeAllOtherInstallations(signingKey: SigningKey) { - val client = v3Client ?: throw XMTPException("Error no V3 client initialized") - val signatureRequest = client.revokeAllOtherInstallations() + val signatureRequest = ffiClient.revokeAllOtherInstallations() signingKey.sign(signatureRequest.signatureText())?.let { signatureRequest.addEcdsaSignature(it.rawData) - client.applySignatureRequest(signatureRequest) + ffiClient.applySignatureRequest(signatureRequest) } } suspend fun inboxState(refreshFromNetwork: Boolean): InboxState { - val client = v3Client ?: throw XMTPException("Error no V3 client initialized") - return InboxState(client.inboxState(refreshFromNetwork)) + return InboxState(ffiClient.inboxState(refreshFromNetwork)) } - - val privateKeyBundle: PrivateKeyBundle - get() = PrivateKeyBundleBuilder.buildFromV1Key(v1keys) - - val v1keys: PrivateKeyBundleV1 - get() = privateKeyBundleV1 ?: throw XMTPException("V2 only function") - - val keys: PrivateKeyBundleV2 - get() = v1keys.toV2() } diff --git a/library/src/main/java/org/xmtp/android/library/Constants.kt b/library/src/main/java/org/xmtp/android/library/Constants.kt deleted file mode 100644 index 56d2f607b..000000000 --- a/library/src/main/java/org/xmtp/android/library/Constants.kt +++ /dev/null @@ -1,5 +0,0 @@ -package org.xmtp.android.library - -object Constants { - const val VERSION = "0.1.3-development" -} diff --git a/library/src/main/java/org/xmtp/android/library/Contacts.kt b/library/src/main/java/org/xmtp/android/library/Contacts.kt deleted file mode 100644 index c0d0b544a..000000000 --- a/library/src/main/java/org/xmtp/android/library/Contacts.kt +++ /dev/null @@ -1,430 +0,0 @@ -package org.xmtp.android.library - -import kotlinx.coroutines.runBlocking -import org.xmtp.android.library.messages.ContactBundle -import org.xmtp.android.library.messages.ContactBundleBuilder -import org.xmtp.android.library.messages.EnvelopeBuilder -import org.xmtp.android.library.messages.Pagination -import org.xmtp.android.library.messages.Topic -import org.xmtp.android.library.messages.walletAddress -import org.xmtp.proto.message.api.v1.MessageApiOuterClass -import org.xmtp.proto.message.contents.PrivatePreferences.PrivatePreferencesAction -import uniffi.xmtpv3.FfiConsent -import uniffi.xmtpv3.FfiConsentEntityType -import uniffi.xmtpv3.FfiConsentState -import java.util.Date - -enum class ConsentState { - ALLOWED, - DENIED, - UNKNOWN; - - companion object { - fun toFfiConsentState(option: ConsentState): FfiConsentState { - return when (option) { - ConsentState.ALLOWED -> FfiConsentState.ALLOWED - ConsentState.DENIED -> FfiConsentState.DENIED - else -> FfiConsentState.UNKNOWN - } - } - - fun fromFfiConsentState(option: FfiConsentState): ConsentState { - return when (option) { - FfiConsentState.ALLOWED -> ConsentState.ALLOWED - FfiConsentState.DENIED -> ConsentState.DENIED - else -> ConsentState.UNKNOWN - } - } - } -} - -enum class EntryType { - ADDRESS, - GROUP_ID, - INBOX_ID; - - companion object { - fun toFfiConsentEntityType(option: EntryType): FfiConsentEntityType { - return when (option) { - EntryType.ADDRESS -> FfiConsentEntityType.ADDRESS - EntryType.GROUP_ID -> FfiConsentEntityType.CONVERSATION_ID - EntryType.INBOX_ID -> FfiConsentEntityType.INBOX_ID - } - } - - fun fromFfiConsentEntityType(option: FfiConsentEntityType): EntryType { - return when (option) { - FfiConsentEntityType.ADDRESS -> EntryType.ADDRESS - FfiConsentEntityType.CONVERSATION_ID -> EntryType.GROUP_ID - FfiConsentEntityType.INBOX_ID -> EntryType.INBOX_ID - } - } - } -} - -data class ConsentListEntry( - val value: String, - val entryType: EntryType, - val consentType: ConsentState, -) { - companion object { - fun address( - address: String, - type: ConsentState = ConsentState.UNKNOWN, - ): ConsentListEntry { - return ConsentListEntry(address, EntryType.ADDRESS, type) - } - - fun groupId( - groupId: String, - type: ConsentState = ConsentState.UNKNOWN, - ): ConsentListEntry { - return ConsentListEntry(groupId, EntryType.GROUP_ID, type) - } - - fun inboxId( - inboxId: String, - type: ConsentState = ConsentState.UNKNOWN, - ): ConsentListEntry { - return ConsentListEntry(inboxId, EntryType.INBOX_ID, type) - } - } - - fun toFfiConsent(): FfiConsent { - return FfiConsent( - EntryType.toFfiConsentEntityType(entryType), - ConsentState.toFfiConsentState(consentType), - value - ) - } - - val key: String - get() = "${entryType.name}-$value" -} - -class ConsentList( - val client: Client, - val entries: MutableMap = mutableMapOf(), -) { - private var lastFetched: Date? = null - - @OptIn(ExperimentalUnsignedTypes::class) - suspend fun load(): List { - 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(), - ) - 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(), - privateKey.toByteArray(), - envelope.message.toByteArray(), - ) - - preferences.add( - PrivatePreferencesAction.parseFrom( - payload.toUByteArray().toByteArray(), - ) - ) - } - - 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() - } - - suspend fun publish(entries: List) { - if (client.v3Client != null) { - setV3ConsentState(entries) - } - if (client.hasV2Client) { - val payload = PrivatePreferencesAction.newBuilder().also { - entries.iterator().forEach { entry -> - when (entry.entryType to entry.consentType) { - EntryType.ADDRESS to ConsentState.ALLOWED -> it.setAllowAddress( - PrivatePreferencesAction.AllowAddress.newBuilder() - .addWalletAddresses(entry.value) - ) - - EntryType.ADDRESS to ConsentState.DENIED -> it.setDenyAddress( - PrivatePreferencesAction.DenyAddress.newBuilder() - .addWalletAddresses(entry.value) - ) - - EntryType.GROUP_ID to ConsentState.ALLOWED -> it.setAllowGroup( - PrivatePreferencesAction.AllowGroup.newBuilder() - .addGroupIds(entry.value) - ) - - EntryType.GROUP_ID to ConsentState.DENIED -> it.setDenyGroup( - PrivatePreferencesAction.DenyGroup.newBuilder().addGroupIds(entry.value) - ) - - EntryType.INBOX_ID to ConsentState.ALLOWED -> it.setAllowInboxId( - PrivatePreferencesAction.AllowInboxId.newBuilder() - .addInboxIds(entry.value) - ) - - EntryType.INBOX_ID to ConsentState.DENIED -> it.setDenyInboxId( - PrivatePreferencesAction.DenyInboxId.newBuilder() - .addInboxIds(entry.value) - ) - - else -> it.clearMessageType() - } - } - }.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(), - privateKey.toByteArray(), - payload.toByteArray(), - ) - - val envelope = EnvelopeBuilder.buildFromTopic( - Topic.preferenceList(identifier), - Date(), - ByteArray(message.size) { message[it] }, - ) - - client.publish(listOf(envelope)) - } - } - - suspend fun setV3ConsentState(entries: List) { - client.v3Client?.setConsentStates(entries.map { it.toFfiConsent() }) - } - - fun allow(address: String): ConsentListEntry { - val entry = ConsentListEntry.address(address, ConsentState.ALLOWED) - entries[entry.key] = entry - - return entry - } - - fun deny(address: String): ConsentListEntry { - val entry = ConsentListEntry.address(address, ConsentState.DENIED) - entries[entry.key] = entry - - return entry - } - - fun allowGroup(groupId: String): ConsentListEntry { - val entry = ConsentListEntry.groupId(groupId, ConsentState.ALLOWED) - entries[entry.key] = entry - - return entry - } - - fun denyGroup(groupId: String): ConsentListEntry { - val entry = ConsentListEntry.groupId(groupId, ConsentState.DENIED) - entries[entry.key] = entry - - return entry - } - - fun allowInboxId(inboxId: String): ConsentListEntry { - val entry = ConsentListEntry.inboxId(inboxId, ConsentState.ALLOWED) - entries[entry.key] = entry - - return entry - } - - fun denyInboxId(inboxId: String): ConsentListEntry { - val entry = ConsentListEntry.inboxId(inboxId, ConsentState.DENIED) - entries[entry.key] = entry - - return entry - } - - suspend fun state(address: String): ConsentState { - client.v3Client?.let { - return ConsentState.fromFfiConsentState( - it.getConsentState( - FfiConsentEntityType.ADDRESS, - address - ) - ) - } - val entry = entries[ConsentListEntry.address(address).key] - return entry?.consentType ?: ConsentState.UNKNOWN - } - - suspend fun groupState(groupId: String): ConsentState { - client.v3Client?.let { - return ConsentState.fromFfiConsentState( - it.getConsentState( - FfiConsentEntityType.CONVERSATION_ID, - groupId - ) - ) - } - val entry = entries[ConsentListEntry.groupId(groupId).key] - return entry?.consentType ?: ConsentState.UNKNOWN - } - - suspend fun inboxIdState(inboxId: String): ConsentState { - client.v3Client?.let { - return ConsentState.fromFfiConsentState( - it.getConsentState( - FfiConsentEntityType.INBOX_ID, - inboxId - ) - ) - } - val entry = entries[ConsentListEntry.inboxId(inboxId).key] - return entry?.consentType ?: ConsentState.UNKNOWN - } -} - -data class Contacts( - var client: Client, - val knownBundles: MutableMap = mutableMapOf(), - val hasIntroduced: MutableMap = mutableMapOf(), - var consentList: ConsentList = ConsentList(client), -) { - - suspend fun refreshConsentList(): ConsentList { - val entries = consentList.load() - consentList.setV3ConsentState(entries) - return consentList - } - - suspend fun allow(addresses: List) { - val entries = addresses.map { - consentList.allow(it) - } - consentList.publish(entries) - } - - suspend fun deny(addresses: List) { - val entries = addresses.map { - consentList.deny(it) - } - consentList.publish(entries) - } - - suspend fun allowGroups(groupIds: List) { - val entries = groupIds.map { - consentList.allowGroup(it) - } - consentList.publish(entries) - } - - suspend fun denyGroups(groupIds: List) { - val entries = groupIds.map { - consentList.denyGroup(it) - } - consentList.publish(entries) - } - - suspend fun allowInboxes(inboxIds: List) { - val entries = inboxIds.map { - consentList.allowInboxId(it) - } - consentList.publish(entries) - } - - suspend fun denyInboxes(inboxIds: List) { - val entries = inboxIds.map { - consentList.denyInboxId(it) - } - consentList.publish(entries) - } - - suspend fun isAllowed(address: String): Boolean { - return consentList.state(address) == ConsentState.ALLOWED - } - - suspend fun isDenied(address: String): Boolean { - return consentList.state(address) == ConsentState.DENIED - } - - suspend fun isGroupAllowed(groupId: String): Boolean { - return consentList.groupState(groupId) == ConsentState.ALLOWED - } - - suspend fun isGroupDenied(groupId: String): Boolean { - return consentList.groupState(groupId) == ConsentState.DENIED - } - - suspend fun isInboxAllowed(inboxId: String): Boolean { - return consentList.inboxIdState(inboxId) == ConsentState.ALLOWED - } - - suspend fun isInboxDenied(inboxId: String): Boolean { - return consentList.inboxIdState(inboxId) == ConsentState.DENIED - } - - fun has(peerAddress: String): Boolean = knownBundles[peerAddress] != null - - fun needsIntroduction(peerAddress: String): Boolean = hasIntroduced[peerAddress] != true - - fun find(peerAddress: String): ContactBundle? { - val knownBundle = knownBundles[peerAddress] - if (knownBundle != null) { - return knownBundle - } - val response = runBlocking { client.query(topic = Topic.contact(peerAddress)) } - - if (response.envelopesList.isNullOrEmpty()) return null - - for (envelope in response.envelopesList) { - val contactBundle = ContactBundleBuilder.buildFromEnvelope(envelope) - knownBundles[peerAddress] = contactBundle - val address = contactBundle.walletAddress - if (address?.lowercase() == peerAddress.lowercase()) { - return contactBundle - } - } - - return null - } -} diff --git a/library/src/main/java/org/xmtp/android/library/Conversation.kt b/library/src/main/java/org/xmtp/android/library/Conversation.kt index 81319ac6f..6cf25c3c4 100644 --- a/library/src/main/java/org/xmtp/android/library/Conversation.kt +++ b/library/src/main/java/org/xmtp/android/library/Conversation.kt @@ -4,7 +4,6 @@ import kotlinx.coroutines.flow.Flow import org.xmtp.android.library.codecs.EncodedContent import org.xmtp.android.library.libxmtp.Member import org.xmtp.android.library.libxmtp.Message -import org.xmtp.android.library.messages.PagingInfoSortDirection import org.xmtp.proto.message.api.v1.MessageApiOuterClass import org.xmtp.proto.message.contents.Invitation.ConsentProofPayload import java.util.Date @@ -68,14 +67,14 @@ sealed class Conversation { } } - suspend fun updateConsentState(state: ConsentState) { + fun updateConsentState(state: ConsentState) { return when (this) { is Group -> group.updateConsentState(state) is Dm -> dm.updateConsentState(state) } } - suspend fun consentState(): ConsentState { + fun consentState(): ConsentState { return when (this) { is Group -> group.consentState() is Dm -> dm.consentState() @@ -134,7 +133,7 @@ sealed class Conversation { limit: Int? = null, before: Date? = null, after: Date? = null, - direction: PagingInfoSortDirection = MessageApiOuterClass.SortDirection.SORT_DIRECTION_DESCENDING, + direction: Message.SortDirection = Message.SortDirection.DESCENDING, ): List { return when (this) { is Group -> { 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 af73066c2..a76db6828 100644 --- a/library/src/main/java/org/xmtp/android/library/Conversations.kt +++ b/library/src/main/java/org/xmtp/android/library/Conversations.kt @@ -6,8 +6,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import org.xmtp.android.library.ConsentState.Companion.toFfiConsentState import org.xmtp.android.library.libxmtp.Message -import org.xmtp.proto.keystore.api.v1.Keystore -import org.xmtp.proto.message.contents.Invitation import uniffi.xmtpv3.FfiConversation import uniffi.xmtpv3.FfiConversationCallback import uniffi.xmtpv3.FfiConversations @@ -28,26 +26,20 @@ import kotlin.time.DurationUnit data class Conversations( var client: Client, - var conversationsByTopic: MutableMap = mutableMapOf(), - private val libXMTPConversations: FfiConversations? = null, + private val ffiConversations: FfiConversations, ) { - companion object { - private const val TAG = "CONVERSATIONS" - } - enum class ConversationOrder { CREATED_AT, LAST_MESSAGE; } suspend fun fromWelcome(envelopeBytes: ByteArray): Conversation { - val conversation = libXMTPConversations?.processStreamedWelcomeMessage(envelopeBytes) - ?: throw XMTPException("Client does not support Groups") - if (conversation.groupMetadata().conversationType() == "dm") { - return Conversation.Dm(Dm(client, conversation)) + val conversation = ffiConversations.processStreamedWelcomeMessage(envelopeBytes) + return if (conversation.groupMetadata().conversationType() == "dm") { + Conversation.Dm(Dm(client, conversation)) } else { - return Conversation.Group(Group(client, conversation)) + Conversation.Group(Group(client, conversation)) } } @@ -104,14 +96,14 @@ data class Conversations( throw XMTPException("Recipient is sender") } val falseAddresses = - if (accountAddresses.isNotEmpty()) client.canMessageV3(accountAddresses) + if (accountAddresses.isNotEmpty()) client.canMessage(accountAddresses) .filter { !it.value }.map { it.key } else emptyList() if (falseAddresses.isNotEmpty()) { throw XMTPException("${falseAddresses.joinToString()} not on network") } val group = - libXMTPConversations?.createGroup( + ffiConversations?.createGroup( accountAddresses, opts = FfiCreateGroupOptions( permissions = permissions, @@ -122,19 +114,18 @@ data class Conversations( customPermissionPolicySet = permissionsPolicySet ) ) ?: throw XMTPException("Client does not support Groups") - client.contacts.allowGroups(groupIds = listOf(group.id().toHex())) return Group(client, group) } // Sync from the network the latest list of conversations suspend fun syncConversations() { - libXMTPConversations?.sync() + ffiConversations.sync() } // Sync all existing local conversation data from the network (Note: call syncConversations() first to get the latest list of conversations) suspend fun syncAllConversations(): UInt? { - return libXMTPConversations?.syncAllConversations() + return ffiConversations.syncAllConversations() } suspend fun newConversation(peerAddress: String): Conversation { @@ -147,16 +138,14 @@ data class Conversations( throw XMTPException("Recipient is sender") } val falseAddresses = - client.canMessageV3(listOf(peerAddress)).filter { !it.value }.map { it.key } + client.canMessage(listOf(peerAddress)).filter { !it.value }.map { it.key } if (falseAddresses.isNotEmpty()) { throw XMTPException("${falseAddresses.joinToString()} not on network") } var dm = client.findDm(peerAddress) if (dm == null) { - val dmConversation = libXMTPConversations?.createDm(peerAddress.lowercase()) - ?: throw XMTPException("Client does not support V3 Dms") + val dmConversation = ffiConversations.createDm(peerAddress.lowercase()) dm = Dm(client, dmConversation) - client.contacts.allowGroups(groupIds = listOf(dm.id)) } return dm } @@ -166,13 +155,13 @@ data class Conversations( before: Date? = null, limit: Int? = null, ): List { - val ffiGroups = libXMTPConversations?.listGroups( + val ffiGroups = ffiConversations.listGroups( opts = FfiListConversationsOptions( after?.time?.nanoseconds?.toLong(DurationUnit.NANOSECONDS), before?.time?.nanoseconds?.toLong(DurationUnit.NANOSECONDS), limit?.toLong() ) - ) ?: throw XMTPException("Client does not support V3 dms") + ) return ffiGroups.map { Group(client, it) @@ -184,13 +173,13 @@ data class Conversations( before: Date? = null, limit: Int? = null, ): List { - val ffiDms = libXMTPConversations?.listDms( + val ffiDms = ffiConversations.listDms( opts = FfiListConversationsOptions( after?.time?.nanoseconds?.toLong(DurationUnit.NANOSECONDS), before?.time?.nanoseconds?.toLong(DurationUnit.NANOSECONDS), limit?.toLong() ) - ) ?: throw XMTPException("Client does not support V3 dms") + ) return ffiDms.map { Dm(client, it) @@ -204,16 +193,13 @@ data class Conversations( order: ConversationOrder = ConversationOrder.CREATED_AT, consentState: ConsentState? = null, ): List { - if (client.hasV2Client) - throw XMTPException("Only supported for V3 only clients.") - - val ffiConversations = libXMTPConversations?.list( + val ffiConversations = ffiConversations.list( FfiListConversationsOptions( after?.time?.nanoseconds?.toLong(DurationUnit.NANOSECONDS), before?.time?.nanoseconds?.toLong(DurationUnit.NANOSECONDS), limit?.toLong() ) - ) ?: throw XMTPException("Client does not support V3 dms") + ) val filteredConversations = filterByConsentState(ffiConversations, consentState) val sortedConversations = sortConversations(filteredConversations, order) @@ -268,102 +254,48 @@ data class Conversations( } } - fun getHmacKeys( - request: Keystore.GetConversationHmacKeysRequest? = null, - ): Keystore.GetConversationHmacKeysResponse { - val thirtyDayPeriodsSinceEpoch = (Date().time / 1000 / 60 / 60 / 24 / 30).toInt() - val hmacKeysResponse = Keystore.GetConversationHmacKeysResponse.newBuilder() - - var topics = conversationsByTopic - - if (!request?.topicsList.isNullOrEmpty()) { - topics = topics.filter { - request!!.topicsList.contains(it.key) - }.toMutableMap() - } - // TODO -// topics.iterator().forEach { -// val conversation = it.value -// val hmacKeys = HmacKeys.newBuilder() -// if (conversation.keyMaterial != null) { -// (thirtyDayPeriodsSinceEpoch - 1..thirtyDayPeriodsSinceEpoch + 1).iterator() -// .forEach { value -> -// val info = "$value-${client.address}" -// val hmacKey = -// Crypto.deriveKey( -// conversation.keyMaterial!!, -// ByteArray(0), -// info.toByteArray(Charsets.UTF_8), -// ) -// val hmacKeyData = HmacKeyData.newBuilder() -// hmacKeyData.hmacKey = hmacKey.toByteString() -// hmacKeyData.thirtyDayPeriodsSinceEpoch = value -// hmacKeys.addValues(hmacKeyData) -// } -// hmacKeysResponse.putHmacKeys(conversation.topic, hmacKeys.build()) -// } -// } - return hmacKeysResponse.build() - } - - private suspend fun handleConsentProof( - consentProof: Invitation.ConsentProofPayload, - peerAddress: String, - ) { - val signature = consentProof.signature - val timestamp = consentProof.timestamp - - if (!KeyUtil.validateConsentSignature(signature, client.address, peerAddress, timestamp)) { - return - } - val contacts = client.contacts - contacts.refreshConsentList() - if (contacts.consentList.state(peerAddress) == ConsentState.UNKNOWN) { - contacts.allow(listOf(peerAddress)) - } - } + fun stream(/*Maybe Put a way to specify group, dm, or both?*/): Flow = + callbackFlow { + val conversationCallback = object : FfiConversationCallback { + override fun onConversation(conversation: FfiConversation) { + if (conversation.groupMetadata().conversationType() == "dm") { + trySend(Conversation.Dm(Dm(client, conversation))) + } else { + trySend(Conversation.Group(Group(client, conversation))) + } + } - fun stream(/*Maybe Put a way to specify group, dm, or both?*/): Flow = callbackFlow { - val conversationCallback = object : FfiConversationCallback { - override fun onConversation(conversation: FfiConversation) { - if (conversation.groupMetadata().conversationType() == "dm") { - trySend(Conversation.Dm(Dm(client, conversation))) - } else { - trySend(Conversation.Group(Group(client, conversation))) + override fun onError(error: FfiSubscribeException) { + Log.e("XMTP Conversation stream", error.message.toString()) } } - override fun onError(error: FfiSubscribeException) { - Log.e("XMTP Conversation stream", error.message.toString()) - } + val stream = ffiConversations.stream(conversationCallback) + awaitClose { stream.end() } } - val stream = libXMTPConversations?.stream(conversationCallback) - ?: throw XMTPException("Client does not support Groups") - awaitClose { stream.end() } - } - fun streamAllMessages(/*Maybe Put a way to specify group, dm, or both?*/): Flow = callbackFlow { - val messageCallback = object : FfiMessageCallback { - override fun onMessage(message: FfiMessage) { - val conversation = client.findConversation(message.convoId.toHex()) - val decodedMessage = Message(client, message).decodeOrNull() - when (conversation?.version) { - Conversation.Version.DM -> { - decodedMessage?.let { trySend(it) } - } - else -> { - decodedMessage?.let { trySend(it) } + fun streamAllMessages(/*Maybe Put a way to specify group, dm, or both?*/): Flow = + callbackFlow { + val messageCallback = object : FfiMessageCallback { + override fun onMessage(message: FfiMessage) { + val conversation = client.findConversation(message.convoId.toHex()) + val decodedMessage = Message(client, message).decodeOrNull() + when (conversation?.version) { + Conversation.Version.DM -> { + decodedMessage?.let { trySend(it) } + } + + else -> { + decodedMessage?.let { trySend(it) } + } } } - } - override fun onError(error: FfiSubscribeException) { - Log.e("XMTP all message stream", error.message.toString()) + override fun onError(error: FfiSubscribeException) { + Log.e("XMTP all message stream", error.message.toString()) + } } - } - val stream = libXMTPConversations?.streamAllMessages(messageCallback) - ?: throw XMTPException("Client does not support Groups") - - awaitClose { stream.end() } - } -} \ No newline at end of file + val stream = ffiConversations.streamAllMessages(messageCallback) + awaitClose { stream.end() } + } +} diff --git a/library/src/main/java/org/xmtp/android/library/Dm.kt b/library/src/main/java/org/xmtp/android/library/Dm.kt index 8676da65b..8b9d7be9b 100644 --- a/library/src/main/java/org/xmtp/android/library/Dm.kt +++ b/library/src/main/java/org/xmtp/android/library/Dm.kt @@ -10,9 +10,7 @@ import org.xmtp.android.library.codecs.compress import org.xmtp.android.library.libxmtp.Member import org.xmtp.android.library.libxmtp.Message import org.xmtp.android.library.libxmtp.Message.* -import org.xmtp.android.library.messages.PagingInfoSortDirection import org.xmtp.android.library.messages.Topic -import org.xmtp.proto.message.api.v1.MessageApiOuterClass.SortDirection import uniffi.xmtpv3.FfiConversation import uniffi.xmtpv3.FfiConversationMetadata import uniffi.xmtpv3.FfiDeliveryStatus @@ -104,7 +102,7 @@ class Dm(val client: Client, private val libXMTPGroup: FfiConversation) { limit: Int? = null, before: Date? = null, after: Date? = null, - direction: PagingInfoSortDirection = SortDirection.SORT_DIRECTION_DESCENDING, + direction: SortDirection = SortDirection.DESCENDING, deliveryStatus: MessageDeliveryStatus = MessageDeliveryStatus.ALL, ): List { return libXMTPGroup.findMessages( @@ -119,7 +117,7 @@ class Dm(val client: Client, private val libXMTPGroup: FfiConversation) { else -> null }, direction = when (direction) { - SortDirection.SORT_DIRECTION_ASCENDING -> FfiDirection.ASCENDING + SortDirection.ASCENDING -> FfiDirection.ASCENDING else -> FfiDirection.DESCENDING } ) @@ -163,15 +161,7 @@ class Dm(val client: Client, private val libXMTPGroup: FfiConversation) { awaitClose { stream.end() } } - suspend fun updateConsentState(state: ConsentState) { - if (client.hasV2Client) { - when (state) { - ConsentState.ALLOWED -> client.contacts.allowGroups(groupIds = listOf(id)) - ConsentState.DENIED -> client.contacts.denyGroups(groupIds = listOf(id)) - ConsentState.UNKNOWN -> Unit - } - } - + fun updateConsentState(state: ConsentState) { val consentState = ConsentState.toFfiConsentState(state) libXMTPGroup.updateConsentState(consentState) } diff --git a/library/src/main/java/org/xmtp/android/library/Group.kt b/library/src/main/java/org/xmtp/android/library/Group.kt index f0fe94747..21c3aa73d 100644 --- a/library/src/main/java/org/xmtp/android/library/Group.kt +++ b/library/src/main/java/org/xmtp/android/library/Group.kt @@ -10,7 +10,6 @@ import org.xmtp.android.library.codecs.compress import org.xmtp.android.library.libxmtp.Member import org.xmtp.android.library.libxmtp.Message import org.xmtp.android.library.libxmtp.Message.* -import org.xmtp.android.library.messages.PagingInfoSortDirection import org.xmtp.android.library.messages.Topic import org.xmtp.proto.message.api.v1.MessageApiOuterClass.SortDirection.SORT_DIRECTION_ASCENDING import org.xmtp.proto.message.api.v1.MessageApiOuterClass.SortDirection.SORT_DIRECTION_DESCENDING @@ -122,7 +121,7 @@ class Group(val client: Client, private val libXMTPGroup: FfiConversation) { limit: Int? = null, before: Date? = null, after: Date? = null, - direction: PagingInfoSortDirection = SORT_DIRECTION_DESCENDING, + direction: SortDirection = SortDirection.DESCENDING, deliveryStatus: MessageDeliveryStatus = MessageDeliveryStatus.ALL, ): List { return libXMTPGroup.findMessages( @@ -137,7 +136,7 @@ class Group(val client: Client, private val libXMTPGroup: FfiConversation) { else -> null }, direction = when (direction) { - SORT_DIRECTION_ASCENDING -> FfiDirection.ASCENDING + SortDirection.ASCENDING -> FfiDirection.ASCENDING else -> FfiDirection.DESCENDING } ) @@ -151,15 +150,7 @@ class Group(val client: Client, private val libXMTPGroup: FfiConversation) { return Message(client, message) } - suspend fun updateConsentState(state: ConsentState) { - if (client.hasV2Client) { - when (state) { - ConsentState.ALLOWED -> client.contacts.allowGroups(groupIds = listOf(id)) - ConsentState.DENIED -> client.contacts.denyGroups(groupIds = listOf(id)) - ConsentState.UNKNOWN -> Unit - } - } - + fun updateConsentState(state: ConsentState) { val consentState = ConsentState.toFfiConsentState(state) libXMTPGroup.updateConsentState(consentState) } diff --git a/library/src/main/java/org/xmtp/android/library/PreparedMessage.kt b/library/src/main/java/org/xmtp/android/library/PreparedMessage.kt deleted file mode 100644 index 6bfc2cf48..000000000 --- a/library/src/main/java/org/xmtp/android/library/PreparedMessage.kt +++ /dev/null @@ -1,38 +0,0 @@ -package org.xmtp.android.library - -import org.web3j.crypto.Hash -import org.xmtp.android.library.messages.Envelope -import org.xmtp.proto.message.api.v1.MessageApiOuterClass.PublishRequest - -// This houses a fully prepared message that can be published -// as soon as the API client has connectivity. -// -// To support persistence layers that queue pending messages (e.g. while offline) -// this struct supports serializing to/from bytes that can be written to disk or elsewhere. -// See toSerializedData() and fromSerializedData() -data class PreparedMessage( - // The first envelope should send the message to the conversation itself. - // Any more are for required intros/invites etc. - // A client can just publish these when it has connectivity. - val envelopes: List -) { - companion object { - fun fromSerializedData(data: ByteArray): PreparedMessage { - val req = PublishRequest.parseFrom(data) - return PreparedMessage(req.envelopesList) - } - } - - fun toSerializedData(): ByteArray { - val req = PublishRequest.newBuilder() - .addAllEnvelopes(envelopes) - .build() - return req.toByteArray() - } - - val messageId: String - get() = Hash.sha256(envelopes.first().message.toByteArray()).toHex() - - val conversationTopic: String - get() = envelopes.first().contentTopic -} diff --git a/library/src/main/java/org/xmtp/android/library/PrivatePreferences.kt b/library/src/main/java/org/xmtp/android/library/PrivatePreferences.kt new file mode 100644 index 000000000..e345327de --- /dev/null +++ b/library/src/main/java/org/xmtp/android/library/PrivatePreferences.kt @@ -0,0 +1,136 @@ +package org.xmtp.android.library + +import uniffi.xmtpv3.FfiConsent +import uniffi.xmtpv3.FfiConsentEntityType +import uniffi.xmtpv3.FfiConsentState +import uniffi.xmtpv3.FfiXmtpClient + +enum class ConsentState { + ALLOWED, + DENIED, + UNKNOWN; + + companion object { + fun toFfiConsentState(option: ConsentState): FfiConsentState { + return when (option) { + ALLOWED -> FfiConsentState.ALLOWED + DENIED -> FfiConsentState.DENIED + UNKNOWN -> FfiConsentState.UNKNOWN + } + } + + fun fromFfiConsentState(option: FfiConsentState): ConsentState { + return when (option) { + FfiConsentState.ALLOWED -> ALLOWED + FfiConsentState.DENIED -> DENIED + FfiConsentState.UNKNOWN -> UNKNOWN + } + } + } +} + +enum class EntryType { + ADDRESS, + CONVERSATION_ID, + INBOX_ID; + + companion object { + fun toFfiConsentEntityType(option: EntryType): FfiConsentEntityType { + return when (option) { + ADDRESS -> FfiConsentEntityType.ADDRESS + CONVERSATION_ID -> FfiConsentEntityType.CONVERSATION_ID + INBOX_ID -> FfiConsentEntityType.INBOX_ID + } + } + + fun fromFfiConsentEntityType(option: FfiConsentEntityType): EntryType { + return when (option) { + FfiConsentEntityType.ADDRESS -> ADDRESS + FfiConsentEntityType.CONVERSATION_ID -> CONVERSATION_ID + FfiConsentEntityType.INBOX_ID -> INBOX_ID + } + } + } +} + +data class ConsentListEntry( + val value: String, + val entryType: EntryType, + val consentType: ConsentState, +) { + companion object { + fun address( + address: String, + type: ConsentState = ConsentState.UNKNOWN, + ): ConsentListEntry { + return ConsentListEntry(address, EntryType.ADDRESS, type) + } + + fun conversationId( + groupId: String, + type: ConsentState = ConsentState.UNKNOWN, + ): ConsentListEntry { + return ConsentListEntry(groupId, EntryType.CONVERSATION_ID, type) + } + + fun inboxId( + inboxId: String, + type: ConsentState = ConsentState.UNKNOWN, + ): ConsentListEntry { + return ConsentListEntry(inboxId, EntryType.INBOX_ID, type) + } + } + + val key: String + get() = "${entryType.name}-$value" +} + +class ConsentList( + val client: Client, + private val ffiClient: FfiXmtpClient, +) { + suspend fun setConsentState(entries: List) { + ffiClient.setConsentStates(entries.map { it.toFfiConsent() }) + } + + private fun ConsentListEntry.toFfiConsent(): FfiConsent { + return FfiConsent( + EntryType.toFfiConsentEntityType(entryType), + ConsentState.toFfiConsentState(consentType), + value + ) + } + + suspend fun addressState(address: String): ConsentState { + return ConsentState.fromFfiConsentState( + ffiClient.getConsentState( + FfiConsentEntityType.ADDRESS, + address + ) + ) + } + + suspend fun conversationState(groupId: String): ConsentState { + return ConsentState.fromFfiConsentState( + ffiClient.getConsentState( + FfiConsentEntityType.CONVERSATION_ID, + groupId + ) + ) + } + + suspend fun inboxIdState(inboxId: String): ConsentState { + return ConsentState.fromFfiConsentState( + ffiClient.getConsentState( + FfiConsentEntityType.INBOX_ID, + inboxId + ) + ) + } +} + +data class PrivatePreferences( + var client: Client, + private val ffiClient: FfiXmtpClient, + var consentList: ConsentList = ConsentList(client, ffiClient), +) diff --git a/library/src/main/java/org/xmtp/android/library/SigningKey.kt b/library/src/main/java/org/xmtp/android/library/SigningKey.kt index f1a10a395..5f34bae3e 100644 --- a/library/src/main/java/org/xmtp/android/library/SigningKey.kt +++ b/library/src/main/java/org/xmtp/android/library/SigningKey.kt @@ -1,20 +1,6 @@ package org.xmtp.android.library -import com.google.protobuf.kotlin.toByteString -import kotlinx.coroutines.runBlocking -import org.web3j.crypto.ECDSASignature -import org.web3j.crypto.Keys -import org.web3j.crypto.Sign -import org.xmtp.android.library.messages.PublicKey -import org.xmtp.android.library.messages.Signature -import org.xmtp.android.library.messages.createIdentityText -import org.xmtp.android.library.messages.ethHash -import org.xmtp.android.library.messages.rawData -import org.xmtp.proto.message.contents.PrivateKeyOuterClass -import org.xmtp.proto.message.contents.PublicKeyOuterClass import org.xmtp.proto.message.contents.SignatureOuterClass -import java.math.BigInteger -import java.util.Date interface SigningKey { val address: String @@ -50,55 +36,3 @@ enum class WalletType { SCW, // Smart Contract Wallet EOA // Externally Owned Account *Default } - -/** - * This prompts the wallet to sign a personal message. - * It authorizes the `identity` key to act on behalf of this wallet. - * e.g. "XMTP : Create Identity ..." - * @param identity key to act on behalf of this wallet - * @return AuthorizedIdentity object that contains the `identity` key signed by the wallet, - * together with a `publicKey` and `address` signed by the `identity` key. - */ -fun SigningKey.createIdentity( - identity: PrivateKeyOuterClass.PrivateKey, - preCreateIdentityCallback: PreEventCallback? = null, -): AuthorizedIdentity { - val slimKey = - PublicKeyOuterClass.PublicKey - .newBuilder() - .apply { - timestamp = Date().time - secp256K1Uncompressed = identity.publicKey.secp256K1Uncompressed - }.build() - - preCreateIdentityCallback?.let { - runBlocking { - it.invoke() - } - } - - val signatureClass = Signature.newBuilder().build() - val signatureText = signatureClass.createIdentityText(key = slimKey.toByteArray()) - val digest = signatureClass.ethHash(message = signatureText) - val signature = runBlocking { sign(signatureText) } ?: throw XMTPException("Illegal signature") - - val signatureData = KeyUtil.getSignatureData(signature.rawData.toByteString().toByteArray()) - val publicKey = - Sign.recoverFromSignature( - BigInteger(1, signatureData.v).toInt(), - ECDSASignature(BigInteger(1, signatureData.r), BigInteger(1, signatureData.s)), - digest, - ) - - val authorized = - PublicKey.newBuilder().also { - it.secp256K1Uncompressed = slimKey.secp256K1Uncompressed - it.timestamp = slimKey.timestamp - it.signature = signature - } - return AuthorizedIdentity( - address = Keys.toChecksumAddress(Keys.getAddress(publicKey)), - authorized = authorized.build(), - identity = identity, - ) -} diff --git a/library/src/main/java/org/xmtp/android/library/frames/FramesClient.kt b/library/src/main/java/org/xmtp/android/library/frames/FramesClient.kt deleted file mode 100644 index d08defdc1..000000000 --- a/library/src/main/java/org/xmtp/android/library/frames/FramesClient.kt +++ /dev/null @@ -1,94 +0,0 @@ -package org.xmtp.android.library.frames - -import android.util.Base64 -import org.xmtp.android.library.Client -import org.xmtp.android.library.XMTPException -import org.xmtp.android.library.frames.FramesConstants.PROTOCOL_VERSION -import org.xmtp.android.library.messages.PrivateKeyBuilder -import org.xmtp.android.library.messages.Signature -import org.xmtp.android.library.messages.getPublicKeyBundle -import org.xmtp.proto.message.contents.PublicKeyOuterClass.SignedPublicKeyBundle -import java.security.MessageDigest -import org.xmtp.proto.message.contents.Frames.FrameActionBody -import org.xmtp.proto.message.contents.Frames.FrameAction -import java.util.Date - -class FramesClient(private val xmtpClient: Client, var proxy: OpenFramesProxy = OpenFramesProxy()) { - - suspend fun signFrameAction(inputs: FrameActionInputs): FramePostPayload { - val opaqueConversationIdentifier = buildOpaqueIdentifier(inputs) - val frameUrl = inputs.frameUrl - val buttonIndex = inputs.buttonIndex - val inputText = inputs.inputText - val state = inputs.state - val now = Date().time * 1_000_000 - val frameActionBuilder = FrameActionBody.newBuilder().also { frame -> - frame.frameUrl = frameUrl - frame.buttonIndex = buttonIndex - frame.opaqueConversationIdentifier = opaqueConversationIdentifier - frame.timestamp = now - frame.unixTimestamp = now.toInt() - if (inputText != null) { - frame.inputText = inputText - } - if (state != null) { - frame.state = state - } - } - - val toSign = frameActionBuilder.build() - val signedAction = Base64.encodeToString(buildSignedFrameAction(toSign), Base64.NO_WRAP) - - val untrustedData = FramePostUntrustedData(frameUrl, now, buttonIndex, inputText, state, xmtpClient.address, opaqueConversationIdentifier, now.toInt()) - val trustedData = FramePostTrustedData(signedAction) - - return FramePostPayload("xmtp@$PROTOCOL_VERSION", untrustedData, trustedData) - } - - private suspend fun signDigest(digest: ByteArray): Signature { - val signedPrivateKey = xmtpClient.keys.identityKey - val privateKey = PrivateKeyBuilder.buildFromSignedPrivateKey(signedPrivateKey) - return PrivateKeyBuilder(privateKey).sign(digest) - } - - private fun getPublicKeyBundle(): SignedPublicKeyBundle { - return xmtpClient.keys.getPublicKeyBundle() - } - - private suspend fun buildSignedFrameAction(actionBodyInputs: FrameActionBody): ByteArray { - val digest = sha256(actionBodyInputs.toByteArray()) - val signature = signDigest(digest) - - val publicKeyBundle = getPublicKeyBundle() - val frameAction = FrameAction.newBuilder().also { - it.actionBody = actionBodyInputs.toByteString() - it.signature = signature - it.signedPublicKeyBundle = publicKeyBundle - }.build() - - return frameAction.toByteArray() - } - - private fun buildOpaqueIdentifier(inputs: FrameActionInputs): String { - return when (inputs.conversationInputs) { - is ConversationActionInputs.Group -> { - val groupInputs = inputs.conversationInputs.inputs - val combined = groupInputs.groupId + groupInputs.groupSecret - val digest = sha256(combined) - Base64.encodeToString(digest, Base64.NO_WRAP) - } - is ConversationActionInputs.Dm -> { - val dmInputs = inputs.conversationInputs.inputs - val conversationTopic = dmInputs.conversationTopic ?: throw XMTPException("No conversation topic") - val combined = (conversationTopic.lowercase() + dmInputs.participantAccountAddresses.map { it.lowercase() }.sorted().joinToString("")).toByteArray() - val digest = sha256(combined) - Base64.encodeToString(digest, Base64.NO_WRAP) - } - } - } - - private fun sha256(input: ByteArray): ByteArray { - val digest = MessageDigest.getInstance("SHA-256") - return digest.digest(input) - } -} diff --git a/library/src/main/java/org/xmtp/android/library/libxmtp/Message.kt b/library/src/main/java/org/xmtp/android/library/libxmtp/Message.kt index 88116451c..8a08794b3 100644 --- a/library/src/main/java/org/xmtp/android/library/libxmtp/Message.kt +++ b/library/src/main/java/org/xmtp/android/library/libxmtp/Message.kt @@ -18,6 +18,11 @@ data class Message(val client: Client, private val libXMTPMessage: FfiMessage) { ALL, PUBLISHED, UNPUBLISHED, FAILED } + enum class SortDirection { + ASCENDING, + DESCENDING; + } + val id: String get() = libXMTPMessage.id.toHex() diff --git a/library/src/main/java/org/xmtp/android/library/messages/AuthData.kt b/library/src/main/java/org/xmtp/android/library/messages/AuthData.kt deleted file mode 100644 index 76ec0f4ea..000000000 --- a/library/src/main/java/org/xmtp/android/library/messages/AuthData.kt +++ /dev/null @@ -1,18 +0,0 @@ -package org.xmtp.android.library.messages - -import org.xmtp.proto.message.api.v1.Authn -import java.util.Date - -typealias AuthData = org.xmtp.proto.message.api.v1.Authn.AuthData - -class AuthDataBuilder { - companion object { - fun buildFromWalletAddress(walletAddress: String, timestamp: Date? = null): Authn.AuthData { - val timestamped = timestamp?.time ?: Date().time - return AuthData.newBuilder().apply { - walletAddr = walletAddress - createdNs = timestamped * 1_000_000 - }.build() - } - } -} diff --git a/library/src/main/java/org/xmtp/android/library/messages/ContactBundle.kt b/library/src/main/java/org/xmtp/android/library/messages/ContactBundle.kt deleted file mode 100644 index 983eafa88..000000000 --- a/library/src/main/java/org/xmtp/android/library/messages/ContactBundle.kt +++ /dev/null @@ -1,100 +0,0 @@ -package org.xmtp.android.library.messages - -import org.bouncycastle.util.Arrays -import org.web3j.crypto.Keys -import org.xmtp.android.library.XMTPException -import org.xmtp.android.library.toHex -import org.xmtp.proto.message.api.v1.MessageApiOuterClass -import org.xmtp.proto.message.contents.Contact - -typealias ContactBundle = org.xmtp.proto.message.contents.Contact.ContactBundle -typealias ContactBundleV1 = org.xmtp.proto.message.contents.Contact.ContactBundleV1 -typealias ContactBundleV2 = org.xmtp.proto.message.contents.Contact.ContactBundleV2 - -class ContactBundleBuilder { - companion object { - fun buildFromEnvelope(envelope: MessageApiOuterClass.Envelope): ContactBundle { - val data = envelope.message - // Try to deserialize legacy v1 bundle - val publicKeyBundle = PublicKeyBundle.parseFrom(data) - return ContactBundle.newBuilder().also { builder -> - builder.v1 = builder.v1.toBuilder().also { - it.keyBundle = publicKeyBundle - }.build() - if (builder.v1.keyBundle.identityKey.secp256K1Uncompressed.bytes.isEmpty) { - builder.mergeFrom(data) - } - }.build() - } - } -} - -fun ContactBundle.toPublicKeyBundle(): PublicKeyBundle { - return when (versionCase) { - Contact.ContactBundle.VersionCase.V1 -> v1.keyBundle - Contact.ContactBundle.VersionCase.V2 -> PublicKeyBundleBuilder.buildFromSignedKeyBundle(v2.keyBundle) - else -> throw XMTPException("Invalid version") - } -} - -fun ContactBundle.toSignedPublicKeyBundle(): SignedPublicKeyBundle { - return when (versionCase) { - Contact.ContactBundle.VersionCase.V1 -> SignedPublicKeyBundleBuilder.buildFromKeyBundle(v1.keyBundle) - Contact.ContactBundle.VersionCase.V2 -> v2.keyBundle - else -> throw XMTPException("Invalid version") - } -} - -/** - * Create a wallet address according to the version - */ -val ContactBundle.walletAddress: String? - get() { - when (versionCase) { - Contact.ContactBundle.VersionCase.V1 -> { - val key = v1.keyBundle.identityKey.recoverWalletSignerPublicKey() - val address = Keys.getAddress( - Arrays.copyOfRange( - key.secp256K1Uncompressed.bytes.toByteArray(), - 1, - key.secp256K1Uncompressed.bytes.toByteArray().size, - ), - ) - return Keys.toChecksumAddress(address.toHex()) - } - - Contact.ContactBundle.VersionCase.V2 -> { - val key = v2.keyBundle.identityKey.recoverWalletSignerPublicKey() - val address = Keys.getAddress( - Arrays.copyOfRange( - key.secp256K1Uncompressed.bytes.toByteArray(), - 1, - key.secp256K1Uncompressed.bytes.toByteArray().size, - ), - ) - return Keys.toChecksumAddress(address.toHex()) - } - - else -> return null - } - } - -/** - * This get the identity key that represents the wallet address according to the version - */ -val ContactBundle.identityAddress: String? - get() { - return when (versionCase) { - Contact.ContactBundle.VersionCase.V1 -> v1.keyBundle.identityKey.walletAddress - Contact.ContactBundle.VersionCase.V2 -> { - val publicKey = try { - PublicKeyBuilder.buildFromSignedPublicKey(v2.keyBundle.identityKey) - } catch (e: Throwable) { - null - } - publicKey?.walletAddress - } - - else -> null - } - } diff --git a/library/src/main/java/org/xmtp/android/library/messages/EncryptedPrivateKeyBundle.kt b/library/src/main/java/org/xmtp/android/library/messages/EncryptedPrivateKeyBundle.kt deleted file mode 100644 index 6835612e5..000000000 --- a/library/src/main/java/org/xmtp/android/library/messages/EncryptedPrivateKeyBundle.kt +++ /dev/null @@ -1,29 +0,0 @@ -package org.xmtp.android.library.messages - -import kotlinx.coroutines.runBlocking -import org.xmtp.android.library.Crypto -import org.xmtp.android.library.PreEventCallback -import org.xmtp.android.library.SigningKey -import org.xmtp.android.library.XMTPException - -typealias EncryptedPrivateKeyBundle = org.xmtp.proto.message.contents.PrivateKeyOuterClass.EncryptedPrivateKeyBundle - -fun EncryptedPrivateKeyBundle.decrypted( - key: SigningKey, - preEnableIdentityCallback: PreEventCallback? = null, -): PrivateKeyBundle { - preEnableIdentityCallback?.let { - runBlocking { - it.invoke() - } - } - - val signature = runBlocking { - key.sign( - message = Signature.newBuilder().build() - .enableIdentityText(key = v1.walletPreKey.toByteArray()), - ) - } ?: throw XMTPException("Illegal signature") - val message = Crypto.decrypt(signature.rawDataWithNormalizedRecovery, v1.ciphertext) - return PrivateKeyBundle.parseFrom(message) -} diff --git a/library/src/main/java/org/xmtp/android/library/messages/Envelope.kt b/library/src/main/java/org/xmtp/android/library/messages/Envelope.kt deleted file mode 100644 index 3dab92581..000000000 --- a/library/src/main/java/org/xmtp/android/library/messages/Envelope.kt +++ /dev/null @@ -1,26 +0,0 @@ -package org.xmtp.android.library.messages - -import com.google.protobuf.kotlin.toByteString -import java.util.Date - -typealias Envelope = org.xmtp.proto.message.api.v1.MessageApiOuterClass.Envelope - -class EnvelopeBuilder { - companion object { - fun buildFromString(topic: String, timestamp: Date, message: ByteArray): Envelope { - return Envelope.newBuilder().apply { - contentTopic = topic - timestampNs = (timestamp.time * 1_000_000) - this.message = message.toByteString() - }.build() - } - - fun buildFromTopic(topic: Topic, timestamp: Date, message: ByteArray): Envelope { - return Envelope.newBuilder().apply { - contentTopic = topic.description - timestampNs = (timestamp.time * 1_000_000) - this.message = message.toByteString() - }.build() - } - } -} diff --git a/library/src/main/java/org/xmtp/android/library/messages/InvitationV1.kt b/library/src/main/java/org/xmtp/android/library/messages/InvitationV1.kt deleted file mode 100644 index f89d419c6..000000000 --- a/library/src/main/java/org/xmtp/android/library/messages/InvitationV1.kt +++ /dev/null @@ -1,120 +0,0 @@ -package org.xmtp.android.library.messages - -import com.google.crypto.tink.subtle.Base64.encodeToString -import com.google.protobuf.kotlin.toByteString -import org.xmtp.android.library.Crypto -import org.xmtp.android.library.toHex -import org.xmtp.proto.message.contents.Invitation -import org.xmtp.proto.message.contents.Invitation.ConsentProofPayload -import org.xmtp.proto.message.contents.Invitation.InvitationV1.Context -import java.security.SecureRandom - -typealias InvitationV1 = org.xmtp.proto.message.contents.Invitation.InvitationV1 - -class InvitationV1Builder { - companion object { - fun buildFromTopic( - topic: Topic, - context: Context? = null, - aes256GcmHkdfSha256: Invitation.InvitationV1.Aes256gcmHkdfsha256, - consentProof: ConsentProofPayload? = null - ): InvitationV1 { - return InvitationV1.newBuilder().apply { - this.topic = topic.description - if (context != null) { - this.context = context - } - if (consentProof != null) { - this.consentProof = consentProof - } - this.aes256GcmHkdfSha256 = aes256GcmHkdfSha256 - }.build() - } - - fun buildContextFromId( - conversationId: String = "", - metadata: Map = mapOf(), - ): Context { - return Context.newBuilder().apply { - this.conversationId = conversationId - this.putAllMetadata(metadata) - }.build() - } - } -} - -fun InvitationV1.createRandom(context: Context? = null): InvitationV1 { - val inviteContext = context ?: Context.newBuilder().build() - val randomBytes = SecureRandom().generateSeed(32) - val randomString = encodeToString(randomBytes, 0).replace(Regex("=*$"), "") - .replace(Regex("[^A-Za-z0-9]"), "") - val topic = Topic.directMessageV2(randomString) - val keyMaterial = SecureRandom().generateSeed(32) - val aes256GcmHkdfSha256 = Invitation.InvitationV1.Aes256gcmHkdfsha256.newBuilder().apply { - this.keyMaterial = keyMaterial.toByteString() - }.build() - - return InvitationV1Builder.buildFromTopic( - topic = topic, - context = inviteContext, - aes256GcmHkdfSha256 = aes256GcmHkdfSha256, - ) -} - -fun InvitationV1.createDeterministic( - sender: PrivateKeyBundleV2, - recipient: SignedPublicKeyBundle, - context: Context? = null, - consentProof: ConsentProofPayload? = null -): InvitationV1 { - val myAddress = sender.toV1().walletAddress - val theirAddress = recipient.walletAddress - - val inviteContext = context ?: Context.newBuilder().build() - val secret = sender.sharedSecret( - peer = recipient, - myPreKey = sender.preKeysList[0].publicKey, - isRecipient = myAddress < theirAddress, - ) - - val addresses = arrayOf(myAddress, theirAddress) - addresses.sort() - - val msg = if (context != null && !context.conversationId.isNullOrBlank()) { - context.conversationId + addresses.joinToString(separator = ",") - } else { - addresses.joinToString(separator = ",") - } - - val topicId = Crypto.calculateMac(secret = secret, message = msg.toByteArray()).toHex() - val topic = Topic.directMessageV2(topicId) - val keyMaterial = Crypto.deriveKey( - secret = secret, - salt = "__XMTP__INVITATION__SALT__XMTP__".toByteArray(), - info = listOf("0").plus(addresses).joinToString(separator = "|").toByteArray(), - ) - val aes256GcmHkdfSha256 = Invitation.InvitationV1.Aes256gcmHkdfsha256.newBuilder().apply { - this.keyMaterial = keyMaterial.toByteString() - }.build() - - return InvitationV1Builder.buildFromTopic( - topic = topic, - context = inviteContext, - aes256GcmHkdfSha256 = aes256GcmHkdfSha256, - consentProof = consentProof - ) -} - -class InvitationV1ContextBuilder { - companion object { - fun buildFromConversation( - conversationId: String = "", - metadata: Map = mapOf(), - ): Context { - return Context.newBuilder().also { - it.conversationId = conversationId - it.putAllMetadata(metadata) - }.build() - } - } -} diff --git a/library/src/main/java/org/xmtp/android/library/messages/PagingInfo.kt b/library/src/main/java/org/xmtp/android/library/messages/PagingInfo.kt deleted file mode 100644 index 2c6d3938c..000000000 --- a/library/src/main/java/org/xmtp/android/library/messages/PagingInfo.kt +++ /dev/null @@ -1,28 +0,0 @@ -package org.xmtp.android.library.messages - -import org.xmtp.proto.message.api.v1.MessageApiOuterClass.Cursor -import org.xmtp.proto.message.api.v1.MessageApiOuterClass.SortDirection -import java.util.Date - -typealias PagingInfo = org.xmtp.proto.message.api.v1.MessageApiOuterClass.PagingInfo -typealias PagingInfoCursor = Cursor -typealias PagingInfoSortDirection = SortDirection - -data class Pagination( - val limit: Int? = null, - val direction: PagingInfoSortDirection? = SortDirection.SORT_DIRECTION_DESCENDING, - val before: Date? = null, - val after: Date? = null, -) { - val pagingInfo: PagingInfo - get() { - return PagingInfo.newBuilder().also { page -> - limit?.let { - page.limit = it - } - if (direction != null) { - page.direction = direction - } - }.build() - } -} diff --git a/library/src/main/java/org/xmtp/android/library/messages/PrivateKey.kt b/library/src/main/java/org/xmtp/android/library/messages/PrivateKey.kt index f9c0a1929..7f251d33b 100644 --- a/library/src/main/java/org/xmtp/android/library/messages/PrivateKey.kt +++ b/library/src/main/java/org/xmtp/android/library/messages/PrivateKey.kt @@ -1,13 +1,10 @@ package org.xmtp.android.library.messages import com.google.protobuf.kotlin.toByteString -import kotlinx.coroutines.runBlocking import org.web3j.crypto.ECKeyPair -import org.web3j.crypto.Hash import org.web3j.crypto.Sign import org.xmtp.android.library.KeyUtil import org.xmtp.android.library.SigningKey -import org.xmtp.proto.message.contents.PublicKeyOuterClass import org.xmtp.proto.message.contents.SignatureOuterClass import java.security.SecureRandom import java.util.Date @@ -59,16 +56,6 @@ class PrivateKeyBuilder : SigningKey { }.build() }.build() } - - fun buildFromSignedPrivateKey(signedPrivateKey: SignedPrivateKey): PrivateKey { - return PrivateKey.newBuilder().apply { - timestamp = signedPrivateKey.createdNs / 1_000_000 - secp256K1 = secp256K1.toBuilder().also { keyBuilder -> - keyBuilder.bytes = signedPrivateKey.secp256K1.bytes - }.build() - publicKey = PublicKeyBuilder.buildFromSignedPublicKey(signedPrivateKey.publicKey) - }.build() - } } fun getPrivateKey(): PrivateKey { @@ -107,15 +94,3 @@ fun PrivateKey.generate(): PrivateKey { val PrivateKey.walletAddress: String get() = publicKey.walletAddress - -fun PrivateKey.sign(key: PublicKeyOuterClass.UnsignedPublicKey): PublicKeyOuterClass.SignedPublicKey { - val bytes = key.toByteArray() - val signedPublicKey = PublicKeyOuterClass.SignedPublicKey.newBuilder() - val builder = PrivateKeyBuilder(this) - val signature = runBlocking { - builder.sign(Hash.sha256(bytes)) - } - signedPublicKey.signature = signature - signedPublicKey.keyBytes = bytes.toByteString() - return signedPublicKey.build() -} diff --git a/library/src/main/java/org/xmtp/android/library/messages/PrivateKeyBundle.kt b/library/src/main/java/org/xmtp/android/library/messages/PrivateKeyBundle.kt deleted file mode 100644 index d353f84d8..000000000 --- a/library/src/main/java/org/xmtp/android/library/messages/PrivateKeyBundle.kt +++ /dev/null @@ -1,50 +0,0 @@ -package org.xmtp.android.library.messages - -import com.google.protobuf.kotlin.toByteString -import kotlinx.coroutines.runBlocking -import org.xmtp.android.library.Crypto -import org.xmtp.android.library.PreEventCallback -import org.xmtp.android.library.SigningKey -import org.xmtp.android.library.XMTPException -import org.xmtp.proto.message.contents.PrivateKeyOuterClass -import java.security.SecureRandom - -typealias PrivateKeyBundle = PrivateKeyOuterClass.PrivateKeyBundle - -class PrivateKeyBundleBuilder { - companion object { - fun buildFromV1Key(v1: PrivateKeyBundleV1): PrivateKeyBundle { - return PrivateKeyBundle.newBuilder().also { - it.v1 = v1 - }.build() - } - } -} - -fun PrivateKeyBundle.encrypted( - key: SigningKey, - preEnableIdentityCallback: PreEventCallback? = null, -): EncryptedPrivateKeyBundle { - val bundleBytes = toByteArray() - val walletPreKey = SecureRandom().generateSeed(32) - - preEnableIdentityCallback?.let { - runBlocking { - it.invoke() - } - } - - val signature = - runBlocking { - key.sign( - message = Signature.newBuilder().build().enableIdentityText(key = walletPreKey) - ) - } ?: throw XMTPException("Illegal signature") - val cipherText = Crypto.encrypt(signature.rawDataWithNormalizedRecovery, bundleBytes) - return EncryptedPrivateKeyBundle.newBuilder().apply { - v1 = v1.toBuilder().also { v1Builder -> - v1Builder.walletPreKey = walletPreKey.toByteString() - v1Builder.ciphertext = cipherText - }.build() - }.build() -} diff --git a/library/src/main/java/org/xmtp/android/library/messages/PrivateKeyBundleV1.kt b/library/src/main/java/org/xmtp/android/library/messages/PrivateKeyBundleV1.kt deleted file mode 100644 index 555c73ce1..000000000 --- a/library/src/main/java/org/xmtp/android/library/messages/PrivateKeyBundleV1.kt +++ /dev/null @@ -1,104 +0,0 @@ -package org.xmtp.android.library.messages - -import com.google.crypto.tink.subtle.Base64 -import kotlinx.coroutines.runBlocking -import org.web3j.crypto.Hash -import org.xmtp.android.library.ClientOptions -import org.xmtp.android.library.SigningKey -import org.xmtp.android.library.XMTPException -import org.xmtp.android.library.createIdentity -import org.xmtp.proto.message.contents.PrivateKeyOuterClass - -typealias PrivateKeyBundleV1 = org.xmtp.proto.message.contents.PrivateKeyOuterClass.PrivateKeyBundleV1 - -class PrivateKeyBundleV1Builder { - companion object { - fun fromEncodedData(data: String): PrivateKeyBundleV1 { - return PrivateKeyBundleV1.parseFrom(Base64.decode(data, Base64.NO_WRAP)) - } - - fun encodeData(privateKeyBundleV1: PrivateKeyBundleV1): String { - return Base64.encodeToString(privateKeyBundleV1.toByteArray(), Base64.NO_WRAP) - } - - fun buildFromBundle(bundleBytes: ByteArray): PrivateKeyBundleV1 { - val keys = PrivateKeyOuterClass.PrivateKeyBundle.parseFrom(bundleBytes) - if (keys.hasV1()) { - return keys.v1 - } else { - throw XMTPException("No v1 bundle present") - } - } - } -} - -fun PrivateKeyBundleV1.generate( - wallet: SigningKey, - options: ClientOptions? = null, -): PrivateKeyBundleV1 { - val privateKey = PrivateKeyBuilder() - val authorizedIdentity = - wallet.createIdentity(privateKey.getPrivateKey(), options?.preCreateIdentityCallback) - var bundle = authorizedIdentity.toBundle - var preKey = PrivateKey.newBuilder().build().generate() - val bytesToSign = UnsignedPublicKeyBuilder.buildFromPublicKey(preKey.publicKey).toByteArray() - val signature = runBlocking { - privateKey.sign(Hash.sha256(bytesToSign)) - } - - preKey = preKey.toBuilder().apply { - publicKey = publicKey.toBuilder().also { - it.signature = signature - }.build() - }.build() - - val signedPublicKey = privateKey.getPrivateKey() - .sign(key = UnsignedPublicKeyBuilder.buildFromPublicKey(preKey.publicKey)) - - preKey = preKey.toBuilder().apply { - publicKey = PublicKeyBuilder.buildFromSignedPublicKey(signedPublicKey) - publicKey = publicKey.toBuilder().also { - it.signature = signedPublicKey.signature - }.build() - }.build() - - bundle = bundle.toBuilder().apply { - v1 = v1.toBuilder().apply { - identityKey = authorizedIdentity.identity - identityKey = identityKey.toBuilder().also { - it.publicKey = authorizedIdentity.authorized - }.build() - addPreKeys(preKey) - }.build() - }.build() - - return bundle.v1 -} - -val PrivateKeyBundleV1.walletAddress: String - get() = identityKey.publicKey.recoverWalletSignerPublicKey().walletAddress - -fun PrivateKeyBundleV1.toV2(): PrivateKeyBundleV2 { - return PrivateKeyBundleV2.newBuilder().also { - it.identityKey = - SignedPrivateKeyBuilder.buildFromLegacy(identityKey) - it.addAllPreKeys(preKeysList.map { key -> SignedPrivateKeyBuilder.buildFromLegacy(key) }) - }.build() -} - -fun PrivateKeyBundleV1.toPublicKeyBundle(): PublicKeyBundle { - return PublicKeyBundle.newBuilder().also { - it.identityKey = identityKey.publicKey - it.preKey = preKeysList[0].publicKey - }.build() -} - -fun PrivateKeyBundleV1.sharedSecret( - peer: PublicKeyBundle, - myPreKey: PublicKey, - isRecipient: Boolean, -): ByteArray { - val peerBundle = SignedPublicKeyBundleBuilder.buildFromKeyBundle(peer) - val preKey = SignedPublicKeyBuilder.buildFromLegacy(myPreKey) - return toV2().sharedSecret(peer = peerBundle, myPreKey = preKey, isRecipient = isRecipient) -} diff --git a/library/src/main/java/org/xmtp/android/library/messages/PrivateKeyBundleV2.kt b/library/src/main/java/org/xmtp/android/library/messages/PrivateKeyBundleV2.kt deleted file mode 100644 index 18e4b4d75..000000000 --- a/library/src/main/java/org/xmtp/android/library/messages/PrivateKeyBundleV2.kt +++ /dev/null @@ -1,72 +0,0 @@ -package org.xmtp.android.library.messages - -import org.xmtp.android.library.XMTPException - -typealias PrivateKeyBundleV2 = org.xmtp.proto.message.contents.PrivateKeyOuterClass.PrivateKeyBundleV2 - -fun PrivateKeyBundleV2.sharedSecret( - peer: SignedPublicKeyBundle, - myPreKey: SignedPublicKey, - isRecipient: Boolean, -): ByteArray { - val dh1: ByteArray - val dh2: ByteArray - val preKey: SignedPrivateKey - if (isRecipient) { - preKey = findPreKey(myPreKey) - dh1 = this.sharedSecret( - preKey.secp256K1.bytes.toByteArray(), - peer.identityKey.secp256K1Uncompressed.bytes.toByteArray() - ) - dh2 = this.sharedSecret( - identityKey.secp256K1.bytes.toByteArray(), - peer.preKey.secp256K1Uncompressed.bytes.toByteArray() - ) - } else { - preKey = findPreKey(myPreKey) - dh1 = this.sharedSecret( - identityKey.secp256K1.bytes.toByteArray(), - peer.preKey.secp256K1Uncompressed.bytes.toByteArray() - ) - dh2 = this.sharedSecret( - preKey.secp256K1.bytes.toByteArray(), - peer.identityKey.secp256K1Uncompressed.bytes.toByteArray() - ) - } - val dh3 = this.sharedSecret( - preKey.secp256K1.bytes.toByteArray(), - peer.preKey.secp256K1Uncompressed.bytes.toByteArray() - ) - return dh1 + dh2 + dh3 -} - -@OptIn(ExperimentalUnsignedTypes::class) -fun PrivateKeyBundleV2.sharedSecret(privateData: ByteArray, publicData: ByteArray): ByteArray { - return uniffi.xmtpv3.diffieHellmanK256(privateData, publicData).toUByteArray().toByteArray() -} - -fun PrivateKeyBundleV2.findPreKey(myPreKey: SignedPublicKey): SignedPrivateKey { - for (preKey in preKeysList) { - if (preKey.matches(myPreKey)) { - return preKey - } - } - throw XMTPException("No Pre key set") -} - -fun PrivateKeyBundleV2.toV1(): PrivateKeyBundleV1 { - return PrivateKeyBundleV1.newBuilder().also { - it.identityKey = PrivateKeyBuilder.buildFromSignedPrivateKey(identityKey) - it.addAllPreKeys(preKeysList.map { key -> PrivateKeyBuilder.buildFromSignedPrivateKey(key) }) - }.build() -} - -fun PrivateKeyBundleV2.getPublicKeyBundle(): SignedPublicKeyBundle { - return SignedPublicKeyBundle.newBuilder().also { - it.identityKey = identityKey.publicKey - it.identityKey = it.identityKey.toBuilder().also { idKeyBuilder -> - idKeyBuilder.signature = identityKey.publicKey.signature.ensureWalletSignature() - }.build() - it.preKey = preKeysList[0].publicKey - }.build() -} diff --git a/library/src/main/java/org/xmtp/android/library/messages/PublicKey.kt b/library/src/main/java/org/xmtp/android/library/messages/PublicKey.kt index c3e6b0387..ada795f49 100644 --- a/library/src/main/java/org/xmtp/android/library/messages/PublicKey.kt +++ b/library/src/main/java/org/xmtp/android/library/messages/PublicKey.kt @@ -1,50 +1,10 @@ package org.xmtp.android.library.messages -import com.google.protobuf.kotlin.toByteString import org.bouncycastle.util.Arrays import org.web3j.crypto.Keys -import org.web3j.crypto.Sign -import org.xmtp.android.library.KeyUtil -import org.xmtp.android.library.XMTPException import org.xmtp.android.library.toHex -import org.xmtp.proto.message.contents.PublicKeyOuterClass -import java.util.Date typealias PublicKey = org.xmtp.proto.message.contents.PublicKeyOuterClass.PublicKey - -class PublicKeyBuilder { - companion object { - fun buildFromSignedPublicKey(signedPublicKey: PublicKeyOuterClass.SignedPublicKey): PublicKey { - val unsignedPublicKey = PublicKey.parseFrom(signedPublicKey.keyBytes) - return PublicKey.newBuilder().apply { - timestamp = unsignedPublicKey.timestamp - secp256K1Uncompressed = secp256K1Uncompressed.toBuilder().also { - it.bytes = unsignedPublicKey.secp256K1Uncompressed.bytes - }.build() - var sig = signedPublicKey.signature - if (!sig.walletEcdsaCompact.bytes.isEmpty) { - sig = sig.toBuilder().apply { - ecdsaCompact = ecdsaCompact.toBuilder().also { - it.bytes = signedPublicKey.signature.walletEcdsaCompact.bytes - it.recovery = signedPublicKey.signature.walletEcdsaCompact.recovery - }.build() - }.build() - } - signature = sig - }.build() - } - - fun buildFromBytes(data: ByteArray): PublicKey { - return PublicKey.newBuilder().apply { - timestamp = Date().time - secp256K1Uncompressed = secp256K1Uncompressed.toBuilder().apply { - bytes = data.toByteString() - }.build() - }.build() - } - } -} - val PublicKey.walletAddress: String get() { val address = Keys.getAddress( @@ -55,25 +15,4 @@ val PublicKey.walletAddress: String ) ) return Keys.toChecksumAddress(address.toHex()) - } - -fun PublicKey.recoverWalletSignerPublicKey(): PublicKey { - if (!hasSignature()) { - throw XMTPException("No signature found") - } - - val slimKey = PublicKey.newBuilder().also { - it.timestamp = timestamp - it.secp256K1Uncompressed = it.secp256K1Uncompressed.toBuilder().also { keyBuilder -> - keyBuilder.bytes = secp256K1Uncompressed.bytes - }.build() - }.build() - val signatureClass = Signature.newBuilder().build() - val sigText = signatureClass.createIdentityText(slimKey.toByteArray()) - val sigHash = signatureClass.ethHash(sigText) - val pubKeyData = Sign.signedMessageHashToKey( - sigHash, - KeyUtil.getSignatureData(signature.rawDataWithNormalizedRecovery) - ) - return PublicKeyBuilder.buildFromBytes(KeyUtil.addUncompressedByte(pubKeyData.toByteArray())) -} + } \ No newline at end of file diff --git a/library/src/main/java/org/xmtp/android/library/messages/PublicKeyBundle.kt b/library/src/main/java/org/xmtp/android/library/messages/PublicKeyBundle.kt deleted file mode 100644 index 2f654b052..000000000 --- a/library/src/main/java/org/xmtp/android/library/messages/PublicKeyBundle.kt +++ /dev/null @@ -1,20 +0,0 @@ -package org.xmtp.android.library.messages - -import org.xmtp.proto.message.contents.PublicKeyOuterClass - -typealias PublicKeyBundle = org.xmtp.proto.message.contents.PublicKeyOuterClass.PublicKeyBundle - -class PublicKeyBundleBuilder { - companion object { - fun buildFromSignedKeyBundle(signedPublicKeyBundle: SignedPublicKeyBundle): PublicKeyBundle { - return PublicKeyBundle.newBuilder().apply { - identityKey = PublicKeyBuilder.buildFromSignedPublicKey(signedPublicKeyBundle.identityKey) - preKey = PublicKeyBuilder.buildFromSignedPublicKey(signedPublicKeyBundle.preKey) - }.build() - } - } -} - -val PublicKeyBundle.walletAddress: String - get() = - (try { identityKey.recoverWalletSignerPublicKey().walletAddress } catch (e: Throwable) { null }) ?: "" diff --git a/library/src/main/java/org/xmtp/android/library/messages/SealedInvitation.kt b/library/src/main/java/org/xmtp/android/library/messages/SealedInvitation.kt deleted file mode 100644 index b54ee2502..000000000 --- a/library/src/main/java/org/xmtp/android/library/messages/SealedInvitation.kt +++ /dev/null @@ -1,50 +0,0 @@ -package org.xmtp.android.library.messages - -import com.google.protobuf.kotlin.toByteString -import org.xmtp.android.library.CipherText -import org.xmtp.android.library.Crypto -import java.util.Date - -typealias SealedInvitation = org.xmtp.proto.message.contents.Invitation.SealedInvitation - -class SealedInvitationBuilder { - companion object { - fun buildFromV1( - sender: PrivateKeyBundleV2, - recipient: SignedPublicKeyBundle, - created: Date, - invitation: InvitationV1 - ): SealedInvitation { - val header = SealedInvitationHeaderV1Builder.buildFromSignedPublicBundle( - sender.getPublicKeyBundle(), - recipient, - (created.time * 1_000_000) - ) - val secret = sender.sharedSecret( - peer = recipient, - myPreKey = sender.preKeysList[0].publicKey, - isRecipient = false - ) - val headerBytes = header.toByteArray() - val invitationBytes = invitation.toByteArray() - val ciphertext = Crypto.encrypt(secret, invitationBytes, additionalData = headerBytes) - return buildFromCipherText(headerBytes, ciphertext) - } - - fun buildFromCipherText(headerBytes: ByteArray, ciphertext: CipherText?): SealedInvitation { - return SealedInvitation.newBuilder().apply { - v1 = v1.toBuilder().also { - it.headerBytes = headerBytes.toByteString() - it.ciphertext = ciphertext - }.build() - }.build() - } - } -} - -fun SealedInvitation.involves(contact: ContactBundle): Boolean { - val contactSignedPublicKeyBundle = contact.toSignedPublicKeyBundle() - return v1.header.recipient.equals(contactSignedPublicKeyBundle) || v1.header.sender.equals( - contactSignedPublicKeyBundle - ) -} diff --git a/library/src/main/java/org/xmtp/android/library/messages/SealedInvitationHeaderV1.kt b/library/src/main/java/org/xmtp/android/library/messages/SealedInvitationHeaderV1.kt deleted file mode 100644 index 0d77edbff..000000000 --- a/library/src/main/java/org/xmtp/android/library/messages/SealedInvitationHeaderV1.kt +++ /dev/null @@ -1,19 +0,0 @@ -package org.xmtp.android.library.messages - -typealias SealedInvitationHeaderV1 = org.xmtp.proto.message.contents.Invitation.SealedInvitationHeaderV1 - -class SealedInvitationHeaderV1Builder { - companion object { - fun buildFromSignedPublicBundle( - sender: SignedPublicKeyBundle, - recipient: SignedPublicKeyBundle, - createdNs: Long - ): SealedInvitationHeaderV1 { - return SealedInvitationHeaderV1.newBuilder().also { - it.sender = sender - it.recipient = recipient - it.createdNs = createdNs - }.build() - } - } -} diff --git a/library/src/main/java/org/xmtp/android/library/messages/SealedInvitationV1.kt b/library/src/main/java/org/xmtp/android/library/messages/SealedInvitationV1.kt deleted file mode 100644 index 0035a0fb8..000000000 --- a/library/src/main/java/org/xmtp/android/library/messages/SealedInvitationV1.kt +++ /dev/null @@ -1,45 +0,0 @@ -package org.xmtp.android.library.messages - -import com.google.protobuf.kotlin.toByteString -import org.xmtp.android.library.CipherText -import org.xmtp.android.library.Crypto -import org.xmtp.android.library.XMTPException - -typealias SealedInvitationV1 = org.xmtp.proto.message.contents.Invitation.SealedInvitationV1 - -class SealedInvitationV1Builder { - companion object { - fun buildFromHeader(headerBytes: ByteArray, ciphtertext: CipherText): SealedInvitationV1 { - return SealedInvitationV1.newBuilder().also { - it.headerBytes = headerBytes.toByteString() - it.ciphertext = ciphtertext - }.build() - } - } -} - -val SealedInvitationV1.header: SealedInvitationHeaderV1 - get() = SealedInvitationHeaderV1.parseFrom(headerBytes) - -fun SealedInvitationV1.getInvitation(viewer: PrivateKeyBundleV2?): InvitationV1 { - val header = header - if (!header.sender.identityKey.hasSignature()) { - throw XMTPException("No signature") - } - val secret = if (viewer != null && viewer.identityKey.matches(header.sender.identityKey)) { - viewer.sharedSecret( - peer = header.recipient, - myPreKey = header.sender.preKey, - isRecipient = false - ) - } else { - viewer?.sharedSecret( - peer = header.sender, - myPreKey = header.recipient.preKey, - isRecipient = true - ) ?: byteArrayOf() - } - val decryptedBytes = - Crypto.decrypt(secret, ciphertext, additionalData = headerBytes.toByteArray()) - return InvitationV1.parseFrom(decryptedBytes) -} diff --git a/library/src/main/java/org/xmtp/android/library/messages/Signature.kt b/library/src/main/java/org/xmtp/android/library/messages/Signature.kt index 6fe92e792..46fac0585 100644 --- a/library/src/main/java/org/xmtp/android/library/messages/Signature.kt +++ b/library/src/main/java/org/xmtp/android/library/messages/Signature.kt @@ -29,26 +29,6 @@ fun Signature.ethHash(message: String): ByteArray { return Util.keccak256(input.toByteArray()) } -/** - * This is the text that users sign when they want to create - * an identity key associated with their wallet. - * @param key bytes contains an unsigned [xmtp.PublicKey] of the identity key to be created. - * @return The resulting signature is then published to prove that the - * identity key is authorized on behalf of the wallet. - */ -fun Signature.createIdentityText(key: ByteArray): String = - ("XMTP : Create Identity\n" + "${key.toHex()}\n" + "\n" + "For more info: https://xmtp.org/signatures/") - -/** - * This is the text that users sign when they want to save (encrypt) - * or to load (decrypt) keys using the network private storage. - * @param key bytes contains the `walletPreKey` of the encrypted bundle. - * @return The resulting signature is the shared secret used to encrypt and - * decrypt the saved keys. - */ -fun Signature.enableIdentityText(key: ByteArray): String = - ("XMTP : Enable Identity\n" + "${key.toHex()}\n" + "\n" + "For more info: https://xmtp.org/signatures/") - fun Signature.consentProofText(peerAddress: String, timestamp: Long): String { val formatter = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'") formatter.timeZone = TimeZone.getTimeZone("UTC") diff --git a/library/src/main/java/org/xmtp/android/library/messages/SignedContent.kt b/library/src/main/java/org/xmtp/android/library/messages/SignedContent.kt deleted file mode 100644 index 1f7800b79..000000000 --- a/library/src/main/java/org/xmtp/android/library/messages/SignedContent.kt +++ /dev/null @@ -1,21 +0,0 @@ -package org.xmtp.android.library.messages - -import com.google.protobuf.kotlin.toByteString - -typealias SignedContent = org.xmtp.proto.message.contents.Content.SignedContent - -class SignedContentBuilder { - companion object { - fun builderFromPayload( - payload: ByteArray, - sender: SignedPublicKeyBundle?, - signature: Signature? - ): SignedContent { - return SignedContent.newBuilder().also { - it.payload = payload.toByteString() - it.sender = sender - it.signature = signature - }.build() - } - } -} diff --git a/library/src/main/java/org/xmtp/android/library/messages/SignedPrivateKey.kt b/library/src/main/java/org/xmtp/android/library/messages/SignedPrivateKey.kt deleted file mode 100644 index b6d9c8842..000000000 --- a/library/src/main/java/org/xmtp/android/library/messages/SignedPrivateKey.kt +++ /dev/null @@ -1,33 +0,0 @@ -package org.xmtp.android.library.messages - -import kotlinx.coroutines.runBlocking - -typealias SignedPrivateKey = org.xmtp.proto.message.contents.PrivateKeyOuterClass.SignedPrivateKey - -class SignedPrivateKeyBuilder { - companion object { - fun buildFromLegacy(key: PrivateKey): SignedPrivateKey { - return SignedPrivateKey.newBuilder().apply { - createdNs = key.timestamp * 1_000_000 - secp256K1 = secp256K1.toBuilder().also { - it.bytes = key.secp256K1.bytes - }.build() - publicKey = SignedPublicKeyBuilder.buildFromLegacy(key.publicKey) - publicKey = publicKey.toBuilder().also { - it.signature = key.publicKey.signature - }.build() - }.build() - } - } -} - -fun SignedPrivateKey.sign(data: ByteArray): Signature { - val key = PrivateKeyBuilder.buildFromPrivateKeyData(secp256K1.bytes.toByteArray()) - return runBlocking { - PrivateKeyBuilder(key).sign(data) - } -} - -fun SignedPrivateKey.matches(signedPublicKey: SignedPublicKey): Boolean { - return publicKey.recoverWalletSignerPublicKey().walletAddress == signedPublicKey.recoverWalletSignerPublicKey().walletAddress -} diff --git a/library/src/main/java/org/xmtp/android/library/messages/SignedPublicKey.kt b/library/src/main/java/org/xmtp/android/library/messages/SignedPublicKey.kt deleted file mode 100644 index 00b9ab822..000000000 --- a/library/src/main/java/org/xmtp/android/library/messages/SignedPublicKey.kt +++ /dev/null @@ -1,63 +0,0 @@ -package org.xmtp.android.library.messages - -import org.web3j.crypto.Sign -import org.xmtp.android.library.KeyUtil -import org.xmtp.proto.message.contents.PublicKeyOuterClass - -typealias SignedPublicKey = org.xmtp.proto.message.contents.PublicKeyOuterClass.SignedPublicKey - -class SignedPublicKeyBuilder { - companion object { - fun buildFromLegacy( - legacyKey: PublicKey - ): SignedPublicKey { - val publicKey = PublicKey.newBuilder().apply { - secp256K1Uncompressed = legacyKey.secp256K1Uncompressed - timestamp = legacyKey.timestamp - }.build() - return SignedPublicKey.newBuilder().apply { - keyBytes = publicKey.toByteString() - signature = legacyKey.signature - }.build() - } - - fun parseFromPublicKey(publicKey: PublicKey, sig: Signature): SignedPublicKey { - val builder = SignedPublicKey.newBuilder().apply { - signature = sig - } - val unsignedKey = PublicKey.newBuilder().apply { - timestamp = publicKey.timestamp - secp256K1Uncompressed = secp256K1Uncompressed.toBuilder().also { - it.bytes = publicKey.secp256K1Uncompressed.bytes - }.build() - }.build() - builder.keyBytes = unsignedKey.toByteString() - return builder.build() - } - } -} - -val SignedPublicKey.secp256K1Uncompressed: PublicKeyOuterClass.PublicKey.Secp256k1Uncompressed - get() { - val key = PublicKey.parseFrom(keyBytes) - return key.secp256K1Uncompressed - } - -fun SignedPublicKey.verify(key: SignedPublicKey): Boolean { - if (!key.hasSignature()) { - return false - } - return signature.verify( - PublicKeyBuilder.buildFromSignedPublicKey(key), - key.keyBytes.toByteArray() - ) -} - -fun SignedPublicKey.recoverWalletSignerPublicKey(): PublicKey { - val publicKey = PublicKeyBuilder.buildFromSignedPublicKey(this) - val sig = Signature.newBuilder().build() - val sigText = sig.createIdentityText(keyBytes.toByteArray()) - val sigHash = sig.ethHash(sigText) - val pubKeyData = Sign.signedMessageHashToKey(sigHash, KeyUtil.getSignatureData(publicKey.signature.rawDataWithNormalizedRecovery)) - return PublicKeyBuilder.buildFromBytes(KeyUtil.addUncompressedByte(pubKeyData.toByteArray())) -} diff --git a/library/src/main/java/org/xmtp/android/library/messages/SignedPublicKeyBundle.kt b/library/src/main/java/org/xmtp/android/library/messages/SignedPublicKeyBundle.kt deleted file mode 100644 index d6d3637e8..000000000 --- a/library/src/main/java/org/xmtp/android/library/messages/SignedPublicKeyBundle.kt +++ /dev/null @@ -1,26 +0,0 @@ -package org.xmtp.android.library.messages - -typealias SignedPublicKeyBundle = org.xmtp.proto.message.contents.PublicKeyOuterClass.SignedPublicKeyBundle - -class SignedPublicKeyBundleBuilder { - companion object { - fun buildFromKeyBundle(publicKeyBundle: PublicKeyBundle): SignedPublicKeyBundle { - return SignedPublicKeyBundle.newBuilder().apply { - identityKey = SignedPublicKeyBuilder.buildFromLegacy(publicKeyBundle.identityKey) - identityKey = identityKey.toBuilder().also { - it.signature = publicKeyBundle.identityKey.signature - }.build() - preKey = SignedPublicKeyBuilder.buildFromLegacy(publicKeyBundle.preKey) - preKey = preKey.toBuilder().also { - it.signature = publicKeyBundle.preKey.signature - }.build() - }.build() - } - } -} - -fun SignedPublicKeyBundle.equals(other: SignedPublicKeyBundle): Boolean = - identityKey == other.identityKey && preKey == other.preKey - -val SignedPublicKeyBundle.walletAddress: String - get() = identityKey.recoverWalletSignerPublicKey().walletAddress diff --git a/library/src/main/java/org/xmtp/android/library/messages/Token.kt b/library/src/main/java/org/xmtp/android/library/messages/Token.kt deleted file mode 100644 index a5eb79142..000000000 --- a/library/src/main/java/org/xmtp/android/library/messages/Token.kt +++ /dev/null @@ -1,3 +0,0 @@ -package org.xmtp.android.library.messages - -typealias Token = org.xmtp.proto.message.api.v1.Authn.Token diff --git a/library/src/main/java/org/xmtp/android/library/messages/Topic.kt b/library/src/main/java/org/xmtp/android/library/messages/Topic.kt index f26f153a4..db3b3750a 100644 --- a/library/src/main/java/org/xmtp/android/library/messages/Topic.kt +++ b/library/src/main/java/org/xmtp/android/library/messages/Topic.kt @@ -1,58 +1,16 @@ package org.xmtp.android.library.messages sealed class Topic { - data class userPrivateStoreKeyBundle(val address: String?) : Topic() - data class contact(val address: String?) : Topic() - data class userIntro(val address: String?) : Topic() - data class userInvite(val address: String?) : Topic() - data class directMessageV1(val address1: String?, val address2: String?) : Topic() - data class directMessageV2(val addresses: String?) : Topic() - data class preferenceList(val identifier: String?) : Topic() data class userWelcome(val installationId: String?) : Topic() data class groupMessage(val groupId: String?) : Topic() - /** - * Getting the [Topic] structured depending if is [userPrivateStoreKeyBundle], [contact], - * [userIntro], [userInvite], [directMessageV1], [directMessageV2] and [preferenceList] - * with the structured string as /xmtp/0/{id}/proto - */ val description: String get() { return when (this) { - is userPrivateStoreKeyBundle -> wrap("privatestore-$address/key_bundle") - is contact -> wrap("contact-$address") - is userIntro -> wrap("intro-$address") - is userInvite -> wrap("invite-$address") - is directMessageV1 -> { - val addresses = arrayOf(address1, address2) - addresses.sort() - wrap("dm-${addresses.joinToString(separator = "-")}") - } - - is directMessageV2 -> wrap("m-$addresses") - is preferenceList -> wrap("userpreferences-$identifier") is groupMessage -> wrapMls("g-$groupId") is userWelcome -> wrapMls("w-$installationId") } } - private fun wrap(value: String): String = "/xmtp/0/$value/proto" private fun wrapMls(value: String): String = "/xmtp/mls/1/$value/proto" - - companion object { - /** - * This method allows to know if the [Topic] is valid according to the accepted characters - * @param topic String that represents the topic that will be evaluated - * @return if the topic is valid - */ - fun isValidTopic(topic: String): Boolean { - val regex = Regex("^[\\x00-\\x7F]+$") // Use this regex to filter non ASCII chars - val index = topic.indexOf("0/") - if (index != -1) { - val unwrappedTopic = topic.substring(index + 2, topic.lastIndexOf("/proto")) - return unwrappedTopic.matches(regex) - } - return false - } - } } diff --git a/library/src/main/java/org/xmtp/android/library/messages/UnsignedPublicKey.kt b/library/src/main/java/org/xmtp/android/library/messages/UnsignedPublicKey.kt deleted file mode 100644 index 1d364ff5e..000000000 --- a/library/src/main/java/org/xmtp/android/library/messages/UnsignedPublicKey.kt +++ /dev/null @@ -1,29 +0,0 @@ -package org.xmtp.android.library.messages - -import java.util.Date - -typealias UnsignedPublicKey = org.xmtp.proto.message.contents.PublicKeyOuterClass.UnsignedPublicKey - -fun UnsignedPublicKey.generate(): UnsignedPublicKey { - val unsigned = UnsignedPublicKey.newBuilder() - val key = PrivateKey.newBuilder().build().generate() - val createdNs = (Date().time * 1_000_000) - unsigned.secp256K1Uncompressed = unsigned.secp256K1Uncompressed.toBuilder().also { - it.bytes = key.publicKey.secp256K1Uncompressed.bytes - }.build() - unsigned.createdNs = createdNs - return unsigned.build() -} - -class UnsignedPublicKeyBuilder { - companion object { - fun buildFromPublicKey(publicKey: PublicKey): UnsignedPublicKey { - return UnsignedPublicKey.newBuilder().apply { - createdNs = publicKey.timestamp - secp256K1Uncompressed = secp256K1Uncompressed.toBuilder().also { - it.bytes = publicKey.secp256K1Uncompressed.bytes - }.build() - }.build() - } - } -} diff --git a/library/src/test/java/org/xmtp/android/library/AuthenticationTest.kt b/library/src/test/java/org/xmtp/android/library/AuthenticationTest.kt deleted file mode 100644 index 3a5ca4f06..000000000 --- a/library/src/test/java/org/xmtp/android/library/AuthenticationTest.kt +++ /dev/null @@ -1,47 +0,0 @@ -package org.xmtp.android.library - -import com.google.protobuf.kotlin.toByteStringUtf8 -import junit.framework.TestCase.fail -import org.junit.Assert.assertEquals -import org.junit.Test -import org.xmtp.android.library.messages.AuthData -import org.xmtp.android.library.messages.PrivateKey -import org.xmtp.android.library.messages.PrivateKeyBuilder -import org.xmtp.android.library.messages.Token -import org.xmtp.android.library.messages.decrypted -import org.xmtp.android.library.messages.encrypted -import org.xmtp.android.library.messages.generate - -class AuthenticationTest { - - @Test - fun testCreateToken() { - val privateKey = PrivateKeyBuilder() - val identity = PrivateKey.newBuilder().build().generate() - // Prompt them to sign "XMTP : Create Identity ..." - val authorized = privateKey.createIdentity(identity) - // Create the `Authorization: Bearer $authToken` for API calls. - val authToken = authorized.createAuthToken() - val tokenData = authToken.toByteStringUtf8().toByteArray() - val base64TokenData = com.google.crypto.tink.subtle.Base64.decode(tokenData, 2) - if (tokenData.isEmpty() || base64TokenData.isEmpty()) { - fail("could not get token data") - return - } - val token = Token.parseFrom(base64TokenData) - val authData = AuthData.parseFrom(token.authDataBytes) - assertEquals(authData.walletAddr, authorized.address) - } - - @Test - fun testEnablingSavingAndLoadingOfStoredKeys() { - val alice = PrivateKeyBuilder() - val identity = PrivateKey.newBuilder().build().generate() - val authorized = alice.createIdentity(identity) - val bundle = authorized.toBundle - val encryptedBundle = bundle.encrypted(alice) - val decrypted = encryptedBundle.decrypted(alice) - assertEquals(decrypted.v1.identityKey.secp256K1.bytes, identity.secp256K1.bytes) - assertEquals(decrypted.v1.identityKey.publicKey, authorized.authorized) - } -} diff --git a/library/src/test/java/org/xmtp/android/library/ContactTest.kt b/library/src/test/java/org/xmtp/android/library/ContactTest.kt deleted file mode 100644 index 0e22b37e8..000000000 --- a/library/src/test/java/org/xmtp/android/library/ContactTest.kt +++ /dev/null @@ -1,66 +0,0 @@ -package org.xmtp.android.library - -import com.google.protobuf.kotlin.toByteString -import org.junit.Assert.assertEquals -import org.junit.Test -import org.xmtp.android.library.messages.ContactBundleBuilder -import org.xmtp.android.library.messages.Envelope -import org.xmtp.android.library.messages.identityAddress -import org.xmtp.android.library.messages.walletAddress - -class ContactTest { - - @Test - fun testParsingV2Bundle() { - val ints = arrayOf( - 18, 181, 2, 10, 178, 2, 10, 150, 1, 10, 76, 8, 140, 241, 170, 138, 182, 48, 26, 67, 10, - 65, 4, 33, 132, 132, 43, 80, 179, 54, 132, 47, 151, 245, 23, 108, 148, 94, 190, 2, 33, - 232, 232, 185, 73, 64, 44, 47, 65, 168, 25, 56, 252, 1, 58, 243, 20, 103, 8, 253, 118, - 10, 1, 108, 158, 125, 149, 128, 37, 28, 250, 204, 1, 66, 194, 61, 119, 197, 121, 158, - 210, 234, 92, 79, 181, 1, 150, 18, 70, 18, 68, 10, 64, 43, 154, 228, 249, 69, 206, 218, - 165, 35, 55, 141, 145, 183, 129, 104, 75, 106, 62, 28, 73, 69, 7, 170, 65, 66, 93, 11, - 184, 229, 204, 140, 101, 71, 74, 0, 227, 140, 89, 53, 35, 203, 180, 87, 102, 89, 176, - 57, 128, 165, 42, 214, 173, 199, 17, 159, 200, 254, 25, 80, 227, 20, 16, 189, 92, 16, 1, - 18, 150, 1, 10, 76, 8, 244, 246, 171, 138, 182, 48, 26, 67, 10, 65, 4, 104, 191, 167, - 212, 49, 159, 46, 123, 133, 52, 69, 73, 137, 157, 76, 63, 233, 223, 129, 64, 138, 86, - 91, 26, 191, 241, 109, 249, 216, 96, 226, 255, 103, 29, 192, 3, 181, 228, 63, 52, 101, - 88, 96, 141, 236, 194, 111, 16, 105, 88, 127, 215, 255, 63, 92, 135, 251, 14, 176, 85, - 65, 211, 88, 80, 18, 70, 10, 68, 10, 64, 252, 165, 96, 161, 187, 19, 203, 60, 89, 195, - 73, 176, 189, 203, 109, 113, 106, 39, 71, 116, 44, 101, 180, 16, 243, 70, 128, 58, 46, - 10, 55, 243, 43, 115, 21, 23, 153, 241, 208, 212, 162, 205, 197, 139, 2, 117, 1, 40, - 200, 252, 136, 148, 18, 125, 39, 175, 130, 113, 103, 83, 120, 60, 232, 109, 16, 1 - ) - val data = - ints.foldIndexed(ByteArray(ints.size)) { i, a, v -> a.apply { set(i, v.toByte()) } } - val envelope = Envelope.newBuilder().apply { - message = data.toByteString() - }.build() - val contactBundle = ContactBundleBuilder.buildFromEnvelope(envelope) - assert(!contactBundle.v1.hasKeyBundle()) - assert(contactBundle.v2.hasKeyBundle()) - assertEquals(contactBundle.walletAddress, "0x66942eC8b0A6d0cff51AEA9C7fd00494556E705F") - } - - @Test - fun testParsingV1Bundle() { - val ints = arrayOf( // This is a serialized PublicKeyBundle (instead of a ContactBundle) - 10, 146, 1, 8, 236, 130, 192, 166, 148, 48, 18, 68, 10, 66, 10, 64, 70, 34, 101, 46, 39, - 87, 114, 210, 103, 135, 87, 49, 162, 200, 82, 177, 11, 4, 137, 31, 235, 91, 185, 46, 177, - 208, 228, 102, 44, 61, 40, 131, 109, 210, 93, 42, 44, 235, 177, 73, 72, 234, 18, 32, 230, - 61, 146, 58, 65, 78, 178, 163, 164, 241, 118, 167, 77, 240, 13, 100, 151, 70, 190, 15, - 26, 67, 10, 65, 4, 8, 71, 173, 223, 174, 185, 150, 4, 179, 111, 144, 35, 5, 210, 6, 60, - 21, 131, 135, 52, 37, 221, 72, 126, 21, 103, 208, 31, 182, 76, 187, 72, 66, 92, 193, 74, - 161, 45, 135, 204, 55, 10, 20, 119, 145, 136, 45, 194, 140, 164, 124, 47, 238, 17, 198, - 243, 102, 171, 67, 128, 164, 117, 7, 83 - ) - val data = - ints.foldIndexed(ByteArray(ints.size)) { i, a, v -> a.apply { set(i, v.toByte()) } } - val envelope = Envelope.newBuilder().apply { - this.message = data.toByteString() - }.build() - val contactBundle = ContactBundleBuilder.buildFromEnvelope(envelope = envelope) - assertEquals(contactBundle.walletAddress, "0x66942eC8b0A6d0cff51AEA9C7fd00494556E705F") - assertEquals(contactBundle.identityAddress, "0xD320f1454e33ab9393c0cc596E6321d80e4b481e") - assert(!contactBundle.v1.keyBundle.hasPreKey()) - } -} diff --git a/library/src/test/java/org/xmtp/android/library/PreparedMessageTest.kt b/library/src/test/java/org/xmtp/android/library/PreparedMessageTest.kt deleted file mode 100644 index e2cbd5d2c..000000000 --- a/library/src/test/java/org/xmtp/android/library/PreparedMessageTest.kt +++ /dev/null @@ -1,30 +0,0 @@ -package org.xmtp.android.library - -import com.google.protobuf.kotlin.toByteStringUtf8 -import org.junit.Assert.assertEquals -import org.junit.Test -import org.xmtp.android.library.messages.Envelope - -class PreparedMessageTest { - - @Test - fun testSerializing() { - val original = PreparedMessage( - listOf( - Envelope.newBuilder().apply { - contentTopic = "topic1" - timestampNs = 1234 - message = "abc123".toByteStringUtf8() - }.build(), - Envelope.newBuilder().apply { - contentTopic = "topic2" - timestampNs = 5678 - message = "def456".toByteStringUtf8() - }.build(), - ) - ) - val serialized = original.toSerializedData() - val unserialized = PreparedMessage.fromSerializedData(serialized) - assertEquals(original, unserialized) - } -} diff --git a/library/src/test/java/org/xmtp/android/library/PrivateKeyBundleTest.kt b/library/src/test/java/org/xmtp/android/library/PrivateKeyBundleTest.kt deleted file mode 100644 index 3c08b00ee..000000000 --- a/library/src/test/java/org/xmtp/android/library/PrivateKeyBundleTest.kt +++ /dev/null @@ -1,43 +0,0 @@ -package org.xmtp.android.library - -import org.junit.Assert.assertEquals -import org.junit.Test -import org.xmtp.android.library.messages.PrivateKeyBuilder -import org.xmtp.android.library.messages.UnsignedPublicKey -import org.xmtp.android.library.messages.generate -import org.xmtp.android.library.messages.getPublicKeyBundle -import org.xmtp.android.library.messages.toPublicKeyBundle -import org.xmtp.android.library.messages.toV2 -import org.xmtp.proto.message.contents.PrivateKeyOuterClass - -class PrivateKeyBundleTest { - - @Test - fun testConversion() { - val wallet = PrivateKeyBuilder() - val v1 = - PrivateKeyOuterClass.PrivateKeyBundleV1.newBuilder().build().generate(wallet = wallet) - val v2 = v1.toV2() - val v2PreKeyPublic = UnsignedPublicKey.parseFrom(v2.preKeysList[0].publicKey.keyBytes) - assertEquals( - v1.preKeysList[0].publicKey.secp256K1Uncompressed.bytes, - v2PreKeyPublic.secp256K1Uncompressed.bytes - ) - } - - @Test - fun testKeyBundlesAreSigned() { - val wallet = PrivateKeyBuilder() - val v1 = - PrivateKeyOuterClass.PrivateKeyBundleV1.newBuilder().build().generate(wallet = wallet) - assert(v1.identityKey.publicKey.hasSignature()) - assert(v1.preKeysList[0].publicKey.hasSignature()) - assert(v1.toPublicKeyBundle().identityKey.hasSignature()) - assert(v1.toPublicKeyBundle().preKey.hasSignature()) - val v2 = v1.toV2() - assert(v2.identityKey.publicKey.hasSignature()) - assert(v2.preKeysList[0].publicKey.hasSignature()) - assert(v2.getPublicKeyBundle().identityKey.hasSignature()) - assert(v2.getPublicKeyBundle().preKey.hasSignature()) - } -} diff --git a/library/src/test/java/org/xmtp/android/library/TestHelpers.kt b/library/src/test/java/org/xmtp/android/library/TestHelpers.kt index 431cb9917..c28598658 100644 --- a/library/src/test/java/org/xmtp/android/library/TestHelpers.kt +++ b/library/src/test/java/org/xmtp/android/library/TestHelpers.kt @@ -1,11 +1,13 @@ package org.xmtp.android.library +import androidx.test.platform.app.InstrumentationRegistry import kotlinx.coroutines.runBlocking import org.xmtp.android.library.codecs.Fetcher import org.xmtp.android.library.messages.PrivateKey import org.xmtp.android.library.messages.PrivateKeyBuilder import java.io.File import java.net.URL +import java.security.SecureRandom class TestFetcher : Fetcher { override fun fetch(url: URL): ByteArray { @@ -16,21 +18,25 @@ class TestFetcher : Fetcher { data class Fixtures( val aliceAccount: PrivateKeyBuilder, val bobAccount: PrivateKeyBuilder, - val clientOptions: ClientOptions? = ClientOptions( - ClientOptions.Api(XMTPEnvironment.LOCAL, isSecure = false) - ), ) { - var aliceClient: Client = runBlocking { Client().create(account = aliceAccount, options = clientOptions) } + val key = SecureRandom().generateSeed(32) + val context = InstrumentationRegistry.getInstrumentation().targetContext + val clientOptions = ClientOptions( + ClientOptions.Api(XMTPEnvironment.LOCAL, isSecure = false), + dbEncryptionKey = key, + appContext = context, + ) + var aliceClient: Client = + runBlocking { Client().create(account = aliceAccount, options = clientOptions) } var alice: PrivateKey = aliceAccount.getPrivateKey() var bob: PrivateKey = bobAccount.getPrivateKey() - var bobClient: Client = runBlocking { Client().create(account = bobAccount, options = clientOptions) } + var bobClient: Client = + runBlocking { Client().create(account = bobAccount, options = clientOptions) } - constructor(clientOptions: ClientOptions?) : this( + constructor() : this( aliceAccount = PrivateKeyBuilder(), bobAccount = PrivateKeyBuilder(), - clientOptions = clientOptions ) } -fun fixtures(clientOptions: ClientOptions? = null): Fixtures = - Fixtures(clientOptions) +fun fixtures(): Fixtures = Fixtures()