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..77751dc68 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/DmTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/DmTest.kt @@ -37,48 +37,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 +96,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 +116,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.groupState(dm.id), ConsentState.ALLOWED) assertEquals(dm.consentState(), ConsentState.ALLOWED) } } @@ -293,15 +260,32 @@ class DmTest { runBlocking { val dm = boClient.conversations.findOrCreateDm(alix.walletAddress) - assert(boClient.contacts.isGroupAllowed(dm.id)) + assertEquals(boClient.preferences.consentList.groupState(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.GROUP_ID, + ConsentState.DENIED + ) + ) + ) + assertEquals(boClient.preferences.consentList.groupState(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.GROUP_ID, + ConsentState.ALLOWED + ) + ) + ) + assertEquals(boClient.preferences.consentList.groupState(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 index db0bc52c8..83b399812 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/FramesTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/FramesTest.kt @@ -1,71 +1,60 @@ 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") +// 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..cde3be6c2 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 @@ -26,7 +25,6 @@ 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.groupState(boGroup.id), + ConsentState.ALLOWED + ) + assertEquals( + alixClient.preferences.consentList.groupState(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.groupState(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.groupState(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.GROUP_ID, + ConsentState.DENIED + ) + ) + ) + assertEquals(boClient.preferences.consentList.groupState(group.id), ConsentState.DENIED) assertEquals(group.consentState(), ConsentState.DENIED) group.updateConsentState(ConsentState.ALLOWED) - assert(boClient.contacts.isGroupAllowed(group.id)) + assertEquals( + boClient.preferences.consentList.groupState(group.id), + ConsentState.ALLOWED + ) assertEquals(group.consentState(), ConsentState.ALLOWED) } } @@ -735,30 +724,64 @@ 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 +832,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..1fb09e925 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 ) @@ -159,15 +158,29 @@ class SmartContractWalletTest { ) ) } - assert(davonSCWClient.contacts.isGroupAllowed(davonGroup.id)) + assertEquals( + davonSCWClient.preferences.consentList.groupState(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.GROUP_ID, + ConsentState.DENIED + ) + ) + ) + assertEquals(davonSCWClient.preferences.consentList.groupState(davonGroup.id), ConsentState.DENIED) assertEquals(davonGroup.consentState(), ConsentState.DENIED) davonGroup.updateConsentState(ConsentState.ALLOWED) - assert(davonSCWClient.contacts.isGroupAllowed(davonGroup.id)) + assertEquals( + davonSCWClient.preferences.consentList.groupState(davonGroup.id), + ConsentState.ALLOWED + ) assertEquals(davonGroup.consentState(), ConsentState.ALLOWED) } } @@ -183,30 +196,64 @@ class SmartContractWalletTest { ) ) } - assert(!davonSCWClient.contacts.isInboxAllowed(boV3Client.inboxId)) - assert(!davonSCWClient.contacts.isInboxDenied(boV3Client.inboxId)) + 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) - 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.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)) + 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) - davonSCWClient.contacts.denyInboxes(listOf(boV3Client.inboxId)) - caroMember = davonGroup.members().firstOrNull { it.inboxId == boV3Client.inboxId } - assertEquals(caroMember!!.consentState, ConsentState.DENIED) + assertEquals( + davonSCWClient.preferences.consentList.inboxIdState(boV3Client.inboxId), + ConsentState.DENIED + ) - assert(!davonSCWClient.contacts.isInboxAllowed(boV3Client.inboxId)) - assert(davonSCWClient.contacts.isInboxDenied(boV3Client.inboxId)) - 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 8fc13a3cf..f22fa5c48 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 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.preferences = PrivatePreferences(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,395 +84,138 @@ class Client() { environment: XMTPEnvironment, ) : this() { this.address = address - this.preferences = PrivatePreferences(client = this) - this.v3Client = libXMTPClient + this.preferences = PrivatePreferences(client = this, ffiClient = libXMTPClient) + this.ffiClient = libXMTPClient this.conversations = Conversations(client = this, libXMTPConversations = libXMTPClient.conversations()) this.dbPath = dbPath this.installationId = installationId this.inboxId = inboxId - this.hasV2Client = false this.environment = environment } - suspend fun buildFrom( - 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 - ) - 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) - } - - 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") { @@ -600,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") { @@ -615,57 +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) - } - - /** - * 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() { @@ -677,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/Conversation.kt b/library/src/main/java/org/xmtp/android/library/Conversation.kt index aa5f58cea..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 @@ -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 503b71589..ec533fb21 100644 --- a/library/src/main/java/org/xmtp/android/library/Conversations.kt +++ b/library/src/main/java/org/xmtp/android/library/Conversations.kt @@ -104,7 +104,7 @@ 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") @@ -146,7 +146,7 @@ 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") } @@ -202,9 +202,6 @@ 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( FfiListConversationsOptions( after?.time?.nanoseconds?.toLong(DurationUnit.NANOSECONDS), 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 6f75372fe..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 } ) 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 c5a96924e..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 } ) 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 index bb3d6049c..50787141b 100644 --- a/library/src/main/java/org/xmtp/android/library/PrivatePreferences.kt +++ b/library/src/main/java/org/xmtp/android/library/PrivatePreferences.kt @@ -3,6 +3,7 @@ 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, @@ -86,9 +87,10 @@ data class ConsentListEntry( class ConsentList( val client: Client, + private val ffiClient: FfiXmtpClient ) { suspend fun setConsentState(entries: List) { - client.v3Client?.setConsentStates(entries.map { it.toFfiConsent() }) + ffiClient.setConsentStates(entries.map { it.toFfiConsent() }) } private fun ConsentListEntry.toFfiConsent(): FfiConsent { @@ -99,44 +101,36 @@ class ConsentList( ) } - suspend fun addressState(address: String): ConsentState? { - client.v3Client?.let { - return ConsentState.fromFfiConsentState( - it.getConsentState( - FfiConsentEntityType.ADDRESS, - address - ) + suspend fun addressState(address: String): ConsentState { + return ConsentState.fromFfiConsentState( + ffiClient.getConsentState( + FfiConsentEntityType.ADDRESS, + address ) - } - return null + ) } - suspend fun groupState(groupId: String): ConsentState? { - client.v3Client?.let { - return ConsentState.fromFfiConsentState( - it.getConsentState( - FfiConsentEntityType.CONVERSATION_ID, - groupId - ) + suspend fun groupState(groupId: String): ConsentState { + return ConsentState.fromFfiConsentState( + ffiClient.getConsentState( + FfiConsentEntityType.CONVERSATION_ID, + groupId ) - } - return null + ) } - suspend fun inboxIdState(inboxId: String): ConsentState? { - client.v3Client?.let { - return ConsentState.fromFfiConsentState( - it.getConsentState( - FfiConsentEntityType.INBOX_ID, - inboxId - ) + suspend fun inboxIdState(inboxId: String): ConsentState { + return ConsentState.fromFfiConsentState( + ffiClient.getConsentState( + FfiConsentEntityType.INBOX_ID, + inboxId ) - } - return null + ) } } data class PrivatePreferences( var client: Client, - var consentList: ConsentList = ConsentList(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 index d08defdc1..f788ea043 100644 --- a/library/src/main/java/org/xmtp/android/library/frames/FramesClient.kt +++ b/library/src/main/java/org/xmtp/android/library/frames/FramesClient.kt @@ -1,94 +1,83 @@ 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) - } +// 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()