From 9b9f6282b943695878997dfc3d9ae630a7a5b91e Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Wed, 6 Nov 2024 20:54:28 -0800 Subject: [PATCH] Update to a V3 only SDK (#312) * remove v1 and v2 conversations * update all the tests to compile * [V3] Remove Decrypted Messages (#313) * remove decrypted messages and only use decoded * [V3] Remove contacts and rename to preferences (#314) * remove contacts and rename to preferences * [V3] Remove the ability to create a V2 client (#315) * remove the ability to create a v2 client entirely * remove a bunch of classes no longer needed * [V3] Fix up all the tests (#316) * get all the tests compiling * fix up the wallet address * more clean up * get all the tests passing * make the ffiClient private so people dont call it directly * [V3] Update example app (#317) * update the example to be v3 only * remove force * remove some impots * update frames * clean up the code * rename and fix up the lint * fix up the lint --- .github/workflows/release.yml | 26 +- README.md | 194 ++-- .../org/xmtp/android/example/ClientManager.kt | 15 +- .../org/xmtp/android/example/MainActivity.kt | 2 +- .../org/xmtp/android/example/MainViewModel.kt | 17 +- .../example/connect/ConnectWalletFragment.kt | 9 +- .../example/connect/ConnectWalletViewModel.kt | 9 +- .../ConversationDetailViewModel.kt | 10 +- .../conversation/ConversationViewHolder.kt | 9 +- .../NewConversationBottomSheet.kt | 2 +- .../conversation/NewGroupBottomSheet.kt | 2 +- .../PushNotificationsService.kt | 19 +- .../org/xmtp/android/example/utils/KeyUtil.kt | 4 +- library/build.gradle | 1 + .../xmtp/android/library/AttachmentTest.kt | 8 +- .../org/xmtp/android/library/ClientTest.kt | 241 +---- .../org/xmtp/android/library/CodecTest.kt | 111 +- .../org/xmtp/android/library/ContactsTest.kt | 59 -- .../xmtp/android/library/ConversationTest.kt | 958 ------------------ .../xmtp/android/library/ConversationsTest.kt | 315 ++---- .../java/org/xmtp/android/library/DmTest.kt | 143 +-- .../org/xmtp/android/library/FramesTest.kt | 71 -- .../android/library/GroupPermissionsTest.kt | 42 +- .../org/xmtp/android/library/GroupTest.kt | 349 +++---- .../xmtp/android/library/GroupUpdatedTest.kt | 21 +- .../xmtp/android/library/InvitationTest.kt | 197 ---- .../android/library/LocalInstrumentedTest.kt | 884 ---------------- .../org/xmtp/android/library/MessageTest.kt | 391 ------- .../org/xmtp/android/library/ReactionTest.kt | 61 +- .../xmtp/android/library/ReadReceiptTest.kt | 8 +- .../org/xmtp/android/library/ReplyTest.kt | 8 +- .../library/SmartContractWalletTest.kt | 122 ++- .../org/xmtp/android/library/TestHelpers.kt | 56 +- .../org/xmtp/android/library/V3ClientTest.kt | 425 -------- .../org/xmtp/android/library/ApiClient.kt | 237 ----- .../android/library/AuthorizedIdentity.kt | 53 - .../java/org/xmtp/android/library/Client.kt | 704 ++----------- .../org/xmtp/android/library/Constants.kt | 5 - .../java/org/xmtp/android/library/Contacts.kt | 430 -------- .../org/xmtp/android/library/Conversation.kt | 368 +------ .../android/library/ConversationExport.kt | 21 - .../xmtp/android/library/ConversationV1.kt | 310 ------ .../xmtp/android/library/ConversationV2.kt | 311 ------ .../org/xmtp/android/library/Conversations.kt | 875 +--------------- .../xmtp/android/library/DecodedMessage.kt | 2 +- .../main/java/org/xmtp/android/library/Dm.kt | 118 +-- .../java/org/xmtp/android/library/Group.kt | 119 +-- .../xmtp/android/library/PreparedMessage.kt | 40 - .../android/library/PrivatePreferences.kt | 136 +++ .../org/xmtp/android/library/SigningKey.kt | 66 -- .../java/org/xmtp/android/library/Util.kt | 11 - .../android/library/frames/FramesClient.kt | 94 -- .../libxmtp/{MessageV3.kt => Message.kt} | 32 +- .../xmtp/android/library/messages/AuthData.kt | 18 - .../android/library/messages/ContactBundle.kt | 100 -- .../library/messages/DecryptedMessage.kt | 13 - .../messages/EncryptedPrivateKeyBundle.kt | 29 - .../xmtp/android/library/messages/Envelope.kt | 26 - .../android/library/messages/InvitationV1.kt | 120 --- .../xmtp/android/library/messages/Message.kt | 33 - .../library/messages/MessageHeaderV1.kt | 19 - .../library/messages/MessageHeaderv2.kt | 16 - .../android/library/messages/MessageV1.kt | 84 -- .../android/library/messages/MessageV2.kt | 174 ---- .../android/library/messages/PagingInfo.kt | 28 - .../android/library/messages/PrivateKey.kt | 25 - .../library/messages/PrivateKeyBundle.kt | 50 - .../library/messages/PrivateKeyBundleV1.kt | 104 -- .../library/messages/PrivateKeyBundleV2.kt | 72 -- .../android/library/messages/PublicKey.kt | 61 -- .../library/messages/PublicKeyBundle.kt | 20 - .../library/messages/SealedInvitation.kt | 50 - .../messages/SealedInvitationHeaderV1.kt | 19 - .../library/messages/SealedInvitationV1.kt | 45 - .../android/library/messages/Signature.kt | 21 - .../android/library/messages/SignedContent.kt | 21 - .../library/messages/SignedPrivateKey.kt | 33 - .../library/messages/SignedPublicKey.kt | 63 -- .../library/messages/SignedPublicKeyBundle.kt | 26 - .../xmtp/android/library/messages/Token.kt | 3 - .../xmtp/android/library/messages/Topic.kt | 42 - .../library/messages/UnsignedPublicKey.kt | 29 - .../android/library/AuthenticationTest.kt | 47 - .../org/xmtp/android/library/ContactTest.kt | 66 -- .../android/library/PreparedMessageTest.kt | 30 - .../android/library/PrivateKeyBundleTest.kt | 43 - .../org/xmtp/android/library/TestHelpers.kt | 24 +- 87 files changed, 918 insertions(+), 9357 deletions(-) delete mode 100644 library/src/androidTest/java/org/xmtp/android/library/ContactsTest.kt delete mode 100644 library/src/androidTest/java/org/xmtp/android/library/ConversationTest.kt delete mode 100644 library/src/androidTest/java/org/xmtp/android/library/FramesTest.kt delete mode 100644 library/src/androidTest/java/org/xmtp/android/library/InvitationTest.kt delete mode 100644 library/src/androidTest/java/org/xmtp/android/library/LocalInstrumentedTest.kt delete mode 100644 library/src/androidTest/java/org/xmtp/android/library/MessageTest.kt delete mode 100644 library/src/androidTest/java/org/xmtp/android/library/V3ClientTest.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/ApiClient.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/AuthorizedIdentity.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/Constants.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/Contacts.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/ConversationExport.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/ConversationV1.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/ConversationV2.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/PreparedMessage.kt create mode 100644 library/src/main/java/org/xmtp/android/library/PrivatePreferences.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/frames/FramesClient.kt rename library/src/main/java/org/xmtp/android/library/libxmtp/{MessageV3.kt => Message.kt} (75%) delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/AuthData.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/ContactBundle.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/DecryptedMessage.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/EncryptedPrivateKeyBundle.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/Envelope.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/InvitationV1.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/Message.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/MessageHeaderV1.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/MessageHeaderv2.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/MessageV1.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/MessageV2.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/PagingInfo.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/PrivateKeyBundle.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/PrivateKeyBundleV1.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/PrivateKeyBundleV2.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/PublicKeyBundle.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/SealedInvitation.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/SealedInvitationHeaderV1.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/SealedInvitationV1.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/SignedContent.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/SignedPrivateKey.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/SignedPublicKey.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/SignedPublicKeyBundle.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/Token.kt delete mode 100644 library/src/main/java/org/xmtp/android/library/messages/UnsignedPublicKey.kt delete mode 100644 library/src/test/java/org/xmtp/android/library/AuthenticationTest.kt delete mode 100644 library/src/test/java/org/xmtp/android/library/ContactTest.kt delete mode 100644 library/src/test/java/org/xmtp/android/library/PreparedMessageTest.kt delete mode 100644 library/src/test/java/org/xmtp/android/library/PrivateKeyBundleTest.kt diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 180e2dbf8..35ede7352 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,30 +13,34 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 + - name: Configure JDK uses: actions/setup-java@v4 with: distribution: 'adopt' java-version: '17' + - name: Setup Gradle uses: gradle/actions/setup-gradle@v3 + - name: Run build with Gradle Wrapper run: ./gradlew build - - name: Bump version and push tag - id: tag_version - uses: mathieudutour/github-tag-action@v6.1 - with: - release_branches: "release" - github_token: ${{ secrets.GITHUB_TOKEN }} - - name: Create a GitHub release + + - name: Tag version 3.0.0 + run: | + git tag 3.0.0 + git push origin 3.0.0 + + - name: Create a GitHub release for version 3.0.0 uses: ncipollo/release-action@v1 with: - tag: ${{ steps.tag_version.outputs.new_version }} - name: Release ${{ steps.tag_version.outputs.new_tag }} - body: ${{ steps.tag_version.outputs.changelog }} + tag: "3.0.0" + name: "Release 3.0.0" + body: "A XMTP MLS only SDK 3.0.0" + - name: Gradle Publish env: - RELEASE_VERSION: ${{ steps.tag_version.outputs.new_version }} + RELEASE_VERSION: "3.0.0" MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }} MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }} SIGN_KEY: ${{ secrets.OSSRH_GPG_SECRET_KEY }} diff --git a/README.md b/README.md index 61bf138a2..9f12a0eec 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # xmtp-android -![Test](https://github.com/xmtp/xmtp-android/actions/workflows/test.yml/badge.svg) ![Lint](https://github.com/xmtp/xmtp-android/actions/workflows/lint.yml/badge.svg) ![Status](https://img.shields.io/badge/Project_Status-Production-31CA54) +![Test](https://github.com/xmtp/xmtp-android/actions/workflows/test.yml/badge.svg) ![Lint](https://github.com/xmtp/xmtp-android/actions/workflows/lint.yml/badge.svg) ![Status](https://img.shields.io/badge/Feature_status-Alpha-orange) `xmtp-android` provides a Kotlin implementation of an XMTP message API client for use with Android apps. @@ -25,7 +25,7 @@ To learn about example app push notifications, see [Enable the quickstart app to ## Install from Maven Central -You can find the latest package version on [Maven Central](https://central.sonatype.com/artifact/org.xmtp/android/0.0.5/versions). +You can find the latest package version on [Maven Central](https://central.sonatype.com/artifact/org.xmtp/android/3.0.0/versions). ```gradle implementation 'org.xmtp:android:X.X.X' @@ -33,73 +33,67 @@ You can find the latest package version on [Maven Central](https://central.sonat ## Usage overview -The XMTP message API revolves around a message API client (client) that allows retrieving and sending messages to other XMTP network participants. A client must connect to a wallet app on startup. If this is the very first time the client is created, the client will generate a key bundle that is used to encrypt and authenticate messages. The key bundle persists encrypted in the network using an account signature. The public side of the key bundle is also regularly advertised on the network to allow parties to establish shared encryption keys. All of this happens transparently, without requiring any additional code. +The XMTP message API revolves around a message API client (client) that allows retrieving and sending messages to other XMTP network participants. A client must connect to a wallet app on startup. If this is the very first time the client is created, the client will generate a identity with a encrypted local database to store and retrieve messages. Each additional log in will create a new installation if a local database is not present. ```kotlin // You'll want to replace this with a wallet from your application. val account = PrivateKeyBuilder() +// A key to encrypt the local database +val encryptionKey = SecureRandom().generateSeed(32) + +// Application context for creating the local database +val context = getApplication() + +// The required client options +val clientOptions = ClientOptions( + ClientOptions.Api(XMTPEnvironment.DEV, isSecure = true), + dbEncryptionKey = encryptionKey, + appContext = context, +) + // Create the client with your wallet. This will connect to the XMTP `dev` network by default. // The account is anything that conforms to the `XMTP.SigningKey` protocol. -val client = Client().create(account = account) +val client = Client().create(account = account, options = clientOptions) -// Start a conversation with XMTP +// Start a dm conversation val conversation = client.conversations.newConversation("0x3F11b27F323b62B159D2642964fa27C46C841897") +// Or a group conversation +val groupConversation = client.conversations.newGroup(listOf("0x3F11b27F323b62B159D2642964fa27C46C841897")) -// Load all messages in the conversation +// Load all messages in the conversations val messages = conversation.messages() + // Send a message conversation.send(text = "gm") + // Listen for new messages in the conversation conversation.streamMessages().collect { - print("${message.senderAddress}: ${message.body}") + print("${it.senderAddress}: ${it.body}") } ``` -## Use local storage - -> **Important** -> If you are building a production-grade app, be sure to use an architecture that includes a local cache backed by an XMTP SDK. - -To learn more, see [Use a local cache](https://xmtp.org/docs/build/local-first). - ## Create a client -A client is created with `Client().create(account: SigningKey): Client` that requires passing in an object capable of creating signatures on your behalf. The client will request a signature in two cases: - -1. To sign the newly generated key bundle. This happens only the very first time when a key bundle is not found in storage. -2. To sign a random salt used to encrypt the key bundle in storage. This happens every time the client is started, including the very first time. +A client is created with `Client().create(account: SigningKey, options: ClientOptions): Client` that requires passing in an object capable of creating signatures on your behalf. The client will request a signature for any new installation. > **Note** > The client connects to the XMTP `dev` environment by default. [Use `ClientOptions`](#configure-the-client) to change this and other parameters of the network connection. ```kotlin // Create the client with a `SigningKey` from your app -val options = ClientOptions(api = ClientOptions.Api(env = XMTPEnvironment.PRODUCTION, isSecure = true)) +val options = ClientOptions(api = ClientOptions.Api(env = XMTPEnvironment.PRODUCTION, isSecure = true), dbEncryptionKey = encryptionKey, appContext = context) val client = Client().create(account = account, options = options) ``` -### Create a client from saved keys +### Create a client from saved encryptionKey -You can save your keys from the client via the `privateKeyBundle` property: +You can save your encryptionKey for the local database and build the client via address: ```kotlin // Create the client with a `SigningKey` from your app -val options = ClientOptions(api = ClientOptions.Api(env = XMTPEnvironment.PRODUCTION, isSecure = true)) -val client = Client().create(account = account, options = options) - -// Get the key bundle -val keys = client.privateKeyBundleV1 - -// Serialize the key bundle and store it somewhere safe -val serializedKeys = PrivateKeyBundleV1Builder.encodeData(v1) -``` - -Once you have those keys, you can create a new client with `Client().buildFrom()`: - -```kotlin -val keys = PrivateKeyBundleV1Builder.fromEncodedData(serializedKeys) -val client = Client().buildFrom(bundle = keys, options = options) +val options = ClientOptions(api = ClientOptions.Api(env = XMTPEnvironment.PRODUCTION, isSecure = true), dbEncryptionKey = encryptionKey, appContext = context) +val client = Client().build(address = account.address, options = options) ``` ### Configure the client @@ -107,44 +101,46 @@ val client = Client().buildFrom(bundle = keys, options = options) You can configure the client with these parameters of `Client.create`: | Parameter | Default | Description | -| ---------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| ---------- |-------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | env | `DEV` | Connect to the specified XMTP network environment. Valid values include `DEV`, `.PRODUCTION`, or `LOCAL`. For important details about working with these environments, see [XMTP `production` and `dev` network environments](#xmtp-production-and-dev-network-environments). | +| appContext | `REQUIRED` | The application context used to create and access the local database. | +| dbEncryptionKey | `REQUIRED` | A 32 ByteArray used to encrypt the local database. | +| historySyncUrl | `https://message-history.dev.ephemera.network/` | The history sync url used to specify where history can be synced from other devices on the network. | | appVersion | `undefined` | Add a client app version identifier that's included with API requests.
For example, you can use the following format: `appVersion: APP_NAME + '/' + APP_VERSION`.
Setting this value provides telemetry that shows which apps are using the XMTP client SDK. This information can help XMTP developers provide app support, especially around communicating important SDK updates, including deprecations and required upgrades. | **Configure `env`** -```kotlin -// Configure the client to use the `production` network -val options = ClientOptions(api = ClientOptions.Api(env = XMTPEnvironment.PRODUCTION, isSecure = true)) -val client = Client().create(account = account, options = options) -``` - -> **Note** -> The `apiUrl`, `keyStoreType`, `codecs`, and `maxContentSize` parameters from the XMTP client SDK for JavaScript (xmtp-js) are not yet supported. - ## Handle conversations Most of the time, when interacting with the network, you'll want to do it through `conversations`. Conversations are between two accounts. +### List all dm & group conversations + +If your app would like to handle groups and dms differently you can check whether a conversation is a dm or group for the type ```kotlin -// Create the client with a wallet from your app -val client = Client().create(account = account) val conversations = client.conversations.list() -``` -### List existing conversations +for (conversation in conversations) { + when (conversation.type) { + is Group -> // Handle group + is Dm -> // Handle dm + } +} +``` -You can get a list of all conversations that have one or more messages. +### List all groups ```kotlin -val allConversations = client.conversations.list() +val conversations = client.conversations.listGroups() +``` -for (conversation in allConversations) { - print("Saying GM to ${conversation.peerAddress}") - conversation.send(text = "gm") -} +### List all dms + +```kotlin +val conversations = client.conversations.listDms() ``` + These conversations include all conversations for a user **regardless of which app created the conversation.** This functionality provides the concept of an [interoperable inbox](https://xmtp.org/docs/concepts/interoperable-inbox), which enables a user to access all of their conversations in any app built with XMTP. ### Listen for new conversations @@ -164,12 +160,16 @@ client.conversations.stream().collect { You can create a new conversation with any Ethereum address on the XMTP network. ```kotlin -val newConversation = client.conversations.newConversation("0x3F11b27F323b62B159D2642964fa27C46C841897") +val newDm = client.conversations.newConversation("0x3F11b27F323b62B159D2642964fa27C46C841897") +``` + +```kotlin +val newGroup = client.conversations.newGroup("listOf(0x3F11b27F323b62B159D2642964fa27C46C841897)") ``` ### Send messages -To be able to send a message, the recipient must have already created a client at least once and consequently advertised their key bundle on the network. Messages are addressed using account addresses. In this example, the message payload is a plain text string. +To be able to send a message, the recipient must have already created a client at least once. Messages are addressed using account addresses. In this example, the message payload is a plain text string. ```kotlin val conversation = client.conversations.newConversation("0x3F11b27F323b62B159D2642964fa27C46C841897") @@ -183,9 +183,7 @@ To learn how to send other types of content, see [Handle different content types You can receive the complete message history in a conversation by calling `conversation.messages()` ```kotlin -for (conversation in client.conversations.list()) { - val messagesInConversation = conversation.messages() -} + conversation.messages() ``` ### List messages in a conversation with pagination @@ -193,10 +191,8 @@ for (conversation in client.conversations.list()) { It may be helpful to retrieve and process the messages in a conversation page by page. You can do this by calling `conversation.messages(limit: Int, before: Date)` which will return the specified number of messages sent before that time. ```kotlin -val conversation = client.conversations.newConversation("0x3F11b27F323b62B159D2642964fa27C46C841897") - val messages = conversation.messages(limit = 25) -val nextPage = conversation.messages(limit = 25, before = messages[0].sent) +val nextPage = conversation.messages(limit = 25, beforeNs = messages[0].sentNs) ``` ### Listen for new messages in a conversation @@ -208,8 +204,6 @@ A successfully received message (that makes it through the decoding and decrypti The flow returned by the `stream` methods is an asynchronous data stream that sequentially emits values and completes normally or with an exception. ```kotlin -val conversation = client.conversations.newConversation("0x3F11b27F323b62B159D2642964fa27C46C841897") - conversation.streamMessages().collect { if (it.senderAddress == client.address) { // This message was sent from me @@ -219,58 +213,6 @@ conversation.streamMessages().collect { } ``` -### Decode a single message - -You can decode a single `Envelope` from XMTP using the `decode` method: - -```kotlin -val conversation = client.conversations.newConversation("0x3F11b27F323b62B159D2642964fa27C46C841897") - -// Assume this function returns an Envelope that contains a message for the above conversation -val envelope = getEnvelopeFromXMTP() - -val decodedMessage = conversation.decode(envelope) -``` - -### Serialize/Deserialize conversations - -You can save a conversation object locally using its `encodedContainer` property. This returns a `ConversationContainer` object which conforms to `Codable`. - -```kotlin -// Get a conversation -val conversation = client.conversations.newConversation("0x3F11b27F323b62B159D2642964fa27C46C841897") - -// Dump it to JSON -val gson = GsonBuilder().create() -val data = gson.toJson(conversation) - -// Get it back from JSON -val containerAgain = gson.fromJson(data.toString(StandardCharsets.UTF_8), ConversationV2Export::class.java) - -// Get an actual Conversation object like we had above -val decodedConversation = containerAgain.decode(client) -decodedConversation.send(text = "hi") -``` - -### Cache conversations - -As a performance optimization, you may want to persist the list of conversations in your application outside of the SDK to speed up the first call to `client.conversations.list()`. - -The exported conversation list contains encryption keys for any V2 conversations included in the list. As such, you should treat it with the same care that you treat private keys. - -You can get a JSON serializable list of conversations by calling: - -```kotlin -val client = Client().create(wallet) -val conversations = client.conversations.export() -saveConversationsSomewhere(JSON.stringify(conversations)) -// To load the conversations in a new SDK instance you can run: - -val client = Client.create(wallet) -val conversations = JSON.parse(loadConversationsFromSomewhere()) -val client.importConversation(conversations) -``` - ## Request and respect user consent ![Feature status](https://img.shields.io/badge/Feature_status-Alpha-orange) @@ -309,16 +251,6 @@ As shown in the example above, you must provide a `contentFallback` value. Use i Beyond this, custom codecs and content types may be proposed as interoperable standards through XRCs. To learn more about the custom content type proposal process, see [XIP-5](https://github.com/xmtp/XIPs/blob/main/XIPs/xip-5-message-content-types.md). -## Compression - -Message content can be optionally compressed using the compression option. The value of the option is the name of the compression algorithm to use. Currently supported are gzip and deflate. Compression is applied to the bytes produced by the content codec. - -Content will be decompressed transparently on the receiving end. Note that Client enforces maximum content size. The default limit can be overridden through the ClientOptions. Consequently a message that would expand beyond that limit on the receiving end will fail to decode. - -```kotlin -conversation.send(text = '#'.repeat(1000), options = ClientOptions.Api(compression = EncodedContentCompression.GZIP)) -``` - ## 🏗 Breaking revisions Because `xmtp-android` is in active development, you should expect breaking revisions that might require you to adopt the latest SDK release to enable your app to continue working as expected. @@ -334,9 +266,9 @@ Older versions of the SDK will eventually be deprecated, which means: The following table provides the deprecation schedule. -| Announced | Effective | Minimum Version | Rationale | -| -------------------------------------------------------------------- | --------- | --------------- | --------- | -| There are no deprecations scheduled for `xmtp-android` at this time. | | | | +| Announced | Effective | Minimum Version | Rationale | +|------------------------|---------------|-----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| No more support for V2 | March 1, 2025 | 3.0.0 | In a move towards better security with MLS and the ability to decentralize we will be shutting down V2 and moving entirely to V3 MLS. You can see the legacy branch here: https://github.com/xmtp/xmtp-android/tree/xmtp-legacy | Bug reports, feature requests, and PRs are welcome in accordance with these [contribution guidelines](https://github.com/xmtp/xmtp-android/blob/main/CONTRIBUTING.md). 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..bbaae6a42 100644 --- a/example/src/main/java/org/xmtp/android/example/MainViewModel.kt +++ b/example/src/main/java/org/xmtp/android/example/MainViewModel.kt @@ -45,23 +45,10 @@ class MainViewModel : ViewModel() { viewModelScope.launch(Dispatchers.IO) { val listItems = mutableListOf() try { - val conversations = ClientManager.client.conversations.list(includeGroups = true) - val hmacKeysResult = ClientManager.client.conversations.getHmacKeys() + val conversations = ClientManager.client.conversations.list() val subscriptions: MutableList = conversations.map { - val hmacKeys = hmacKeysResult.hmacKeysMap - val result = hmacKeys[it.topic]?.valuesList?.map { hmacKey -> - Service.Subscription.HmacKey.newBuilder().also { sub_key -> - sub_key.key = hmacKey.hmacKey - sub_key.thirtyDayPeriodsSinceEpoch = hmacKey.thirtyDayPeriodsSinceEpoch - }.build() - } - Service.Subscription.newBuilder().also { sub -> - if (!result.isNullOrEmpty()) { - sub.addAllHmacKeys(result) - } sub.topic = it.topic - sub.isSilent = it.version == Conversation.Version.V1 }.build() }.toMutableList() @@ -105,7 +92,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 7696b4d92..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 ) @@ -214,7 +98,7 @@ class ClientTest { runBlocking { client.conversations.newGroup(listOf(client2.address)) - client.conversations.syncGroups() + client.conversations.syncConversations() assertEquals(client.conversations.listGroups().size, 1) } @@ -226,20 +110,19 @@ class ClientTest { account = fakeWallet, options = ClientOptions( ClientOptions.Api(XMTPEnvironment.LOCAL, false), - enableV3 = true, appContext = context, dbEncryptionKey = key ) ) } runBlocking { - client.conversations.syncGroups() + client.conversations.syncConversations() assertEquals(client.conversations.listGroups().size, 0) } } @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 ) @@ -422,7 +216,7 @@ class ClientTest { runBlocking { boClient.conversations.newGroup(listOf(alixClient.address)) - boClient.conversations.syncGroups() + boClient.conversations.syncConversations() } runBlocking { @@ -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 1b8ecdf30..b25d160af 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/CodecTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/CodecTest.kt @@ -4,21 +4,13 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.protobuf.kotlin.toByteStringUtf8 import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith -import org.xmtp.android.library.Crypto.Companion.verifyHmacSignature import org.xmtp.android.library.codecs.ContentCodec import org.xmtp.android.library.codecs.ContentTypeId import org.xmtp.android.library.codecs.ContentTypeIdBuilder import org.xmtp.android.library.codecs.EncodedContent -import org.xmtp.android.library.codecs.TextCodec -import org.xmtp.android.library.messages.InvitationV1ContextBuilder -import org.xmtp.android.library.messages.MessageV2Builder -import org.xmtp.android.library.messages.PrivateKeyBuilder import org.xmtp.android.library.messages.walletAddress -import java.time.Instant data class NumberCodec( override var contentType: ContentTypeId = ContentTypeIdBuilder.builderFromAuthorityId( @@ -57,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( @@ -68,106 +60,11 @@ 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) } } - - @Test - @Ignore("Flaky: CI") - fun testCanGetPushInfoBeforeDecoded() { - val codec = NumberCodec() - Client.register(codec = codec) - val fixtures = fixtures() - val aliceClient = fixtures.aliceClient - val aliceConversation = runBlocking { - aliceClient.conversations.newConversation(fixtures.bob.walletAddress) - } - runBlocking { - aliceConversation.send( - content = 3.14, - options = SendOptions(contentType = codec.contentType), - ) - } - val messages = runBlocking { aliceConversation.messages() } - assert(messages.isNotEmpty()) - - val message = MessageV2Builder.buildEncode( - client = aliceClient, - encodedContent = messages[0].encodedContent, - topic = aliceConversation.topic, - keyMaterial = aliceConversation.keyMaterial!!, - codec = codec, - ) - - assertEquals(false, message.shouldPush) - assertEquals(true, message.senderHmac?.isNotEmpty()) - } - - @Test - fun testReturnsAllHMACKeys() { - val alix = PrivateKeyBuilder() - val clientOptions = - ClientOptions(api = ClientOptions.Api(env = XMTPEnvironment.LOCAL, isSecure = false)) - val alixClient = runBlocking { Client().create(alix, clientOptions) } - val conversations = mutableListOf() - repeat(5) { - val account = PrivateKeyBuilder() - val client = runBlocking { Client().create(account, clientOptions) } - runBlocking { - conversations.add( - alixClient.conversations.newConversation( - client.address, - context = InvitationV1ContextBuilder.buildFromConversation(conversationId = "hi") - ) - ) - } - } - - val thirtyDayPeriodsSinceEpoch = Instant.now().epochSecond / 60 / 60 / 24 / 30 - - val hmacKeys = alixClient.conversations.getHmacKeys() - - val topics = hmacKeys.hmacKeysMap.keys - conversations.forEach { convo -> - assertTrue(topics.contains(convo.topic)) - } - - val topicHmacs = mutableMapOf() - val headerBytes = ByteArray(10) - - conversations.forEach { conversation -> - val topic = conversation.topic - val payload = TextCodec().encode(content = "Hello, world!") - - val message = MessageV2Builder.buildEncode( - client = alixClient, - encodedContent = payload, - topic = topic, - keyMaterial = headerBytes, - codec = TextCodec() - ) - - val keyMaterial = conversation.keyMaterial - val info = "$thirtyDayPeriodsSinceEpoch-${alixClient.address}" - val key = Crypto.deriveKey(keyMaterial!!, ByteArray(0), info.toByteArray()) - val hmac = Crypto.calculateMac(key, headerBytes) - - topicHmacs[topic] = hmac - } - - hmacKeys.hmacKeysMap.forEach { (topic, hmacData) -> - hmacData.valuesList.forEachIndexed { idx, hmacKeyThirtyDayPeriod -> - val valid = verifyHmacSignature( - hmacKeyThirtyDayPeriod.hmacKey.toByteArray(), - topicHmacs[topic]!!, - headerBytes - ) - assertTrue(valid == (idx == 1)) - } - } - } } 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/ConversationTest.kt b/library/src/androidTest/java/org/xmtp/android/library/ConversationTest.kt deleted file mode 100644 index c8b8a6afb..000000000 --- a/library/src/androidTest/java/org/xmtp/android/library/ConversationTest.kt +++ /dev/null @@ -1,958 +0,0 @@ -package org.xmtp.android.library - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.google.protobuf.kotlin.toByteString -import com.google.protobuf.kotlin.toByteStringUtf8 -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertThrows -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Ignore -import org.junit.Test -import org.junit.runner.RunWith -import org.web3j.crypto.Hash -import org.xmtp.android.library.codecs.TextCodec -import org.xmtp.android.library.messages.EnvelopeBuilder -import org.xmtp.android.library.messages.InvitationV1 -import org.xmtp.android.library.messages.InvitationV1ContextBuilder -import org.xmtp.android.library.messages.MessageBuilder -import org.xmtp.android.library.messages.MessageHeaderV2Builder -import org.xmtp.android.library.messages.MessageV1Builder -import org.xmtp.android.library.messages.MessageV2Builder -import org.xmtp.android.library.messages.Pagination -import org.xmtp.android.library.messages.PrivateKey -import org.xmtp.android.library.messages.PrivateKeyBuilder -import org.xmtp.android.library.messages.SealedInvitationBuilder -import org.xmtp.android.library.messages.SealedInvitationHeaderV1 -import org.xmtp.android.library.messages.SignedContentBuilder -import org.xmtp.android.library.messages.Topic -import org.xmtp.android.library.messages.createDeterministic -import org.xmtp.android.library.messages.getPublicKeyBundle -import org.xmtp.android.library.messages.header -import org.xmtp.android.library.messages.recoverWalletSignerPublicKey -import org.xmtp.android.library.messages.sign -import org.xmtp.android.library.messages.toPublicKeyBundle -import org.xmtp.android.library.messages.toSignedPublicKeyBundle -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.contents.Invitation -import org.xmtp.proto.message.contents.Invitation.InvitationV1.Context -import java.nio.charset.StandardCharsets -import java.util.Date -import kotlin.time.Duration.Companion.nanoseconds -import kotlin.time.DurationUnit - -@RunWith(AndroidJUnit4::class) -class ConversationTest { - lateinit var aliceWallet: PrivateKeyBuilder - lateinit var bobWallet: PrivateKeyBuilder - lateinit var alice: PrivateKey - lateinit var aliceClient: Client - lateinit var bob: PrivateKey - lateinit var bobClient: Client - lateinit var fixtures: Fixtures - - @Before - fun setUp() { - fixtures = fixtures() - aliceWallet = fixtures.aliceAccount - alice = fixtures.alice - bobWallet = fixtures.bobAccount - bob = fixtures.bob - aliceClient = fixtures.aliceClient - bobClient = fixtures.bobClient - } - - @Test - fun testDoesNotAllowConversationWithSelf() { - val client = runBlocking { Client().create(account = aliceWallet) } - assertThrows("Recipient is sender", XMTPException::class.java) { - runBlocking { client.conversations.newConversation(alice.walletAddress) } - } - } - - @Test - fun testCanFindExistingV1Conversation() { - val encoder = TextCodec() - val encodedContent = encoder.encode(content = "hi alice") - // Get a date that's roughly two weeks ago to test with - val someTimeAgo = Date(System.currentTimeMillis() - 2_000_000) - val messageV1 = MessageV1Builder.buildEncode( - sender = bobClient.privateKeyBundleV1!!, - recipient = aliceClient.privateKeyBundleV1?.toPublicKeyBundle()!!, - message = encodedContent.toByteArray(), - timestamp = someTimeAgo, - ) - // Overwrite contact as legacy - runBlocking { - bobClient.publishUserContact(legacy = true) - aliceClient.publishUserContact(legacy = true) - } - runBlocking { - bobClient.publish( - envelopes = listOf( - EnvelopeBuilder.buildFromTopic( - topic = Topic.userIntro(bob.walletAddress), - timestamp = someTimeAgo, - message = MessageBuilder.buildFromMessageV1(v1 = messageV1).toByteArray(), - ), - EnvelopeBuilder.buildFromTopic( - topic = Topic.userIntro(alice.walletAddress), - timestamp = someTimeAgo, - message = MessageBuilder.buildFromMessageV1(v1 = messageV1).toByteArray(), - ), - EnvelopeBuilder.buildFromTopic( - topic = Topic.directMessageV1( - bob.walletAddress, - alice.walletAddress, - ), - timestamp = someTimeAgo, - message = MessageBuilder.buildFromMessageV1(v1 = messageV1).toByteArray(), - ), - ), - ) - } - var conversation = - runBlocking { aliceClient.conversations.newConversation(bob.walletAddress) } - assertEquals(conversation.peerAddress, bob.walletAddress) - assertEquals(conversation.createdAt, someTimeAgo) - conversation = runBlocking { bobClient.conversations.newConversation(alice.walletAddress) } - assertEquals(conversation.peerAddress, alice.walletAddress) - assertEquals(conversation.createdAt, someTimeAgo) - } - - @Test - fun testCanLoadV1Messages() { - // Overwrite contact as legacy so we can get v1 - fixtures.publishLegacyContact(client = bobClient) - fixtures.publishLegacyContact(client = aliceClient) - val bobConversation = - runBlocking { bobClient.conversations.newConversation(aliceWallet.address) } - val aliceConversation = - runBlocking { aliceClient.conversations.newConversation(bobWallet.address) } - - runBlocking { bobConversation.send(content = "hey alice") } - runBlocking { bobConversation.send(content = "hey alice again") } - val messages = runBlocking { aliceConversation.messages() } - assertEquals(2, messages.size) - assertEquals("hey alice", messages[1].body) - assertEquals(bobWallet.address, messages[1].senderAddress) - } - - @Test - fun testCanLoadV2Messages() { - val bobConversation = runBlocking { - bobClient.conversations.newConversation( - aliceWallet.address, - InvitationV1ContextBuilder.buildFromConversation("hi"), - ) - } - - val aliceConversation = runBlocking { - aliceClient.conversations.newConversation( - bobWallet.address, - InvitationV1ContextBuilder.buildFromConversation("hi"), - ) - } - runBlocking { bobConversation.send(content = "hey alice") } - val messages = runBlocking { aliceConversation.messages() } - assertEquals(1, messages.size) - assertEquals("hey alice", messages[0].body) - assertEquals(bobWallet.address, messages[0].senderAddress) - } - - @Test - fun testVerifiesV2MessageSignature() { - val aliceConversation = runBlocking { - aliceClient.conversations.newConversation( - bobWallet.address, - context = InvitationV1ContextBuilder.buildFromConversation(conversationId = "hi"), - ) - } - - val codec = TextCodec() - val originalContent = codec.encode(content = "hello") - val tamperedContent = codec.encode(content = "this is a fake") - val originalPayload = originalContent.toByteArray() - val tamperedPayload = tamperedContent.toByteArray() - val date = Date() - val header = MessageHeaderV2Builder.buildFromTopic(aliceConversation.topic, created = date) - val headerBytes = header.toByteArray() - val digest = Hash.sha256(headerBytes + tamperedPayload) - val preKey = aliceClient.keys?.preKeysList?.get(0) - val signature = preKey?.sign(digest) - val bundle = aliceClient.privateKeyBundleV1?.toV2()?.getPublicKeyBundle() - val signedContent = SignedContentBuilder.builderFromPayload( - payload = originalPayload, - sender = bundle, - signature = signature, - ) - val signedBytes = signedContent.toByteArray() - val ciphertext = Crypto.encrypt( - aliceConversation.keyMaterial!!, - signedBytes, - additionalData = headerBytes, - ) - val thirtyDayPeriodsSinceEpoch = - (Date().time / 1000 / 60 / 60 / 24 / 30).toInt() - val info = "$thirtyDayPeriodsSinceEpoch-${aliceClient.address}" - val infoEncoded = info.toByteStringUtf8().toByteArray() - val senderHmacGenerated = - Crypto.calculateMac( - Crypto.deriveKey(aliceConversation.keyMaterial!!, ByteArray(0), infoEncoded), - headerBytes - ) - val tamperedMessage = - MessageV2Builder.buildFromCipherText( - headerBytes = headerBytes, - ciphertext = ciphertext, - senderHmac = senderHmacGenerated, - shouldPush = codec.shouldPush("this is a fake"), - ) - val tamperedEnvelope = EnvelopeBuilder.buildFromString( - topic = aliceConversation.topic, - timestamp = Date(), - message = MessageBuilder.buildFromMessageV2(v2 = tamperedMessage.messageV2) - .toByteArray(), - ) - runBlocking { aliceClient.publish(envelopes = listOf(tamperedEnvelope)) } - val bobConversation = runBlocking { - bobClient.conversations.newConversation( - aliceWallet.address, - InvitationV1ContextBuilder.buildFromConversation("hi"), - ) - } - assertThrows("Invalid signature", XMTPException::class.java) { - bobConversation.decode(tamperedEnvelope) - } - // But it should be properly discarded from the message listing. - runBlocking { - assertEquals(0, bobConversation.messages().size) - } - } - - @Test - fun testCanSendGzipCompressedV1Messages() { - fixtures.publishLegacyContact(client = bobClient) - fixtures.publishLegacyContact(client = aliceClient) - val bobConversation = - runBlocking { bobClient.conversations.newConversation(aliceWallet.address) } - val aliceConversation = - runBlocking { aliceClient.conversations.newConversation(bobWallet.address) } - runBlocking { - bobConversation.send( - text = MutableList(1000) { "A" }.toString(), - sendOptions = SendOptions(compression = EncodedContentCompression.GZIP), - ) - } - val messages = runBlocking { aliceConversation.messages() } - assertEquals(1, messages.size) - assertEquals(MutableList(1000) { "A" }.toString(), messages[0].content()) - } - - @Test - fun testCanSendDeflateCompressedV1Messages() { - fixtures.publishLegacyContact(client = bobClient) - fixtures.publishLegacyContact(client = aliceClient) - val bobConversation = - runBlocking { bobClient.conversations.newConversation(aliceWallet.address) } - val aliceConversation = - runBlocking { aliceClient.conversations.newConversation(bobWallet.address) } - runBlocking { - bobConversation.send( - content = MutableList(1000) { "A" }.toString(), - options = SendOptions(compression = EncodedContentCompression.DEFLATE), - ) - } - val messages = runBlocking { aliceConversation.messages() } - assertEquals(1, messages.size) - assertEquals(MutableList(1000) { "A" }.toString(), messages[0].content()) - } - - @Test - fun testCanSendGzipCompressedV2Messages() { - val bobConversation = runBlocking { - bobClient.conversations.newConversation( - aliceWallet.address, - InvitationV1ContextBuilder.buildFromConversation(conversationId = "hi"), - ) - } - val aliceConversation = runBlocking { - aliceClient.conversations.newConversation( - bobWallet.address, - InvitationV1ContextBuilder.buildFromConversation(conversationId = "hi"), - ) - } - runBlocking { - bobConversation.send( - text = MutableList(1000) { "A" }.toString(), - sendOptions = SendOptions(compression = EncodedContentCompression.GZIP), - ) - } - val messages = runBlocking { aliceConversation.messages() } - assertEquals(1, messages.size) - assertEquals(MutableList(1000) { "A" }.toString(), messages[0].body) - assertEquals(bobWallet.address, messages[0].senderAddress) - } - - @Test - fun testCanSendDeflateCompressedV2Messages() { - val bobConversation = runBlocking { - bobClient.conversations.newConversation( - aliceWallet.address, - InvitationV1ContextBuilder.buildFromConversation(conversationId = "hi"), - ) - } - val aliceConversation = runBlocking { - aliceClient.conversations.newConversation( - bobWallet.address, - InvitationV1ContextBuilder.buildFromConversation(conversationId = "hi"), - ) - } - runBlocking { - bobConversation.send( - content = MutableList(1000) { "A" }.toString(), - options = SendOptions(compression = EncodedContentCompression.DEFLATE), - ) - } - val messages = runBlocking { aliceConversation.messages() } - assertEquals(1, messages.size) - assertEquals(MutableList(1000) { "A" }.toString(), messages[0].body) - assertEquals(bobWallet.address, messages[0].senderAddress) - } - - @Test - fun testEndToEndConversation() { - val fakeContactWallet = PrivateKeyBuilder() - val fakeContactClient = runBlocking { Client().create(account = fakeContactWallet) } - runBlocking { fakeContactClient.publishUserContact() } - val fakeWallet = PrivateKeyBuilder() - val client = runBlocking { Client().create(account = fakeWallet) } - val contact = client.getUserContact(peerAddress = fakeContactWallet.address)!! - assertEquals(contact.walletAddress, fakeContactWallet.address) - val created = Date() - val invitationContext = Invitation.InvitationV1.Context.newBuilder().also { - it.conversationId = "https://example.com/1" - }.build() - val invitationv1 = InvitationV1.newBuilder().build().createDeterministic( - sender = client.keys, - recipient = fakeContactClient.keys.getPublicKeyBundle(), - context = invitationContext, - ) - val senderBundle = client.privateKeyBundleV1?.toV2() - assertEquals( - senderBundle?.identityKey?.publicKey?.recoverWalletSignerPublicKey()?.walletAddress, - fakeWallet.address, - ) - val invitation = SealedInvitationBuilder.buildFromV1( - sender = client.privateKeyBundleV1!!.toV2(), - recipient = contact.toSignedPublicKeyBundle(), - created = created, - invitation = invitationv1, - ) - val inviteHeader = invitation.v1.header - assertEquals(inviteHeader.sender.walletAddress, fakeWallet.address) - assertEquals(inviteHeader.recipient.walletAddress, fakeContactWallet.address) - val header = SealedInvitationHeaderV1.parseFrom(invitation.v1.headerBytes) - val conversation = - ConversationV2.create(client = client, invitation = invitationv1, header = header) - assertEquals(fakeContactWallet.address, conversation.peerAddress) - - runBlocking { conversation.send(content = "hello world") } - - val conversationList = runBlocking { client.conversations.list() } - val recipientConversation = conversationList.lastOrNull() - - val messages = runBlocking { recipientConversation?.messages() } - val message = messages?.firstOrNull() - if (message != null) { - assertEquals("hello world", message.body) - } - } - - @Test - @Ignore("Rust seems to be Flaky with V1") - fun testCanPaginateV1Messages() { - // Overwrite contact as legacy so we can get v1 - fixtures.publishLegacyContact(client = bobClient) - fixtures.publishLegacyContact(client = aliceClient) - val bobConversation = - runBlocking { bobClient.conversations.newConversation(alice.walletAddress) } - val aliceConversation = - runBlocking { aliceClient.conversations.newConversation(bob.walletAddress) } - - val date = Date() - date.time = date.time - 1000000 - runBlocking { bobConversation.send(text = "hey alice 1", sentAt = date) } - runBlocking { bobConversation.send(text = "hey alice 2") } - runBlocking { bobConversation.send(text = "hey alice 3") } - val messages = runBlocking { aliceConversation.messages(limit = 1) } - assertEquals(1, messages.size) - assertEquals("hey alice 3", messages[0].body) - } - - @Test - fun testCanPaginateV2Messages() { - val bobConversation = runBlocking { - bobClient.conversations.newConversation( - alice.walletAddress, - context = InvitationV1ContextBuilder.buildFromConversation("hi"), - ) - } - val aliceConversation = runBlocking { - aliceClient.conversations.newConversation( - bob.walletAddress, - context = InvitationV1ContextBuilder.buildFromConversation("hi"), - ) - } - val date = Date() - date.time = date.time - 1000000 - runBlocking { - bobConversation.send(text = "hey alice 1", sentAt = date) - bobConversation.send(text = "hey alice 2") - bobConversation.send(text = "hey alice 3") - val messages = aliceConversation.messages(limit = 1) - assertEquals(1, messages.size) - assertEquals("hey alice 3", messages[0].body) - val messages2 = aliceConversation.messages( - limit = 1, - afterNs = date.time.nanoseconds.toLong( - DurationUnit.NANOSECONDS - ) - ) - assertEquals(1, messages2.size) - assertEquals("hey alice 3", messages2[0].body) - val messagesAsc = - aliceConversation.messages(direction = MessageApiOuterClass.SortDirection.SORT_DIRECTION_ASCENDING) - assertEquals("hey alice 1", messagesAsc[0].body) - val messagesDesc = - aliceConversation.messages(direction = MessageApiOuterClass.SortDirection.SORT_DIRECTION_DESCENDING) - assertEquals("hey alice 3", messagesDesc[0].body) - } - } - - @Test - fun testListBatchMessages() { - val bobConversation = - runBlocking { aliceClient.conversations.newConversation(bob.walletAddress) } - val steveConversation = runBlocking { - aliceClient.conversations.newConversation(fixtures.caro.walletAddress) - } - - runBlocking { bobConversation.send(text = "hey alice 1") } - runBlocking { bobConversation.send(text = "hey alice 2") } - runBlocking { steveConversation.send(text = "hey alice 3") } - val messages = runBlocking { - aliceClient.conversations.listBatchMessages( - listOf( - Pair(steveConversation.topic, null), - Pair(bobConversation.topic, null), - ), - ) - } - val isSteveOrBobConversation = { topic: String -> - (topic.equals(steveConversation.topic) || topic.equals(bobConversation.topic)) - } - assertEquals(3, messages.size) - assertTrue( - "isSteveOrBobConversation message 0", - isSteveOrBobConversation(messages[0].topic) - ) - assertTrue( - "isSteveOrBobConversation message 1", - isSteveOrBobConversation(messages[1].topic) - ) - assertTrue( - "isSteveOrBobConversation message 2", - isSteveOrBobConversation(messages[2].topic) - ) - } - - @Test - fun testListBatchDecryptedMessages() { - val bobConversation = - runBlocking { aliceClient.conversations.newConversation(bob.walletAddress) } - val steveConversation = runBlocking { - aliceClient.conversations.newConversation(fixtures.caro.walletAddress) - } - - runBlocking { - bobConversation.send(text = "hey alice 1") - bobConversation.send(text = "hey alice 2") - steveConversation.send(text = "hey alice 3") - } - val messages = runBlocking { - aliceClient.conversations.listBatchDecryptedMessages( - listOf( - Pair(steveConversation.topic, null), - Pair(bobConversation.topic, null), - ), - ) - } - assertEquals(3, messages.size) - } - - @Test - fun testListBatchMessagesWithPagination() { - val bobConversation = - runBlocking { aliceClient.conversations.newConversation(bob.walletAddress) } - val steveConversation = - runBlocking { aliceClient.conversations.newConversation(fixtures.caro.walletAddress) } - - runBlocking { - bobConversation.send(text = "hey alice 1 bob") - steveConversation.send(text = "hey alice 1 steve") - } - - Thread.sleep(100) - val date = Date() - - runBlocking { - bobConversation.send(text = "hey alice 2 bob") - bobConversation.send(text = "hey alice 3 bob") - steveConversation.send(text = "hey alice 2 steve") - steveConversation.send(text = "hey alice 3 steve") - } - - val messages = runBlocking { - aliceClient.conversations.listBatchMessages( - listOf( - Pair(steveConversation.topic, Pagination(after = date)), - Pair(bobConversation.topic, Pagination(after = date)), - ), - ) - } - - assertEquals(4, messages.size) - } - - @Test - fun testImportV1ConversationFromJS() { - val jsExportJSONData = - (""" { "version": "v1", "peerAddress": "0x5DAc8E2B64b8523C11AF3e5A2E087c2EA9003f14", "createdAt": "2022-09-20T09:32:50.329Z" } """).toByteArray( - StandardCharsets.UTF_8, - ) - val conversation = aliceClient.importConversation(jsExportJSONData) - assertEquals(conversation.peerAddress, "0x5DAc8E2B64b8523C11AF3e5A2E087c2EA9003f14") - } - - @Test - fun testImportV2ConversationFromJS() { - val jsExportJSONData = - (""" {"version":"v2","topic":"/xmtp/0/m-2SkdN5Qa0ZmiFI5t3RFbfwIS-OLv5jusqndeenTLvNg/proto","keyMaterial":"ATA1L0O2aTxHmskmlGKCudqfGqwA1H+bad3W/GpGOr8=","peerAddress":"0x436D906d1339fC4E951769b1699051f020373D04","createdAt":"2023-01-26T22:58:45.068Z","context":{"conversationId":"pat/messageid","metadata":{}}} """).toByteArray( - StandardCharsets.UTF_8, - ) - val conversation = aliceClient.importConversation(jsExportJSONData) - assertEquals(conversation.peerAddress, "0x436D906d1339fC4E951769b1699051f020373D04") - } - - @Test - fun testImportV2ConversationWithNoContextFromJS() { - val jsExportJSONData = - (""" {"version":"v2","topic":"/xmtp/0/m-2SkdN5Qa0ZmiFI5t3RFbfwIS-OLv5jusqndeenTLvNg/proto","keyMaterial":"ATA1L0O2aTxHmskmlGKCudqfGqwA1H+bad3W/GpGOr8=","peerAddress":"0x436D906d1339fC4E951769b1699051f020373D04","createdAt":"2023-01-26T22:58:45.068Z"} """).toByteArray( - StandardCharsets.UTF_8, - ) - val conversation = aliceClient.importConversation(jsExportJSONData) - assertEquals(conversation.peerAddress, "0x436D906d1339fC4E951769b1699051f020373D04") - } - - @Test - fun testCanStreamConversationsV2() { - val allMessages = mutableListOf() - - val job = CoroutineScope(Dispatchers.IO).launch { - try { - bobClient.conversations.stream() - .collect { message -> - allMessages.add(message.topic) - } - } catch (e: Exception) { - } - } - Thread.sleep(2500) - - runBlocking { - bobClient.conversations.newConversation(alice.walletAddress) - } - - Thread.sleep(1000) - - assertEquals(1, allMessages.size) - - job.cancel() - } - - @Test - fun testStreamingMessagesFromV1Conversation() { - // Overwrite contact as legacy - fixtures.publishLegacyContact(client = bobClient) - fixtures.publishLegacyContact(client = aliceClient) - val conversation = - runBlocking { aliceClient.conversations.newConversation(bob.walletAddress) } - val allMessages = mutableListOf() - - val job = CoroutineScope(Dispatchers.IO).launch { - try { - conversation.streamMessages().collect { message -> - allMessages.add(message) - } - } catch (e: Exception) { - } - } - Thread.sleep(2500) - - for (i in 0 until 5) { - runBlocking { conversation.send(text = "Message $i") } - Thread.sleep(1000) - } - - assertEquals(allMessages.size, 5) - job.cancel() - } - - @Test - fun testStreamingMessagesFromV2Conversations() { - val conversation = - runBlocking { aliceClient.conversations.newConversation(bob.walletAddress) } - val allMessages = mutableListOf() - - val job = CoroutineScope(Dispatchers.IO).launch { - try { - conversation.streamMessages().collect { message -> - allMessages.add(message) - } - } catch (e: Exception) { - } - } - Thread.sleep(2500) - - for (i in 0 until 5) { - runBlocking { conversation.send(text = "Message $i") } - Thread.sleep(1000) - } - - assertEquals(allMessages.size, 5) - job.cancel() - } - - @Test - fun testV2RejectsSpoofedContactBundles() { - val topic = "/xmtp/0/m-Gdb7oj5nNdfZ3MJFLAcS4WTABgr6al1hePy6JV1-QUE/proto" - val envelopeMessage = - com.google.crypto.tink.subtle.Base64.decode("Er0ECkcIwNruhKLgkKUXEjsveG10cC8wL20tR2RiN29qNW5OZGZaM01KRkxBY1M0V1RBQmdyNmFsMWhlUHk2SlYxLVFVRS9wcm90bxLxAwruAwognstLoG6LWgiBRsWuBOt+tYNJz+CqCj9zq6hYymLoak8SDFsVSy+cVAII0/r3sxq7A/GCOrVtKH6J+4ggfUuI5lDkFPJ8G5DHlysCfRyFMcQDIG/2SFUqSILAlpTNbeTC9eSI2hUjcnlpH9+ncFcBu8StGfmilVGfiADru2fGdThiQ+VYturqLIJQXCHO2DkvbbUOg9xI66E4Hj41R9vE8yRGeZ/eRGRLRm06HftwSQgzAYf2AukbvjNx/k+xCMqti49Qtv9AjzxVnwttLiA/9O+GDcOsiB1RQzbZZzaDjQ/nLDTF6K4vKI4rS9QwzTJqnoCdp0SbMZFf+KVZpq3VWnMGkMxLW5Fr6gMvKny1e1LAtUJSIclI/1xPXu5nsKd4IyzGb2ZQFXFQ/BVL9Z4CeOZTsjZLGTOGS75xzzGHDtKohcl79+0lgIhAuSWSLDa2+o2OYT0fAjChp+qqxXcisAyrD5FB6c9spXKfoDZsqMV/bnCg3+udIuNtk7zBk7jdTDMkofEtE3hyIm8d3ycmxKYOakDPqeo+Nk1hQ0ogxI8Z7cEoS2ovi9+rGBMwREzltUkTVR3BKvgV2EOADxxTWo7y8WRwWxQ+O6mYPACsiFNqjX5Nvah5lRjihphQldJfyVOG8Rgf4UwkFxmI") - val keyMaterial = - com.google.crypto.tink.subtle.Base64.decode("R0BBM5OPftNEuavH/991IKyJ1UqsgdEG4SrdxlIG2ZY=") - - val conversation = ConversationV2( - topic = topic, - keyMaterial = keyMaterial, - context = Context.newBuilder().build(), - peerAddress = "0x2f25e33D7146602Ec08D43c1D6B1b65fc151A677", - client = aliceClient, - header = Invitation.SealedInvitationHeaderV1.newBuilder().build(), - ) - val envelope = EnvelopeBuilder.buildFromString( - topic = topic, - timestamp = Date(), - message = envelopeMessage, - ) - assertThrows("pre-key not signed by identity key", XMTPException::class.java) { - conversation.decodeEnvelope(envelope) - } - } - - @Test - fun testCanPrepareV1Message() { - // Publish legacy contacts so we can get v1 conversations - fixtures.publishLegacyContact(client = bobClient) - fixtures.publishLegacyContact(client = aliceClient) - val conversation = - runBlocking { aliceClient.conversations.newConversation(bob.walletAddress) } - assertEquals(conversation.version, Conversation.Version.V1) - val preparedMessage = conversation.prepareMessage(content = "hi") - val messageID = preparedMessage.messageId - runBlocking { conversation.send(prepared = preparedMessage) } - val messages = runBlocking { conversation.messages() } - val message = messages[0] - assertEquals("hi", message.body) - assertEquals(message.id, messageID) - } - - @Test - fun testCanPrepareV2Message() { - val conversation = - runBlocking { aliceClient.conversations.newConversation(bob.walletAddress) } - val preparedMessage = conversation.prepareMessage(content = "hi") - val messageID = preparedMessage.messageId - runBlocking { conversation.send(prepared = preparedMessage) } - val messages = runBlocking { conversation.messages() } - val message = messages[0] - assertEquals("hi", message.body) - assertEquals(message.id, messageID) - } - - @Test - fun testCanSendPreparedMessageWithoutConversation() { - val conversation = - runBlocking { aliceClient.conversations.newConversation(bob.walletAddress) } - val preparedMessage = conversation.prepareMessage(content = "hi") - val messageID = preparedMessage.messageId - - // This does not need the `conversation` to `.publish` the message. - // This simulates a background task publishing all pending messages upon connection. - runBlocking { aliceClient.publish(envelopes = preparedMessage.envelopes) } - - val messages = runBlocking { conversation.messages() } - val message = messages[0] - assertEquals("hi", message.body) - assertEquals(message.id, messageID) - } - - @Test - fun testFetchConversation() { - // 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)) } - runBlocking { - val conversations = client.conversations.list() - assertEquals(1, conversations.size) - val topic = conversations[0].topic - val conversation = client.fetchConversation(topic) - assertEquals(conversations[0].topic, conversation?.topic) - assertEquals(conversations[0].peerAddress, conversation?.peerAddress) - - val noConversation = client.fetchConversation("invalid_topic") - assertEquals(null, noConversation) - } - } - - @Test - fun testCanSendEncodedContentV1Message() { - fixtures.publishLegacyContact(client = bobClient) - fixtures.publishLegacyContact(client = aliceClient) - val bobConversation = - runBlocking { bobClient.conversations.newConversation(aliceWallet.address) } - val aliceConversation = - runBlocking { aliceClient.conversations.newConversation(bobWallet.address) } - val encodedContent = TextCodec().encode(content = "hi") - runBlocking { bobConversation.send(encodedContent = encodedContent) } - val messages = runBlocking { aliceConversation.messages() } - assertEquals(1, messages.size) - assertEquals("hi", messages[0].content()) - } - - @Test - fun testCanSendEncodedContentV2Message() { - val bobConversation = - runBlocking { bobClient.conversations.newConversation(aliceWallet.address) } - val encodedContent = TextCodec().encode(content = "hi") - runBlocking { bobConversation.send(encodedContent = encodedContent) } - val messages = runBlocking { bobConversation.messages() } - assertEquals(1, messages.size) - assertEquals("hi", messages[0].content()) - } - - @Test - @Ignore("TODO: Fix Flaky Test") - fun testCanHaveConsentState() { - runBlocking { - val bobConversation = - runBlocking { bobClient.conversations.newConversation(alice.walletAddress, null) } - Thread.sleep(1000) - val isAllowed = bobConversation.consentState() == ConsentState.ALLOWED - // Conversations you start should start as allowed - assertTrue("Bob convo should be allowed", isAllowed) - assertTrue( - "Bob contacts should be allowed", - bobClient.contacts.isAllowed(alice.walletAddress) - ) - - runBlocking { - bobClient.contacts.deny(listOf(alice.walletAddress)) - bobClient.contacts.refreshConsentList() - } - val isDenied = bobConversation.consentState() == ConsentState.DENIED - assertEquals(bobClient.contacts.consentList.entries.size, 1) - assertTrue("Bob Conversation should be denied", isDenied) - - val aliceConversation = runBlocking { aliceClient.conversations.list()[0] } - val isUnknown = aliceConversation.consentState() == ConsentState.UNKNOWN - - // Conversations started with you should start as unknown - assertTrue("Alice conversation should be unknown", isUnknown) - - runBlocking { aliceClient.contacts.allow(listOf(bob.walletAddress)) } - - val isBobAllowed = aliceConversation.consentState() == ConsentState.ALLOWED - assertTrue("Bob should be allowed from alice conversation", isBobAllowed) - - val aliceClient2 = runBlocking { Client().create(aliceWallet) } - val aliceConversation2 = runBlocking { aliceClient2.conversations.list()[0] } - - runBlocking { aliceClient2.contacts.refreshConsentList() } - - // Allow state should sync across clients - val isBobAllowed2 = aliceConversation2.consentState() == ConsentState.ALLOWED - - assertTrue("Bob should be allowed from conversation 2", isBobAllowed2) - } - } - - @Test - @Ignore("TODO: Fix Flaky Test") - fun testCanHaveImplicitConsentOnMessageSend() { - runBlocking { - val bobConversation = bobClient.conversations.newConversation(alice.walletAddress, null) - Thread.sleep(1000) - val isAllowed = bobConversation.consentState() == ConsentState.ALLOWED - - // Conversations you start should start as allowed - assertTrue("Bob convo should be allowed", isAllowed) - - val aliceConversation = aliceClient.conversations.list()[0] - val isUnknown = aliceConversation.consentState() == ConsentState.UNKNOWN - - // Conversations you receive should start as unknown - assertTrue("Alice convo should be unknown", isUnknown) - - aliceConversation.send(content = "hey bob") - aliceClient.contacts.refreshConsentList() - val isNowAllowed = aliceConversation.consentState() == ConsentState.ALLOWED - - // Conversations you send a message to get marked as allowed - assertTrue("Should now be allowed", isNowAllowed) - } - } - - @Test - @Ignore("TODO: Fix Flaky Test") - fun testCanPublishMultipleAddressConsentState() { - runBlocking { - val bobConversation = bobClient.conversations.newConversation(alice.walletAddress) - val caroConversation = - bobClient.conversations.newConversation(fixtures.caro.walletAddress) - bobClient.contacts.refreshConsentList() - Thread.sleep(1000) - assertEquals(bobClient.contacts.consentList.entries.size, 2) - assertTrue( - "Bob convo should be allowed", - bobConversation.consentState() == ConsentState.ALLOWED - ) - assertTrue( - "Caro convo should be allowed", - caroConversation.consentState() == ConsentState.ALLOWED - ) - bobClient.contacts.deny(listOf(alice.walletAddress, fixtures.caro.walletAddress)) - assertEquals(bobClient.contacts.consentList.entries.size, 2) - assertTrue( - "Bob convo should be denied", - bobConversation.consentState() == ConsentState.DENIED - ) - assertTrue( - "Caro convo should be denied", - caroConversation.consentState() == ConsentState.DENIED - ) - } - } - - @Test - fun testCanValidateTopicsInsideConversation() { - val validId = "sdfsadf095b97a9284dcd82b2274856ccac8a21de57bebe34e7f9eeb855fb21126d3b8f" - - // Creation of all known types of topics - val privateStore = Topic.userPrivateStoreKeyBundle(validId).description - val contact = Topic.contact(validId).description - val userIntro = Topic.userIntro(validId).description - val userInvite = Topic.userInvite(validId).description - val directMessageV1 = Topic.directMessageV1(validId, "sd").description - val directMessageV2 = Topic.directMessageV2(validId).description - val preferenceList = Topic.preferenceList(validId).description - - // check if validation of topics accepts all types - assertTrue("Private Store should be valid topic", Topic.isValidTopic(privateStore)) - assertTrue("Contact should be valid topic", Topic.isValidTopic(contact)) - assertTrue("User Intro should be valid topic", Topic.isValidTopic(userIntro)) - assertTrue("userInvite should be valid topic", Topic.isValidTopic(userInvite)) - assertTrue("directMessageV1 should be valid topic", Topic.isValidTopic(directMessageV1)) - assertTrue("directMessageV2 should be valid topic", Topic.isValidTopic(directMessageV2)) - assertTrue("preferenceList should be valid topic", Topic.isValidTopic(preferenceList)) - } - - @Test - fun testCannotValidateTopicsInsideConversation() { - val invalidId = "��\\u0005�!\\u000b���5\\u00001\\u0007�蛨\\u001f\\u00172��.����K9K`�" - - // Creation of all known types of topics - val privateStore = Topic.userPrivateStoreKeyBundle(invalidId).description - val contact = Topic.contact(invalidId).description - val userIntro = Topic.userIntro(invalidId).description - val userInvite = Topic.userInvite(invalidId).description - val directMessageV1 = Topic.directMessageV1(invalidId, "sd").description - val directMessageV2 = Topic.directMessageV2(invalidId).description - val preferenceList = Topic.preferenceList(invalidId).description - - // check if validation of topics no accept all types with invalid topic - assertFalse(Topic.isValidTopic(privateStore)) - assertFalse(Topic.isValidTopic(contact)) - assertFalse(Topic.isValidTopic(userIntro)) - assertFalse(Topic.isValidTopic(userInvite)) - assertFalse(Topic.isValidTopic(directMessageV1)) - assertFalse(Topic.isValidTopic(directMessageV2)) - assertFalse(Topic.isValidTopic(preferenceList)) - } -} diff --git a/library/src/androidTest/java/org/xmtp/android/library/ConversationsTest.kt b/library/src/androidTest/java/org/xmtp/android/library/ConversationsTest.kt index 3ee56c5e2..e779cb564 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/ConversationsTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/ConversationsTest.kt @@ -6,261 +6,162 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertTrue import org.junit.Before -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith -import org.xmtp.android.library.codecs.TextCodec -import org.xmtp.android.library.messages.EnvelopeBuilder -import org.xmtp.android.library.messages.InvitationV1 -import org.xmtp.android.library.messages.MessageBuilder -import org.xmtp.android.library.messages.MessageV1Builder import org.xmtp.android.library.messages.PrivateKey import org.xmtp.android.library.messages.PrivateKeyBuilder -import org.xmtp.android.library.messages.SealedInvitationBuilder -import org.xmtp.android.library.messages.Signature -import org.xmtp.android.library.messages.Topic -import org.xmtp.android.library.messages.consentProofText -import org.xmtp.android.library.messages.createDeterministic -import org.xmtp.android.library.messages.getPublicKeyBundle -import org.xmtp.android.library.messages.rawDataWithNormalizedRecovery -import org.xmtp.android.library.messages.toPublicKeyBundle import org.xmtp.android.library.messages.walletAddress -import org.xmtp.proto.message.contents.Invitation -import org.xmtp.proto.message.contents.Invitation.ConsentProofPayload -import java.lang.Thread.sleep -import java.util.Date @RunWith(AndroidJUnit4::class) class ConversationsTest { - lateinit var alixWallet: PrivateKeyBuilder - lateinit var boWallet: PrivateKeyBuilder - lateinit var alix: PrivateKey - lateinit var alixClient: Client - lateinit var bo: PrivateKey - lateinit var boClient: Client - lateinit var caroClient: Client - lateinit var fixtures: Fixtures + 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.aliceAccount - alix = fixtures.alice - boWallet = fixtures.bobAccount - bo = fixtures.bob - alixClient = fixtures.aliceClient - boClient = fixtures.bobClient + 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 testCanGetConversationFromIntroEnvelope() { - val created = Date() - val newWallet = PrivateKeyBuilder() - val newClient = runBlocking { Client().create(account = newWallet) } - val message = MessageV1Builder.buildEncode( - sender = newClient.v1keys, - recipient = fixtures.aliceClient.v1keys.toPublicKeyBundle(), - message = TextCodec().encode(content = "hello").toByteArray(), - timestamp = created - ) - val envelope = EnvelopeBuilder.buildFromTopic( - topic = Topic.userIntro(alixClient.address), - timestamp = created, - message = MessageBuilder.buildFromMessageV1(v1 = message).toByteArray() + 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() { + runBlocking { boClient.conversations.findOrCreateDm(caro.walletAddress) } + 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 ) - val conversation = alixClient.conversations.fromIntro(envelope = envelope) - assertEquals(conversation.peerAddress, newWallet.address) - assertEquals(conversation.createdAt.time, created.time) + assertEquals(runBlocking { caroClient.conversations.listGroups().size }, 1) } @Test - fun testCanGetConversationFromInviteEnvelope() { - val created = Date() - val newWallet = PrivateKeyBuilder() - val newClient = runBlocking { Client().create(account = newWallet) } - val invitation = InvitationV1.newBuilder().build().createDeterministic( - sender = newClient.keys, - recipient = alixClient.keys.getPublicKeyBundle() + fun testsCanListConversationsFiltered() { + 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 ) - val sealed = SealedInvitationBuilder.buildFromV1( - sender = newClient.keys, - recipient = alixClient.keys.getPublicKeyBundle(), - created = created, - invitation = invitation + runBlocking { group.updateConsentState(ConsentState.DENIED) } + assertEquals( + runBlocking { boClient.conversations.list(consentState = ConsentState.ALLOWED).size }, + 1 ) - val peerAddress = alix.walletAddress - val envelope = EnvelopeBuilder.buildFromTopic( - topic = Topic.userInvite(peerAddress), - timestamp = created, - message = sealed.toByteArray() + assertEquals( + runBlocking { boClient.conversations.list(consentState = ConsentState.DENIED).size }, + 1 ) - val conversation = alixClient.conversations.fromInvite(envelope = envelope) - assertEquals(conversation.peerAddress, newWallet.address) - assertEquals(conversation.createdAt.time, created.time) + assertEquals(runBlocking { boClient.conversations.list().size }, 2) } @Test - fun testStreamAllMessages() { - val boConversation = - runBlocking { boClient.conversations.newConversation(alixClient.address) } + 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() } - // Record message stream across all conversations val allMessages = mutableListOf() val job = CoroutineScope(Dispatchers.IO).launch { try { - alixClient.conversations.streamAllMessages().collect { message -> - allMessages.add(message) - } + boClient.conversations.streamAllMessages() + .collect { message -> + allMessages.add(message) + } } catch (e: Exception) { } } - sleep(2500) - - for (i in 0 until 5) { - runBlocking { boConversation.send(text = "Message $i") } - sleep(1000) - } - assertEquals(5, allMessages.size) - - val caroConversation = - runBlocking { caroClient.conversations.newConversation(alixClient.address) } - sleep(2500) - - for (i in 0 until 5) { - runBlocking { caroConversation.send(text = "Message $i") } - sleep(1000) + Thread.sleep(1000) + runBlocking { + group.send("hi") + conversation.send("hi") } - - assertEquals(10, allMessages.size) - + Thread.sleep(1000) + assertEquals(2, allMessages.size) job.cancel() - - CoroutineScope(Dispatchers.IO).launch { - try { - alixClient.conversations.streamAllMessages().collect { message -> - allMessages.add(message) - } - } catch (e: Exception) { - } - } - sleep(2500) - - for (i in 0 until 5) { - runBlocking { boConversation.send(text = "Message $i") } - sleep(1000) - } - - assertEquals(15, allMessages.size) } @Test - @Ignore("TODO: Fix Flaky Test") - fun testStreamTimeOutsAllMessages() { - val boConversation = - runBlocking { boClient.conversations.newConversation(alixClient.address) } - - // Record message stream across all conversations - val allMessages = mutableListOf() + fun testCanStreamGroupsAndConversations() { + val allMessages = mutableListOf() val job = CoroutineScope(Dispatchers.IO).launch { try { - alixClient.conversations.streamAllMessages().collect { message -> - allMessages.add(message) - } + boClient.conversations.stream() + .collect { message -> + allMessages.add(message.topic) + } } catch (e: Exception) { } } - sleep(2500) + Thread.sleep(1000) - runBlocking { boConversation.send(text = "first message") } - sleep(2000) - assertEquals(allMessages.size, 1) - sleep(121000) - runBlocking { boConversation.send(text = "second message") } - sleep(2000) - assertEquals(allMessages.size, 2) - } - - @Test - @Ignore("TODO: Fix Flaky Test") - fun testSendConversationWithConsentSignature() { - val timestamp = Date().time - val signatureClass = Signature.newBuilder().build() - val signatureText = signatureClass.consentProofText(boClient.address, timestamp) - val signature = runBlocking { alixWallet.sign(signatureText) } - val hex = signature.rawDataWithNormalizedRecovery.toHex() - val consentProofPayload = ConsentProofPayload.newBuilder().also { - it.signature = hex - it.timestamp = timestamp - it.payloadVersion = Invitation.ConsentProofPayloadVersion.CONSENT_PROOF_PAYLOAD_VERSION_1 - }.build() - val boConversation = - runBlocking { boClient.conversations.newConversation(alixClient.address, null, consentProofPayload) } - val alixConversations = runBlocking { - alixClient.conversations.list() - } - val alixConversation = alixConversations.find { - it.topic == boConversation.topic + runBlocking { + caroClient.conversations.newGroup(listOf(bo.walletAddress)) + Thread.sleep(1000) + boClient.conversations.findOrCreateDm(caro.walletAddress) } - assertNotNull("Alix Conversation should exist " + alixConversations.size, alixConversation) -// Commenting out for now, the signature being created is not valid - val isAllowed = runBlocking { alixClient.contacts.isAllowed(boClient.address) } - assertTrue(isAllowed) - } - - @Test - @Ignore("TODO: Fix Flaky Test") - fun testNetworkConsentOverConsentProof() { - val timestamp = Date().time - val signatureText = Signature.newBuilder().build().consentProofText(boClient.address, timestamp) - val signature = runBlocking { alixWallet.sign(signatureText) } - val hex = signature.rawDataWithNormalizedRecovery.toHex() - val consentProofPayload = ConsentProofPayload.newBuilder().also { - it.signature = hex - it.timestamp = timestamp - it.payloadVersion = Invitation.ConsentProofPayloadVersion.CONSENT_PROOF_PAYLOAD_VERSION_1 - }.build() - runBlocking { alixClient.contacts.deny(listOf(boClient.address)) } - val boConversation = runBlocking { boClient.conversations.newConversation(alixClient.address, null, consentProofPayload) } - val alixConversations = runBlocking { alixClient.conversations.list() } - val alixConversation = alixConversations.find { it.topic == boConversation.topic } - assertNotNull(alixConversation) - val isDenied = runBlocking { alixClient.contacts.isDenied(boClient.address) } - assertTrue(isDenied) - } - @Test - @Ignore("TODO: Fix Flaky Test") - fun testConsentProofInvalidSignature() { - val timestamp = Date().time - val signatureText = - Signature.newBuilder().build().consentProofText(boClient.address, timestamp + 1) - val signature = runBlocking { alixWallet.sign(signatureText) } - val hex = signature.rawDataWithNormalizedRecovery.toHex() - val consentProofPayload = ConsentProofPayload.newBuilder().also { - it.signature = hex - it.timestamp = timestamp - it.payloadVersion = - Invitation.ConsentProofPayloadVersion.CONSENT_PROOF_PAYLOAD_VERSION_1 - }.build() - - val boConversation = runBlocking { - boClient.conversations.newConversation( - alixClient.address, - null, - consentProofPayload - ) - } - val alixConversations = runBlocking { alixClient.conversations.list() } - val alixConversation = alixConversations.find { it.topic == boConversation.topic } - assertNotNull("Alix conversation should exist" + alixConversations.size, alixConversation) - val isAllowed = runBlocking { alixClient.contacts.isAllowed(boClient.address) } - assertFalse("Should not be allowed", isAllowed) + 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 4eb2617b8..33be0fbfc 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/DmTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/DmTest.kt @@ -1,7 +1,6 @@ package org.xmtp.android.library import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry import app.cash.turbine.test import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -17,12 +16,10 @@ import org.xmtp.android.library.codecs.Reaction import org.xmtp.android.library.codecs.ReactionAction import org.xmtp.android.library.codecs.ReactionCodec import org.xmtp.android.library.codecs.ReactionSchema -import org.xmtp.android.library.messages.DecryptedMessage -import org.xmtp.android.library.messages.MessageDeliveryStatus +import org.xmtp.android.library.libxmtp.Message.MessageDeliveryStatus import org.xmtp.android.library.messages.PrivateKey import org.xmtp.android.library.messages.PrivateKeyBuilder import org.xmtp.android.library.messages.walletAddress -import java.security.SecureRandom @RunWith(AndroidJUnit4::class) class DmTest { @@ -38,48 +35,17 @@ class DmTest { @Before fun setUp() { - val key = SecureRandom().generateSeed(32) - val context = InstrumentationRegistry.getInstrumentation().targetContext - alixWallet = PrivateKeyBuilder() - alix = alixWallet.getPrivateKey() - alixClient = runBlocking { - Client().createV3( - account = alixWallet, - options = ClientOptions( - ClientOptions.Api(XMTPEnvironment.LOCAL, false), - enableV3 = true, - appContext = context, - dbEncryptionKey = key - ) - ) - } - boWallet = PrivateKeyBuilder() - bo = boWallet.getPrivateKey() - boClient = runBlocking { - Client().createV3( - account = boWallet, - options = ClientOptions( - ClientOptions.Api(XMTPEnvironment.LOCAL, false), - enableV3 = true, - appContext = context, - dbEncryptionKey = key - ) - ) - } - - caroWallet = PrivateKeyBuilder() - caro = caroWallet.getPrivateKey() - caroClient = runBlocking { - Client().createV3( - account = caroWallet, - options = ClientOptions( - ClientOptions.Api(XMTPEnvironment.LOCAL, false), - enableV3 = true, - appContext = context, - dbEncryptionKey = key - ) - ) - } + val fixtures = fixtures() + alixWallet = fixtures.alixAccount + alix = fixtures.alix + boWallet = fixtures.boAccount + bo = fixtures.bo + caroWallet = fixtures.caroAccount + caro = fixtures.caro + + alixClient = fixtures.alixClient + boClient = fixtures.boClient + caroClient = fixtures.caroClient } @Test @@ -128,7 +94,6 @@ class DmTest { fun testCannotCreateDmWithMemberNotOnV3() { val chuxAccount = PrivateKeyBuilder() val chux: PrivateKey = chuxAccount.getPrivateKey() - runBlocking { Client().create(account = chuxAccount) } assertThrows("Recipient not on network", XMTPException::class.java) { runBlocking { boClient.conversations.findOrCreateDm(chux.walletAddress) } @@ -149,8 +114,7 @@ class DmTest { dm.send("howdy") dm.send("gm") dm.sync() - assert(boClient.contacts.isGroupAllowed(dm.id)) - assertEquals(boClient.contacts.consentList.groupState(dm.id), ConsentState.ALLOWED) + assertEquals(boClient.preferences.consentList.conversationState(dm.id), ConsentState.ALLOWED) assertEquals(dm.consentState(), ConsentState.ALLOWED) } } @@ -249,7 +213,7 @@ class DmTest { val job = CoroutineScope(Dispatchers.IO).launch { try { - alixClient.conversations.streamAllConversationMessages().collect { message -> + alixClient.conversations.streamAllMessages().collect { message -> allMessages.add(message) } } catch (e: Exception) { @@ -277,59 +241,9 @@ class DmTest { job.cancel() } - @Test - fun testCanStreamDecryptedDmMessages() = kotlinx.coroutines.test.runTest { - val dm = boClient.conversations.findOrCreateDm(alix.walletAddress) - alixClient.conversations.syncConversations() - val alixDm = alixClient.findDm(bo.walletAddress) - dm.streamDecryptedMessages().test { - alixDm?.send("hi") - assertEquals("hi", awaitItem().encodedContent.content.toStringUtf8()) - alixDm?.send("hi again") - assertEquals("hi again", awaitItem().encodedContent.content.toStringUtf8()) - } - } - - @Test - fun testCanStreamAllDecryptedDmMessages() { - val dm = runBlocking { boClient.conversations.findOrCreateDm(alix.walletAddress) } - runBlocking { alixClient.conversations.syncConversations() } - - val allMessages = mutableListOf() - - val job = CoroutineScope(Dispatchers.IO).launch { - try { - alixClient.conversations.streamAllConversationDecryptedMessages().collect { message -> - allMessages.add(message) - } - } catch (e: Exception) { - } - } - Thread.sleep(2500) - - for (i in 0 until 2) { - runBlocking { dm.send(text = "Message $i") } - Thread.sleep(100) - } - assertEquals(2, allMessages.size) - - val caroDm = - runBlocking { caroClient.conversations.findOrCreateDm(alixClient.address) } - Thread.sleep(2500) - - for (i in 0 until 2) { - runBlocking { caroDm.send(text = "Message $i") } - Thread.sleep(100) - } - - assertEquals(4, allMessages.size) - - job.cancel() - } - @Test fun testCanStreamConversations() = kotlinx.coroutines.test.runTest { - boClient.conversations.streamConversations().test { + boClient.conversations.stream().test { val dm = alixClient.conversations.findOrCreateDm(bo.walletAddress) assertEquals(dm.id, awaitItem().id) @@ -344,15 +258,32 @@ class DmTest { runBlocking { val dm = boClient.conversations.findOrCreateDm(alix.walletAddress) - assert(boClient.contacts.isGroupAllowed(dm.id)) + assertEquals(boClient.preferences.consentList.conversationState(dm.id), ConsentState.ALLOWED) + assertEquals(dm.consentState(), ConsentState.ALLOWED) - boClient.contacts.denyGroups(listOf(dm.id)) - assert(boClient.contacts.isGroupDenied(dm.id)) + boClient.preferences.consentList.setConsentState( + listOf( + ConsentListEntry( + dm.id, + EntryType.CONVERSATION_ID, + ConsentState.DENIED + ) + ) + ) + assertEquals(boClient.preferences.consentList.conversationState(dm.id), ConsentState.DENIED) assertEquals(dm.consentState(), ConsentState.DENIED) - dm.updateConsentState(ConsentState.ALLOWED) - assert(boClient.contacts.isGroupAllowed(dm.id)) + boClient.preferences.consentList.setConsentState( + listOf( + ConsentListEntry( + dm.id, + EntryType.CONVERSATION_ID, + ConsentState.ALLOWED + ) + ) + ) + assertEquals(boClient.preferences.consentList.conversationState(dm.id), ConsentState.ALLOWED) assertEquals(dm.consentState(), ConsentState.ALLOWED) } } diff --git a/library/src/androidTest/java/org/xmtp/android/library/FramesTest.kt b/library/src/androidTest/java/org/xmtp/android/library/FramesTest.kt deleted file mode 100644 index db0bc52c8..000000000 --- a/library/src/androidTest/java/org/xmtp/android/library/FramesTest.kt +++ /dev/null @@ -1,71 +0,0 @@ -package org.xmtp.android.library - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import kotlinx.coroutines.runBlocking -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotNull -import org.junit.Test -import org.junit.runner.RunWith -import org.xmtp.android.library.frames.ConversationActionInputs -import org.xmtp.android.library.frames.DmActionInputs -import org.xmtp.android.library.frames.FrameActionInputs -import org.xmtp.android.library.frames.FramePostPayload -import org.xmtp.android.library.frames.FramesClient -import org.xmtp.android.library.frames.GetMetadataResponse -import java.net.HttpURLConnection -import java.net.URL - -@RunWith(AndroidJUnit4::class) -class FramesTest { - @Test - fun testFramesClient() { - val frameUrl = "https://fc-polls-five.vercel.app/polls/01032f47-e976-42ee-9e3d-3aac1324f4b8" - val fixtures = fixtures() - val aliceClient = fixtures.aliceClient - - val framesClient = FramesClient(xmtpClient = aliceClient) - val conversationTopic = "foo" - val participantAccountAddresses = listOf("alix", "bo") - val metadata: GetMetadataResponse - runBlocking { - metadata = framesClient.proxy.readMetadata(url = frameUrl) - } - - val dmInputs = DmActionInputs( - conversationTopic = conversationTopic, - participantAccountAddresses = participantAccountAddresses - ) - val conversationInputs = ConversationActionInputs.Dm(dmInputs) - val frameInputs = FrameActionInputs( - frameUrl = frameUrl, - buttonIndex = 1, - inputText = null, - state = null, - conversationInputs = conversationInputs - ) - val signedPayload: FramePostPayload - runBlocking { - signedPayload = framesClient.signFrameAction(inputs = frameInputs) - } - val postUrl = metadata.extractedTags["fc:frame:post_url"] - assertNotNull(postUrl) - val response: GetMetadataResponse - runBlocking { - response = framesClient.proxy.post(url = postUrl!!, payload = signedPayload) - } - - assertEquals(response.extractedTags["fc:frame"], "vNext") - - val imageUrl = response.extractedTags["fc:frame:image"] - assertNotNull(imageUrl) - - val mediaUrl = framesClient.proxy.mediaUrl(url = imageUrl!!) - - val url = URL(mediaUrl) - val connection = url.openConnection() as HttpURLConnection - connection.requestMethod = "GET" - val responseCode = connection.responseCode - assertEquals(responseCode, 200) - assertEquals(connection.contentType, "image/png") - } -} diff --git a/library/src/androidTest/java/org/xmtp/android/library/GroupPermissionsTest.kt b/library/src/androidTest/java/org/xmtp/android/library/GroupPermissionsTest.kt index c5b39331c..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,31 +35,23 @@ 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 } @Test fun testGroupCreatedWithCorrectAdminList() { val boGroup = runBlocking { boClient.conversations.newGroup(listOf(alix.walletAddress)) } - runBlocking { alixClient.conversations.syncGroups() } + runBlocking { alixClient.conversations.syncConversations() } val alixGroup = runBlocking { alixClient.conversations.listGroups().first() } assert(!boGroup.isAdmin(boClient.inboxId)) @@ -83,7 +75,7 @@ class GroupPermissionsTest { @Test fun testGroupCanUpdateAdminList() { val boGroup = runBlocking { boClient.conversations.newGroup(listOf(alix.walletAddress, caro.walletAddress), GroupPermissionPreconfiguration.ADMIN_ONLY) } - runBlocking { alixClient.conversations.syncGroups() } + runBlocking { alixClient.conversations.syncConversations() } val alixGroup = runBlocking { alixClient.conversations.listGroups().first() } assert(!boGroup.isAdmin(boClient.inboxId)) @@ -177,7 +169,7 @@ class GroupPermissionsTest { @Test fun testGroupCanUpdateSuperAdminList() { val boGroup = runBlocking { boClient.conversations.newGroup(listOf(alix.walletAddress, caro.walletAddress), GroupPermissionPreconfiguration.ADMIN_ONLY) } - runBlocking { alixClient.conversations.syncGroups() } + runBlocking { alixClient.conversations.syncConversations() } val alixGroup = runBlocking { alixClient.conversations.listGroups().first() } assert(boGroup.isSuperAdmin(boClient.inboxId)) @@ -218,7 +210,7 @@ class GroupPermissionsTest { @Test fun testGroupMembersAndPermissionLevel() { val group = runBlocking { boClient.conversations.newGroup(listOf(alix.walletAddress, caro.walletAddress), GroupPermissionPreconfiguration.ADMIN_ONLY) } - runBlocking { alixClient.conversations.syncGroups() } + runBlocking { alixClient.conversations.syncConversations() } val alixGroup = runBlocking { alixClient.conversations.listGroups().first() } // Initial checks for group members and their permissions @@ -267,7 +259,7 @@ class GroupPermissionsTest { @Test fun testCanCommitAfterInvalidPermissionsCommit() { val boGroup = runBlocking { boClient.conversations.newGroup(listOf(alix.walletAddress, caro.walletAddress), GroupPermissionPreconfiguration.ALL_MEMBERS) } - runBlocking { alixClient.conversations.syncGroups() } + runBlocking { alixClient.conversations.syncConversations() } val alixGroup = runBlocking { alixClient.conversations.listGroups().first() } // Verify that alix can NOT add an admin @@ -298,7 +290,7 @@ class GroupPermissionsTest { @Test fun testCanUpdatePermissions() { val boGroup = runBlocking { boClient.conversations.newGroup(listOf(alix.walletAddress, caro.walletAddress), GroupPermissionPreconfiguration.ADMIN_ONLY) } - runBlocking { alixClient.conversations.syncGroups() } + runBlocking { alixClient.conversations.syncConversations() } val alixGroup = runBlocking { alixClient.conversations.listGroups().first() } // Verify that alix can NOT update group name @@ -336,7 +328,7 @@ class GroupPermissionsTest { @Test fun testCanUpdatePinnedFrameUrl() { val boGroup = runBlocking { boClient.conversations.newGroup(listOf(alix.walletAddress, caro.walletAddress), GroupPermissionPreconfiguration.ADMIN_ONLY) } - runBlocking { alixClient.conversations.syncGroups() } + runBlocking { alixClient.conversations.syncConversations() } val alixGroup = runBlocking { alixClient.conversations.listGroups().first() } // Verify that alix can NOT update pinned frame @@ -389,7 +381,7 @@ class GroupPermissionsTest { permissionPolicySet = permissionPolicySet, ) } - runBlocking { alixClient.conversations.syncGroups() } + runBlocking { alixClient.conversations.syncConversations() } val alixGroup = runBlocking { alixClient.conversations.listGroups().first() } // Verify permission look correct @@ -439,7 +431,7 @@ class GroupPermissionsTest { ) // Valid custom policy works as expected - runBlocking { alixClient.conversations.syncGroups() } + runBlocking { alixClient.conversations.syncConversations() } assert(runBlocking { alixClient.conversations.listGroups() }.isEmpty()) val boGroup = runBlocking { @@ -448,7 +440,7 @@ class GroupPermissionsTest { permissionPolicySet = permissionPolicySetValid, ) } - runBlocking { alixClient.conversations.syncGroups() } + runBlocking { alixClient.conversations.syncConversations() } assert(runBlocking { alixClient.conversations.listGroups() }.size == 1) } } 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 3b3d23a1b..df0cabe05 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/GroupTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/GroupTest.kt @@ -1,7 +1,6 @@ package org.xmtp.android.library import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry import app.cash.turbine.test import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -19,15 +18,13 @@ import org.xmtp.android.library.codecs.Reaction import org.xmtp.android.library.codecs.ReactionAction import org.xmtp.android.library.codecs.ReactionCodec import org.xmtp.android.library.codecs.ReactionSchema -import org.xmtp.android.library.messages.DecryptedMessage -import org.xmtp.android.library.messages.MessageDeliveryStatus +import org.xmtp.android.library.libxmtp.Message.MessageDeliveryStatus import org.xmtp.android.library.messages.PrivateKey import org.xmtp.android.library.messages.PrivateKeyBuilder import org.xmtp.android.library.messages.walletAddress import org.xmtp.proto.mls.message.contents.TranscriptMessages import uniffi.xmtpv3.org.xmtp.android.library.libxmtp.GroupPermissionPreconfiguration import uniffi.xmtpv3.org.xmtp.android.library.libxmtp.PermissionOption -import java.security.SecureRandom @RunWith(AndroidJUnit4::class) class GroupTest { @@ -40,79 +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 - fun testsCanDualSendConversations() { - val v2Convo = runBlocking { alixClient.conversations.newConversation(bo.walletAddress) } - runBlocking { - alixClient.conversations.syncConversations() - boClient.conversations.syncConversations() - } - val alixDm = runBlocking { alixClient.findDm(bo.walletAddress) } - val boDm = runBlocking { boClient.findDm(alix.walletAddress) } - - assertEquals(alixDm?.id, boDm?.id) - assertEquals(runBlocking { alixClient.conversations.list().size }, 1) - assertEquals(runBlocking { alixClient.conversations.listDms().size }, 1) - assertEquals(runBlocking { boClient.conversations.listDms().size }, 1) - assertEquals(runBlocking { boClient.conversations.list().size }, 1) - assertEquals(v2Convo.topic, runBlocking { boClient.conversations.list().first().topic }) - } - - @Test - fun testsCanDualSendMessages() { - val alixV2Convo = runBlocking { alixClient.conversations.newConversation(bo.walletAddress) } - val boV2Convo = runBlocking { boClient.conversations.list().first() } - runBlocking { boClient.conversations.syncConversations() } - val alixDm = runBlocking { alixClient.findDm(bo.walletAddress) } - val boDm = runBlocking { boClient.findDm(alix.walletAddress) } - - runBlocking { alixV2Convo.send("first") } - runBlocking { boV2Convo.send("second") } - - runBlocking { - alixDm?.sync() - boDm?.sync() - } - - assertEquals(runBlocking { alixV2Convo.messages().size }, 2) - assertEquals(runBlocking { alixV2Convo.messages().size }, runBlocking { boV2Convo.messages().size }) - assertEquals(boDm?.messages()?.size, 2) - assertEquals(alixDm?.messages()?.size, 3) // We send the group membership update to the dm } @Test @@ -121,7 +60,7 @@ class GroupTest { boClient.conversations.newGroup(listOf(alix.walletAddress)) } runBlocking { - alixClient.conversations.syncGroups() + alixClient.conversations.syncConversations() boGroup.sync() } val alixGroup = runBlocking { alixClient.conversations.listGroups().first() } @@ -165,15 +104,18 @@ class GroupTest { permissions = GroupPermissionPreconfiguration.ADMIN_ONLY ) } - runBlocking { alixClient.conversations.syncGroups() } + runBlocking { alixClient.conversations.syncConversations() } val alixGroup = runBlocking { alixClient.conversations.listGroups().first() } assert(boGroup.id.isNotEmpty()) assert(alixGroup.id.isNotEmpty()) runBlocking { - assertEquals(boClient.contacts.consentList.groupState(boGroup.id), ConsentState.ALLOWED) assertEquals( - alixClient.contacts.consentList.groupState(alixGroup.id), + boClient.preferences.consentList.conversationState(boGroup.id), + ConsentState.ALLOWED + ) + assertEquals( + alixClient.preferences.consentList.conversationState(alixGroup.id), ConsentState.UNKNOWN ) } @@ -239,14 +181,6 @@ class GroupTest { ).sorted() ) - assertEquals( - Conversation.Group(group).peerAddresses.sorted(), - listOf( - caroClient.inboxId, - alixClient.inboxId, - ).sorted() - ) - assertEquals( runBlocking { group.peerInboxIds().sorted() }, listOf( @@ -271,7 +205,7 @@ class GroupTest { boGroup.updateGroupName("This Is A Great Group") boGroup.updateGroupImageUrlSquare("thisisanewurl.com") boGroup.sync() - alixClient.conversations.syncGroups() + alixClient.conversations.syncConversations() } val alixGroup = runBlocking { alixClient.conversations.listGroups().first() } runBlocking { alixGroup.sync() } @@ -327,10 +261,10 @@ class GroupTest { } runBlocking { boGroup.addAdmin(alixClient.inboxId) - alixClient.conversations.syncGroups() + alixClient.conversations.syncConversations() } val group = runBlocking { - alixClient.conversations.syncGroups() + alixClient.conversations.syncConversations() alixClient.conversations.listGroups().first() } runBlocking { @@ -384,13 +318,13 @@ class GroupTest { fun testMessageTimeIsCorrect() { val alixGroup = runBlocking { alixClient.conversations.newGroup(listOf(boClient.address)) } runBlocking { alixGroup.send("Hello") } - assertEquals(alixGroup.decryptedMessages().size, 2) + assertEquals(alixGroup.messages().size, 2) runBlocking { alixGroup.sync() } - val message2 = alixGroup.decryptedMessages().last() + val message2 = alixGroup.messages().last() runBlocking { alixGroup.sync() } - val message3 = alixGroup.decryptedMessages().last() + val message3 = alixGroup.messages().last() assertEquals(message3.id, message2.id) - assertEquals(message3.sentAt.time, message2.sentAt.time) + assertEquals(message3.sent.time, message2.sent.time) } @Test @@ -403,7 +337,7 @@ class GroupTest { ) ) } - runBlocking { caroClient.conversations.syncGroups() } + runBlocking { caroClient.conversations.syncConversations() } val caroGroup = runBlocking { caroClient.conversations.listGroups().first() } runBlocking { caroGroup.sync() } assert(caroGroup.isActive()) @@ -418,14 +352,14 @@ class GroupTest { @Test fun testAddedByAddress() { - val group = runBlocking { + runBlocking { alixClient.conversations.newGroup( listOf( boClient.address, ) ) } - runBlocking { boClient.conversations.syncGroups() } + runBlocking { boClient.conversations.syncConversations() } val boGroup = runBlocking { boClient.conversations.listGroups().first() } assertEquals(boGroup.addedByInboxId(), alixClient.inboxId) } @@ -435,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() } @@ -448,10 +381,9 @@ 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(includeGroups = true) } + val convos = runBlocking { boClient.conversations.list() } assertEquals(convos.size, 3) } @@ -459,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)) } @@ -486,8 +417,11 @@ class GroupTest { group.send("howdy") group.send("gm") group.sync() - assert(boClient.contacts.isGroupAllowed(group.id)) - assertEquals(boClient.contacts.consentList.groupState(group.id), ConsentState.ALLOWED) + assertEquals(group.consentState(), ConsentState.ALLOWED) + assertEquals( + boClient.preferences.consentList.conversationState(group.id), + ConsentState.ALLOWED + ) } } @@ -498,11 +432,8 @@ class GroupTest { var messageCallbacks = 0 val job = CoroutineScope(Dispatchers.IO).launch { - try { - boClient.conversations.streamAllGroupMessages().collect { message -> - messageCallbacks++ - } - } catch (e: Exception) { + boClient.conversations.streamAllMessages().collect { _ -> + messageCallbacks++ } } Thread.sleep(1000) @@ -512,7 +443,7 @@ class GroupTest { runBlocking { alixGroup.send("hello1") alixGroup.updateGroupName("hello") - boClient.conversations.syncGroups() + boClient.conversations.syncConversations() } val boGroups = runBlocking { boClient.conversations.listGroups() } @@ -544,6 +475,7 @@ class GroupTest { Thread.sleep(1000) assertEquals(secondMsgCheck, messageCallbacks) + job.cancel() } @Test @@ -557,7 +489,7 @@ class GroupTest { assertEquals(group.messages().first().deliveryStatus, MessageDeliveryStatus.PUBLISHED) assertEquals(group.messages().size, 3) - runBlocking { alixClient.conversations.syncGroups() } + runBlocking { alixClient.conversations.syncConversations() } val sameGroup = runBlocking { alixClient.conversations.listGroups().last() } runBlocking { sameGroup.sync() } assertEquals(sameGroup.messages().size, 2) @@ -579,7 +511,7 @@ class GroupTest { assertEquals(group.messages(deliveryStatus = MessageDeliveryStatus.UNPUBLISHED).size, 0) assertEquals(group.messages(deliveryStatus = MessageDeliveryStatus.PUBLISHED).size, 3) - runBlocking { alixClient.conversations.syncGroups() } + runBlocking { alixClient.conversations.syncConversations() } val sameGroup = runBlocking { alixClient.conversations.listGroups().last() } runBlocking { sameGroup.sync() } assertEquals(sameGroup.messages(deliveryStatus = MessageDeliveryStatus.PUBLISHED).size, 2) @@ -648,7 +580,7 @@ class GroupTest { val membershipChange = TranscriptMessages.GroupUpdated.newBuilder().build() val group = boClient.conversations.newGroup(listOf(alix.walletAddress.lowercase())) - alixClient.conversations.syncGroups() + alixClient.conversations.syncConversations() val alixGroup = alixClient.conversations.listGroups().first() group.streamMessages().test { alixGroup.send("hi") @@ -665,14 +597,13 @@ 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.syncGroups() } + runBlocking { alixClient.conversations.syncConversations() } val allMessages = mutableListOf() val job = CoroutineScope(Dispatchers.IO).launch { try { - alixClient.conversations.streamAllGroupMessages().collect { message -> + alixClient.conversations.streamAllMessages().collect { message -> allMessages.add(message) } } catch (e: Exception) { @@ -680,7 +611,6 @@ class GroupTest { } Thread.sleep(2500) - runBlocking { dm.send("should not stream") } for (i in 0 until 2) { runBlocking { group.send(text = "Message $i") @@ -706,7 +636,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() } @@ -715,93 +644,7 @@ class GroupTest { val job = CoroutineScope(Dispatchers.IO).launch { try { - alixClient.conversations.streamAllMessages(includeGroups = true) - .collect { message -> - allMessages.add(message) - } - } catch (e: Exception) { - } - } - Thread.sleep(2500) - - runBlocking { - group.send("hi") - conversation.send("hi") - dm.send("should not stream") - } - - Thread.sleep(1000) - - assertEquals(2, allMessages.size) - - job.cancel() - } - - @Test - fun testCanStreamDecryptedGroupMessages() = kotlinx.coroutines.test.runTest { - val group = boClient.conversations.newGroup(listOf(alix.walletAddress)) - alixClient.conversations.syncGroups() - val alixGroup = alixClient.conversations.listGroups().first() - group.streamDecryptedMessages().test { - alixGroup.send("hi") - assertEquals("hi", awaitItem().encodedContent.content.toStringUtf8()) - alixGroup.send("hi again") - assertEquals("hi again", awaitItem().encodedContent.content.toStringUtf8()) - } - } - - @Test - fun testCanStreamAllDecryptedGroupMessages() { - val group = runBlocking { caroClient.conversations.newGroup(listOf(alix.walletAddress)) } - val dm = runBlocking { davonV3Client.conversations.findOrCreateDm(alix.walletAddress) } - runBlocking { alixClient.conversations.syncConversations() } - - val allMessages = mutableListOf() - - val job = CoroutineScope(Dispatchers.IO).launch { - try { - alixClient.conversations.streamAllGroupDecryptedMessages().collect { message -> - allMessages.add(message) - } - } catch (e: Exception) { - } - } - Thread.sleep(2500) - - runBlocking { dm.send("Should not stream") } - for (i in 0 until 2) { - runBlocking { group.send(text = "Message $i") } - Thread.sleep(100) - } - assertEquals(2, allMessages.size) - - val caroGroup = - runBlocking { caroClient.conversations.newGroup(listOf(alixClient.address)) } - Thread.sleep(2500) - - for (i in 0 until 2) { - runBlocking { caroGroup.send(text = "Message $i") } - Thread.sleep(100) - } - - assertEquals(4, allMessages.size) - - job.cancel() - } - - @Test - fun testCanStreamAllDecryptedMessages() { - val group = runBlocking { caroClient.conversations.newGroup(listOf(alix.walletAddress)) } - val dm = runBlocking { davonV3Client.conversations.findOrCreateDm(alix.walletAddress) } - val conversation = - runBlocking { boClient.conversations.newConversation(alix.walletAddress) } - runBlocking { alixClient.conversations.syncGroups() } - - val allMessages = mutableListOf() - - val job = CoroutineScope(Dispatchers.IO).launch { - try { - alixClient.conversations.streamAllDecryptedMessages(includeGroups = true) + alixClient.conversations.streamAllMessages() .collect { message -> allMessages.add(message) } @@ -811,7 +654,6 @@ class GroupTest { Thread.sleep(2500) runBlocking { - dm.send("should not stream") group.send("hi") conversation.send("hi") } @@ -825,16 +667,13 @@ class GroupTest { @Test fun testCanStreamGroups() = kotlinx.coroutines.test.runTest { - boClient.conversations.streamGroups().test { + boClient.conversations.stream().test { val group = alixClient.conversations.newGroup(listOf(bo.walletAddress)) assertEquals(group.id, awaitItem().id) val group2 = caroClient.conversations.newGroup(listOf(bo.walletAddress)) assertEquals(group2.id, awaitItem().id) - davonV3Client.conversations.findOrCreateDm(bo.walletAddress) - expectNoEvents() - cancelAndConsumeRemainingEvents() } } @@ -844,7 +683,7 @@ class GroupTest { val job = CoroutineScope(Dispatchers.IO).launch { try { - alixClient.conversations.streamAll() + alixClient.conversations.stream() .collect { message -> allMessages.add(message.topic) } @@ -854,7 +693,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)) @@ -877,15 +715,29 @@ class GroupTest { caro.walletAddress ) ) - assert(boClient.contacts.isGroupAllowed(group.id)) + assertEquals( + boClient.preferences.consentList.conversationState(group.id), + ConsentState.ALLOWED + ) assertEquals(group.consentState(), ConsentState.ALLOWED) - boClient.contacts.denyGroups(listOf(group.id)) - assert(boClient.contacts.isGroupDenied(group.id)) + boClient.preferences.consentList.setConsentState( + listOf( + ConsentListEntry( + group.id, + EntryType.CONVERSATION_ID, + ConsentState.DENIED + ) + ) + ) + assertEquals(boClient.preferences.consentList.conversationState(group.id), ConsentState.DENIED) assertEquals(group.consentState(), ConsentState.DENIED) group.updateConsentState(ConsentState.ALLOWED) - assert(boClient.contacts.isGroupAllowed(group.id)) + assertEquals( + boClient.preferences.consentList.conversationState(group.id), + ConsentState.ALLOWED + ) assertEquals(group.consentState(), ConsentState.ALLOWED) } } @@ -894,30 +746,63 @@ class GroupTest { fun testCanAllowAndDenyInboxId() { runBlocking { val boGroup = boClient.conversations.newGroup(listOf(alix.walletAddress)) - assert(!boClient.contacts.isInboxAllowed(alixClient.inboxId)) - assert(!boClient.contacts.isInboxDenied(alixClient.inboxId)) - - boClient.contacts.allowInboxes(listOf(alixClient.inboxId)) + assertEquals( + boClient.preferences.consentList.inboxIdState(alixClient.inboxId), + ConsentState.UNKNOWN + ) + boClient.preferences.consentList.setConsentState( + listOf( + ConsentListEntry( + alixClient.inboxId, + EntryType.INBOX_ID, + ConsentState.ALLOWED + ) + ) + ) var alixMember = boGroup.members().firstOrNull { it.inboxId == alixClient.inboxId } assertEquals(alixMember!!.consentState, ConsentState.ALLOWED) - assert(boClient.contacts.isInboxAllowed(alixClient.inboxId)) - assert(!boClient.contacts.isInboxDenied(alixClient.inboxId)) + assertEquals( + boClient.preferences.consentList.inboxIdState(alixClient.inboxId), + ConsentState.ALLOWED + ) - boClient.contacts.denyInboxes(listOf(alixClient.inboxId)) + boClient.preferences.consentList.setConsentState( + listOf( + ConsentListEntry( + alixClient.inboxId, + EntryType.INBOX_ID, + ConsentState.DENIED + ) + ) + ) alixMember = boGroup.members().firstOrNull { it.inboxId == alixClient.inboxId } assertEquals(alixMember!!.consentState, ConsentState.DENIED) - assert(!boClient.contacts.isInboxAllowed(alixClient.inboxId)) - assert(boClient.contacts.isInboxDenied(alixClient.inboxId)) + assertEquals( + boClient.preferences.consentList.inboxIdState(alixClient.inboxId), + ConsentState.DENIED + ) - boClient.contacts.allow(listOf(alixClient.address)) + boClient.preferences.consentList.setConsentState( + listOf( + ConsentListEntry( + alixClient.address, + EntryType.ADDRESS, + ConsentState.ALLOWED + ) + ) + ) alixMember = boGroup.members().firstOrNull { it.inboxId == alixClient.inboxId } assertEquals(alixMember!!.consentState, ConsentState.ALLOWED) - assert(boClient.contacts.isInboxAllowed(alixClient.inboxId)) - assert(!boClient.contacts.isInboxDenied(alixClient.inboxId)) - assert(boClient.contacts.isAllowed(alixClient.address)) - assert(!boClient.contacts.isDenied(alixClient.address)) + assertEquals( + boClient.preferences.consentList.inboxIdState(alixClient.inboxId), + ConsentState.ALLOWED + ) + assertEquals( + boClient.preferences.consentList.addressState(alixClient.address), + ConsentState.ALLOWED + ) } } @@ -931,7 +816,7 @@ class GroupTest { ) ) } - runBlocking { alixClient.conversations.syncGroups() } + runBlocking { alixClient.conversations.syncConversations() } val alixGroup = alixClient.findGroup(boGroup.id) assertEquals(alixGroup?.id, boGroup.id) @@ -948,7 +833,7 @@ class GroupTest { ) } val boMessageId = runBlocking { boGroup.send("Hello") } - runBlocking { alixClient.conversations.syncGroups() } + runBlocking { alixClient.conversations.syncConversations() } val alixGroup = alixClient.findGroup(boGroup.id) runBlocking { alixGroup?.sync() } val alixMessage = alixClient.findMessage(boMessageId) @@ -966,11 +851,11 @@ class GroupTest { ) ) } - runBlocking { alixClient.conversations.syncGroups() } + 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) @@ -1005,7 +890,7 @@ class GroupTest { ) ) } - runBlocking { alixClient.conversations.syncGroups() } + runBlocking { alixClient.conversations.syncConversations() } val alixGroup: Group = alixClient.findGroup(boGroup.id)!! val alixGroup2: Group = alixClient.findGroup(boGroup2.id)!! var numGroups: UInt? @@ -1016,7 +901,7 @@ class GroupTest { runBlocking { boGroup.send("hi") boGroup2.send("hi") - numGroups = alixClient.conversations.syncAllGroups() + numGroups = alixClient.conversations.syncAllConversations() } assertEquals(alixGroup.messages().size, 1) @@ -1029,7 +914,7 @@ class GroupTest { boGroup.send("hi") boGroup2.send("hi") boGroup2.send("hi") - numGroups = alixClient.conversations.syncAllGroups() + numGroups = alixClient.conversations.syncAllConversations() } assertEquals(alixGroup.messages().size, 3) @@ -1038,7 +923,7 @@ class GroupTest { assertEquals(numGroups, 2u) runBlocking { - numGroups = alixClient.conversations.syncAllGroups() + numGroups = alixClient.conversations.syncAllConversations() } // Next syncAllGroups will not include the inactive group assertEquals(numGroups, 1u) 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/LocalInstrumentedTest.kt b/library/src/androidTest/java/org/xmtp/android/library/LocalInstrumentedTest.kt deleted file mode 100644 index 21c1aa060..000000000 --- a/library/src/androidTest/java/org/xmtp/android/library/LocalInstrumentedTest.kt +++ /dev/null @@ -1,884 +0,0 @@ -package org.xmtp.android.library - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.runBlocking -import org.junit.Assert.assertEquals -import org.junit.Ignore -import org.junit.Test -import org.junit.runner.RunWith -import org.xmtp.android.library.messages.Envelope -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.PrivateKeyBundleBuilder -import org.xmtp.android.library.messages.PrivateKeyBundleV1Builder -import org.xmtp.android.library.messages.Topic -import org.xmtp.android.library.messages.encrypted -import org.xmtp.android.library.messages.generate -import org.xmtp.android.library.messages.secp256K1Uncompressed -import org.xmtp.android.library.messages.toPublicKeyBundle -import org.xmtp.android.library.messages.walletAddress -import org.xmtp.proto.keystore.api.v1.Keystore -import org.xmtp.proto.message.api.v1.MessageApiOuterClass -import org.xmtp.proto.message.api.v1.MessageApiOuterClass.QueryRequest -import org.xmtp.proto.message.contents.Contact -import org.xmtp.proto.message.contents.InvitationV1Kt.context -import org.xmtp.proto.message.contents.PrivateKeyOuterClass -import org.xmtp.proto.message.contents.PrivateKeyOuterClass.PrivateKeyBundle -import uniffi.xmtpv3.createV2Client -import java.util.Date -import kotlin.time.Duration.Companion.nanoseconds -import kotlin.time.DurationUnit - -@RunWith(AndroidJUnit4::class) -class LocalInstrumentedTest { - @Test - fun testPublishingAndFetchingContactBundlesWithWhileGeneratingKeys() { - val aliceWallet = PrivateKeyBuilder() - val alicePrivateKey = aliceWallet.getPrivateKey() - val clientOptions = - ClientOptions( - api = ClientOptions.Api( - env = XMTPEnvironment.LOCAL, - isSecure = false, - appVersion = "XMTPTest/v1.0.0" - ) - ) - val client = runBlocking { Client().create(aliceWallet, clientOptions) } - runBlocking { - client.publishUserContact() - } - val contact = client.getUserContact(peerAddress = alicePrivateKey.walletAddress) - assert( - contact?.v2?.keyBundle?.identityKey?.secp256K1Uncompressed?.bytes?.toByteArray() - .contentEquals(client.privateKeyBundleV1?.identityKey?.publicKey?.secp256K1Uncompressed?.bytes?.toByteArray()) - ) - assert(contact?.v2?.keyBundle?.identityKey?.hasSignature() ?: false) - assert(contact?.v2?.keyBundle?.preKey?.hasSignature() ?: false) - } - - @Test - fun testSaveKey() { - val alice = PrivateKeyBuilder() - val identity = PrivateKey.newBuilder().build().generate() - val authorized = alice.createIdentity(identity) - val authToken = authorized.createAuthToken() - val v2Client = runBlocking { - createV2Client( - host = XMTPEnvironment.LOCAL.getUrl(), - isSecure = false - ) - } - val api = GRPCApiClient( - environment = XMTPEnvironment.LOCAL, - rustV2Client = v2Client - ) - api.setAuthToken(authToken) - val encryptedBundle = authorized.toBundle.encrypted(alice) - val envelope = Envelope.newBuilder().also { - it.contentTopic = Topic.userPrivateStoreKeyBundle(authorized.address).description - it.timestampNs = Date().time * 1_000_000 - it.message = encryptedBundle.toByteString() - }.build() - runBlocking { - api.publish(envelopes = listOf(envelope)) - } - Thread.sleep(2_000) - val result = - runBlocking { api.queryTopic(topic = Topic.userPrivateStoreKeyBundle(authorized.address)) } - assertEquals(result.envelopesList.size, 1) - } - - @Test - @Ignore("Flaky test") - fun testPublishingAndFetchingContactBundlesWithSavedKeys() { - val aliceWallet = PrivateKeyBuilder() - val alice = PrivateKeyOuterClass.PrivateKeyBundleV1.newBuilder().build() - .generate(wallet = aliceWallet) - // Save keys - val identity = PrivateKeyBuilder().getPrivateKey() - val authorized = aliceWallet.createIdentity(identity) - val authToken = authorized.createAuthToken() - val v2Client = runBlocking { - createV2Client( - host = XMTPEnvironment.LOCAL.getUrl(), - isSecure = false - ) - } - val api = GRPCApiClient( - environment = XMTPEnvironment.LOCAL, - rustV2Client = v2Client - ) - api.setAuthToken(authToken) - val encryptedBundle = - PrivateKeyBundleBuilder.buildFromV1Key(v1 = alice).encrypted(aliceWallet) - val envelope = Envelope.newBuilder().also { - it.contentTopic = Topic.userPrivateStoreKeyBundle(authorized.address).description - it.timestampNs = Date().time * 1_000_000 - it.message = encryptedBundle.toByteString() - }.build() - runBlocking { - api.publish(envelopes = listOf(envelope)) - } - - // Done saving keys - val clientOptions = - ClientOptions(api = ClientOptions.Api(env = XMTPEnvironment.LOCAL, isSecure = false)) - val client = runBlocking { Client().create(account = aliceWallet, options = clientOptions) } - val contact = client.getUserContact(peerAddress = aliceWallet.address) - assertEquals( - contact?.v2?.keyBundle?.identityKey?.secp256K1Uncompressed, - client.privateKeyBundleV1?.identityKey?.publicKey?.secp256K1Uncompressed - ) - assert(contact!!.v2.keyBundle.identityKey.hasSignature()) - assert(contact.v2.keyBundle.preKey.hasSignature()) - } - - @Test - fun testCanPaginateV2Messages() { - val bob = PrivateKeyBuilder() - val alice = PrivateKeyBuilder() - val clientOptions = - ClientOptions(api = ClientOptions.Api(env = XMTPEnvironment.LOCAL, isSecure = false)) - val bobClient = runBlocking { Client().create(bob, clientOptions) } - // Publish alice's contact - runBlocking { Client().create(account = alice, clientOptions) } - val convo = runBlocking { - bobClient.conversations.newConversation( - alice.address, - context = InvitationV1ContextBuilder.buildFromConversation("hi") - ) - } - // Say this message is sent in the past - val date = Date() - date.time = date.time - 5000 - runBlocking { - convo.send(text = "10 seconds ago", sentAt = date) - Thread.sleep(5000) - convo.send(text = "now first") - convo.send(text = "now") - val messages = convo.messages() - assertEquals(3, messages.size) - val messagesLimit = convo.messages(limit = 2) - assertEquals(2, messagesLimit.size) - val nowMessage = messages[0] - assertEquals("now", nowMessage.body) - val messages2 = convo.messages( - limit = 1, - beforeNs = nowMessage.sent.time.nanoseconds.toLong( - DurationUnit.NANOSECONDS - ) - ) - val tenSecondsAgoMessage = messages2[0] - assertEquals("now first", tenSecondsAgoMessage.body) - val messages3 = convo.messages( - afterNs = tenSecondsAgoMessage.sent.time.nanoseconds.toLong( - DurationUnit.NANOSECONDS - ) - ) - val nowMessage2 = messages3[0] - assertEquals("now", nowMessage2.body) - val messagesAsc = - convo.messages(direction = MessageApiOuterClass.SortDirection.SORT_DIRECTION_ASCENDING) - assertEquals("10 seconds ago", messagesAsc[0].body) - val messagesDesc = - convo.messages(direction = MessageApiOuterClass.SortDirection.SORT_DIRECTION_DESCENDING) - assertEquals("now", messagesDesc[0].body) - } - } - - @Test - fun testListingConversations() { - val alice = runBlocking { - Client().create( - PrivateKeyBuilder(), - ClientOptions( - api = ClientOptions.Api( - env = XMTPEnvironment.LOCAL, - isSecure = false - ) - ) - ) - } - val bob = runBlocking { - Client().create( - PrivateKeyBuilder(), - ClientOptions( - api = ClientOptions.Api( - env = XMTPEnvironment.LOCAL, - isSecure = false - ) - ) - ) - } - - // First Bob starts a conversation with Alice - val c1 = runBlocking { - bob.conversations.newConversation( - alice.address, - context = context { - conversationId = "example.com/alice-bob-1" - metadata["title"] = "First Chat" - } - ) - } - runBlocking { c1.send("hello Alice!") } - delayToPropagate() - - // So Alice should see just that one conversation. - var aliceConvoList = runBlocking { alice.conversations.list() } - assertEquals(1, aliceConvoList.size) - assertEquals("example.com/alice-bob-1", aliceConvoList[0].conversationId) - - // And later when Bob starts a second conversation with Alice - val c2 = runBlocking { - bob.conversations.newConversation( - alice.address, - context = context { - conversationId = "example.com/alice-bob-2" - metadata["title"] = "Second Chat" - } - ) - } - runBlocking { c2.send("hello again Alice!") } - delayToPropagate() - - // Then Alice should see both conversations, the newer one first. - aliceConvoList = runBlocking { alice.conversations.list() } - assertEquals(2, aliceConvoList.size) - assertEquals("example.com/alice-bob-2", aliceConvoList[0].conversationId) - assertEquals("example.com/alice-bob-1", aliceConvoList[1].conversationId) - } - - @Test - fun testUsingSavedCredentialsAndKeyMaterial() { - val options = ClientOptions(ClientOptions.Api(XMTPEnvironment.LOCAL, isSecure = false)) - val alice = runBlocking { Client().create(PrivateKeyBuilder(), options) } - val bob = runBlocking { Client().create(PrivateKeyBuilder(), options) } - - // Alice starts a conversation with Bob - val aliceConvo = runBlocking { - alice.conversations.newConversation( - bob.address, - context = context { - conversationId = "example.com/alice-bob-1" - metadata["title"] = "Chatting Using Saved Credentials" - } - ) - } - runBlocking { aliceConvo.send("Hello Bob") } - delayToPropagate() - - // Alice stores her credentials and conversations to her device - val keyBundle = alice.privateKeyBundle.toByteArray() - val topicData = aliceConvo.toTopicData().toByteArray() - - // Meanwhile, Bob sends a reply. - val bobConvos = runBlocking { bob.conversations.list() } - val bobConvo = bobConvos[0] - runBlocking { bobConvo.send("Oh, hello Alice") } - delayToPropagate() - - // When Alice's device wakes up, it uses her saved credentials - val alice2 = runBlocking { - Client().buildFromBundle( - PrivateKeyBundle.parseFrom(keyBundle), - options - ) - } - // And it uses the saved topic data for the conversation - val aliceConvo2 = alice2.conversations.importTopicData( - Keystore.TopicMap.TopicData.parseFrom(topicData) - ) - assertEquals("example.com/alice-bob-1", aliceConvo2.conversationId) - - // Now Alice should be able to load message using her saved key material. - val messages = runBlocking { aliceConvo2.messages() } - assertEquals("Hello Bob", messages[1].body) - assertEquals("Oh, hello Alice", messages[0].body) - } - - @Test - fun testCanPaginateV1Messages() { - val bob = PrivateKeyBuilder() - val alice = PrivateKeyBuilder() - val clientOptions = - ClientOptions(api = ClientOptions.Api(env = XMTPEnvironment.LOCAL, isSecure = false)) - val bobClient = runBlocking { Client().create(bob, clientOptions) } - // Publish alice's contact - runBlocking { Client().create(account = alice, clientOptions) } - val convo = ConversationV1(client = bobClient, peerAddress = alice.address, sentAt = Date()) - // Say this message is sent in the past - runBlocking { convo.send(text = "10 seconds ago") } - Thread.sleep(10000) - runBlocking { convo.send(text = "now") } - val messages = runBlocking { convo.messages() } - assertEquals(2, messages.size) - val nowMessage = messages[0] - assertEquals("now", nowMessage.body) - } - - @OptIn(ExperimentalCoroutinesApi::class) - @Test - fun testStreamAllMessagesWorksWithInvites() { - val bob = PrivateKeyBuilder() - val alice = PrivateKeyBuilder() - val clientOptions = - ClientOptions(api = ClientOptions.Api(env = XMTPEnvironment.LOCAL, isSecure = false)) - val bobClient = runBlocking { Client().create(bob, clientOptions) } - val aliceClient = runBlocking { Client().create(alice, clientOptions) } - aliceClient.conversations.streamAllMessages().mapLatest { - assertEquals("hi", it.encodedContent.content.toStringUtf8()) - } - val bobConversation = - runBlocking { bobClient.conversations.newConversation(aliceClient.address) } - runBlocking { bobConversation.send(text = "hi") } - } - - @OptIn(ExperimentalCoroutinesApi::class) - @Test - fun testStreamAllMessagesWorksWithIntros() { - val bob = PrivateKeyBuilder() - val alice = PrivateKeyBuilder() - val clientOptions = - ClientOptions(api = ClientOptions.Api(env = XMTPEnvironment.LOCAL, isSecure = false)) - val bobClient = runBlocking { Client().create(bob, clientOptions) } - val aliceClient = runBlocking { Client().create(alice, clientOptions) } - - // Overwrite contact as legacy - publishLegacyContact(client = bobClient) - publishLegacyContact(client = aliceClient) - - aliceClient.conversations.streamAllMessages().mapLatest { - assertEquals("hi", it.encodedContent.content.toStringUtf8()) - } - val bobConversation = - runBlocking { bobClient.conversations.newConversation(aliceClient.address) } - assertEquals(bobConversation.version, Conversation.Version.V1) - runBlocking { bobConversation.send(text = "hi") } - } - - private fun publishLegacyContact(client: Client) { - val contactBundle = Contact.ContactBundle.newBuilder().also { - it.v1 = it.v1.toBuilder().apply { - keyBundle = client.v1keys.toPublicKeyBundle() - }.build() - }.build() - val envelope = Envelope.newBuilder().also { - it.contentTopic = Topic.contact(client.address).description - it.timestampNs = Date().time * 1_000_000 - it.message = contactBundle.toByteString() - }.build() - - runBlocking { client.publish(envelopes = listOf(envelope)) } - } - - @Test - fun testBundleMatchesWhatJSGenerates() { - val jsBytes = arrayOf( - 10, - 134, - 3, - 10, - 192, - 1, - 8, - 212, - 239, - 181, - 224, - 235, - 48, - 18, - 34, - 10, - 32, - 253, - 223, - 55, - 200, - 191, - 179, - 50, - 251, - 142, - 186, - 142, - 144, - 120, - 55, - 133, - 66, - 62, - 227, - 207, - 137, - 96, - 29, - 252, - 171, - 22, - 50, - 211, - 201, - 114, - 170, - 219, - 35, - 26, - 146, - 1, - 8, - 212, - 239, - 181, - 224, - 235, - 48, - 18, - 68, - 10, - 66, - 10, - 64, - 128, - 94, - 43, - 155, - 99, - 38, - 128, - 57, - 37, - 120, - 14, - 252, - 31, - 231, - 47, - 9, - 128, - 134, - 90, - 150, - 231, - 9, - 36, - 119, - 119, - 177, - 93, - 241, - 169, - 185, - 104, - 166, - 105, - 25, - 244, - 26, - 197, - 83, - 94, - 171, - 35, - 9, - 189, - 13, - 103, - 141, - 68, - 129, - 134, - 121, - 23, - 84, - 209, - 102, - 56, - 207, - 194, - 238, - 9, - 213, - 72, - 74, - 220, - 198, - 26, - 67, - 10, - 65, - 4, - 93, - 157, - 228, - 228, - 120, - 5, - 159, - 157, - 196, - 163, - 132, - 142, - 147, - 218, - 144, - 247, - 192, - 180, - 221, - 177, - 31, - 97, - 59, - 48, - 110, - 204, - 155, - 208, - 233, - 140, - 180, - 54, - 136, - 127, - 78, - 81, - 49, - 185, - 30, - 73, - 110, - 43, - 50, - 179, - 76, - 230, - 99, - 118, - 58, - 150, - 51, - 136, - 13, - 188, - 69, - 79, - 81, - 135, - 70, - 115, - 91, - 58, - 177, - 95, - 18, - 192, - 1, - 8, - 215, - 150, - 182, - 224, - 235, - 48, - 18, - 34, - 10, - 32, - 157, - 32, - 14, - 227, - 139, - 112, - 46, - 218, - 54, - 217, - 214, - 220, - 159, - 105, - 220, - 13, - 164, - 50, - 168, - 234, - 81, - 48, - 224, - 112, - 187, - 138, - 18, - 160, - 129, - 195, - 187, - 30, - 26, - 146, - 1, - 8, - 215, - 150, - 182, - 224, - 235, - 48, - 18, - 68, - 10, - 66, - 10, - 64, - 248, - 197, - 168, - 69, - 172, - 44, - 172, - 107, - 56, - 177, - 111, - 167, - 54, - 162, - 189, - 76, - 115, - 240, - 113, - 202, - 235, - 50, - 168, - 137, - 161, - 188, - 111, - 139, - 185, - 215, - 159, - 145, - 38, - 250, - 224, - 77, - 107, - 107, - 9, - 226, - 93, - 235, - 71, - 215, - 85, - 247, - 141, - 14, - 156, - 85, - 144, - 200, - 94, - 160, - 108, - 190, - 111, - 219, - 29, - 61, - 11, - 57, - 237, - 156, - 26, - 67, - 10, - 65, - 4, - 123, - 22, - 77, - 71, - 125, - 86, - 127, - 27, - 156, - 189, - 27, - 30, - 102, - 185, - 38, - 134, - 239, - 69, - 53, - 232, - 48, - 104, - 70, - 118, - 242, - 114, - 201, - 89, - 36, - 94, - 133, - 210, - 228, - 205, - 1, - 17, - 119, - 121, - 20, - 113, - 160, - 64, - 102, - 224, - 193, - 9, - 76, - 166, - 7, - 4, - 155, - 241, - 217, - 116, - 135, - 206, - 62, - 77, - 216, - 54, - 204, - 39, - 24, - 96 - ) - val bytes = jsBytes.foldIndexed(ByteArray(jsBytes.size)) { i, a, v -> - a.apply { - set( - i, - v.toByte() - ) - } - } - val options = - ClientOptions(api = ClientOptions.Api(env = XMTPEnvironment.LOCAL, isSecure = true)) - val keys = PrivateKeyBundleV1Builder.buildFromBundle(bytes) - runBlocking { Client().buildFrom(bundle = keys, options = options) } - } - - @Test - fun testBatchQuery() { - val alice = PrivateKeyBuilder() - val identity = PrivateKey.newBuilder().build().generate() - val authorized = alice.createIdentity(identity) - val authToken = authorized.createAuthToken() - val v2Client = runBlocking { - createV2Client( - host = XMTPEnvironment.LOCAL.getUrl(), - isSecure = false - ) - } - val api = GRPCApiClient( - environment = XMTPEnvironment.LOCAL, - rustV2Client = v2Client - ) - api.setAuthToken(authToken) - val encryptedBundle = authorized.toBundle.encrypted(alice) - val envelope = Envelope.newBuilder().also { - it.contentTopic = Topic.userPrivateStoreKeyBundle(authorized.address).description - it.timestampNs = Date().time * 1_000_000 - it.message = encryptedBundle.toByteString() - }.build() - runBlocking { - api.publish(envelopes = listOf(envelope)) - } - Thread.sleep(2_000) - val request = QueryRequest.newBuilder() - .addContentTopics(Topic.userPrivateStoreKeyBundle(authorized.address).description) - .build() - val result = - runBlocking { api.batchQuery(requests = listOf(request)) } - - assertEquals(result.responsesOrBuilderList.size, 1) - } - - // A delay to allow messages to propagate before making assertions. - private fun delayToPropagate() { - Thread.sleep(500) - } - - @Test - fun testStreamEphemeralInV1Conversation() { - val bob = PrivateKeyBuilder() - val alice = PrivateKeyBuilder() - val clientOptions = - ClientOptions(api = ClientOptions.Api(env = XMTPEnvironment.LOCAL, isSecure = false)) - val bobClient = runBlocking { Client().create(bob, clientOptions) } - val aliceClient = runBlocking { Client().create(account = alice, options = clientOptions) } - runBlocking { - aliceClient.publishUserContact(legacy = true) - bobClient.publishUserContact(legacy = true) - } - val convo = ConversationV1(client = bobClient, peerAddress = alice.address, sentAt = Date()) - convo.streamEphemeral().mapLatest { - assertEquals("hi", it.message.toStringUtf8()) - } - runBlocking { convo.send(content = "hi", options = SendOptions(ephemeral = true)) } - val messages = runBlocking { convo.messages() } - assertEquals(0, messages.size) - } - - @Test - fun testStreamEphemeralInV2Conversation() { - val bob = PrivateKeyBuilder() - val alice = PrivateKeyBuilder() - val clientOptions = - ClientOptions(api = ClientOptions.Api(env = XMTPEnvironment.LOCAL, isSecure = false)) - val bobClient = runBlocking { Client().create(bob, clientOptions) } - val aliceClient = runBlocking { Client().create(account = alice, options = clientOptions) } - val aliceConversation = runBlocking { - aliceClient.conversations.newConversation( - bob.address, - context = InvitationV1ContextBuilder.buildFromConversation("https://example.com/3") - ) - } - val bobConversation = runBlocking { - bobClient.conversations.newConversation( - alice.address, - context = InvitationV1ContextBuilder.buildFromConversation("https://example.com/3") - ) - } - - bobConversation.streamEphemeral().mapLatest { - assertEquals("hi", it.message.toStringUtf8()) - } - runBlocking { - aliceConversation.send( - content = "hi", - options = SendOptions(ephemeral = true) - ) - } - val messages = runBlocking { aliceConversation.messages() } - assertEquals(0, messages.size) - } -} diff --git a/library/src/androidTest/java/org/xmtp/android/library/MessageTest.kt b/library/src/androidTest/java/org/xmtp/android/library/MessageTest.kt deleted file mode 100644 index 40a20c394..000000000 --- a/library/src/androidTest/java/org/xmtp/android/library/MessageTest.kt +++ /dev/null @@ -1,391 +0,0 @@ -package org.xmtp.android.library - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.google.protobuf.kotlin.toByteString -import com.google.protobuf.kotlin.toByteStringUtf8 -import kotlinx.coroutines.runBlocking -import org.junit.Assert -import org.junit.Assert.assertEquals -import org.junit.Ignore -import org.junit.Test -import org.junit.runner.RunWith -import org.web3j.utils.Numeric -import org.xmtp.android.library.codecs.TextCodec -import org.xmtp.android.library.messages.InvitationV1 -import org.xmtp.android.library.messages.InvitationV1ContextBuilder -import org.xmtp.android.library.messages.MessageV1Builder -import org.xmtp.android.library.messages.MessageV2Builder -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.PublicKeyBundle -import org.xmtp.android.library.messages.SealedInvitationBuilder -import org.xmtp.android.library.messages.SignedPublicKeyBundleBuilder -import org.xmtp.android.library.messages.createDeterministic -import org.xmtp.android.library.messages.decrypt -import org.xmtp.android.library.messages.generate -import org.xmtp.android.library.messages.getPublicKeyBundle -import org.xmtp.android.library.messages.recipientAddress -import org.xmtp.android.library.messages.senderAddress -import org.xmtp.android.library.messages.sharedSecret -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.contents.Invitation -import org.xmtp.proto.message.contents.Invitation.InvitationV1.Context -import org.xmtp.proto.message.contents.PrivateKeyOuterClass -import java.nio.charset.StandardCharsets.UTF_8 -import java.util.Date - -@RunWith(AndroidJUnit4::class) -class MessageTest { - - @Test - fun testFullyEncodesDecodesMessagesV1() { - repeat(10) { - val aliceWallet = PrivateKeyBuilder() - val bobWallet = PrivateKeyBuilder() - val alice = PrivateKeyBundleV1.newBuilder().build().generate(wallet = aliceWallet) - val bob = PrivateKeyBundleV1.newBuilder().build().generate(wallet = bobWallet) - val content = "Yo!".toByteStringUtf8().toByteArray() - val message1 = MessageV1Builder.buildEncode( - sender = alice, - recipient = bob.toPublicKeyBundle(), - message = content, - timestamp = Date(), - ) - assertEquals(aliceWallet.getPrivateKey().walletAddress, message1.senderAddress) - assertEquals(bobWallet.getPrivateKey().walletAddress, message1.recipientAddress) - val decrypted = message1.decrypt(alice) - val text = decrypted?.toByteString()?.toStringUtf8() - assertEquals(text, "Yo!") - } - } - - @Test - fun testFullyEncodesDecodesMessagesV2() { - val aliceWallet = PrivateKeyBuilder() - val bobWallet = PrivateKeyBuilder() - val alice = PrivateKeyBundleV1.newBuilder().build().generate(wallet = aliceWallet) - val bob = PrivateKeyBundleV1.newBuilder().build().generate(wallet = bobWallet) - val client = runBlocking { Client().create(account = aliceWallet) } - val invitationContext = Invitation.InvitationV1.Context.newBuilder().apply { - conversationId = "https://example.com/1" - }.build() - val invitationv1 = - InvitationV1.newBuilder().build().createDeterministic( - sender = alice.toV2(), - recipient = bob.toV2().getPublicKeyBundle(), - context = invitationContext, - ) - val sealedInvitation = SealedInvitationBuilder.buildFromV1( - sender = alice.toV2(), - recipient = bob.toV2().getPublicKeyBundle(), - created = Date(), - invitation = invitationv1, - ) - val encoder = TextCodec() - val encodedContent = encoder.encode(content = "Yo!") - val message1 = MessageV2Builder.buildEncode( - client = client, - encodedContent, - topic = invitationv1.topic, - keyMaterial = invitationv1.aes256GcmHkdfSha256.keyMaterial.toByteArray(), - codec = encoder, - ) - val decoded = MessageV2Builder.buildDecode( - id = "", - client = client, - message = message1.messageV2, - keyMaterial = invitationv1.aes256GcmHkdfSha256.keyMaterial.toByteArray(), - topic = invitationv1.topic, - ) - val result: String? = decoded.content() - assertEquals(result, "Yo!") - } - - @Test - fun testCanDecrypt() { - // All of these values were generated from xmtp-js - val content = - Numeric.hexStringToByteArray("0a120a08786d74702e6f7267120474657874180112110a08656e636f64696e6712055554462d3822026869") - val salt = - Numeric.hexStringToByteArray("48c6c40ce9998a8684937b2bd90c492cef66c9cd92b4a30a4f811b43fd0aed79") - val nonce = Numeric.hexStringToByteArray("31f78d2c989a37d8471a5d40") - val secret = - Numeric.hexStringToByteArray("04c86317929a0c223f44827dcf1290012b5e6538a54282beac85c2b16062fc8f781b52bea90e8c7c028254c6ba57ac144a56f054d569c340e73c6ff37aee4e68fc04a0fdb4e9c404f5d246a9fe2308f950f8374b0696dd98cc1c97fcbdbc54383ac862abee69c107723e1aa809cfbc587253b943476dc89c126af4f6515161a826ca04801742d6c45ee150a28f80cbcffd78a0210fe73ffdd74e4af8fd6307fb3d622d873653ca4bd47deb4711ef02611e5d64b4bcefcc481e236979af2b6156863e68") - val payload = - Numeric.hexStringToByteArray("d752fb09ee0390fe5902a1bd7b2f530da7e5b3a2bd91bad9df8fa284ab63327b86a59620fd3e2d2cf9183f46bd0fe75bda3caca893420c38416b1f") - val additionalData = - Numeric.hexStringToByteArray( - "0aac020a940108d995eeadcc3012460a440a408f20c9fc03909edeb21538b0a568c423f8829e95c0270779ca704f72a45f02416f6071f6faaf421cac3bacc6bb432fc4b5f92bc4391349953c7c98f12253cdd710011a430a4104b7eb7b56059a4f08bf3dd8f1b329e21d486e39822f17db15bad0d7f689f6c8081ae2800b9014fc9ef355a39e10503fddfdfa0b07ccc1946c2275b10e660d5ded12920108e995eeadcc3012440a420a40da669aa014468ffe34d5b962443d8b1e353b1e39f252bbcffa5c6c70adf9f7d2484de944213f345bac869e8c1942657b9c59f6fc12d139171b22789bc76ffb971a430a4104901d3a7f728bde1f871bcf46d44dcf34eead4c532135913583268d35bd93ca0a1571a8cb6546ab333f2d77c3bb9839be7e8f27795ea4d6e979b6670dec20636d12aa020a920108bad3eaadcc3012440a420a4016d83a6e44ee8b9764f18fbb390f2a4049d92ff904ebd75c76a71d58a7f943744f8bed7d3696f9fb41ce450c5ab9f4a7f9a83e3d10f401bbe85e3992c5156d491a430a41047cebe3a23e573672363665d13220d368d37776e10232de9bd382d5af36392956dbd806f8b78bec5cdc111763e4ef4aff7dee65a8a15fee8d338c387320c5b23912920108bad3eaadcc3012440a420a404a751f28001f34a4136529a99e738279856da6b32a1ee9dba20849d9cd84b6165166a6abeae1139ed8df8be3b4594d9701309075f2b8d5d4de1f713fb62ae37e1a430a41049c45e552ac9f69c083bd358acac31a2e3cf7d9aa9298fef11b43252730949a39c68272302a61b548b13452e19272c119b5189a5d7b5c3283a37d5d9db5ed0c6818b286deaecc30", - ) - val ciphertext = CipherText.newBuilder().apply { - aes256GcmHkdfSha256 = aes256GcmHkdfSha256.toBuilder().also { - it.gcmNonce = nonce.toByteString() - it.hkdfSalt = salt.toByteString() - it.payload = payload.toByteString() - }.build() - }.build() - - val decrypted = Crypto.decrypt(secret, ciphertext, additionalData = additionalData) - - assertEquals(content.toByteString(), decrypted?.toByteString()) - } - - @Test - @Ignore("Dev network flaky should be moved to local") - fun testCanReadGzipCompressedMessages() { - val ints = arrayOf( - 225, 2, 36, 98, 37, 243, 68, 234, - 42, 126, 248, 246, 126, 83, 186, 197, - 204, 186, 19, 173, 51, 0, 64, 0, - 155, 8, 249, 247, 163, 185, 124, 159, - ) - val keyBytes = - ints.foldIndexed(ByteArray(ints.size)) { i, a, v -> a.apply { set(i, v.toByte()) } } - - val key = PrivateKeyOuterClass.PrivateKey.newBuilder().also { - it.secp256K1 = it.secp256K1.toBuilder().also { builder -> - builder.bytes = keyBytes.toByteString() - }.build() - it.publicKey = it.publicKey.toBuilder().also { builder -> - builder.secp256K1Uncompressed = - builder.secp256K1Uncompressed.toBuilder().also { keyBuilder -> - keyBuilder.bytes = - KeyUtil.addUncompressedByte(KeyUtil.getPublicKey(keyBytes)) - .toByteString() - }.build() - }.build() - }.build() - - val client = runBlocking { Client().create(account = PrivateKeyBuilder(key)) } - runBlocking { - val convo = client.conversations.list()[0] - val message = convo.messages()[0] - assertEquals("Test message", message.content()) - } - } - - @Test - @Ignore("Dev network flaky should be moved to local") - fun testCanReadZipCompressedMessages() { - val ints = arrayOf( - 60, 45, 240, 192, 223, 2, 14, 166, - 122, 65, 231, 31, 122, 178, 158, 137, - 192, 97, 139, 83, 133, 245, 149, 250, - 25, 125, 25, 11, 203, 97, 12, 200, - ) - val keyBytes = - ints.foldIndexed(ByteArray(ints.size)) { i, a, v -> a.apply { set(i, v.toByte()) } } - - val key = PrivateKeyOuterClass.PrivateKey.newBuilder().also { - it.secp256K1 = it.secp256K1.toBuilder().also { builder -> - builder.bytes = keyBytes.toByteString() - }.build() - it.publicKey = it.publicKey.toBuilder().also { builder -> - builder.secp256K1Uncompressed = - builder.secp256K1Uncompressed.toBuilder().also { keyBuilder -> - keyBuilder.bytes = - KeyUtil.addUncompressedByte(KeyUtil.getPublicKey(keyBytes)) - .toByteString() - }.build() - }.build() - }.build() - - val client = runBlocking { Client().create(account = PrivateKeyBuilder(key)) } - runBlocking { - val convo = client.conversations.list()[0] - convo.send( - text = "hello deflate from kotlin again", - SendOptions(compression = EncodedContentCompression.DEFLATE), - ) - val message = convo.messages().lastOrNull()!! - assertEquals("hello deflate from kotlin again", message.content()) - } - } - - @Test - @Ignore("Dev network flaky should be moved to local") - fun testCanLoadAllConversations() { - val ints = arrayOf( - 105, 207, 193, 11, 240, 115, 115, 204, - 117, 134, 201, 10, 56, 59, 52, 90, - 229, 103, 15, 66, 20, 113, 118, 137, - 44, 62, 130, 90, 30, 158, 182, 178, - ) - val keyBytes = - ints.foldIndexed(ByteArray(ints.size)) { i, a, v -> a.apply { set(i, v.toByte()) } } - - val key = PrivateKeyOuterClass.PrivateKey.newBuilder().also { - it.secp256K1 = it.secp256K1.toBuilder().also { builder -> - builder.bytes = keyBytes.toByteString() - }.build() - it.publicKey = it.publicKey.toBuilder().also { builder -> - builder.secp256K1Uncompressed = - builder.secp256K1Uncompressed.toBuilder().also { keyBuilder -> - keyBuilder.bytes = - KeyUtil.addUncompressedByte(KeyUtil.getPublicKey(keyBytes)) - .toByteString() - }.build() - }.build() - }.build() - val client = runBlocking { Client().create(account = PrivateKeyBuilder(key)) } - val conversations = runBlocking { client.conversations.list() } - assertEquals(201, conversations.size) - } - - @Test - fun canReceiveV1MessagesFromJS() { - val wallet = FakeWallet.generate() - val client = runBlocking { Client().create(account = wallet) } - val convo = ConversationV1( - client = client, - peerAddress = "0xf4BF19Ed562651837bc11ff975472ABd239D35B5", - sentAt = Date(), - ) - runBlocking { convo.send(text = "hello from kotlin") } - val messages = runBlocking { convo.messages() } - assertEquals(1, messages.size) - assertEquals("hello from kotlin", messages[0].body) - assertEquals(convo.topic.description, messages[0].topic) - } - - @Test - fun canReceiveV2MessagesFromJS() { - val wallet = PrivateKeyBuilder() - val client = runBlocking { Client().create(account = wallet) } - val convo = runBlocking { - client.conversations.newConversation( - "0xf4BF19Ed562651837bc11ff975472ABd239D35B5", - InvitationV1ContextBuilder.buildFromConversation("https://example.com/4"), - ) - } - - runBlocking { convo.send(content = "hello from kotlin") } - val messages = runBlocking { convo.messages() } - assertEquals(1, messages.size) - assertEquals("hello from kotlin", messages[0].body) - assertEquals(convo.topic, messages[0].topic) - } - - @Test - fun testGetsV2ID() { - val envelopeMessageData = - Numeric.hexStringToByteArray( - "12bf040a470880dedf9dafc0ff9e17123b2f786d74702f302f6d2d32536b644e355161305a6d694649357433524662667749532d4f4c76356a7573716e6465656e544c764e672f70726f746f12f3030af0030a20439174a205643a50af33c7670341338526dbb9c1cf0560687ff8a742e957282d120c090ba2b385b40639867493ce1abd037648c947f72e5c62e8691d7748e78f9a346ff401c97a628ebecf627d722829ff9cfb7d7c3e0b9e26b5801f2b5a39fd58757cc5771427bfefad6243f52cfc84b384fa042873ebeb90948aa80ca34f26ff883d64720c9228ed6bcd1a5c46953a12ae8732fd70260651455674e2e2c23bc8d64ed35562fef4cdfc55d38e72ad9cf2d597e68f48b6909967b0f5d0b4f33c0af3efce55c739fbc93888d20b833df15811823970a356b26622936564d830434d3ecde9a013f7433142e366f1df5589131e440251be54d5d6deef9aaaa9facac26eb54fb7b74eb48c5a2a9a2e2956633b123cc5b91dec03e4dba30683be03bd7510f16103d3f81712dccf2be003f2f77f9e1f162bc47f6c1c38a1068abd3403952bef31d75e8024e7a62d9a8cbd48f1872a0156abb559d01de689b4370a28454658957061c46f47fc5594808d15753876d4b5408b3a3410d0555c016e427dfceae9c05a4a21fd7ce4cfbb11b2a696170443cf310e0083b0a48e357fc2f00c688c0b56821c8a14c2bb44ddfa31d680dfc85efe4811e86c6aa3adfc373ad5731ddab83960774d98d60075b8fd70228da5d748bfb7a5334bd07e1cc4a9fbf3d5de50860d0684bb27786b5b4e00d415", - ) - val envelope = MessageApiOuterClass.Envelope.newBuilder().also { - it.contentTopic = "/xmtp/0/m-2SkdN5Qa0ZmiFI5t3RFbfwIS-OLv5jusqndeenTLvNg/proto" - it.message = envelopeMessageData.toByteString() - it.timestampNs = Date().time * 1_000_000 - }.build() - val ints = arrayOf( - 80, 84, 15, 126, 14, 105, 216, 8, 61, 147, 153, 232, 103, 69, 219, 13, - 99, 118, 68, 56, 160, 94, 58, 22, 140, 247, 221, 172, 14, 188, 52, 88, - ) - val bytes = - ints.foldIndexed(ByteArray(ints.size)) { i, a, v -> a.apply { set(i, v.toByte()) } } - val key = PrivateKeyOuterClass.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 keyBundleData = - Numeric.hexStringToByteArray("0a86030ac001089387b882df3012220a204a393d6ac64c10770a2585def70329f10ca480517311f0b321a5cfbbae0119951a9201089387b882df3012440a420a4092f66532cf0266d146a17060fb64148e4a6adc673c14511e45f40ac66551234a336a8feb6ef3fabdf32ea259c2a3bca32b9550c3d34e004ea59e86b42f8001ac1a430a41041c919edda3399ab7f20f5e1a9339b1c2e666e80a164fb1c6d8bc1b7dbf2be158f87c837a6364c7fb667a40c2d234d198a7c2168a928d39409ad7d35d653d319912c00108a087b882df3012220a202ade2eefefa5f8855e557d685278e8717e3f57682b66c3d73aa87896766acddc1a920108a087b882df3012440a420a404f4a90ef10e1536e4588f12c2320229008d870d2abaecd1acfefe9ca91eb6f6d56b1380b1bdebdcf9c46fb19ceb3247d5d986a4dd2bce40a4bdf694c24b08fbb1a430a4104a51efe7833c46d2f683e2eb1c07811bb96ab5e4c2000a6f06124968e8842ff8be737ad7ca92b2dabb13550cdc561df15771c8494eca7b7ca5519f6da02f76489") - val keyBundle = PrivateKeyOuterClass.PrivateKeyBundle.parseFrom(keyBundleData) - val client = runBlocking { Client().buildFrom(bundle = keyBundle.v1) } - val conversationJSON = - (""" {"version":"v2","topic":"/xmtp/0/m-2SkdN5Qa0ZmiFI5t3RFbfwIS-OLv5jusqndeenTLvNg/proto","keyMaterial":"ATA1L0O2aTxHmskmlGKCudqfGqwA1H+bad3W/GpGOr8=","peerAddress":"0x436D906d1339fC4E951769b1699051f020373D04","createdAt":"2023-01-26T22:58:45.068Z","context":{"conversationId":"pat/messageid","metadata":{}}} """).toByteArray( - UTF_8, - ) - val decodedConversation = client.importConversation(conversationJSON) - val conversation = ConversationV2( - topic = decodedConversation.topic, - keyMaterial = decodedConversation.keyMaterial!!, - context = Context.newBuilder().build(), - peerAddress = decodedConversation.peerAddress, - client = client, - header = Invitation.SealedInvitationHeaderV1.newBuilder().build(), - ) - val decodedMessage = conversation.decodeEnvelope(envelope) - assertEquals( - decodedMessage.id, - "e42a7dd44d0e1214824eab093cb89cfe6f666298d0af2d54fe0c914c8b72eff3", - ) - } - - @Test - fun testMessages() { - val aliceWallet = PrivateKeyBuilder() - val bobWallet = PrivateKeyBuilder() - val alice = PrivateKeyOuterClass.PrivateKeyBundleV1.newBuilder().build() - .generate(wallet = aliceWallet) - val bob = PrivateKeyOuterClass.PrivateKeyBundleV1.newBuilder().build() - .generate(wallet = bobWallet) - val msg = "Hello world" - val decrypted = msg.toByteStringUtf8().toByteArray() - val alicePublic = alice.toPublicKeyBundle() - val bobPublic = bob.toPublicKeyBundle() - val aliceSecret = - alice.sharedSecret(peer = bobPublic, myPreKey = alicePublic.preKey, isRecipient = false) - val encrypted = Crypto.encrypt(aliceSecret, decrypted) - val bobSecret = - bob.sharedSecret(peer = alicePublic, myPreKey = bobPublic.preKey, isRecipient = true) - val bobDecrypted = Crypto.decrypt(bobSecret, encrypted!!) - val decryptedText = String(bobDecrypted!!, Charsets.UTF_8) - Assert.assertEquals(decryptedText, msg) - } - - @Test - fun testSharedSecret() { - val alice = PrivateKeyBuilder() - val alicePrivateBundle = - PrivateKeyOuterClass.PrivateKeyBundleV1.newBuilder().build().generate(wallet = alice) - .toV2() - val alicePublicBundle = alicePrivateBundle.getPublicKeyBundle() - val bob = PrivateKeyBuilder() - val bobPrivateBundle = - PrivateKeyOuterClass.PrivateKeyBundleV1.newBuilder().build().generate(wallet = bob) - .toV2() - val bobPublicBundle = bobPrivateBundle.getPublicKeyBundle() - val aliceSharedSecret = alicePrivateBundle.sharedSecret( - peer = bobPublicBundle, - myPreKey = alicePublicBundle.preKey, - isRecipient = true, - ) - val bobSharedSecret = bobPrivateBundle.sharedSecret( - peer = alicePublicBundle, - myPreKey = bobPublicBundle.preKey, - isRecipient = false, - ) - assert(aliceSharedSecret.contentEquals(bobSharedSecret)) - } - - @Test - fun testSharedSecretMatchesWhatJSGenerates() { - val meBundleData = - Numeric.hexStringToByteArray("0a86030ac00108a687b5d8cc3012220a20db73e1b4b5aeffb6cecd37526d842327730433e1751bceb5824d937f779797541a920108a687b5d8cc3012440a420a40d35c081d9ab59b3fb13e27cb03a225c7134bc4ce4ce51f80273481c31d803e1e4fa8ae43e7ec20b06a81b694ad28470f85fc971b8050867f5a4821c03a67f0e81a430a410443631548a55a60f06989ce1bc3fa43fdbe463ea4748dcb509e09fc58514c6e56edfac83e1fff5f382bc110fa066762f4b862db8df53be7d48268b3fdf649adc812c00108b787b5d8cc3012220a209e2631f34af8fc1ec0f75bd15ee4e110ac424300f39bff26c7a990a75a49ac641a920108b787b5d8cc3012440a420a40202a68a2e95d446511ecf22f5487b998989989adfc0a60e1ce201e0bab64d836066ccda987cda99c0e588babb8c334a820d6a6e360100ba7ba08e0e339a303681a430a4104c9733798111d89446264db365bc0dde54b5f9202eeb309eec2f18c572ce11e267fe91e184207676d7af5eaf2ad65de0881093623030f6096ea5bf3ecd252c482") - val youBundleData = - Numeric.hexStringToByteArray("0a940108c487b5d8cc3012460a440a40c51e611e662117991b19f60b6a7f6d9f08671c3d55241e959954c2e0f2ec47d15b872986d2a279ffe55df01709b000fbdcc9e85c1946876e187f90a0fd32222c10011a430a41049cccf02f766f7d4c322eeb498f2ac0283a011992fc77f9e0d5687b826aafd48d8319f48f773ec959221bf7bf7d3da4b09e59af540a633c588df2f1b6f465d6a712940108cb87b5d8cc3012460a440a40b7b0e89ce4789f6e78502357864979abe9e26cd44a36ed75578368a02cdc3bda7d56721660cb2066b76a4a6dd5a78d99df4b096cc4622a2065cf05b2f32b94be10011a430a410438f2b23a4e0f9c61e716b8cf4b23f2709d92b4feb71429a385b6878c31085384701bc787def9396b441bfb8751c042432785c352f8ee9bfb9c6cd5d6871b2d1a") - val secretData = - Numeric.hexStringToByteArray("049f4cd17426f9dfac528f400db858a9cbc87488879d6df5bea3595beaeb37415f1b24227e571dd4969406f366841e682795f284b54952a22b2dcff87971580fa604c0a97d550ce3ce5dac2e5469a2e3ece7232d80247a789044ebef0478c6911d63400a13090de6e8aeb4a1bcb878ca73b1d7eb13ab3012e564cfef74a8182467cc047d999bb077e5b223509fab7a08642c29359b8c3144ffa30002e45f09e4a515927f682eb71b68bd52f498d5d464c6bb14d3c07aefc86a1ab8e2528a21ffd41912") - val meBundle = PrivateKeyBundle.parseFrom(meBundleData).v1.toV2() - val youBundlePublic = - SignedPublicKeyBundleBuilder.buildFromKeyBundle(PublicKeyBundle.parseFrom(youBundleData)) - val secret = meBundle.sharedSecret( - peer = youBundlePublic, - myPreKey = meBundle.preKeysList[0].publicKey, - isRecipient = true, - ) - assert(secretData.contentEquals(secret)) - } -} 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 42482aaf4..94f292961 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/ReactionTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/ReactionTest.kt @@ -4,7 +4,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.protobuf.kotlin.toByteStringUtf8 import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.xmtp.android.library.codecs.ContentTypeReaction @@ -13,7 +12,6 @@ import org.xmtp.android.library.codecs.Reaction import org.xmtp.android.library.codecs.ReactionAction import org.xmtp.android.library.codecs.ReactionCodec import org.xmtp.android.library.codecs.ReactionSchema -import org.xmtp.android.library.messages.MessageV2Builder import org.xmtp.android.library.messages.walletAddress @RunWith(AndroidJUnit4::class) @@ -68,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") } @@ -91,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) @@ -100,55 +98,4 @@ class ReactionTest { assertEquals(ReactionSchema.Unicode, content?.schema) } } - - @Test - @Ignore("Flaky: CI") - fun testShouldPushMustBeTrue() { - Client.register(codec = ReactionCodec()) - - val fixtures = fixtures() - val aliceClient = fixtures.aliceClient - val aliceConversation = runBlocking { - aliceClient.conversations.newConversation(fixtures.bob.walletAddress) - } - - runBlocking { aliceConversation.send(text = "hey alice 2 bob") } - - val messageToReact = runBlocking { aliceConversation.messages()[0] } - - val attachment = Reaction( - reference = messageToReact.id, - action = ReactionAction.Added, - content = "U+1F603", - schema = ReactionSchema.Unicode, - ) - - runBlocking { - aliceConversation.send( - content = attachment, - options = SendOptions(contentType = ContentTypeReaction), - ) - } - val messages = runBlocking { aliceConversation.messages() } - assertEquals(messages.size, 2) - - val message = MessageV2Builder.buildEncode( - client = aliceClient, - encodedContent = messages[0].encodedContent, - topic = aliceConversation.topic, - keyMaterial = aliceConversation.keyMaterial!!, - codec = ReactionCodec(), - ) - - if (messages.size == 2) { - val content: Reaction? = messages.first().content() - assertEquals("U+1F603", content?.content) - assertEquals(messageToReact.id, content?.reference) - assertEquals(ReactionAction.Added, content?.action) - assertEquals(ReactionSchema.Unicode, content?.schema) - } - - assertEquals(true, message.shouldPush) - assertEquals(true, message.senderHmac?.isNotEmpty()) - } } 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 18f757ac9..27c46bb3c 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/SmartContractWalletTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/SmartContractWalletTest.kt @@ -10,7 +10,7 @@ import org.junit.Assert.assertEquals import org.junit.BeforeClass import org.junit.Test import org.junit.runner.RunWith -import org.xmtp.android.library.messages.MessageDeliveryStatus +import org.xmtp.android.library.libxmtp.Message import org.xmtp.android.library.messages.PrivateKey import org.xmtp.android.library.messages.PrivateKeyBuilder import org.xmtp.android.library.messages.walletAddress @@ -39,7 +39,6 @@ class SmartContractWalletTest { val context = InstrumentationRegistry.getInstrumentation().targetContext options = ClientOptions( ClientOptions.Api(XMTPEnvironment.LOCAL, false), - enableV3 = true, appContext = context, dbEncryptionKey = key ) @@ -48,7 +47,7 @@ class SmartContractWalletTest { boV3Wallet = PrivateKeyBuilder() boV3 = boV3Wallet.getPrivateKey() boV3Client = runBlocking { - Client().createV3( + Client().create( account = boV3Wallet, options = options ) @@ -57,7 +56,7 @@ class SmartContractWalletTest { // SCW davonSCW = FakeSCWWallet.generate(ANVIL_TEST_PRIVATE_KEY_1) davonSCWClient = runBlocking { - Client().createV3( + Client().create( account = davonSCW, options = options ) @@ -66,7 +65,7 @@ class SmartContractWalletTest { // SCW eriSCW = FakeSCWWallet.generate(ANVIL_TEST_PRIVATE_KEY_2) eriSCWClient = runBlocking { - Client().createV3( + Client().create( account = eriSCW, options = options ) @@ -77,7 +76,7 @@ class SmartContractWalletTest { @Test fun testCanBuildASCW() { val davonSCWClient2 = runBlocking { - Client().buildV3( + Client().build( address = davonSCW.address, options = options ) @@ -130,17 +129,20 @@ class SmartContractWalletTest { runBlocking { boGroup.sync() } assertEquals(boGroup.messages().first().body, "gm") assertEquals(boGroup.messages().first().id, messageId) - assertEquals(boGroup.messages().first().deliveryStatus, MessageDeliveryStatus.PUBLISHED) + assertEquals( + boGroup.messages().first().deliveryStatus, + Message.MessageDeliveryStatus.PUBLISHED + ) assertEquals(boGroup.messages().size, 3) - runBlocking { davonSCWClient.conversations.syncGroups() } + runBlocking { davonSCWClient.conversations.syncConversations() } val davonGroup = runBlocking { davonSCWClient.conversations.listGroups().last() } runBlocking { davonGroup.sync() } assertEquals(davonGroup.messages().size, 2) assertEquals(davonGroup.messages().first().body, "gm") runBlocking { davonGroup.send("from davon") } - runBlocking { eriSCWClient.conversations.syncGroups() } + runBlocking { eriSCWClient.conversations.syncConversations() } val eriGroup = runBlocking { davonSCWClient.findGroup(davonGroup.id) } runBlocking { eriGroup?.sync() } assertEquals(eriGroup?.messages()?.size, 3) @@ -159,15 +161,32 @@ class SmartContractWalletTest { ) ) } - assert(davonSCWClient.contacts.isGroupAllowed(davonGroup.id)) + assertEquals( + davonSCWClient.preferences.consentList.conversationState(davonGroup.id), + ConsentState.ALLOWED + ) assertEquals(davonGroup.consentState(), ConsentState.ALLOWED) - davonSCWClient.contacts.denyGroups(listOf(davonGroup.id)) - assert(davonSCWClient.contacts.isGroupDenied(davonGroup.id)) + davonSCWClient.preferences.consentList.setConsentState( + listOf( + ConsentListEntry( + davonGroup.id, + EntryType.CONVERSATION_ID, + ConsentState.DENIED + ) + ) + ) + assertEquals( + davonSCWClient.preferences.consentList.conversationState(davonGroup.id), + ConsentState.DENIED + ) assertEquals(davonGroup.consentState(), ConsentState.DENIED) davonGroup.updateConsentState(ConsentState.ALLOWED) - assert(davonSCWClient.contacts.isGroupAllowed(davonGroup.id)) + assertEquals( + davonSCWClient.preferences.consentList.conversationState(davonGroup.id), + ConsentState.ALLOWED + ) assertEquals(davonGroup.consentState(), ConsentState.ALLOWED) } } @@ -183,30 +202,63 @@ class SmartContractWalletTest { ) ) } - assert(!davonSCWClient.contacts.isInboxAllowed(boV3Client.inboxId)) - assert(!davonSCWClient.contacts.isInboxDenied(boV3Client.inboxId)) - - davonSCWClient.contacts.allowInboxes(listOf(boV3Client.inboxId)) - var caroMember = davonGroup.members().firstOrNull { it.inboxId == boV3Client.inboxId } - assertEquals(caroMember!!.consentState, ConsentState.ALLOWED) + assertEquals( + davonSCWClient.preferences.consentList.inboxIdState(boV3Client.inboxId), + ConsentState.UNKNOWN + ) + davonSCWClient.preferences.consentList.setConsentState( + listOf( + ConsentListEntry( + boV3Client.inboxId, + EntryType.INBOX_ID, + ConsentState.ALLOWED + ) + ) + ) + var alixMember = davonGroup.members().firstOrNull { it.inboxId == boV3Client.inboxId } + assertEquals(alixMember!!.consentState, ConsentState.ALLOWED) - assert(davonSCWClient.contacts.isInboxAllowed(boV3Client.inboxId)) - assert(!davonSCWClient.contacts.isInboxDenied(boV3Client.inboxId)) - assert(davonSCWClient.contacts.isAllowed(boV3Client.address)) - assert(!davonSCWClient.contacts.isDenied(boV3Client.address)) + assertEquals( + davonSCWClient.preferences.consentList.inboxIdState(boV3Client.inboxId), + ConsentState.ALLOWED + ) - davonSCWClient.contacts.denyInboxes(listOf(boV3Client.inboxId)) - caroMember = davonGroup.members().firstOrNull { it.inboxId == boV3Client.inboxId } - assertEquals(caroMember!!.consentState, ConsentState.DENIED) + davonSCWClient.preferences.consentList.setConsentState( + listOf( + ConsentListEntry( + boV3Client.inboxId, + EntryType.INBOX_ID, + ConsentState.DENIED + ) + ) + ) + alixMember = davonGroup.members().firstOrNull { it.inboxId == boV3Client.inboxId } + assertEquals(alixMember!!.consentState, ConsentState.DENIED) - assert(!davonSCWClient.contacts.isInboxAllowed(boV3Client.inboxId)) - assert(davonSCWClient.contacts.isInboxDenied(boV3Client.inboxId)) + assertEquals( + davonSCWClient.preferences.consentList.inboxIdState(boV3Client.inboxId), + ConsentState.DENIED + ) - davonSCWClient.contacts.allow(listOf(eriSCWClient.address)) - assert(davonSCWClient.contacts.isAllowed(eriSCWClient.address)) - assert(!davonSCWClient.contacts.isDenied(eriSCWClient.address)) - assert(davonSCWClient.contacts.isInboxAllowed(eriSCWClient.inboxId)) - assert(!davonSCWClient.contacts.isInboxDenied(eriSCWClient.inboxId)) + davonSCWClient.preferences.consentList.setConsentState( + listOf( + ConsentListEntry( + eriSCWClient.address, + EntryType.ADDRESS, + ConsentState.ALLOWED + ) + ) + ) + alixMember = davonGroup.members().firstOrNull { it.inboxId == eriSCWClient.inboxId } + assertEquals(alixMember!!.consentState, ConsentState.ALLOWED) + assertEquals( + davonSCWClient.preferences.consentList.inboxIdState(eriSCWClient.inboxId), + ConsentState.ALLOWED + ) + assertEquals( + davonSCWClient.preferences.consentList.addressState(eriSCWClient.address), + ConsentState.ALLOWED + ) } } @@ -236,7 +288,7 @@ class SmartContractWalletTest { val job = CoroutineScope(Dispatchers.IO).launch { try { - davonSCWClient.conversations.streamAllConversationMessages() + davonSCWClient.conversations.streamAllMessages() .collect { message -> allMessages.add(message) } @@ -261,7 +313,7 @@ class SmartContractWalletTest { val job = CoroutineScope(Dispatchers.IO).launch { try { - davonSCWClient.conversations.streamConversations() + davonSCWClient.conversations.stream() .collect { message -> allMessages.add(message.topic) } 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 5ae75845d..000000000 --- a/library/src/androidTest/java/org/xmtp/android/library/V3ClientTest.kt +++ /dev/null @@ -1,425 +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.messages.DecryptedMessage -import org.xmtp.android.library.messages.MessageDeliveryStatus -import org.xmtp.android.library.messages.PrivateKey -import org.xmtp.android.library.messages.PrivateKeyBuilder -import org.xmtp.android.library.messages.walletAddress -import java.security.SecureRandom - -@RunWith(AndroidJUnit4::class) -class V3ClientTest { - private lateinit var alixV2Wallet: PrivateKeyBuilder - private lateinit var boV3Wallet: PrivateKeyBuilder - private lateinit var alixV2: PrivateKey - private lateinit var alixV2Client: Client - private lateinit var boV3: PrivateKey - private lateinit var boV3Client: Client - private lateinit var caroV2V3Wallet: PrivateKeyBuilder - private lateinit var caroV2V3: PrivateKey - private lateinit var caroV2V3Client: Client - - @Before - fun setUp() { - val key = SecureRandom().generateSeed(32) - val context = InstrumentationRegistry.getInstrumentation().targetContext - - // Pure V2 - alixV2Wallet = PrivateKeyBuilder() - alixV2 = alixV2Wallet.getPrivateKey() - alixV2Client = runBlocking { - Client().create( - account = alixV2Wallet, - options = ClientOptions( - ClientOptions.Api(XMTPEnvironment.LOCAL, isSecure = false) - ) - ) - } - - // Pure V3 - boV3Wallet = PrivateKeyBuilder() - boV3 = boV3Wallet.getPrivateKey() - boV3Client = runBlocking { - Client().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.listConversations().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(includeGroups = true).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.listConversations().size }, 2) - assertEquals( - runBlocking { boV3Client.conversations.listConversations(consentState = ConsentState.ALLOWED).size }, - 2 - ) - runBlocking { group.updateConsentState(ConsentState.DENIED) } - assertEquals( - runBlocking { boV3Client.conversations.listConversations(consentState = ConsentState.ALLOWED).size }, - 1 - ) - assertEquals( - runBlocking { boV3Client.conversations.listConversations(consentState = ConsentState.DENIED).size }, - 1 - ) - assertEquals(runBlocking { boV3Client.conversations.listConversations().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.listConversations() } - val conversationsOrdered = - runBlocking { boV3Client.conversations.listConversations(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.streamAllConversationMessages() - .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 testCanStreamAllDecryptedMessagesFromV3Users() { - val group = - runBlocking { caroV2V3Client.conversations.newGroup(listOf(boV3.walletAddress)) } - val conversation = - runBlocking { boV3Client.conversations.findOrCreateDm(caroV2V3.walletAddress) } - runBlocking { boV3Client.conversations.syncConversations() } - - val allMessages = mutableListOf() - - val job = CoroutineScope(Dispatchers.IO).launch { - try { - boV3Client.conversations.streamAllConversationDecryptedMessages() - .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.streamConversations() - .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(includeGroups = true) - .collect { message -> - allMessages.add(message) - } - } catch (e: Exception) { - } - } - Thread.sleep(1000) - runBlocking { - group.send("hi") - conversation.send("hi") - } - Thread.sleep(1000) - assertEquals(2, allMessages.size) - job.cancel() - } - - @Test - fun testCanStreamGroupsAndConversationsFromV2andV3Users() { - val allMessages = mutableListOf() - - val job = CoroutineScope(Dispatchers.IO).launch { - try { - caroV2V3Client.conversations.streamAll() - .collect { message -> - allMessages.add(message.topic) - } - } catch (e: Exception) { - } - } - Thread.sleep(1000) - - runBlocking { - alixV2Client.conversations.newConversation(caroV2V3.walletAddress) - Thread.sleep(1000) - boV3Client.conversations.newGroup(listOf(caroV2V3.walletAddress)) - } - - Thread.sleep(2000) - assertEquals(2, allMessages.size) - job.cancel() - } -} diff --git a/library/src/main/java/org/xmtp/android/library/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 ed72e2609..55d6e2508 100644 --- a/library/src/main/java/org/xmtp/android/library/Client.kt +++ b/library/src/main/java/org/xmtp/android/library/Client.kt @@ -1,78 +1,33 @@ package org.xmtp.android.library import android.content.Context -import android.os.Build -import android.util.Log -import com.google.crypto.tink.subtle.Base64 -import com.google.gson.GsonBuilder import kotlinx.coroutines.runBlocking -import org.web3j.crypto.Keys -import org.web3j.crypto.Keys.toChecksumAddress import org.xmtp.android.library.codecs.ContentCodec import org.xmtp.android.library.codecs.TextCodec -import org.xmtp.android.library.libxmtp.MessageV3 +import org.xmtp.android.library.libxmtp.Message import org.xmtp.android.library.libxmtp.XMTPLogger -import org.xmtp.android.library.messages.ContactBundle -import org.xmtp.android.library.messages.EncryptedPrivateKeyBundle -import org.xmtp.android.library.messages.Envelope -import org.xmtp.android.library.messages.EnvelopeBuilder -import org.xmtp.android.library.messages.InvitationV1ContextBuilder -import org.xmtp.android.library.messages.Pagination -import org.xmtp.android.library.messages.PrivateKeyBundle -import org.xmtp.android.library.messages.PrivateKeyBundleBuilder -import org.xmtp.android.library.messages.PrivateKeyBundleV1 -import org.xmtp.android.library.messages.PrivateKeyBundleV2 -import org.xmtp.android.library.messages.SealedInvitationHeaderV1 -import org.xmtp.android.library.messages.Topic -import org.xmtp.android.library.messages.decrypted -import org.xmtp.android.library.messages.encrypted -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.nio.charset.StandardCharsets -import java.text.SimpleDateFormat -import java.time.Instant -import java.util.Date -import java.util.Locale -import java.util.TimeZone - -typealias PublishResponse = org.xmtp.proto.message.api.v1.MessageApiOuterClass.PublishResponse -typealias QueryResponse = org.xmtp.proto.message.api.v1.MessageApiOuterClass.QueryResponse + 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, @@ -83,18 +38,15 @@ data class ClientOptions( class Client() { lateinit var address: String - lateinit var contacts: Contacts + lateinit var inboxId: String + lateinit var installationId: String + lateinit var preferences: PrivatePreferences lateinit var conversations: Conversations - var privateKeyBundleV1: PrivateKeyBundleV1? = null - var apiClient: ApiClient? = null + lateinit var environment: XMTPEnvironment + lateinit var dbPath: String var logger: XMTPLogger = XMTPLogger() val libXMTPVersion: String = getVersionInfo() - var installationId: String = "" - var v3Client: FfiXmtpClient? = null - var dbPath: String = "" - lateinit var inboxId: String - var hasV2Client: Boolean = true - lateinit var environment: XMTPEnvironment + private lateinit var ffiClient: FfiXmtpClient companion object { private const val TAG = "Client" @@ -121,85 +73,6 @@ class Client() { fun register(codec: ContentCodec<*>) { codecRegistry.register(codec = codec) } - - /** - * Use the {@param api} to fetch any stored keys belonging to {@param address}. - * - * The user will need to be prompted to sign to decrypt each bundle. - */ - suspend fun authCheck(api: ApiClient, address: String): List { - val topic = Topic.userPrivateStoreKeyBundle(toChecksumAddress(address)) - val res = api.queryTopic(topic) - return res.envelopesList.mapNotNull { - try { - EncryptedPrivateKeyBundle.parseFrom(it.message) - } catch (e: Exception) { - Log.e(TAG, "discarding malformed private key bundle: ${e.message}", e) - null - } - } - } - - /** - * Use the {@param api} to save the {@param encryptedKeys} for {@param address}. - * - * The {@param keys} are used to authorize the publish request. - */ - suspend fun authSave( - api: ApiClient, - v1Key: PrivateKeyBundleV1, - encryptedKeys: EncryptedPrivateKeyBundle, - ) { - val authorizedIdentity = AuthorizedIdentity(v1Key) - authorizedIdentity.address = v1Key.walletAddress - val authToken = authorizedIdentity.createAuthToken() - api.setAuthToken(authToken) - api.publish( - envelopes = listOf( - EnvelopeBuilder.buildFromTopic( - topic = Topic.userPrivateStoreKeyBundle(v1Key.walletAddress), - timestamp = Date(), - message = encryptedKeys.toByteArray(), - ), - ), - ) - } - - suspend fun canMessage(peerAddress: String, options: ClientOptions? = null): Boolean { - val clientOptions = options ?: ClientOptions() - val v2Client = - createV2Client( - host = clientOptions.api.env.getUrl(), - isSecure = clientOptions.api.isSecure - ) - clientOptions.api.appVersion?.let { v2Client.setAppVersion(it) } - val api = GRPCApiClient(environment = clientOptions.api.env, rustV2Client = v2Client) - val topics = api.queryTopic(Topic.contact(peerAddress)).envelopesList - return topics.isNotEmpty() - } - } - - constructor( - address: String, - privateKeyBundleV1: PrivateKeyBundleV1, - apiClient: ApiClient, - libXMTPClient: FfiXmtpClient? = null, - dbPath: String = "", - installationId: String = "", - inboxId: String, - ) : this() { - this.address = address - this.privateKeyBundleV1 = privateKeyBundleV1 - this.apiClient = apiClient - this.contacts = Contacts(client = this) - this.v3Client = libXMTPClient - this.conversations = - Conversations(client = this, libXMTPConversations = libXMTPClient?.conversations()) - this.dbPath = dbPath - this.installationId = installationId - this.inboxId = inboxId - this.hasV2Client = true - this.environment = apiClient.environment } constructor( @@ -211,411 +84,138 @@ class Client() { environment: XMTPEnvironment, ) : this() { this.address = address - this.contacts = Contacts(client = this) - this.v3Client = libXMTPClient + this.preferences = PrivatePreferences(client = this, ffiClient = libXMTPClient) + this.ffiClient = libXMTPClient this.conversations = - Conversations(client = this, libXMTPConversations = libXMTPClient.conversations()) + Conversations(client = this, ffiConversations = libXMTPClient.conversations()) this.dbPath = dbPath this.installationId = installationId this.inboxId = inboxId - this.hasV2Client = false this.environment = environment } - suspend fun buildFrom( - bundle: PrivateKeyBundleV1, - options: ClientOptions? = null, - account: SigningKey? = null, - ): Client { - return buildFromV1Bundle(bundle, options, account) - } - - suspend fun create( - account: SigningKey, - options: ClientOptions? = null, - ): Client { - val clientOptions = options ?: ClientOptions() - val v2Client = - createV2Client( - host = clientOptions.api.env.getUrl(), - isSecure = clientOptions.api.isSecure - ) - clientOptions.api.appVersion?.let { v2Client.setAppVersion(it) } - val apiClient = GRPCApiClient(environment = clientOptions.api.env, rustV2Client = v2Client) - return create( - account = account, - apiClient = apiClient, - options = options, - ) - } - - suspend fun create( - account: SigningKey, - apiClient: ApiClient, - options: ClientOptions? = null, - ): Client { - val clientOptions = options ?: ClientOptions() - try { - val privateKeyBundleV1 = loadOrCreateKeys( - account, - apiClient, - clientOptions - ) - val inboxId = getOrCreateInboxId(clientOptions, account.address) - val (libXMTPClient, dbPath) = - ffiXmtpClient( - clientOptions, - account, - clientOptions.appContext, - privateKeyBundleV1, - account.address, - inboxId - ) - - val client = - Client( - account.address, - privateKeyBundleV1, - apiClient, - libXMTPClient, - dbPath, - libXMTPClient?.installationId()?.toHex() ?: "", - libXMTPClient?.inboxId() ?: inboxId - ) - client.ensureUserContactPublished() - return client - } catch (e: java.lang.Exception) { - throw XMTPException("Error creating client ${e.message}", e) - } - } - private suspend fun initializeV3Client( - accountAddress: String, + address: String, clientOptions: ClientOptions, signingKey: SigningKey? = null, ): Client { + val accountAddress = address.lowercase() val inboxId = getOrCreateInboxId(clientOptions, accountAddress) - val (libXMTPClient, dbPath) = ffiXmtpClient( + val (ffiClient, dbPath) = createFfiClient( + accountAddress, + inboxId, clientOptions, signingKey, clientOptions.appContext, - null, - accountAddress, - inboxId ) - libXMTPClient?.let { client -> - return Client( - accountAddress, - client, - dbPath, - client.installationId().toHex(), - client.inboxId(), - clientOptions.api.env - ) - } ?: throw XMTPException("Error creating V3 client: libXMTPClient is null") + return Client( + accountAddress, + ffiClient, + dbPath, + ffiClient.installationId().toHex(), + ffiClient.inboxId(), + clientOptions.api.env + ) } // Function to create a V3 client with a signing key - suspend fun createV3( + suspend fun create( account: SigningKey, - options: ClientOptions? = null, + options: ClientOptions, ): Client { - this.hasV2Client = false - val clientOptions = options ?: ClientOptions(enableV3 = true) - val accountAddress = account.address.lowercase() return try { - initializeV3Client(accountAddress, clientOptions, account) + initializeV3Client(account.address, options, account) } catch (e: Exception) { throw XMTPException("Error creating V3 client: ${e.message}", e) } } - // Function to build a V3 client without a signing key (using only address (& chainId for SCW)) - suspend fun buildV3( + // Function to build a V3 client from a address + suspend fun build( address: String, - options: ClientOptions? = null, + options: ClientOptions, ): Client { - this.hasV2Client = false - val clientOptions = options ?: ClientOptions(enableV3 = true) - val accountAddress = address.lowercase() return try { - initializeV3Client(accountAddress, clientOptions) + initializeV3Client(address, options) } catch (e: Exception) { throw XMTPException("Error creating V3 client: ${e.message}", e) } } - suspend fun buildFromBundle( - bundle: PrivateKeyBundle, - options: ClientOptions? = null, - account: SigningKey? = null, - ): Client = - buildFromV1Bundle(v1Bundle = bundle.v1, account = account, options = options) - - suspend fun buildFromV1Bundle( - v1Bundle: PrivateKeyBundleV1, - options: ClientOptions? = null, - account: SigningKey? = null, - ): Client { - val address = v1Bundle.identityKey.publicKey.recoverWalletSignerPublicKey().walletAddress - val newOptions = options ?: ClientOptions() - val v2Client = - createV2Client( - host = newOptions.api.env.getUrl(), - isSecure = newOptions.api.isSecure - ) - newOptions.api.appVersion?.let { v2Client.setAppVersion(it) } - val apiClient = GRPCApiClient(environment = newOptions.api.env, rustV2Client = v2Client) - val inboxId = getOrCreateInboxId(newOptions, address) - val (v3Client, dbPath) = if (isV3Enabled(options)) { - ffiXmtpClient( - newOptions, - account, - options?.appContext, - v1Bundle, - address, - inboxId - ) - } else Pair(null, "") - - return Client( - address = address, - privateKeyBundleV1 = v1Bundle, - apiClient = apiClient, - libXMTPClient = v3Client, - dbPath = dbPath, - installationId = v3Client?.installationId()?.toHex() ?: "", - inboxId = v3Client?.inboxId() ?: inboxId - ) - } - - private fun isV3Enabled(options: ClientOptions?): Boolean { - return (options != null && options.enableV3 && options.appContext != null) - } - - private suspend fun ffiXmtpClient( - options: ClientOptions, - account: SigningKey?, - appContext: Context?, - privateKeyBundleV1: PrivateKeyBundleV1?, - address: String, + private suspend fun createFfiClient( + accountAddress: String, inboxId: String, - ): Pair { - var dbPath = "" - val accountAddress = address.lowercase() - val v3Client: FfiXmtpClient? = - if (isV3Enabled(options)) { - val alias = "xmtp-${options.api.env}-$inboxId" - - val mlsDbDirectory = options.dbDirectory - val directoryFile = if (mlsDbDirectory != null) { - File(mlsDbDirectory) - } else { - File(appContext?.filesDir?.absolutePath, "xmtp_db") - } - directoryFile.mkdir() - dbPath = directoryFile.absolutePath + "/$alias.db3" - - val encryptionKey = options.dbEncryptionKey - ?: throw XMTPException("No encryption key passed for the database. Please store and provide a secure encryption key.") - - createClient( - logger = logger, - host = options.api.env.getUrl(), - isSecure = options.api.isSecure, - db = dbPath, - encryptionKey = encryptionKey, - accountAddress = accountAddress, - inboxId = inboxId, - nonce = 0.toULong(), - legacySignedPrivateKeyProto = privateKeyBundleV1?.toV2()?.identityKey?.toByteArray(), - historySyncUrl = options.historySyncUrl - ) - } else { - null - } - - if (v3Client != null) { - options.preAuthenticateToInboxCallback?.let { - runBlocking { - it.invoke() - } - } - v3Client.signatureRequest()?.let { signatureRequest -> - if (account != null) { - if (account.type == WalletType.SCW) { - val chainId = account.chainId ?: throw XMTPException("ChainId is required for smart contract wallets") - signatureRequest.addScwSignature( - account.signSCW(signatureRequest.signatureText()), - account.address.lowercase(), - chainId.toULong(), - account.blockNumber?.toULong() - ) - } else { - account.sign(signatureRequest.signatureText())?.let { - signatureRequest.addEcdsaSignature(it.rawData) - } - } - - v3Client.registerIdentity(signatureRequest) - } else { - throw XMTPException("No signer passed but signer was required.") - } - } - } - Log.i(TAG, "LibXMTP $libXMTPVersion") - return Pair(v3Client, dbPath) - } - - /** - * This authenticates using [account] acquired from network storage - * encrypted using the [wallet]. - * - * e.g. this might be called the first time a user logs in from a new device. - * The next time they launch the app they can [buildFromV1Key]. - * - * If there are stored keys then this asks the [wallet] to - * [encrypted] so that we can decrypt the stored [keys]. - * - * If there are no stored keys then this generates a new identityKey - * and asks the [wallet] to both [createIdentity] and enable Identity Saving - * so we can then store it encrypted for the next time. - */ - private suspend fun loadOrCreateKeys( - account: SigningKey, - apiClient: ApiClient, - options: ClientOptions? = null, - ): PrivateKeyBundleV1 { - val keys = loadPrivateKeys(account, apiClient, options) - return if (keys != null) { - keys + options: ClientOptions, + signingKey: SigningKey?, + appContext: Context, + ): Pair { + val alias = "xmtp-${options.api.env}-$inboxId" + + val mlsDbDirectory = options.dbDirectory + val directoryFile = if (mlsDbDirectory != null) { + File(mlsDbDirectory) } else { - val v1Keys = PrivateKeyBundleV1.newBuilder().build().generate(account, options) - val keyBundle = PrivateKeyBundleBuilder.buildFromV1Key(v1Keys) - val encryptedKeys = keyBundle.encrypted(account, options?.preEnableIdentityCallback) - authSave(apiClient, keyBundle.v1, encryptedKeys) - v1Keys + File(appContext.filesDir.absolutePath, "xmtp_db") } - } + directoryFile.mkdir() + dbPath = directoryFile.absolutePath + "/$alias.db3" + + val ffiClient = createClient( + logger = logger, + host = options.api.env.getUrl(), + isSecure = options.api.isSecure, + db = dbPath, + encryptionKey = options.dbEncryptionKey, + accountAddress = accountAddress, + inboxId = inboxId, + nonce = 0.toULong(), + legacySignedPrivateKeyProto = null, + historySyncUrl = options.historySyncUrl + ) - /** - * This authenticates with [keys] directly received. - * e.g. this might be called on subsequent app launches once we - * have already stored the keys from a previous session. - */ - private suspend fun loadPrivateKeys( - account: SigningKey, - apiClient: ApiClient, - options: ClientOptions? = null, - ): PrivateKeyBundleV1? { - val encryptedBundles = authCheck(apiClient, account.address) - for (encryptedBundle in encryptedBundles) { - try { - val bundle = - encryptedBundle.decrypted(account, options?.preEnableIdentityCallback) - return bundle.v1 - } catch (e: Throwable) { - print("Error decoding encrypted private key bundle: $e") - continue + options.preAuthenticateToInboxCallback?.let { + runBlocking { + it.invoke() } } - return null - } + ffiClient.signatureRequest()?.let { signatureRequest -> + if (signingKey != null) { + if (signingKey.type == WalletType.SCW) { + val chainId = signingKey.chainId + ?: throw XMTPException("ChainId is required for smart contract wallets") + signatureRequest.addScwSignature( + signingKey.signSCW(signatureRequest.signatureText()), + signingKey.address.lowercase(), + chainId.toULong(), + signingKey.blockNumber?.toULong() + ) + } else { + signingKey.sign(signatureRequest.signatureText())?.let { + signatureRequest.addEcdsaSignature(it.rawData) + } + } - suspend fun publishUserContact(legacy: Boolean = false) { - val envelopes: MutableList = mutableListOf() - if (legacy) { - val contactBundle = ContactBundle.newBuilder().also { - it.v1 = it.v1.toBuilder().also { v1Builder -> - v1Builder.keyBundle = v1keys.toPublicKeyBundle() - }.build() - }.build() - - val envelope = MessageApiOuterClass.Envelope.newBuilder().apply { - contentTopic = Topic.contact(address).description - timestampNs = Date().time * 1_000_000 - message = contactBundle.toByteString() - }.build() - - envelopes.add(envelope) + ffiClient.registerIdentity(signatureRequest) + } else { + throw XMTPException("No signer passed but signer was required.") + } } - val contactBundle = ContactBundle.newBuilder().also { - it.v2 = it.v2.toBuilder().also { v2Builder -> - v2Builder.keyBundle = keys.getPublicKeyBundle() - }.build() - it.v2 = it.v2.toBuilder().also { v2Builder -> - v2Builder.keyBundle = v2Builder.keyBundle.toBuilder().also { keyBuilder -> - keyBuilder.identityKey = - keyBuilder.identityKey.toBuilder().also { idBuilder -> - idBuilder.signature = - it.v2.keyBundle.identityKey.signature.ensureWalletSignature() - }.build() - }.build() - }.build() - }.build() - val envelope = MessageApiOuterClass.Envelope.newBuilder().apply { - contentTopic = Topic.contact(address).description - timestampNs = Date().time * 1_000_000 - message = contactBundle.toByteString() - }.build() - envelopes.add(envelope) - publish(envelopes = envelopes) - } - - fun getUserContact(peerAddress: String): ContactBundle? { - return contacts.find(Keys.toChecksumAddress(peerAddress)) - } - - suspend fun query(topic: Topic, pagination: Pagination? = null): QueryResponse { - val client = apiClient ?: throw XMTPException("V2 only function") - return client.queryTopic(topic = topic, pagination = pagination) - } - suspend fun batchQuery(requests: List): BatchQueryResponse { - val client = apiClient ?: throw XMTPException("V2 only function") - return client.batchQuery(requests) + return Pair(ffiClient, dbPath) } - 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) - } - - suspend fun fetchConversation( - topic: String?, - includeGroups: Boolean = false, - ): Conversation? { - if (topic.isNullOrBlank()) return null - return conversations.list(includeGroups = includeGroups).firstOrNull { - it.topic == topic - } - } - - @Deprecated("Find now includes DMs and Groups", replaceWith = ReplaceWith("findConversation")) 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") { @@ -626,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") { @@ -641,121 +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): MessageV3? { - val client = v3Client ?: throw XMTPException("Error no V3 client initialized") + fun findMessage(messageId: String): Message? { return try { - MessageV3(this, client.message(messageId.hexToByteArray())) + Message(this, ffiClient.message(messageId.hexToByteArray())) } catch (e: Exception) { null } } - suspend fun publish(envelopes: List) { - val client = apiClient ?: throw XMTPException("V2 only function") - val authorized = AuthorizedIdentity( - address = address, - authorized = v1keys.identityKey.publicKey, - identity = v1keys.identityKey, - ) - val authToken = authorized.createAuthToken() - client.setAuthToken(authToken) - - client.publish(envelopes = envelopes) - } - - suspend fun ensureUserContactPublished() { - val contact = getUserContact(peerAddress = address) - if (contact != null && keys.getPublicKeyBundle() == contact.v2.keyBundle) { - return - } - - publishUserContact(legacy = true) - } - - fun importConversation(conversationData: ByteArray): Conversation { - val gson = GsonBuilder().create() - val v2Export = gson.fromJson( - conversationData.toString(StandardCharsets.UTF_8), - ConversationV2Export::class.java, - ) - return try { - importV2Conversation(export = v2Export) - } catch (e: java.lang.Exception) { - val v1Export = gson.fromJson( - conversationData.toString(StandardCharsets.UTF_8), - ConversationV1Export::class.java, - ) - try { - importV1Conversation(export = v1Export) - } catch (e: java.lang.Exception) { - throw XMTPException("Invalid input data", e) - } - } - } - - fun importV2Conversation(export: ConversationV2Export): Conversation { - val keyMaterial = Base64.decode(export.keyMaterial) - return Conversation.V2( - ConversationV2( - topic = export.topic, - keyMaterial = keyMaterial, - context = InvitationV1ContextBuilder.buildFromConversation( - conversationId = export.context?.conversationId ?: "", - metadata = export.context?.metadata ?: mapOf(), - ), - peerAddress = export.peerAddress, - client = this, - header = SealedInvitationHeaderV1.newBuilder().build(), - ), - ) - } - - fun importV1Conversation(export: ConversationV1Export): Conversation { - val sentAt = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - Date.from(Instant.parse(export.createdAt)) - } else { - val df = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()) - df.timeZone = TimeZone.getTimeZone("UTC") - df.parse(export.createdAt) - } - return Conversation.V1( - ConversationV1( - client = this, - peerAddress = export.peerAddress, - sentAt = sentAt, - ), - ) - } - - /** - * 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() { @@ -767,37 +274,26 @@ class Client() { message = "This function is delicate and should be used with caution. App will error if database not properly reconnected. See: reconnectLocalDatabase()", ) fun dropLocalDatabaseConnection() { - v3Client?.releaseDbConnection() + ffiClient.releaseDbConnection() } suspend fun reconnectLocalDatabase() { - v3Client?.dbReconnect() ?: throw XMTPException("Error no V3 client initialized") + ffiClient.dbReconnect() } suspend fun requestMessageHistorySync() { - v3Client?.requestHistorySync() ?: throw XMTPException("Error no V3 client initialized") + ffiClient.requestHistorySync() } suspend fun revokeAllOtherInstallations(signingKey: SigningKey) { - val client = v3Client ?: throw XMTPException("Error no V3 client initialized") - val signatureRequest = client.revokeAllOtherInstallations() + val signatureRequest = ffiClient.revokeAllOtherInstallations() signingKey.sign(signatureRequest.signatureText())?.let { signatureRequest.addEcdsaSignature(it.rawData) - client.applySignatureRequest(signatureRequest) + ffiClient.applySignatureRequest(signatureRequest) } } suspend fun inboxState(refreshFromNetwork: Boolean): InboxState { - val client = v3Client ?: throw XMTPException("Error no V3 client initialized") - return InboxState(client.inboxState(refreshFromNetwork)) + return InboxState(ffiClient.inboxState(refreshFromNetwork)) } - - val privateKeyBundle: PrivateKeyBundle - get() = PrivateKeyBundleBuilder.buildFromV1Key(v1keys) - - val v1keys: PrivateKeyBundleV1 - get() = privateKeyBundleV1 ?: throw XMTPException("V2 only function") - - val keys: PrivateKeyBundleV2 - get() = v1keys.toV2() } diff --git a/library/src/main/java/org/xmtp/android/library/Constants.kt b/library/src/main/java/org/xmtp/android/library/Constants.kt deleted file mode 100644 index 56d2f607b..000000000 --- a/library/src/main/java/org/xmtp/android/library/Constants.kt +++ /dev/null @@ -1,5 +0,0 @@ -package org.xmtp.android.library - -object Constants { - const val VERSION = "0.1.3-development" -} diff --git a/library/src/main/java/org/xmtp/android/library/Contacts.kt b/library/src/main/java/org/xmtp/android/library/Contacts.kt deleted file mode 100644 index c0d0b544a..000000000 --- a/library/src/main/java/org/xmtp/android/library/Contacts.kt +++ /dev/null @@ -1,430 +0,0 @@ -package org.xmtp.android.library - -import kotlinx.coroutines.runBlocking -import org.xmtp.android.library.messages.ContactBundle -import org.xmtp.android.library.messages.ContactBundleBuilder -import org.xmtp.android.library.messages.EnvelopeBuilder -import org.xmtp.android.library.messages.Pagination -import org.xmtp.android.library.messages.Topic -import org.xmtp.android.library.messages.walletAddress -import org.xmtp.proto.message.api.v1.MessageApiOuterClass -import org.xmtp.proto.message.contents.PrivatePreferences.PrivatePreferencesAction -import uniffi.xmtpv3.FfiConsent -import uniffi.xmtpv3.FfiConsentEntityType -import uniffi.xmtpv3.FfiConsentState -import java.util.Date - -enum class ConsentState { - ALLOWED, - DENIED, - UNKNOWN; - - companion object { - fun toFfiConsentState(option: ConsentState): FfiConsentState { - return when (option) { - ConsentState.ALLOWED -> FfiConsentState.ALLOWED - ConsentState.DENIED -> FfiConsentState.DENIED - else -> FfiConsentState.UNKNOWN - } - } - - fun fromFfiConsentState(option: FfiConsentState): ConsentState { - return when (option) { - FfiConsentState.ALLOWED -> ConsentState.ALLOWED - FfiConsentState.DENIED -> ConsentState.DENIED - else -> ConsentState.UNKNOWN - } - } - } -} - -enum class EntryType { - ADDRESS, - GROUP_ID, - INBOX_ID; - - companion object { - fun toFfiConsentEntityType(option: EntryType): FfiConsentEntityType { - return when (option) { - EntryType.ADDRESS -> FfiConsentEntityType.ADDRESS - EntryType.GROUP_ID -> FfiConsentEntityType.CONVERSATION_ID - EntryType.INBOX_ID -> FfiConsentEntityType.INBOX_ID - } - } - - fun fromFfiConsentEntityType(option: FfiConsentEntityType): EntryType { - return when (option) { - FfiConsentEntityType.ADDRESS -> EntryType.ADDRESS - FfiConsentEntityType.CONVERSATION_ID -> EntryType.GROUP_ID - FfiConsentEntityType.INBOX_ID -> EntryType.INBOX_ID - } - } - } -} - -data class ConsentListEntry( - val value: String, - val entryType: EntryType, - val consentType: ConsentState, -) { - companion object { - fun address( - address: String, - type: ConsentState = ConsentState.UNKNOWN, - ): ConsentListEntry { - return ConsentListEntry(address, EntryType.ADDRESS, type) - } - - fun groupId( - groupId: String, - type: ConsentState = ConsentState.UNKNOWN, - ): ConsentListEntry { - return ConsentListEntry(groupId, EntryType.GROUP_ID, type) - } - - fun inboxId( - inboxId: String, - type: ConsentState = ConsentState.UNKNOWN, - ): ConsentListEntry { - return ConsentListEntry(inboxId, EntryType.INBOX_ID, type) - } - } - - fun toFfiConsent(): FfiConsent { - return FfiConsent( - EntryType.toFfiConsentEntityType(entryType), - ConsentState.toFfiConsentState(consentType), - value - ) - } - - val key: String - get() = "${entryType.name}-$value" -} - -class ConsentList( - val client: Client, - val entries: MutableMap = mutableMapOf(), -) { - private var lastFetched: Date? = null - - @OptIn(ExperimentalUnsignedTypes::class) - suspend fun load(): List { - if (client.hasV2Client) { - val newDate = Date() - val publicKey = - client.v1keys.identityKey.publicKey.secp256K1Uncompressed.bytes - val privateKey = client.v1keys.identityKey.secp256K1.bytes - val identifier: String = - uniffi.xmtpv3.generatePrivatePreferencesTopicIdentifier( - privateKey.toByteArray(), - ) - val envelopes = - client.apiClient!!.envelopes( - Topic.preferenceList(identifier).description, - Pagination( - after = lastFetched, - direction = MessageApiOuterClass.SortDirection.SORT_DIRECTION_ASCENDING, - limit = 500 - ), - ) - - lastFetched = newDate - val preferences: MutableList = mutableListOf() - for (envelope in envelopes) { - val payload = - uniffi.xmtpv3.userPreferencesDecrypt( - publicKey.toByteArray(), - privateKey.toByteArray(), - envelope.message.toByteArray(), - ) - - preferences.add( - PrivatePreferencesAction.parseFrom( - payload.toUByteArray().toByteArray(), - ) - ) - } - - preferences.iterator().forEach { preference -> - preference.allowAddress?.walletAddressesList?.forEach { address -> - allow(address) - } - preference.denyAddress?.walletAddressesList?.forEach { address -> - deny(address) - } - preference.allowGroup?.groupIdsList?.forEach { groupId -> - allowGroup(groupId) - } - preference.denyGroup?.groupIdsList?.forEach { groupId -> - denyGroup(groupId) - } - - preference.allowInboxId?.inboxIdsList?.forEach { inboxId -> - allowInboxId(inboxId) - } - preference.denyInboxId?.inboxIdsList?.forEach { inboxId -> - denyInboxId(inboxId) - } - } - } - return entries.values.toList() - } - - suspend fun publish(entries: List) { - if (client.v3Client != null) { - setV3ConsentState(entries) - } - if (client.hasV2Client) { - val payload = PrivatePreferencesAction.newBuilder().also { - entries.iterator().forEach { entry -> - when (entry.entryType to entry.consentType) { - EntryType.ADDRESS to ConsentState.ALLOWED -> it.setAllowAddress( - PrivatePreferencesAction.AllowAddress.newBuilder() - .addWalletAddresses(entry.value) - ) - - EntryType.ADDRESS to ConsentState.DENIED -> it.setDenyAddress( - PrivatePreferencesAction.DenyAddress.newBuilder() - .addWalletAddresses(entry.value) - ) - - EntryType.GROUP_ID to ConsentState.ALLOWED -> it.setAllowGroup( - PrivatePreferencesAction.AllowGroup.newBuilder() - .addGroupIds(entry.value) - ) - - EntryType.GROUP_ID to ConsentState.DENIED -> it.setDenyGroup( - PrivatePreferencesAction.DenyGroup.newBuilder().addGroupIds(entry.value) - ) - - EntryType.INBOX_ID to ConsentState.ALLOWED -> it.setAllowInboxId( - PrivatePreferencesAction.AllowInboxId.newBuilder() - .addInboxIds(entry.value) - ) - - EntryType.INBOX_ID to ConsentState.DENIED -> it.setDenyInboxId( - PrivatePreferencesAction.DenyInboxId.newBuilder() - .addInboxIds(entry.value) - ) - - else -> it.clearMessageType() - } - } - }.build() - - val publicKey = - client.v1keys.identityKey.publicKey.secp256K1Uncompressed.bytes - val privateKey = client.v1keys.identityKey.secp256K1.bytes - val identifier: String = - uniffi.xmtpv3.generatePrivatePreferencesTopicIdentifier( - privateKey.toByteArray(), - ) - - val message = - uniffi.xmtpv3.userPreferencesEncrypt( - publicKey.toByteArray(), - privateKey.toByteArray(), - payload.toByteArray(), - ) - - val envelope = EnvelopeBuilder.buildFromTopic( - Topic.preferenceList(identifier), - Date(), - ByteArray(message.size) { message[it] }, - ) - - client.publish(listOf(envelope)) - } - } - - suspend fun setV3ConsentState(entries: List) { - client.v3Client?.setConsentStates(entries.map { it.toFfiConsent() }) - } - - fun allow(address: String): ConsentListEntry { - val entry = ConsentListEntry.address(address, ConsentState.ALLOWED) - entries[entry.key] = entry - - return entry - } - - fun deny(address: String): ConsentListEntry { - val entry = ConsentListEntry.address(address, ConsentState.DENIED) - entries[entry.key] = entry - - return entry - } - - fun allowGroup(groupId: String): ConsentListEntry { - val entry = ConsentListEntry.groupId(groupId, ConsentState.ALLOWED) - entries[entry.key] = entry - - return entry - } - - fun denyGroup(groupId: String): ConsentListEntry { - val entry = ConsentListEntry.groupId(groupId, ConsentState.DENIED) - entries[entry.key] = entry - - return entry - } - - fun allowInboxId(inboxId: String): ConsentListEntry { - val entry = ConsentListEntry.inboxId(inboxId, ConsentState.ALLOWED) - entries[entry.key] = entry - - return entry - } - - fun denyInboxId(inboxId: String): ConsentListEntry { - val entry = ConsentListEntry.inboxId(inboxId, ConsentState.DENIED) - entries[entry.key] = entry - - return entry - } - - suspend fun state(address: String): ConsentState { - client.v3Client?.let { - return ConsentState.fromFfiConsentState( - it.getConsentState( - FfiConsentEntityType.ADDRESS, - address - ) - ) - } - val entry = entries[ConsentListEntry.address(address).key] - return entry?.consentType ?: ConsentState.UNKNOWN - } - - suspend fun groupState(groupId: String): ConsentState { - client.v3Client?.let { - return ConsentState.fromFfiConsentState( - it.getConsentState( - FfiConsentEntityType.CONVERSATION_ID, - groupId - ) - ) - } - val entry = entries[ConsentListEntry.groupId(groupId).key] - return entry?.consentType ?: ConsentState.UNKNOWN - } - - suspend fun inboxIdState(inboxId: String): ConsentState { - client.v3Client?.let { - return ConsentState.fromFfiConsentState( - it.getConsentState( - FfiConsentEntityType.INBOX_ID, - inboxId - ) - ) - } - val entry = entries[ConsentListEntry.inboxId(inboxId).key] - return entry?.consentType ?: ConsentState.UNKNOWN - } -} - -data class Contacts( - var client: Client, - val knownBundles: MutableMap = mutableMapOf(), - val hasIntroduced: MutableMap = mutableMapOf(), - var consentList: ConsentList = ConsentList(client), -) { - - suspend fun refreshConsentList(): ConsentList { - val entries = consentList.load() - consentList.setV3ConsentState(entries) - return consentList - } - - suspend fun allow(addresses: List) { - val entries = addresses.map { - consentList.allow(it) - } - consentList.publish(entries) - } - - suspend fun deny(addresses: List) { - val entries = addresses.map { - consentList.deny(it) - } - consentList.publish(entries) - } - - suspend fun allowGroups(groupIds: List) { - val entries = groupIds.map { - consentList.allowGroup(it) - } - consentList.publish(entries) - } - - suspend fun denyGroups(groupIds: List) { - val entries = groupIds.map { - consentList.denyGroup(it) - } - consentList.publish(entries) - } - - suspend fun allowInboxes(inboxIds: List) { - val entries = inboxIds.map { - consentList.allowInboxId(it) - } - consentList.publish(entries) - } - - suspend fun denyInboxes(inboxIds: List) { - val entries = inboxIds.map { - consentList.denyInboxId(it) - } - consentList.publish(entries) - } - - suspend fun isAllowed(address: String): Boolean { - return consentList.state(address) == ConsentState.ALLOWED - } - - suspend fun isDenied(address: String): Boolean { - return consentList.state(address) == ConsentState.DENIED - } - - suspend fun isGroupAllowed(groupId: String): Boolean { - return consentList.groupState(groupId) == ConsentState.ALLOWED - } - - suspend fun isGroupDenied(groupId: String): Boolean { - return consentList.groupState(groupId) == ConsentState.DENIED - } - - suspend fun isInboxAllowed(inboxId: String): Boolean { - return consentList.inboxIdState(inboxId) == ConsentState.ALLOWED - } - - suspend fun isInboxDenied(inboxId: String): Boolean { - return consentList.inboxIdState(inboxId) == ConsentState.DENIED - } - - fun has(peerAddress: String): Boolean = knownBundles[peerAddress] != null - - fun needsIntroduction(peerAddress: String): Boolean = hasIntroduced[peerAddress] != true - - fun find(peerAddress: String): ContactBundle? { - val knownBundle = knownBundles[peerAddress] - if (knownBundle != null) { - return knownBundle - } - val response = runBlocking { client.query(topic = Topic.contact(peerAddress)) } - - if (response.envelopesList.isNullOrEmpty()) return null - - for (envelope in response.envelopesList) { - val contactBundle = ContactBundleBuilder.buildFromEnvelope(envelope) - knownBundles[peerAddress] = contactBundle - val address = contactBundle.walletAddress - if (address?.lowercase() == peerAddress.lowercase()) { - return contactBundle - } - } - - return null - } -} diff --git a/library/src/main/java/org/xmtp/android/library/Conversation.kt b/library/src/main/java/org/xmtp/android/library/Conversation.kt index fa48a04a3..8177e858e 100644 --- a/library/src/main/java/org/xmtp/android/library/Conversation.kt +++ b/library/src/main/java/org/xmtp/android/library/Conversation.kt @@ -1,53 +1,27 @@ package org.xmtp.android.library -import android.util.Log -import com.google.protobuf.kotlin.toByteString import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.runBlocking -import org.xmtp.android.library.codecs.EncodedContent import org.xmtp.android.library.libxmtp.Member -import org.xmtp.android.library.libxmtp.MessageV3 -import org.xmtp.android.library.messages.DecryptedMessage -import org.xmtp.android.library.messages.Envelope -import org.xmtp.android.library.messages.PagingInfoSortDirection -import org.xmtp.proto.keystore.api.v1.Keystore.TopicMap.TopicData -import org.xmtp.proto.message.api.v1.MessageApiOuterClass -import org.xmtp.proto.message.contents.Invitation -import org.xmtp.proto.message.contents.Invitation.ConsentProofPayload -import org.xmtp.proto.message.contents.Invitation.InvitationV1.Aes256gcmHkdfsha256 +import org.xmtp.android.library.libxmtp.Message import java.util.Date -/** - * This represents an ongoing conversation. - * It can be provided to [Client] to [messages] and [send]. - * The [Client] also allows you to [streamMessages] from this [Conversation]. - * - * It attempts to give uniform shape to v1 and v2 conversations. - */ sealed class Conversation { - data class V1(val conversationV1: ConversationV1) : Conversation() - data class V2(val conversationV2: ConversationV2) : Conversation() - data class Group(val group: org.xmtp.android.library.Group) : Conversation() data class Dm(val dm: org.xmtp.android.library.Dm) : Conversation() - enum class Version { V1, V2, GROUP, DM } + enum class Type { GROUP, DM } - val version: Version + val type: Type get() { return when (this) { - is V1 -> Version.V1 - is V2 -> Version.V2 - is Group -> Version.GROUP - is Dm -> Version.DM + is Group -> Type.GROUP + is Dm -> Type.DM } } val id: String get() { return when (this) { - is V1 -> throw XMTPException("Only supported for V3") - is V2 -> throw XMTPException("Only supported for V3") is Group -> group.id is Dm -> dm.id } @@ -56,8 +30,6 @@ sealed class Conversation { val topic: String get() { return when (this) { - is V1 -> conversationV1.topic.description - is V2 -> conversationV2.topic is Group -> group.topic is Dm -> dm.topic } @@ -66,53 +38,34 @@ sealed class Conversation { val createdAt: Date get() { return when (this) { - is V1 -> conversationV1.sentAt - is V2 -> conversationV2.createdAt is Group -> group.createdAt is Dm -> dm.createdAt } } - fun isCreator(): Boolean { - return when (this) { - is V1 -> throw XMTPException("Only supported for V3") - is V2 -> throw XMTPException("Only supported for V3") - is Group -> group.isCreator() - is Dm -> dm.isCreator() - } - } - suspend fun members(): List { return when (this) { - is V1 -> throw XMTPException("Only supported for V3") - is V2 -> throw XMTPException("Only supported for V3") is Group -> group.members() is Dm -> dm.members() } } - suspend fun updateConsentState(state: ConsentState) { + fun updateConsentState(state: ConsentState) { return when (this) { - is V1 -> throw XMTPException("Only supported for V3") - is V2 -> throw XMTPException("Only supported for V3") is Group -> group.updateConsentState(state) is Dm -> dm.updateConsentState(state) } } - suspend fun consentState(): ConsentState { + fun consentState(): ConsentState { return when (this) { - is V1 -> conversationV1.client.contacts.consentList.state(address = peerAddress) - is V2 -> conversationV2.client.contacts.consentList.state(address = peerAddress) is Group -> group.consentState() is Dm -> dm.consentState() } } - suspend fun prepareMessageV3(content: T, options: SendOptions? = null): String { + fun prepareMessage(content: T, options: SendOptions? = null): String { return when (this) { - is V1 -> throw XMTPException("Only supported for V3") - is V2 -> throw XMTPException("Only supported for V3") is Group -> group.prepareMessage(content, options) is Dm -> dm.prepareMessage(content, options) } @@ -120,344 +73,57 @@ sealed class Conversation { suspend fun send(content: T, options: SendOptions? = null): String { return when (this) { - is V1 -> conversationV1.send(content = content, options = options) - is V2 -> conversationV2.send(content = content, options = options) is Group -> group.send(content = content, options = options) is Dm -> dm.send(content = content, options = options) } } - suspend fun send(text: String, sendOptions: SendOptions? = null, sentAt: Date? = null): String { + suspend fun send(text: String): String { return when (this) { - is V1 -> conversationV1.send(text = text, sendOptions, sentAt) - is V2 -> conversationV2.send(text = text, sendOptions, sentAt) is Group -> group.send(text) is Dm -> dm.send(text) } } - suspend fun send(encodedContent: EncodedContent, options: SendOptions? = null): String { - return when (this) { - is V1 -> conversationV1.send(encodedContent = encodedContent, options = options) - is V2 -> conversationV2.send(encodedContent = encodedContent, options = options) - is Group -> group.send(encodedContent = encodedContent) - is Dm -> dm.send(encodedContent = encodedContent) - } - } - suspend fun sync() { return when (this) { - is V1 -> throw XMTPException("Only supported for V3") - is V2 -> throw XMTPException("Only supported for V3") is Group -> group.sync() is Dm -> dm.sync() } } - /** - * This lists messages sent to the [Conversation]. - * @param before initial date to filter - * @param after final date to create a range of dates and filter - * @param limit is the number of result that will be returned - * @param direction is the way of srting the information, by default is descending, you can - * know more about it in class [MessageApiOuterClass]. - * @see MessageApiOuterClass.SortDirection - * @return The list of messages sent. If [before] or [after] are specified then this will only list messages - * sent at or [after] and at or [before]. - * If [limit] is specified then results are pulled in pages of that size. - * If [direction] is specified then that will control the sort order of te messages. - */ - suspend fun messages( + fun messages( limit: Int? = null, beforeNs: Long? = null, afterNs: Long? = null, - direction: PagingInfoSortDirection = MessageApiOuterClass.SortDirection.SORT_DIRECTION_DESCENDING, + direction: Message.SortDirection = Message.SortDirection.DESCENDING, + deliveryStatus: Message.MessageDeliveryStatus = Message.MessageDeliveryStatus.ALL, ): List { return when (this) { - is V1 -> conversationV1.messages( - limit = limit, - before = beforeNs?.let { Date(it / 1_000_000) }, - after = afterNs?.let { Date(it / 1_000_000) }, - direction = direction, - ) - - is V2 -> - conversationV2.messages( - limit = limit, - before = beforeNs?.let { Date(it / 1_000_000) }, - after = afterNs?.let { Date(it / 1_000_000) }, - direction = direction, - ) - - is Group -> { - group.messages( - limit = limit, - beforeNs = beforeNs, - afterNs = afterNs, - direction = direction, - ) - } - - is Dm -> dm.messages(limit, beforeNs, afterNs, direction) - } - } - - suspend fun decryptedMessages( - limit: Int? = null, - beforeNs: Long? = null, - afterNs: Long? = null, - direction: PagingInfoSortDirection = MessageApiOuterClass.SortDirection.SORT_DIRECTION_DESCENDING, - ): List { - return when (this) { - is V1 -> conversationV1.decryptedMessages( - limit = limit, - before = beforeNs?.let { Date(it / 1_000_000) }, - after = afterNs?.let { Date(it / 1_000_000) }, - direction = direction, - ) - - is V2 -> - conversationV2.decryptedMessages( - limit = limit, - before = beforeNs?.let { Date(it / 1_000_000) }, - after = afterNs?.let { Date(it / 1_000_000) }, - direction = direction, - ) - - is Group -> { - group.decryptedMessages( - limit = limit, - beforeNs = beforeNs, - afterNs = afterNs, - direction = direction, - ) - } - - is Dm -> dm.decryptedMessages(limit, beforeNs, afterNs, direction) - } - } - - fun decryptV3( - message: MessageV3, - ): DecryptedMessage { - return when (this) { - is V1 -> throw XMTPException("Only supported for V3") - is V2 -> throw XMTPException("Only supported for V3") - is Group -> message.decrypt() - is Dm -> message.decrypt() - } - } - - fun decodeV3(message: MessageV3): DecodedMessage { - return when (this) { - is V1 -> throw XMTPException("Only supported for V3") - is V2 -> throw XMTPException("Only supported for V3") - is Group -> message.decode() - is Dm -> message.decode() + is Group -> group.messages(limit, beforeNs, afterNs, direction, deliveryStatus) + is Dm -> dm.messages(limit, beforeNs, afterNs, direction, deliveryStatus) } } - suspend fun processMessage(envelopeBytes: ByteArray): MessageV3 { + suspend fun processMessage(messageBytes: ByteArray): Message { return when (this) { - is V1 -> throw XMTPException("Only supported for V3") - is V2 -> throw XMTPException("Only supported for V3") - is Group -> group.processMessage(envelopeBytes) - is Dm -> dm.processMessage(envelopeBytes) + is Group -> group.processMessage(messageBytes) + is Dm -> dm.processMessage(messageBytes) } } - val consentProof: ConsentProofPayload? - get() { - return when (this) { - is V1 -> return null - is V2 -> conversationV2.consentProof - is Group -> return null - is Dm -> return null - } - } - - // Get the client according to the version val client: Client get() { return when (this) { - is V1 -> conversationV1.client - is V2 -> conversationV2.client is Group -> group.client is Dm -> dm.client } } - /** - * This exposes a stream of new messages sent to the [Conversation]. - * @return Stream of messages according to the version - */ fun streamMessages(): Flow { return when (this) { - is V1 -> conversationV1.streamMessages() - is V2 -> conversationV2.streamMessages() is Group -> group.streamMessages() is Dm -> dm.streamMessages() } } - - fun streamDecryptedMessages(): Flow { - return when (this) { - is V1 -> conversationV1.streamDecryptedMessages() - is V2 -> conversationV2.streamDecryptedMessages() - is Group -> group.streamDecryptedMessages() - is Dm -> dm.streamDecryptedMessages() - } - } - - // ------- V1 V2 to be deprecated ------ - - fun decrypt( - envelope: Envelope, - ): DecryptedMessage { - return when (this) { - is V1 -> conversationV1.decrypt(envelope) - is V2 -> conversationV2.decrypt(envelope) - is Group -> throw XMTPException("Use decryptV3 instead") - is Dm -> throw XMTPException("Use decryptV3 instead") - } - } - - fun decode(envelope: Envelope): DecodedMessage { - return when (this) { - is V1 -> conversationV1.decode(envelope) - is V2 -> conversationV2.decodeEnvelope(envelope) - is Group -> throw XMTPException("Use decodeV3 instead") - is Dm -> throw XMTPException("Use decodeV3 instead") - } - } - - // This is the address of the peer that I am talking to. - val peerAddress: String - get() { - return when (this) { - is V1 -> conversationV1.peerAddress - is V2 -> conversationV2.peerAddress - is Group -> runBlocking { group.peerInboxIds().joinToString(",") } - is Dm -> dm.peerInboxId - } - } - - val peerAddresses: List - get() { - return when (this) { - is V1 -> listOf(conversationV1.peerAddress) - is V2 -> listOf(conversationV2.peerAddress) - is Group -> runBlocking { group.peerInboxIds() } - is Dm -> listOf(dm.peerInboxId) - } - } - - // This distinctly identifies between two addresses. - // Note: this will be empty for older v1 conversations. - val conversationId: String? - get() { - return when (this) { - is V1 -> null - is V2 -> conversationV2.context.conversationId - is Group -> null - is Dm -> null - } - } - - val keyMaterial: ByteArray? - get() { - return when (this) { - is V1 -> null - is V2 -> conversationV2.keyMaterial - is Group -> null - is Dm -> null - } - } - - /** - * This method is to create a TopicData object - * @return [TopicData] that contains all the information about the Topic, the conversation - * context and the necessary encryption data for it. - */ - fun toTopicData(): TopicData { - val data = TopicData.newBuilder() - .setCreatedNs(createdAt.time * 1_000_000) - .setPeerAddress(peerAddress) - return when (this) { - is V1 -> data.build() - is V2 -> data.setInvitation( - Invitation.InvitationV1.newBuilder() - .setTopic(topic) - .setContext(conversationV2.context) - .setAes256GcmHkdfSha256( - Aes256gcmHkdfsha256.newBuilder() - .setKeyMaterial(conversationV2.keyMaterial.toByteString()), - ), - ).build() - - is Group -> throw XMTPException("Groups do not support topics") - is Dm -> throw XMTPException("DMs do not support topics") - } - } - - fun decodeOrNull(envelope: Envelope): DecodedMessage? { - return try { - decode(envelope) - } catch (e: Exception) { - Log.d("CONVERSATION", "discarding message that failed to decode", e) - null - } - } - - fun prepareMessage(content: T, options: SendOptions? = null): PreparedMessage { - return when (this) { - is V1 -> conversationV1.prepareMessage(content = content, options = options) - is V2 -> conversationV2.prepareMessage(content = content, options = options) - is Group -> throw XMTPException("Use prepareMessageV3 instead") - is Dm -> throw XMTPException("Use prepareMessageV3 instead") - } - } - - fun prepareMessage( - encodedContent: EncodedContent, - options: SendOptions? = null, - ): PreparedMessage { - return when (this) { - is V1 -> conversationV1.prepareMessage( - encodedContent = encodedContent, - options = options - ) - - is V2 -> conversationV2.prepareMessage( - encodedContent = encodedContent, - options = options - ) - - is Group -> throw XMTPException("Use prepareMessageV3 instead") - is Dm -> throw XMTPException("Use prepareMessageV3 instead") - } - } - - suspend fun send(prepared: PreparedMessage): String { - return when (this) { - is V1 -> conversationV1.send(prepared = prepared) - is V2 -> conversationV2.send(prepared = prepared) - is Group -> throw XMTPException("Groups do not support sending prepared messages call sync instead") - is Dm -> throw XMTPException("DMs do not support sending prepared messages call sync instead") - } - } - - val clientAddress: String - get() { - return client.address - } - - fun streamEphemeral(): Flow { - return when (this) { - is V1 -> return conversationV1.streamEphemeral() - is V2 -> return conversationV2.streamEphemeral() - is Group -> throw XMTPException("Groups do not support ephemeral messages") - is Dm -> throw XMTPException("DMs do not support ephemeral messages") - } - } } diff --git a/library/src/main/java/org/xmtp/android/library/ConversationExport.kt b/library/src/main/java/org/xmtp/android/library/ConversationExport.kt deleted file mode 100644 index 2349b0a87..000000000 --- a/library/src/main/java/org/xmtp/android/library/ConversationExport.kt +++ /dev/null @@ -1,21 +0,0 @@ -package org.xmtp.android.library - -data class ConversationV1Export( - var version: String, - var peerAddress: String, - var createdAt: String, -) - -data class ConversationV2Export( - var version: String, - var topic: String, - var keyMaterial: String, - var peerAddress: String, - var createdAt: String, - var context: ConversationV2ContextExport? = null, -) - -data class ConversationV2ContextExport( - var conversationId: String, - var metadata: Map, -) diff --git a/library/src/main/java/org/xmtp/android/library/ConversationV1.kt b/library/src/main/java/org/xmtp/android/library/ConversationV1.kt deleted file mode 100644 index ef0cf66d2..000000000 --- a/library/src/main/java/org/xmtp/android/library/ConversationV1.kt +++ /dev/null @@ -1,310 +0,0 @@ -package org.xmtp.android.library - -import android.util.Log -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.launch -import org.web3j.crypto.Hash -import org.xmtp.android.library.Util.Companion.envelopeFromFFi -import org.xmtp.android.library.codecs.ContentCodec -import org.xmtp.android.library.codecs.EncodedContent -import org.xmtp.android.library.codecs.compress -import org.xmtp.android.library.messages.DecryptedMessage -import org.xmtp.android.library.messages.Envelope -import org.xmtp.android.library.messages.EnvelopeBuilder -import org.xmtp.android.library.messages.Message -import org.xmtp.android.library.messages.MessageBuilder -import org.xmtp.android.library.messages.MessageV1Builder -import org.xmtp.android.library.messages.Pagination -import org.xmtp.android.library.messages.PagingInfoSortDirection -import org.xmtp.android.library.messages.Topic -import org.xmtp.android.library.messages.decrypt -import org.xmtp.android.library.messages.header -import org.xmtp.android.library.messages.sentAt -import org.xmtp.android.library.messages.toPublicKeyBundle -import org.xmtp.android.library.messages.walletAddress -import org.xmtp.proto.message.api.v1.MessageApiOuterClass -import uniffi.xmtpv3.FfiEnvelope -import uniffi.xmtpv3.FfiV2SubscriptionCallback -import java.util.Date - -data class ConversationV1( - val client: Client, - val peerAddress: String, - val sentAt: Date, -) { - - val topic: Topic - get() = Topic.directMessageV1(client.address, peerAddress) - - /** - * Get the stream of all messages of the current [Client] - * @return Flow object of [DecodedMessage] that represents all the messages of the - * current [Client] as userInvite and userIntro - * @see Conversations.streamAllMessages - */ - fun streamMessages(): Flow = callbackFlow { - val streamCallback = object : FfiV2SubscriptionCallback { - override fun onMessage(message: FfiEnvelope) { - trySend(decode(envelope = envelopeFromFFi(message))) - } - } - val stream = client.subscribe(listOf(topic.description), streamCallback) - awaitClose { launch { stream.end() } } - } - - /** - * This lists messages sent to the [Conversation]. - * @param before initial date to filter - * @param after final date to create a range of dates and filter - * @param limit is the number of result that will be returned - * @param direction is the way of srting the information, by default is descending, you can - * know more about it in class [MessageApiOuterClass]. - * @see MessageApiOuterClass.SortDirection - * @return The list of messages sent. If [before] or [after] are specified then this will only list messages - * sent at or [after] and at or [before]. - * If [limit] is specified then results are pulled in pages of that size. - * If [direction] is specified then that will control the sort order of te messages. - * @see Conversation.messages - */ - suspend fun messages( - limit: Int? = null, - before: Date? = null, - after: Date? = null, - direction: PagingInfoSortDirection = MessageApiOuterClass.SortDirection.SORT_DIRECTION_DESCENDING, - ): List { - val pagination = - Pagination(limit = limit, before = before, after = after, direction = direction) - val apiClient = client.apiClient ?: throw XMTPException("V2 only function") - val result = apiClient.envelopes(topic = topic.description, pagination = pagination) - - return result.mapNotNull { envelope -> - decodeOrNull(envelope = envelope) - } - } - - /** - * This lists decrypted messages sent to the [Conversation]. - * @param before initial date to filter - * @param after final date to create a range of dates and filter - * @param limit is the number of result that will be returned - * @param direction is the way of srting the information, by default is descending, you can - * know more about it in class [MessageApiOuterClass]. - * @see MessageApiOuterClass.SortDirection - * @return The list of messages sent. If [before] or [after] are specified then this will only list messages - * sent at or [after] and at or [before]. - * If [limit] is specified then results are pulled in pages of that size. - * If [direction] is specified then that will control the sort order of te messages. - */ - suspend fun decryptedMessages( - limit: Int? = null, - before: Date? = null, - after: Date? = null, - direction: PagingInfoSortDirection = MessageApiOuterClass.SortDirection.SORT_DIRECTION_DESCENDING, - ): List { - val pagination = - Pagination(limit = limit, before = before, after = after, direction = direction) - - val apiClient = client.apiClient ?: throw XMTPException("V2 only function") - val envelopes = - apiClient.envelopes( - topic = Topic.directMessageV1(client.address, peerAddress).description, - pagination = pagination, - ) - - return envelopes.map { decrypt(it) } - } - - /** - * This decrypts a message - * @param envelope Object that contains all the information of the encrypted message - * @return [DecryptedMessage] object - */ - fun decrypt(envelope: Envelope): DecryptedMessage { - try { - val message = Message.parseFrom(envelope.message) - val decrypted = message.v1.decrypt(client.privateKeyBundleV1) - - val encodedMessage = EncodedContent.parseFrom(decrypted) - val header = message.v1.header - - return DecryptedMessage( - id = generateId(envelope), - encodedContent = encodedMessage, - senderAddress = header.sender.walletAddress, - sentAt = message.v1.sentAt, - ) - } catch (e: Exception) { - throw XMTPException("Error decrypting message", e) - } - } - - /** - * This encrypts a message - * @param envelope Object that contains all the information of the decrypted message - * @return [DecodedMessage] object - */ - fun decode(envelope: Envelope): DecodedMessage { - try { - val decryptedMessage = decrypt(envelope) - - return DecodedMessage( - id = generateId(envelope), - client = client, - topic = envelope.contentTopic, - encodedContent = decryptedMessage.encodedContent, - senderAddress = decryptedMessage.senderAddress, - sent = decryptedMessage.sentAt, - ) - } catch (e: Exception) { - throw XMTPException("Error decoding message", e) - } - } - - private fun decodeOrNull(envelope: Envelope): DecodedMessage? { - return try { - decode(envelope) - } catch (e: Exception) { - Log.d("CONV_V1", "discarding message that failed to decode", e) - null - } - } - - suspend fun send(text: String, options: SendOptions? = null): String { - return send(text = text, sendOptions = options, sentAt = null) - } - - internal suspend fun send( - text: String, - sendOptions: SendOptions? = null, - sentAt: Date? = null, - ): String { - val preparedMessage = prepareMessage(content = text, options = sendOptions) - return send(preparedMessage) - } - - suspend fun send(content: T, options: SendOptions? = null): String { - val preparedMessage = prepareMessage(content = content, options = options) - return send(preparedMessage) - } - - suspend fun send(encodedContent: EncodedContent, options: SendOptions? = null): String { - val preparedMessage = prepareMessage(encodedContent = encodedContent, options = options) - return send(preparedMessage) - } - - suspend fun send(prepared: PreparedMessage): String { - if (client.v3Client != null) { - try { - val dm = client.conversations.findOrCreateDm(peerAddress) - prepared.encodedContent?.let { - dm.send(it) - } - } catch (e: Exception) { - Log.e("ConversationV1 send", e.message.toString()) - } - } - client.publish(envelopes = prepared.envelopes) - if (client.contacts.consentList.state(address = peerAddress) == ConsentState.UNKNOWN) { - client.contacts.allow(addresses = listOf(peerAddress)) - } - return prepared.messageId - } - - fun prepareMessage(content: T, options: SendOptions?): PreparedMessage { - val codec = Client.codecRegistry.find(options?.contentType) - - fun > encode(codec: Codec, content: Any?): EncodedContent { - val contentType = content as? T - if (contentType != null) { - return codec.encode(content = contentType) - } else { - throw XMTPException("Codec type is not registered") - } - } - - var encoded = encode(codec = codec as ContentCodec, content = content) - - val fallback = codec.fallback(content) - if (!fallback.isNullOrBlank()) { - encoded = encoded.toBuilder().also { - it.fallback = fallback - }.build() - } - val compression = options?.compression - if (compression != null) { - encoded = encoded.compress(compression) - } - return prepareMessage(encodedContent = encoded, options = options) - } - - fun prepareMessage( - encodedContent: EncodedContent, - options: SendOptions? = null, - ): PreparedMessage { - val contact = client.contacts.find(peerAddress) ?: throw XMTPException("address not found") - val recipient = contact.toPublicKeyBundle() - if (!recipient.identityKey.hasSignature()) { - throw Exception("no signature for id key") - } - val date = Date() - val message = MessageV1Builder.buildEncode( - sender = client.v1keys, - recipient = recipient, - message = encodedContent.toByteArray(), - timestamp = date, - ) - - val isEphemeral: Boolean = options != null && options.ephemeral - - val env = - EnvelopeBuilder.buildFromString( - topic = if (isEphemeral) ephemeralTopic else topic.description, - timestamp = date, - message = MessageBuilder.buildFromMessageV1(v1 = message).toByteArray(), - ) - - val envelopes = mutableListOf(env) - if (client.contacts.needsIntroduction(peerAddress) && !isEphemeral) { - envelopes.addAll( - listOf( - env.toBuilder().apply { - contentTopic = Topic.userIntro(peerAddress).description - }.build(), - env.toBuilder().apply { - contentTopic = Topic.userIntro(client.address).description - }.build(), - ), - ) - client.contacts.hasIntroduced[peerAddress] = true - } - return PreparedMessage(envelopes, encodedContent) - } - - private fun generateId(envelope: Envelope): String = - Hash.sha256(envelope.message.toByteArray()).toHex() - - val ephemeralTopic: String - get() = topic.description.replace("/xmtp/0/dm-", "/xmtp/0/dmE-") - - fun streamEphemeral(): Flow = callbackFlow { - val streamCallback = object : FfiV2SubscriptionCallback { - override fun onMessage(message: FfiEnvelope) { - trySend(envelopeFromFFi(message)) - } - } - val stream = client.subscribe(listOf(ephemeralTopic), streamCallback) - awaitClose { launch { stream.end() } } - } - - fun streamDecryptedMessages(): Flow = callbackFlow { - val streamCallback = object : FfiV2SubscriptionCallback { - override fun onMessage(message: FfiEnvelope) { - trySend(decrypt(envelope = envelopeFromFFi(message))) - } - } - val stream = client.subscribe(listOf(topic.description), streamCallback) - awaitClose { launch { stream.end() } } - } -} diff --git a/library/src/main/java/org/xmtp/android/library/ConversationV2.kt b/library/src/main/java/org/xmtp/android/library/ConversationV2.kt deleted file mode 100644 index 90a1b80d0..000000000 --- a/library/src/main/java/org/xmtp/android/library/ConversationV2.kt +++ /dev/null @@ -1,311 +0,0 @@ -package org.xmtp.android.library - -import android.util.Log -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.launch -import org.web3j.crypto.Hash -import org.xmtp.android.library.codecs.ContentCodec -import org.xmtp.android.library.codecs.EncodedContent -import org.xmtp.android.library.codecs.compress -import org.xmtp.android.library.messages.DecryptedMessage -import org.xmtp.android.library.messages.Envelope -import org.xmtp.android.library.messages.EnvelopeBuilder -import org.xmtp.android.library.messages.Message -import org.xmtp.android.library.messages.MessageBuilder -import org.xmtp.android.library.messages.MessageV2Builder -import org.xmtp.android.library.messages.Pagination -import org.xmtp.android.library.messages.PagingInfoSortDirection -import org.xmtp.android.library.messages.SealedInvitationHeaderV1 -import org.xmtp.android.library.messages.getPublicKeyBundle -import org.xmtp.android.library.messages.walletAddress -import org.xmtp.proto.message.api.v1.MessageApiOuterClass -import org.xmtp.proto.message.contents.Invitation -import uniffi.xmtpv3.FfiEnvelope -import uniffi.xmtpv3.FfiV2SubscriptionCallback -import java.util.Date - -data class ConversationV2( - val topic: String, - val keyMaterial: ByteArray, - val context: Invitation.InvitationV1.Context, - var consentProof: Invitation.ConsentProofPayload? = null, - val peerAddress: String, - val client: Client, - val createdAtNs: Long? = null, - private val header: SealedInvitationHeaderV1, -) { - - companion object { - fun create( - client: Client, - invitation: Invitation.InvitationV1, - header: SealedInvitationHeaderV1, - ): ConversationV2 { - val myKeys = client.keys.getPublicKeyBundle() - val peer = - if (myKeys.walletAddress == (header.sender.walletAddress)) header.recipient else header.sender - val peerAddress = peer.walletAddress - val keyMaterial = invitation.aes256GcmHkdfSha256.keyMaterial.toByteArray() - return ConversationV2( - topic = invitation.topic, - keyMaterial = keyMaterial, - context = invitation.context, - peerAddress = peerAddress, - client = client, - createdAtNs = header.createdNs, - header = header, - consentProof = if (invitation.hasConsentProof()) invitation.consentProof else null - ) - } - } - - val createdAt: Date = Date((createdAtNs ?: header.createdNs) / 1_000_000) - - /** - * This lists messages sent to the [Conversation]. - * @param before initial date to filter - * @param after final date to create a range of dates and filter - * @param limit is the number of result that will be returned - * @param direction is the way of srting the information, by default is descending, you can - * know more about it in class [MessageApiOuterClass]. - * @see MessageApiOuterClass.SortDirection - * @return The list of messages sent. If [before] or [after] are specified then this will only list messages - * sent at or [after] and at or [before]. - * If [limit] is specified then results are pulled in pages of that size. - * If [direction] is specified then that will control the sort order of te messages. - * @see Conversation.messages - */ - suspend fun messages( - limit: Int? = null, - before: Date? = null, - after: Date? = null, - direction: PagingInfoSortDirection = MessageApiOuterClass.SortDirection.SORT_DIRECTION_DESCENDING, - ): List { - val pagination = - Pagination(limit = limit, before = before, after = after, direction = direction) - val apiClient = client.apiClient ?: throw XMTPException("V2 only function") - val result = - apiClient.envelopes( - topic = topic, - pagination = pagination, - ) - - return result.mapNotNull { envelope -> - decodeEnvelopeOrNull(envelope) - } - } - - /** - * This lists decrypted messages sent to the [Conversation]. - * @param before initial date to filter - * @param after final date to create a range of dates and filter - * @param limit is the number of result that will be returned - * @param direction is the way of srting the information, by default is descending, you can - * know more about it in class [MessageApiOuterClass]. - * @see MessageApiOuterClass.SortDirection - * @return The list of messages sent. If [before] or [after] are specified then this will only list messages - * sent at or [after] and at or [before]. - * If [limit] is specified then results are pulled in pages of that size. - * If [direction] is specified then that will control the sort order of te messages. - */ - suspend fun decryptedMessages( - limit: Int? = null, - before: Date? = null, - after: Date? = null, - direction: PagingInfoSortDirection = MessageApiOuterClass.SortDirection.SORT_DIRECTION_DESCENDING, - ): List { - val pagination = - Pagination(limit = limit, before = before, after = after, direction = direction) - val apiClient = client.apiClient ?: throw XMTPException("V2 only function") - val envelopes = apiClient.envelopes(topic, pagination) - - return envelopes.map { envelope -> - decrypt(envelope) - } - } - - /** - * This decrypts a message - * @param envelope Object that contains all the information of the encrypted message - * @return [DecryptedMessage] object - */ - fun decrypt(envelope: Envelope): DecryptedMessage { - val message = Message.parseFrom(envelope.message) - return MessageV2Builder.buildDecrypt( - id = generateId(envelope = envelope), - topic, - message.v2, - keyMaterial, - client, - ) - } - - fun streamMessages(): Flow = callbackFlow { - val streamCallback = object : FfiV2SubscriptionCallback { - override fun onMessage(message: FfiEnvelope) { - decodeEnvelopeOrNull(envelope = Util.envelopeFromFFi(message))?.let { - trySend(it) - } - } - } - val stream = client.subscribe(listOf(topic), streamCallback) - awaitClose { launch { stream.end() } } - } - - /** - * This encrypts a message - * @param envelope Object that contains all the information of the decrypted message - * @return [DecodedMessage] object - */ - fun decodeEnvelope(envelope: Envelope): DecodedMessage { - val message = Message.parseFrom(envelope.message) - return MessageV2Builder.buildDecode( - generateId(envelope = envelope), - topic = topic, - message.v2, - keyMaterial = keyMaterial, - client = client, - ) - } - - /** - * This encrypts a message - * @param envelope Object that contains all the information of the decrypted message - * @return [DecodedMessage] object if is not possible will return null - */ - private fun decodeEnvelopeOrNull(envelope: Envelope): DecodedMessage? { - return try { - decodeEnvelope(envelope) - } catch (e: Exception) { - Log.d("CONV_V2", "discarding message that failed to decode", e) - null - } - } - - suspend fun send(content: T, options: SendOptions? = null): String { - val preparedMessage = prepareMessage(content = content, options = options) - return send(preparedMessage) - } - - suspend fun send(text: String, options: SendOptions? = null, sentAt: Date? = null): String { - val preparedMessage = prepareMessage(content = text, options = options) - return send(preparedMessage) - } - - suspend fun send(encodedContent: EncodedContent, options: SendOptions?): String { - val preparedMessage = prepareMessage(encodedContent = encodedContent, options = options) - return send(preparedMessage) - } - - suspend fun send(prepared: PreparedMessage): String { - if (client.v3Client != null) { - try { - val dm = client.conversations.findOrCreateDm(peerAddress) - prepared.encodedContent?.let { - dm.send(it) - } - } catch (e: Exception) { - Log.e("ConversationV1 send", e.message.toString()) - } - } - client.publish(envelopes = prepared.envelopes) - if (client.contacts.consentList.state(address = peerAddress) == ConsentState.UNKNOWN) { - client.contacts.allow(addresses = listOf(peerAddress)) - } - return prepared.messageId - } - - fun , T> encode(codec: Codec, content: T): ByteArray { - val encodedContent = codec.encode(content = content) - val message = MessageV2Builder.buildEncode( - client = client, - encodedContent = encodedContent, - topic = topic, - keyMaterial = keyMaterial, - codec = codec, - ) - val envelope = EnvelopeBuilder.buildFromString( - topic = topic, - timestamp = Date(), - message = MessageBuilder.buildFromMessageV2(v2 = message.messageV2).toByteArray(), - ) - return envelope.toByteArray() - } - - fun prepareMessage(content: T, options: SendOptions?): PreparedMessage { - val codec = Client.codecRegistry.find(options?.contentType) - - fun > encode(codec: Codec, content: Any?): EncodedContent { - val contentType = content as? T - if (contentType != null) { - return codec.encode(contentType) - } else { - throw XMTPException("Codec type is not registered") - } - } - - var encoded = encode(codec = codec as ContentCodec, content = content) - val fallback = codec.fallback(content) - if (!fallback.isNullOrBlank()) { - encoded = encoded.toBuilder().also { - it.fallback = fallback - }.build() - } - val compression = options?.compression - if (compression != null) { - encoded = encoded.compress(compression) - } - return prepareMessage(encoded, options = options) - } - - fun prepareMessage( - encodedContent: EncodedContent, - options: SendOptions?, - ): PreparedMessage { - val codec = Client.codecRegistry.find(options?.contentType) - val message = MessageV2Builder.buildEncode( - client = client, - encodedContent = encodedContent, - topic = topic, - keyMaterial = keyMaterial, - codec = codec, - ) - - val newTopic = if (options?.ephemeral == true) ephemeralTopic else topic - - val envelope = EnvelopeBuilder.buildFromString( - topic = newTopic, - timestamp = Date(), - message = MessageBuilder.buildFromMessageV2(v2 = message.messageV2).toByteArray(), - ) - return PreparedMessage(listOf(envelope), encodedContent) - } - - private fun generateId(envelope: Envelope): String = - Hash.sha256(envelope.message.toByteArray()).toHex() - - val ephemeralTopic: String - get() = topic.replace("/xmtp/0/m", "/xmtp/0/mE") - - fun streamEphemeral(): Flow = callbackFlow { - val streamCallback = object : FfiV2SubscriptionCallback { - override fun onMessage(message: FfiEnvelope) { - trySend(Util.envelopeFromFFi(message)) - } - } - val stream = client.subscribe(listOf(ephemeralTopic), streamCallback) - awaitClose { launch { stream.end() } } - } - - fun streamDecryptedMessages(): Flow = callbackFlow { - val streamCallback = object : FfiV2SubscriptionCallback { - override fun onMessage(message: FfiEnvelope) { - trySend(decrypt(envelope = Util.envelopeFromFFi(message))) - } - } - val stream = client.subscribe(listOf(topic), streamCallback) - awaitClose { launch { stream.end() } } - } -} 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 8c9b65dcd..dd78f35ce 100644 --- a/library/src/main/java/org/xmtp/android/library/Conversations.kt +++ b/library/src/main/java/org/xmtp/android/library/Conversations.kt @@ -1,48 +1,16 @@ package org.xmtp.android.library import android.util.Log -import com.google.protobuf.kotlin.toByteString import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.merge -import kotlinx.coroutines.launch import org.xmtp.android.library.ConsentState.Companion.toFfiConsentState -import org.xmtp.android.library.GRPCApiClient.Companion.makeQueryRequest -import org.xmtp.android.library.Util.Companion.envelopeFromFFi -import org.xmtp.android.library.libxmtp.MessageV3 -import org.xmtp.android.library.messages.DecryptedMessage -import org.xmtp.android.library.messages.Envelope -import org.xmtp.android.library.messages.EnvelopeBuilder -import org.xmtp.android.library.messages.InvitationV1 -import org.xmtp.android.library.messages.MessageV1Builder -import org.xmtp.android.library.messages.Pagination -import org.xmtp.android.library.messages.SealedInvitation -import org.xmtp.android.library.messages.SealedInvitationBuilder -import org.xmtp.android.library.messages.SignedPublicKeyBundle -import org.xmtp.android.library.messages.Topic -import org.xmtp.android.library.messages.createDeterministic -import org.xmtp.android.library.messages.decrypt -import org.xmtp.android.library.messages.getInvitation -import org.xmtp.android.library.messages.header -import org.xmtp.android.library.messages.involves -import org.xmtp.android.library.messages.recipientAddress -import org.xmtp.android.library.messages.senderAddress -import org.xmtp.android.library.messages.sentAt -import org.xmtp.android.library.messages.toSignedPublicKeyBundle -import org.xmtp.android.library.messages.walletAddress -import org.xmtp.proto.keystore.api.v1.Keystore -import org.xmtp.proto.keystore.api.v1.Keystore.GetConversationHmacKeysResponse.HmacKeyData -import org.xmtp.proto.keystore.api.v1.Keystore.GetConversationHmacKeysResponse.HmacKeys -import org.xmtp.proto.keystore.api.v1.Keystore.TopicMap.TopicData -import org.xmtp.proto.message.contents.Contact -import org.xmtp.proto.message.contents.Invitation +import org.xmtp.android.library.libxmtp.Message import uniffi.xmtpv3.FfiConversation import uniffi.xmtpv3.FfiConversationCallback import uniffi.xmtpv3.FfiConversations import uniffi.xmtpv3.FfiCreateGroupOptions import uniffi.xmtpv3.FfiDirection -import uniffi.xmtpv3.FfiEnvelope import uniffi.xmtpv3.FfiGroupPermissionsOptions import uniffi.xmtpv3.FfiListConversationsOptions import uniffi.xmtpv3.FfiListMessagesOptions @@ -50,10 +18,6 @@ import uniffi.xmtpv3.FfiMessage import uniffi.xmtpv3.FfiMessageCallback import uniffi.xmtpv3.FfiPermissionPolicySet import uniffi.xmtpv3.FfiSubscribeException -import uniffi.xmtpv3.FfiV2SubscribeRequest -import uniffi.xmtpv3.FfiV2Subscription -import uniffi.xmtpv3.FfiV2SubscriptionCallback -import uniffi.xmtpv3.NoPointer import uniffi.xmtpv3.org.xmtp.android.library.libxmtp.GroupPermissionPreconfiguration import uniffi.xmtpv3.org.xmtp.android.library.libxmtp.PermissionPolicySet import java.util.Date @@ -62,35 +26,23 @@ import kotlin.time.DurationUnit data class Conversations( var client: Client, - var conversationsByTopic: MutableMap = mutableMapOf(), - private val libXMTPConversations: FfiConversations? = null, + private val ffiConversations: FfiConversations, ) { - companion object { - private const val TAG = "CONVERSATIONS" - } - enum class ConversationOrder { CREATED_AT, LAST_MESSAGE; } - suspend fun conversationFromWelcome(envelopeBytes: ByteArray): Conversation { - val conversation = libXMTPConversations?.processStreamedWelcomeMessage(envelopeBytes) - ?: throw XMTPException("Client does not support Groups") - if (conversation.groupMetadata().conversationType() == "dm") { - return Conversation.Dm(Dm(client, conversation)) + suspend fun fromWelcome(envelopeBytes: ByteArray): Conversation { + val conversation = ffiConversations.processStreamedWelcomeMessage(envelopeBytes) + return if (conversation.groupMetadata().conversationType() == "dm") { + Conversation.Dm(Dm(client, conversation)) } else { - return Conversation.Group(Group(client, conversation)) + Conversation.Group(Group(client, conversation)) } } - suspend fun fromWelcome(envelopeBytes: ByteArray): Group { - val group = libXMTPConversations?.processStreamedWelcomeMessage(envelopeBytes) - ?: throw XMTPException("Client does not support Groups") - return Group(client, group) - } - suspend fun newGroup( accountAddresses: List, permissions: GroupPermissionPreconfiguration = GroupPermissionPreconfiguration.ALL_MEMBERS, @@ -144,14 +96,14 @@ data class Conversations( throw XMTPException("Recipient is sender") } val falseAddresses = - if (accountAddresses.isNotEmpty()) client.canMessageV3(accountAddresses) + if (accountAddresses.isNotEmpty()) client.canMessage(accountAddresses) .filter { !it.value }.map { it.key } else emptyList() if (falseAddresses.isNotEmpty()) { throw XMTPException("${falseAddresses.joinToString()} not on network") } val group = - libXMTPConversations?.createGroup( + ffiConversations.createGroup( accountAddresses, opts = FfiCreateGroupOptions( permissions = permissions, @@ -161,35 +113,24 @@ data class Conversations( groupPinnedFrameUrl = groupPinnedFrameUrl, customPermissionPolicySet = permissionsPolicySet ) - ) ?: throw XMTPException("Client does not support Groups") - client.contacts.allowGroups(groupIds = listOf(group.id().toHex())) + ) return Group(client, group) } - // Sync from the network the latest list of groups - @Deprecated("Sync now includes DMs and Groups", replaceWith = ReplaceWith("syncConversations")) - suspend fun syncGroups() { - libXMTPConversations?.sync() - } - // Sync from the network the latest list of conversations suspend fun syncConversations() { - libXMTPConversations?.sync() + ffiConversations.sync() } // Sync all existing local conversation data from the network (Note: call syncConversations() first to get the latest list of conversations) - suspend fun syncAllConversations(): UInt? { - return libXMTPConversations?.syncAllConversations() + suspend fun syncAllConversations(): UInt { + return ffiConversations.syncAllConversations() } - // Sync all existing local groups data from the network (Note: call syncGroups() first to get the latest list of groups) - @Deprecated( - "Sync now includes DMs and Groups", - replaceWith = ReplaceWith("syncAllConversations") - ) - suspend fun syncAllGroups(): UInt? { - return libXMTPConversations?.syncAllConversations() + suspend fun newConversation(peerAddress: String): Conversation { + val dm = findOrCreateDm(peerAddress) + return Conversation.Dm(dm) } suspend fun findOrCreateDm(peerAddress: String): Dm { @@ -197,130 +138,30 @@ data class Conversations( throw XMTPException("Recipient is sender") } val falseAddresses = - client.canMessageV3(listOf(peerAddress)).filter { !it.value }.map { it.key } + client.canMessage(listOf(peerAddress)).filter { !it.value }.map { it.key } if (falseAddresses.isNotEmpty()) { throw XMTPException("${falseAddresses.joinToString()} not on network") } var dm = client.findDm(peerAddress) if (dm == null) { - val dmConversation = libXMTPConversations?.createDm(peerAddress.lowercase()) - ?: throw XMTPException("Client does not support V3 Dms") + val dmConversation = ffiConversations.createDm(peerAddress.lowercase()) dm = Dm(client, dmConversation) - client.contacts.allowGroups(groupIds = listOf(dm.id)) } return dm } - /** - * This creates a new [Conversation] using a specified address - * @param peerAddress The address of the client that you want to start a new conversation - * @param context Context of the invitation. - * @return New [Conversation] using the address and according to that address is able to find - * the topics if exists for that new conversation. - */ - suspend fun newConversation( - peerAddress: String, - context: Invitation.InvitationV1.Context? = null, - consentProof: Invitation.ConsentProofPayload? = null, - ): Conversation { - if (peerAddress.lowercase() == client.address.lowercase()) { - throw XMTPException("Recipient is sender") - } - val existingConversation = conversationsByTopic.values.firstOrNull { - it.peerAddress == peerAddress && it.conversationId == context?.conversationId - } - if (existingConversation != null) { - return existingConversation - } - val contact = client.contacts.find(peerAddress) - ?: throw XMTPException("Recipient not on network") - // See if we have an existing v1 convo - if (context?.conversationId.isNullOrEmpty()) { - val invitationPeers = listIntroductionPeers() - val peerSeenAt = invitationPeers[peerAddress] - if (peerSeenAt != null) { - val conversation = Conversation.V1( - ConversationV1( - client = client, - peerAddress = peerAddress, - sentAt = peerSeenAt, - ), - ) - conversationsByTopic[conversation.topic] = conversation - return conversation - } - } - - // If the contact is v1, start a v1 conversation - if (Contact.ContactBundle.VersionCase.V1 == contact.versionCase && context?.conversationId.isNullOrEmpty()) { - val conversation = Conversation.V1( - ConversationV1( - client = client, - peerAddress = peerAddress, - sentAt = Date(), - ), - ) - conversationsByTopic[conversation.topic] = conversation - return conversation - } - // See if we have a v2 conversation - for (sealedInvitation in listInvitations()) { - if (!sealedInvitation.involves(contact)) { - continue - } - val invite = sealedInvitation.v1.getInvitation(viewer = client.keys) - if (invite.context.conversationId == context?.conversationId && invite.context.conversationId != "") { - val conversation = Conversation.V2( - ConversationV2( - topic = invite.topic, - keyMaterial = invite.aes256GcmHkdfSha256.keyMaterial.toByteArray(), - context = invite.context, - peerAddress = peerAddress, - client = client, - header = sealedInvitation.v1.header, - consentProof = if (invite.hasConsentProof()) invite.consentProof else null - ), - ) - conversationsByTopic[conversation.topic] = conversation - return conversation - } - } - // We don't have an existing conversation, make a v2 one - val recipient = contact.toSignedPublicKeyBundle() - val invitation = Invitation.InvitationV1.newBuilder().build() - .createDeterministic(client.keys, recipient, context, consentProof) - val sealedInvitation = - sendInvitation(recipient = recipient, invitation = invitation, created = Date()) - val conversationV2 = ConversationV2.create( - client = client, - invitation = invitation, - header = sealedInvitation.v1.header, - ) - client.contacts.allow(addresses = listOf(peerAddress)) - val conversation = Conversation.V2(conversationV2) - conversationsByTopic[conversation.topic] = conversation - if (client.v3Client != null) { - try { - client.conversations.findOrCreateDm(peerAddress) - } catch (e: Exception) { - Log.e("newConversation", e.message.toString()) - } - } - return conversation - } - suspend fun listGroups( after: Date? = null, before: Date? = null, limit: Int? = null, ): List { - val ffiGroups = libXMTPConversations?.listGroups( + val ffiGroups = ffiConversations.listGroups( opts = FfiListConversationsOptions( after?.time?.nanoseconds?.toLong(DurationUnit.NANOSECONDS), before?.time?.nanoseconds?.toLong(DurationUnit.NANOSECONDS), limit?.toLong() ) - ) ?: throw XMTPException("Client does not support V3 dms") + ) return ffiGroups.map { Group(client, it) @@ -332,33 +173,33 @@ data class Conversations( before: Date? = null, limit: Int? = null, ): List { - val ffiDms = libXMTPConversations?.listDms( + val ffiDms = ffiConversations.listDms( opts = FfiListConversationsOptions( after?.time?.nanoseconds?.toLong(DurationUnit.NANOSECONDS), before?.time?.nanoseconds?.toLong(DurationUnit.NANOSECONDS), limit?.toLong() ) - ) ?: throw XMTPException("Client does not support V3 dms") + ) return ffiDms.map { Dm(client, it) } } - suspend fun listConversations( + suspend fun list( after: Date? = null, before: Date? = null, limit: Int? = null, order: ConversationOrder = ConversationOrder.CREATED_AT, consentState: ConsentState? = null, ): List { - val ffiConversations = libXMTPConversations?.list( + val ffiConversations = ffiConversations.list( FfiListConversationsOptions( after?.time?.nanoseconds?.toLong(DurationUnit.NANOSECONDS), before?.time?.nanoseconds?.toLong(DurationUnit.NANOSECONDS), limit?.toLong() ) - ) ?: throw XMTPException("Client does not support V3 dms") + ) val filteredConversations = filterByConsentState(ffiConversations, consentState) val sortedConversations = sortConversations(filteredConversations, order) @@ -413,661 +254,39 @@ data class Conversations( } } - /** - * Get the list of conversations that current user has - * @return The list of [Conversation] that the current [Client] has. - */ - suspend fun list(includeGroups: Boolean = false): List { - val newConversations = mutableListOf() - val mostRecent = conversationsByTopic.values.maxOfOrNull { it.createdAt } - val pagination = Pagination(after = mostRecent) - val seenPeers = listIntroductionPeers(pagination = pagination) - for ((peerAddress, sentAt) in seenPeers) { - newConversations.add( - Conversation.V1( - ConversationV1( - client = client, - peerAddress = peerAddress, - sentAt = sentAt, - ), - ), - ) - } - val invitations = listInvitations(pagination = pagination) - for (sealedInvitation in invitations) { - try { - val newConversation = Conversation.V2(conversation(sealedInvitation)) - newConversations.add(newConversation) - val consentProof = newConversation.consentProof - if (consentProof != null) { - handleConsentProof(consentProof, newConversation.peerAddress) - } - } catch (e: Exception) { - Log.d(TAG, e.message.toString()) - } - } - - conversationsByTopic += newConversations.filter { - it.peerAddress != client.address && Topic.isValidTopic(it.topic) - }.map { Pair(it.topic, it) } - - if (includeGroups) { - syncConversations() - val groups = listGroups() - conversationsByTopic += groups.map { Pair(it.topic, Conversation.Group(it)) } - } - return conversationsByTopic.values.sortedByDescending { it.createdAt } - } - - fun getHmacKeys( - request: Keystore.GetConversationHmacKeysRequest? = null, - ): Keystore.GetConversationHmacKeysResponse { - val thirtyDayPeriodsSinceEpoch = (Date().time / 1000 / 60 / 60 / 24 / 30).toInt() - val hmacKeysResponse = Keystore.GetConversationHmacKeysResponse.newBuilder() - - var topics = conversationsByTopic - - if (!request?.topicsList.isNullOrEmpty()) { - topics = topics.filter { - request!!.topicsList.contains(it.key) - }.toMutableMap() - } - - topics.iterator().forEach { - val conversation = it.value - val hmacKeys = HmacKeys.newBuilder() - if (conversation.keyMaterial != null) { - (thirtyDayPeriodsSinceEpoch - 1..thirtyDayPeriodsSinceEpoch + 1).iterator() - .forEach { value -> - val info = "$value-${client.address}" - val hmacKey = - Crypto.deriveKey( - conversation.keyMaterial!!, - ByteArray(0), - info.toByteArray(Charsets.UTF_8), - ) - val hmacKeyData = HmacKeyData.newBuilder() - hmacKeyData.hmacKey = hmacKey.toByteString() - hmacKeyData.thirtyDayPeriodsSinceEpoch = value - hmacKeys.addValues(hmacKeyData) - } - hmacKeysResponse.putHmacKeys(conversation.topic, hmacKeys.build()) - } - } - return hmacKeysResponse.build() - } - - private suspend fun handleConsentProof( - consentProof: Invitation.ConsentProofPayload, - peerAddress: String, - ) { - val signature = consentProof.signature - val timestamp = consentProof.timestamp - - if (!KeyUtil.validateConsentSignature(signature, client.address, peerAddress, timestamp)) { - return - } - val contacts = client.contacts - contacts.refreshConsentList() - if (contacts.consentList.state(peerAddress) == ConsentState.UNKNOWN) { - contacts.allow(listOf(peerAddress)) - } - } - - fun stream(): Flow = callbackFlow { - val streamedConversationTopics: MutableSet = mutableSetOf() - val subscriptionCallback = object : FfiV2SubscriptionCallback { - override fun onMessage(message: FfiEnvelope) { - val envelope = envelopeFromFFi(message) - if (envelope.contentTopic == Topic.userIntro(client.address).description) { - val conversationV1 = fromIntro(envelope = envelope) - if (!streamedConversationTopics.contains(conversationV1.topic)) { - streamedConversationTopics.add(conversationV1.topic) - trySend(conversationV1) - } - } - - if (envelope.contentTopic == Topic.userInvite(client.address).description) { - val conversationV2 = fromInvite(envelope = envelope) - if (!streamedConversationTopics.contains(conversationV2.topic)) { - streamedConversationTopics.add(conversationV2.topic) - trySend(conversationV2) - } - } - } - } - - val stream = client.subscribe2( - FfiV2SubscribeRequest( - listOf( - Topic.userIntro(client.address).description, - Topic.userInvite(client.address).description - ) - ), - subscriptionCallback - ) - - awaitClose { launch { stream.end() } } - } - - fun streamAll(): Flow { - return merge(streamGroupConversations(), stream()) - } - - fun streamConversations(): Flow = callbackFlow { - val conversationCallback = object : FfiConversationCallback { - override fun onConversation(conversation: FfiConversation) { - if (conversation.groupMetadata().conversationType() == "dm") { - trySend(Conversation.Dm(Dm(client, conversation))) - } else { - trySend(Conversation.Group(Group(client, conversation))) - } - } - - override fun onError(error: FfiSubscribeException) { - Log.e("XMTP Conversation stream", error.message.toString()) - } - } - val stream = libXMTPConversations?.stream(conversationCallback) - ?: throw XMTPException("Client does not support Groups") - awaitClose { stream.end() } - } - - private fun streamGroupConversations(): Flow = callbackFlow { - val groupCallback = object : FfiConversationCallback { - override fun onConversation(conversation: FfiConversation) { - trySend(Conversation.Group(Group(client, conversation))) - } - - override fun onError(error: FfiSubscribeException) { - Log.e("XMTP Conversation stream", error.message.toString()) - } - } - - val stream = libXMTPConversations?.streamGroups(groupCallback) - ?: throw XMTPException("Client does not support Groups") - awaitClose { stream.end() } - } - - fun streamGroups(): Flow = callbackFlow { - val groupCallback = object : FfiConversationCallback { - override fun onConversation(conversation: FfiConversation) { - trySend(Group(client, conversation)) - } - - override fun onError(error: FfiSubscribeException) { - Log.e("XMTP Group stream", error.message.toString()) - } - } - val stream = libXMTPConversations?.streamGroups(groupCallback) - ?: throw XMTPException("Client does not support Groups") - awaitClose { stream.end() } - } - - fun streamAllMessages(includeGroups: Boolean = false): Flow { - return if (includeGroups) { - merge(streamAllV2Messages(), streamAllGroupMessages()) - } else { - streamAllV2Messages() - } - } - - fun streamAllDecryptedMessages(includeGroups: Boolean = false): Flow { - return if (includeGroups) { - merge(streamAllV2DecryptedMessages(), streamAllGroupDecryptedMessages()) - } else { - streamAllV2DecryptedMessages() - } - } - - fun streamAllGroupMessages(): Flow = callbackFlow { - val messageCallback = object : FfiMessageCallback { - override fun onMessage(message: FfiMessage) { - val decodedMessage = MessageV3(client, message).decodeOrNull() - decodedMessage?.let { - trySend(it) - } - } - - override fun onError(error: FfiSubscribeException) { - Log.e("XMTP all group message stream", error.message.toString()) - } - } - val stream = libXMTPConversations?.streamAllGroupMessages(messageCallback) - ?: throw XMTPException("Client does not support Groups") - awaitClose { stream.end() } - } - - fun streamAllGroupDecryptedMessages(): Flow = callbackFlow { - val messageCallback = object : FfiMessageCallback { - override fun onMessage(message: FfiMessage) { - val decryptedMessage = MessageV3(client, message).decryptOrNull() - decryptedMessage?.let { - trySend(it) - } - } - - override fun onError(error: FfiSubscribeException) { - Log.e("XMTP all group message stream", error.message.toString()) - } - } - val stream = libXMTPConversations?.streamAllGroupMessages(messageCallback) - ?: throw XMTPException("Client does not support Groups") - awaitClose { stream.end() } - } - - fun streamAllConversationMessages(): Flow = callbackFlow { - val messageCallback = object : FfiMessageCallback { - override fun onMessage(message: FfiMessage) { - val conversation = client.findConversation(message.convoId.toHex()) - val decodedMessage = MessageV3(client, message).decodeOrNull() - when (conversation?.version) { - Conversation.Version.DM -> { - decodedMessage?.let { trySend(it) } - } - - else -> { - decodedMessage?.let { trySend(it) } + fun stream(/*Maybe Put a way to specify group, dm, or both?*/): Flow = + callbackFlow { + val conversationCallback = object : FfiConversationCallback { + override fun onConversation(conversation: FfiConversation) { + if (conversation.groupMetadata().conversationType() == "dm") { + trySend(Conversation.Dm(Dm(client, conversation))) + } else { + trySend(Conversation.Group(Group(client, conversation))) } } - } - - override fun onError(error: FfiSubscribeException) { - Log.e("XMTP all message stream", error.message.toString()) - } - } - val stream = libXMTPConversations?.streamAllMessages(messageCallback) - ?: throw XMTPException("Client does not support Groups") - - awaitClose { stream.end() } - } - - fun streamAllConversationDecryptedMessages(): Flow = callbackFlow { - val messageCallback = object : FfiMessageCallback { - override fun onMessage(message: FfiMessage) { - val conversation = client.findConversation(message.convoId.toHex()) - val decryptedMessage = MessageV3(client, message).decryptOrNull() - - when (conversation?.version) { - Conversation.Version.DM -> { - decryptedMessage?.let { trySend(it) } - } - - else -> { - decryptedMessage?.let { trySend(it) } - } + override fun onError(error: FfiSubscribeException) { + Log.e("XMTP Conversation stream", error.message.toString()) } } - - override fun onError(error: FfiSubscribeException) { - Log.e("XMTP all message stream", error.message.toString()) - } - } - - val stream = libXMTPConversations?.streamAllMessages(messageCallback) - ?: throw XMTPException("Client does not support Groups") - - awaitClose { stream.end() } - } - - // ------- V1 V2 to be deprecated ------ - - /** - * @return This lists messages sent to the [Conversation]. - * This pulls messages from multiple conversations in a single call. - * @see Conversation.messages - */ - suspend fun listBatchMessages( - topics: List>, - ): List { - if (!client.hasV2Client) throw XMTPException("Not supported for V3. The local database handles persistence of messages. Use listConversations order lastMessage") - - val requests = topics.map { (topic, page) -> - makeQueryRequest(topic = topic, pagination = page) - } - - // The maximum number of requests permitted in a single batch call. - val maxQueryRequestsPerBatch = 50 - val messages: MutableList = mutableListOf() - val batches = requests.chunked(maxQueryRequestsPerBatch) - for (batch in batches) { - messages.addAll( - client.batchQuery(batch).responsesOrBuilderList.flatMap { res -> - res.envelopesList.mapNotNull { envelope -> - val conversation = conversationsByTopic[envelope.contentTopic] - if (conversation == null) { - Log.d(TAG, "discarding message, unknown conversation $envelope") - return@mapNotNull null - } - val msg = conversation.decodeOrNull(envelope) - msg - } - }, - ) - } - return messages - } - - /** - * @return This lists messages sent to the [Conversation] when the messages are encrypted. - * This pulls messages from multiple conversations in a single call. - * @see listBatchMessages - */ - suspend fun listBatchDecryptedMessages( - topics: List>, - ): List { - if (!client.hasV2Client) throw XMTPException("Not supported for V3. The local database handles persistence of messages. Use listConversations order lastMessage") - - val requests = topics.map { (topic, page) -> - makeQueryRequest(topic = topic, pagination = page) - } - - // The maximum number of requests permitted in a single batch call. - val maxQueryRequestsPerBatch = 50 - val messages: MutableList = mutableListOf() - val batches = requests.chunked(maxQueryRequestsPerBatch) - for (batch in batches) { - messages.addAll( - client.batchQuery(batch).responsesOrBuilderList.flatMap { res -> - res.envelopesList.mapNotNull { envelope -> - val conversation = conversationsByTopic[envelope.contentTopic] - if (conversation == null) { - Log.d(TAG, "discarding message, unknown conversation $envelope") - return@mapNotNull null - } - try { - val msg = conversation.decrypt(envelope) - msg - } catch (e: Exception) { - Log.e(TAG, "Error decrypting message: $envelope", e) - null - } - } - }, - ) - } - return messages - } - - fun importTopicData(data: TopicData): Conversation { - if (!client.hasV2Client) throw XMTPException("Not supported for V3. The local database handles persistence.") - val conversation: Conversation - if (!data.hasInvitation()) { - val sentAt = Date(data.createdNs / 1_000_000) - conversation = Conversation.V1( - ConversationV1( - client, - data.peerAddress, - sentAt, - ), - ) - } else { - conversation = Conversation.V2( - ConversationV2( - topic = data.invitation.topic, - keyMaterial = data.invitation.aes256GcmHkdfSha256.keyMaterial.toByteArray(), - context = data.invitation.context, - peerAddress = data.peerAddress, - client = client, - createdAtNs = data.createdNs, - header = Invitation.SealedInvitationHeaderV1.getDefaultInstance(), - consentProof = if (data.invitation.hasConsentProof()) data.invitation.consentProof else null - ), - ) - } - conversationsByTopic[conversation.topic] = conversation - return conversation - } - - /** - * This method creates a new conversation from an invitation. - * @param envelope Object that contains the information of the current [Client] such as topic - * and timestamp. - * @return [Conversation] from an invitation suing the current [Client]. - */ - fun fromInvite(envelope: Envelope): Conversation { - if (!client.hasV2Client) throw XMTPException("Not supported for V3. Use conversationFromWelcome.") - val sealedInvitation = Invitation.SealedInvitation.parseFrom(envelope.message) - val unsealed = sealedInvitation.v1.getInvitation(viewer = client.keys) - return Conversation.V2( - ConversationV2.create( - client = client, - invitation = unsealed, - header = sealedInvitation.v1.header, - ), - ) - } - - /** - * This method creates a new conversation from an Intro. - * @param envelope Object that contains the information of the current [Client] such as topic - * and timestamp. - * @return [Conversation] from an Intro suing the current [Client]. - */ - fun fromIntro(envelope: Envelope): Conversation { - if (!client.hasV2Client) throw XMTPException("Not supported for V3. Use conversationFromWelcome.") - val messageV1 = MessageV1Builder.buildFromBytes(envelope.message.toByteArray()) - val senderAddress = messageV1.header.sender.walletAddress - val recipientAddress = messageV1.header.recipient.walletAddress - val peerAddress = if (client.address == senderAddress) recipientAddress else senderAddress - return Conversation.V1( - ConversationV1( - client = client, - peerAddress = peerAddress, - sentAt = messageV1.sentAt, - ), - ) - } - - fun conversation(sealedInvitation: SealedInvitation): ConversationV2 { - if (!client.hasV2Client) throw XMTPException("Not supported for V3. Use client.findDm to find the dm.") - val unsealed = sealedInvitation.v1.getInvitation(viewer = client.keys) - return ConversationV2.create( - client = client, - invitation = unsealed, - header = sealedInvitation.v1.header, - ) - } - - /** - * Send an invitation from the current [Client] to the specified recipient (Client) - * @param recipient The public key of the client that you want to send the invitation - * @param invitation Invitation object that will be send - * @param created Specified date creation for this invitation. - * @return [SealedInvitation] with the specified information. - */ - suspend fun sendInvitation( - recipient: SignedPublicKeyBundle, - invitation: InvitationV1, - created: Date, - ): SealedInvitation { - if (!client.hasV2Client) throw XMTPException("Not supported for V3. Use newConversation to create welcome.") - client.keys.let { - val sealed = SealedInvitationBuilder.buildFromV1( - sender = it, - recipient = recipient, - created = created, - invitation = invitation, - ) - val peerAddress = recipient.walletAddress - - client.publish( - envelopes = listOf( - EnvelopeBuilder.buildFromTopic( - topic = Topic.userInvite( - client.address, - ), - timestamp = created, - message = sealed.toByteArray(), - ), - EnvelopeBuilder.buildFromTopic( - topic = Topic.userInvite( - peerAddress, - ), - timestamp = created, - message = sealed.toByteArray(), - ), - ), - ) - return sealed - } - } - - /** - * Get the stream of all messages of the current [Client] - * @return Flow object of [DecodedMessage] that represents all the messages of the - * current [Client] as userInvite and userIntro - */ - private fun streamAllV2Messages(): Flow = callbackFlow { - val topics = mutableListOf( - Topic.userInvite(client.address).description, - Topic.userIntro(client.address).description, - ) - - for (conversation in list()) { - topics.add(conversation.topic) + val stream = ffiConversations.stream(conversationCallback) + awaitClose { stream.end() } } - val subscriptionRequest = FfiV2SubscribeRequest(topics) - var stream = FfiV2Subscription(NoPointer) - - val subscriptionCallback = object : FfiV2SubscriptionCallback { - override fun onMessage(message: FfiEnvelope) { - when { - conversationsByTopic.containsKey(message.contentTopic) -> { - val conversation = conversationsByTopic[message.contentTopic] - val decoded = conversation?.decode(envelopeFromFFi(message)) - decoded?.let { trySend(it) } - } - - message.contentTopic.startsWith("/xmtp/0/invite-") -> { - val conversation = fromInvite(envelope = envelopeFromFFi(message)) - conversationsByTopic[conversation.topic] = conversation - topics.add(conversation.topic) - subscriptionRequest.contentTopics = topics - launch { stream.update(subscriptionRequest) } - } - - message.contentTopic.startsWith("/xmtp/0/intro-") -> { - val conversation = fromIntro(envelope = envelopeFromFFi(message)) - conversationsByTopic[conversation.topic] = conversation - val decoded = conversation.decode(envelopeFromFFi(message)) - trySend(decoded) - topics.add(conversation.topic) - subscriptionRequest.contentTopics = topics - launch { stream.update(subscriptionRequest) } - } - - else -> {} + fun streamAllMessages(/*Maybe Put a way to specify group, dm, or both?*/): Flow = + callbackFlow { + val messageCallback = object : FfiMessageCallback { + override fun onMessage(message: FfiMessage) { + val decodedMessage = Message(client, message).decodeOrNull() + decodedMessage?.let { trySend(it) } } - } - } - stream = client.subscribe2(subscriptionRequest, subscriptionCallback) - - awaitClose { launch { stream.end() } } - } - - private fun streamAllV2DecryptedMessages(): Flow = callbackFlow { - val topics = mutableListOf( - Topic.userInvite(client.address).description, - Topic.userIntro(client.address).description, - ) - - for (conversation in list()) { - topics.add(conversation.topic) - } - - val subscriptionRequest = FfiV2SubscribeRequest(topics) - var stream = FfiV2Subscription(NoPointer) - - val subscriptionCallback = object : FfiV2SubscriptionCallback { - override fun onMessage(message: FfiEnvelope) { - when { - conversationsByTopic.containsKey(message.contentTopic) -> { - val conversation = conversationsByTopic[message.contentTopic] - val decrypted = conversation?.decrypt(envelopeFromFFi(message)) - decrypted?.let { trySend(it) } - } - - message.contentTopic.startsWith("/xmtp/0/invite-") -> { - val conversation = fromInvite(envelope = envelopeFromFFi(message)) - conversationsByTopic[conversation.topic] = conversation - topics.add(conversation.topic) - subscriptionRequest.contentTopics = topics - launch { stream.update(subscriptionRequest) } - } - - message.contentTopic.startsWith("/xmtp/0/intro-") -> { - val conversation = fromIntro(envelope = envelopeFromFFi(message)) - conversationsByTopic[conversation.topic] = conversation - val decrypted = conversation.decrypt(envelopeFromFFi(message)) - trySend(decrypted) - topics.add(conversation.topic) - subscriptionRequest.contentTopics = topics - launch { stream.update(subscriptionRequest) } - } - - else -> {} + override fun onError(error: FfiSubscribeException) { + Log.e("XMTP all message stream", error.message.toString()) } } - } - stream = client.subscribe2(subscriptionRequest, subscriptionCallback) - - awaitClose { launch { stream.end() } } - } - - /** - * Get the list of invitations using the data sent [pagination] - * @param pagination Information of the topics, ranges (dates), etc. - * @return List of [SealedInvitation] that are inside of the range specified by [pagination] - */ - private suspend fun listInvitations(pagination: Pagination? = null): List { - if (!client.hasV2Client) throw XMTPException("Not supported for V3. Use conversationFromWelcome.") - val apiClient = client.apiClient ?: throw XMTPException("V2 only function") - val envelopes = - apiClient.envelopes(Topic.userInvite(client.address).description, pagination) - return envelopes.map { envelope -> - SealedInvitation.parseFrom(envelope.message) + val stream = ffiConversations.streamAllMessages(messageCallback) + awaitClose { stream.end() } } - } - - private suspend fun listIntroductionPeers(pagination: Pagination? = null): Map { - if (!client.hasV2Client) throw XMTPException("Not supported for V3. Use conversationFromWelcome.") - val apiClient = client.apiClient ?: throw XMTPException("V2 only function") - val envelopes = apiClient.queryTopic( - topic = Topic.userIntro(client.address), - pagination = pagination, - ).envelopesList - val messages = envelopes.mapNotNull { envelope -> - try { - val message = MessageV1Builder.buildFromBytes(envelope.message.toByteArray()) - // Attempt to decrypt, just to make sure we can - message.decrypt(client.privateKeyBundleV1) - message - } catch (e: Exception) { - Log.d(TAG, e.message.toString()) - null - } - } - val seenPeers: MutableMap = mutableMapOf() - for (message in messages) { - val recipientAddress = message.recipientAddress - val senderAddress = message.senderAddress - val sentAt = message.sentAt - val peerAddress = - if (recipientAddress == client.address) senderAddress else recipientAddress - val existing = seenPeers[peerAddress] - if (existing == null) { - seenPeers[peerAddress] = sentAt - continue - } - if (existing > sentAt) { - seenPeers[peerAddress] = sentAt - } - } - return seenPeers - } } diff --git a/library/src/main/java/org/xmtp/android/library/DecodedMessage.kt b/library/src/main/java/org/xmtp/android/library/DecodedMessage.kt index eab903b1a..dda478e8e 100644 --- a/library/src/main/java/org/xmtp/android/library/DecodedMessage.kt +++ b/library/src/main/java/org/xmtp/android/library/DecodedMessage.kt @@ -2,7 +2,7 @@ package org.xmtp.android.library import org.xmtp.android.library.codecs.TextCodec import org.xmtp.android.library.codecs.decoded -import org.xmtp.android.library.messages.MessageDeliveryStatus +import org.xmtp.android.library.libxmtp.Message.MessageDeliveryStatus import org.xmtp.proto.message.contents.Content import java.util.Date diff --git a/library/src/main/java/org/xmtp/android/library/Dm.kt b/library/src/main/java/org/xmtp/android/library/Dm.kt index a1e3c785a..5eb6dbccd 100644 --- a/library/src/main/java/org/xmtp/android/library/Dm.kt +++ b/library/src/main/java/org/xmtp/android/library/Dm.kt @@ -8,12 +8,10 @@ import org.xmtp.android.library.codecs.ContentCodec import org.xmtp.android.library.codecs.EncodedContent import org.xmtp.android.library.codecs.compress import org.xmtp.android.library.libxmtp.Member -import org.xmtp.android.library.libxmtp.MessageV3 -import org.xmtp.android.library.messages.DecryptedMessage -import org.xmtp.android.library.messages.MessageDeliveryStatus -import org.xmtp.android.library.messages.PagingInfoSortDirection +import org.xmtp.android.library.libxmtp.Message +import org.xmtp.android.library.libxmtp.Message.MessageDeliveryStatus +import org.xmtp.android.library.libxmtp.Message.SortDirection 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 @@ -59,31 +57,29 @@ class Dm(val client: Client, private val libXMTPGroup: FfiConversation) { fun encodeContent(content: T, options: SendOptions?): EncodedContent { val codec = Client.codecRegistry.find(options?.contentType) - - fun > encode(codec: Codec, content: Any?): EncodedContent { - val contentType = content as? T - if (contentType != null) { - return codec.encode(contentType) - } else { - throw XMTPException("Codec type is not registered") - } - } - - var encoded = encode(codec = codec as ContentCodec, content = content) - val fallback = codec.fallback(content) - if (!fallback.isNullOrBlank()) { - encoded = encoded.toBuilder().also { - it.fallback = fallback - }.build() + fun > encode(codec: Codec, content: T): EncodedContent { + return codec.encode(content) } - val compression = options?.compression - if (compression != null) { - encoded = encoded.compress(compression) + try { + @Suppress("UNCHECKED_CAST") + var encoded = encode(codec as ContentCodec, content) + val fallback = codec.fallback(content) + if (!fallback.isNullOrBlank()) { + encoded = encoded.toBuilder().also { + it.fallback = fallback + }.build() + } + val compression = options?.compression + if (compression != null) { + encoded = encoded.compress(compression) + } + return encoded + } catch (e: Exception) { + throw XMTPException("Codec type is not registered") } - return encoded } - suspend fun prepareMessage(content: T, options: SendOptions? = null): String { + fun prepareMessage(content: T, options: SendOptions? = null): String { if (consentState() == ConsentState.UNKNOWN) { updateConsentState(ConsentState.ALLOWED) } @@ -103,7 +99,7 @@ class Dm(val client: Client, private val libXMTPGroup: FfiConversation) { limit: Int? = null, beforeNs: Long? = null, afterNs: Long? = null, - direction: PagingInfoSortDirection = SortDirection.SORT_DIRECTION_DESCENDING, + direction: SortDirection = SortDirection.DESCENDING, deliveryStatus: MessageDeliveryStatus = MessageDeliveryStatus.ALL, ): List { return libXMTPGroup.findMessages( @@ -118,46 +114,18 @@ 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 } ) ).mapNotNull { - MessageV3(client, it).decodeOrNull() + Message(client, it).decodeOrNull() } } - fun decryptedMessages( - limit: Int? = null, - beforeNs: Long? = null, - afterNs: Long? = null, - direction: PagingInfoSortDirection = SortDirection.SORT_DIRECTION_DESCENDING, - deliveryStatus: MessageDeliveryStatus = MessageDeliveryStatus.ALL, - ): List { - return libXMTPGroup.findMessages( - opts = FfiListMessagesOptions( - sentBeforeNs = beforeNs, - sentAfterNs = afterNs, - limit = limit?.toLong(), - deliveryStatus = when (deliveryStatus) { - MessageDeliveryStatus.PUBLISHED -> FfiDeliveryStatus.PUBLISHED - MessageDeliveryStatus.UNPUBLISHED -> FfiDeliveryStatus.UNPUBLISHED - MessageDeliveryStatus.FAILED -> FfiDeliveryStatus.FAILED - else -> null - }, - direction = when (direction) { - SortDirection.SORT_DIRECTION_ASCENDING -> FfiDirection.ASCENDING - else -> FfiDirection.DESCENDING - } - ) - ).mapNotNull { - MessageV3(client, it).decryptOrNull() - } - } - - suspend fun processMessage(envelopeBytes: ByteArray): MessageV3 { - val message = libXMTPGroup.processStreamedConversationMessage(envelopeBytes) - return MessageV3(client, message) + suspend fun processMessage(messageBytes: ByteArray): Message { + val message = libXMTPGroup.processStreamedConversationMessage(messageBytes) + return Message(client, message) } fun creatorInboxId(): String { @@ -175,7 +143,7 @@ class Dm(val client: Client, private val libXMTPGroup: FfiConversation) { fun streamMessages(): Flow = callbackFlow { val messageCallback = object : FfiMessageCallback { override fun onMessage(message: FfiMessage) { - val decodedMessage = MessageV3(client, message).decodeOrNull() + val decodedMessage = Message(client, message).decodeOrNull() decodedMessage?.let { trySend(it) } @@ -190,33 +158,7 @@ class Dm(val client: Client, private val libXMTPGroup: FfiConversation) { awaitClose { stream.end() } } - fun streamDecryptedMessages(): Flow = callbackFlow { - val messageCallback = object : FfiMessageCallback { - override fun onMessage(message: FfiMessage) { - val decryptedMessage = MessageV3(client, message).decryptOrNull() - decryptedMessage?.let { - trySend(it) - } - } - - override fun onError(error: FfiSubscribeException) { - Log.e("XMTP Dm stream", error.message.toString()) - } - } - - val stream = libXMTPGroup.stream(messageCallback) - awaitClose { stream.end() } - } - - suspend fun updateConsentState(state: ConsentState) { - if (client.hasV2Client) { - when (state) { - ConsentState.ALLOWED -> client.contacts.allowGroups(groupIds = listOf(id)) - ConsentState.DENIED -> client.contacts.denyGroups(groupIds = listOf(id)) - ConsentState.UNKNOWN -> Unit - } - } - + fun updateConsentState(state: ConsentState) { val consentState = ConsentState.toFfiConsentState(state) libXMTPGroup.updateConsentState(consentState) } diff --git a/library/src/main/java/org/xmtp/android/library/Group.kt b/library/src/main/java/org/xmtp/android/library/Group.kt index 3fbfbe190..5d362ba9b 100644 --- a/library/src/main/java/org/xmtp/android/library/Group.kt +++ b/library/src/main/java/org/xmtp/android/library/Group.kt @@ -8,13 +8,10 @@ import org.xmtp.android.library.codecs.ContentCodec import org.xmtp.android.library.codecs.EncodedContent import org.xmtp.android.library.codecs.compress import org.xmtp.android.library.libxmtp.Member -import org.xmtp.android.library.libxmtp.MessageV3 -import org.xmtp.android.library.messages.DecryptedMessage -import org.xmtp.android.library.messages.MessageDeliveryStatus -import org.xmtp.android.library.messages.PagingInfoSortDirection +import org.xmtp.android.library.libxmtp.Message +import org.xmtp.android.library.libxmtp.Message.MessageDeliveryStatus +import org.xmtp.android.library.libxmtp.Message.SortDirection 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 import uniffi.xmtpv3.FfiConversation import uniffi.xmtpv3.FfiConversationMetadata import uniffi.xmtpv3.FfiDeliveryStatus @@ -77,31 +74,29 @@ class Group(val client: Client, private val libXMTPGroup: FfiConversation) { fun encodeContent(content: T, options: SendOptions?): EncodedContent { val codec = Client.codecRegistry.find(options?.contentType) - - fun > encode(codec: Codec, content: Any?): EncodedContent { - val contentType = content as? T - if (contentType != null) { - return codec.encode(contentType) - } else { - throw XMTPException("Codec type is not registered") - } - } - - var encoded = encode(codec = codec as ContentCodec, content = content) - val fallback = codec.fallback(content) - if (!fallback.isNullOrBlank()) { - encoded = encoded.toBuilder().also { - it.fallback = fallback - }.build() + fun > encode(codec: Codec, content: T): EncodedContent { + return codec.encode(content) } - val compression = options?.compression - if (compression != null) { - encoded = encoded.compress(compression) + try { + @Suppress("UNCHECKED_CAST") + var encoded = encode(codec as ContentCodec, content) + val fallback = codec.fallback(content) + if (!fallback.isNullOrBlank()) { + encoded = encoded.toBuilder().also { + it.fallback = fallback + }.build() + } + val compression = options?.compression + if (compression != null) { + encoded = encoded.compress(compression) + } + return encoded + } catch (e: Exception) { + throw XMTPException("Codec type is not registered") } - return encoded } - suspend fun prepareMessage(content: T, options: SendOptions? = null): String { + fun prepareMessage(content: T, options: SendOptions? = null): String { if (consentState() == ConsentState.UNKNOWN) { updateConsentState(ConsentState.ALLOWED) } @@ -121,7 +116,7 @@ class Group(val client: Client, private val libXMTPGroup: FfiConversation) { limit: Int? = null, beforeNs: Long? = null, afterNs: Long? = null, - direction: PagingInfoSortDirection = SORT_DIRECTION_DESCENDING, + direction: SortDirection = SortDirection.DESCENDING, deliveryStatus: MessageDeliveryStatus = MessageDeliveryStatus.ALL, ): List { return libXMTPGroup.findMessages( @@ -136,57 +131,21 @@ 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 } ) ).mapNotNull { - MessageV3(client, it).decodeOrNull() + Message(client, it).decodeOrNull() } } - fun decryptedMessages( - limit: Int? = null, - beforeNs: Long? = null, - afterNs: Long? = null, - direction: PagingInfoSortDirection = SORT_DIRECTION_DESCENDING, - deliveryStatus: MessageDeliveryStatus = MessageDeliveryStatus.ALL, - ): List { - return libXMTPGroup.findMessages( - opts = FfiListMessagesOptions( - sentBeforeNs = beforeNs, - sentAfterNs = afterNs, - limit = limit?.toLong(), - deliveryStatus = when (deliveryStatus) { - MessageDeliveryStatus.PUBLISHED -> FfiDeliveryStatus.PUBLISHED - MessageDeliveryStatus.UNPUBLISHED -> FfiDeliveryStatus.UNPUBLISHED - MessageDeliveryStatus.FAILED -> FfiDeliveryStatus.FAILED - else -> null - }, - direction = when (direction) { - SORT_DIRECTION_ASCENDING -> FfiDirection.ASCENDING - else -> FfiDirection.DESCENDING - } - ) - ).mapNotNull { - MessageV3(client, it).decryptOrNull() - } - } - - suspend fun processMessage(envelopeBytes: ByteArray): MessageV3 { - val message = libXMTPGroup.processStreamedConversationMessage(envelopeBytes) - return MessageV3(client, message) + suspend fun processMessage(messageBytes: ByteArray): Message { + val message = libXMTPGroup.processStreamedConversationMessage(messageBytes) + return Message(client, message) } - suspend fun updateConsentState(state: ConsentState) { - if (client.hasV2Client) { - when (state) { - ConsentState.ALLOWED -> client.contacts.allowGroups(groupIds = listOf(id)) - ConsentState.DENIED -> client.contacts.denyGroups(groupIds = listOf(id)) - ConsentState.UNKNOWN -> Unit - } - } - + fun updateConsentState(state: ConsentState) { val consentState = ConsentState.toFfiConsentState(state) libXMTPGroup.updateConsentState(consentState) } @@ -404,7 +363,7 @@ class Group(val client: Client, private val libXMTPGroup: FfiConversation) { fun streamMessages(): Flow = callbackFlow { val messageCallback = object : FfiMessageCallback { override fun onMessage(message: FfiMessage) { - val decodedMessage = MessageV3(client, message).decodeOrNull() + val decodedMessage = Message(client, message).decodeOrNull() decodedMessage?.let { trySend(it) } @@ -418,22 +377,4 @@ class Group(val client: Client, private val libXMTPGroup: FfiConversation) { val stream = libXMTPGroup.stream(messageCallback) awaitClose { stream.end() } } - - fun streamDecryptedMessages(): Flow = callbackFlow { - val messageCallback = object : FfiMessageCallback { - override fun onMessage(message: FfiMessage) { - val decryptedMessage = MessageV3(client, message).decryptOrNull() - decryptedMessage?.let { - trySend(it) - } - } - - override fun onError(error: FfiSubscribeException) { - Log.e("XMTP Group stream", error.message.toString()) - } - } - - val stream = libXMTPGroup.stream(messageCallback) - awaitClose { stream.end() } - } } diff --git a/library/src/main/java/org/xmtp/android/library/PreparedMessage.kt b/library/src/main/java/org/xmtp/android/library/PreparedMessage.kt deleted file mode 100644 index e2f762dd1..000000000 --- a/library/src/main/java/org/xmtp/android/library/PreparedMessage.kt +++ /dev/null @@ -1,40 +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 -import org.xmtp.proto.message.contents.Content.EncodedContent - -// 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, - val encodedContent: EncodedContent? = null -) { - companion object { - fun fromSerializedData(data: ByteArray): PreparedMessage { - val req = PublishRequest.parseFrom(data) - return PreparedMessage(req.envelopesList) - } - } - - fun toSerializedData(): ByteArray { - val req = PublishRequest.newBuilder() - .addAllEnvelopes(envelopes) - .build() - return req.toByteArray() - } - - val messageId: String - get() = Hash.sha256(envelopes.first().message.toByteArray()).toHex() - - val conversationTopic: String - get() = envelopes.first().contentTopic -} diff --git a/library/src/main/java/org/xmtp/android/library/PrivatePreferences.kt b/library/src/main/java/org/xmtp/android/library/PrivatePreferences.kt new file mode 100644 index 000000000..e345327de --- /dev/null +++ b/library/src/main/java/org/xmtp/android/library/PrivatePreferences.kt @@ -0,0 +1,136 @@ +package org.xmtp.android.library + +import uniffi.xmtpv3.FfiConsent +import uniffi.xmtpv3.FfiConsentEntityType +import uniffi.xmtpv3.FfiConsentState +import uniffi.xmtpv3.FfiXmtpClient + +enum class ConsentState { + ALLOWED, + DENIED, + UNKNOWN; + + companion object { + fun toFfiConsentState(option: ConsentState): FfiConsentState { + return when (option) { + ALLOWED -> FfiConsentState.ALLOWED + DENIED -> FfiConsentState.DENIED + UNKNOWN -> FfiConsentState.UNKNOWN + } + } + + fun fromFfiConsentState(option: FfiConsentState): ConsentState { + return when (option) { + FfiConsentState.ALLOWED -> ALLOWED + FfiConsentState.DENIED -> DENIED + FfiConsentState.UNKNOWN -> UNKNOWN + } + } + } +} + +enum class EntryType { + ADDRESS, + CONVERSATION_ID, + INBOX_ID; + + companion object { + fun toFfiConsentEntityType(option: EntryType): FfiConsentEntityType { + return when (option) { + ADDRESS -> FfiConsentEntityType.ADDRESS + CONVERSATION_ID -> FfiConsentEntityType.CONVERSATION_ID + INBOX_ID -> FfiConsentEntityType.INBOX_ID + } + } + + fun fromFfiConsentEntityType(option: FfiConsentEntityType): EntryType { + return when (option) { + FfiConsentEntityType.ADDRESS -> ADDRESS + FfiConsentEntityType.CONVERSATION_ID -> CONVERSATION_ID + FfiConsentEntityType.INBOX_ID -> INBOX_ID + } + } + } +} + +data class ConsentListEntry( + val value: String, + val entryType: EntryType, + val consentType: ConsentState, +) { + companion object { + fun address( + address: String, + type: ConsentState = ConsentState.UNKNOWN, + ): ConsentListEntry { + return ConsentListEntry(address, EntryType.ADDRESS, type) + } + + fun conversationId( + groupId: String, + type: ConsentState = ConsentState.UNKNOWN, + ): ConsentListEntry { + return ConsentListEntry(groupId, EntryType.CONVERSATION_ID, type) + } + + fun inboxId( + inboxId: String, + type: ConsentState = ConsentState.UNKNOWN, + ): ConsentListEntry { + return ConsentListEntry(inboxId, EntryType.INBOX_ID, type) + } + } + + val key: String + get() = "${entryType.name}-$value" +} + +class ConsentList( + val client: Client, + private val ffiClient: FfiXmtpClient, +) { + suspend fun setConsentState(entries: List) { + ffiClient.setConsentStates(entries.map { it.toFfiConsent() }) + } + + private fun ConsentListEntry.toFfiConsent(): FfiConsent { + return FfiConsent( + EntryType.toFfiConsentEntityType(entryType), + ConsentState.toFfiConsentState(consentType), + value + ) + } + + suspend fun addressState(address: String): ConsentState { + return ConsentState.fromFfiConsentState( + ffiClient.getConsentState( + FfiConsentEntityType.ADDRESS, + address + ) + ) + } + + suspend fun conversationState(groupId: String): ConsentState { + return ConsentState.fromFfiConsentState( + ffiClient.getConsentState( + FfiConsentEntityType.CONVERSATION_ID, + groupId + ) + ) + } + + suspend fun inboxIdState(inboxId: String): ConsentState { + return ConsentState.fromFfiConsentState( + ffiClient.getConsentState( + FfiConsentEntityType.INBOX_ID, + inboxId + ) + ) + } +} + +data class PrivatePreferences( + var client: Client, + private val ffiClient: FfiXmtpClient, + var consentList: ConsentList = ConsentList(client, ffiClient), +) diff --git a/library/src/main/java/org/xmtp/android/library/SigningKey.kt b/library/src/main/java/org/xmtp/android/library/SigningKey.kt index f1a10a395..5f34bae3e 100644 --- a/library/src/main/java/org/xmtp/android/library/SigningKey.kt +++ b/library/src/main/java/org/xmtp/android/library/SigningKey.kt @@ -1,20 +1,6 @@ package org.xmtp.android.library -import com.google.protobuf.kotlin.toByteString -import kotlinx.coroutines.runBlocking -import org.web3j.crypto.ECDSASignature -import org.web3j.crypto.Keys -import org.web3j.crypto.Sign -import org.xmtp.android.library.messages.PublicKey -import org.xmtp.android.library.messages.Signature -import org.xmtp.android.library.messages.createIdentityText -import org.xmtp.android.library.messages.ethHash -import org.xmtp.android.library.messages.rawData -import org.xmtp.proto.message.contents.PrivateKeyOuterClass -import org.xmtp.proto.message.contents.PublicKeyOuterClass import org.xmtp.proto.message.contents.SignatureOuterClass -import java.math.BigInteger -import java.util.Date interface SigningKey { val address: String @@ -50,55 +36,3 @@ enum class WalletType { SCW, // Smart Contract Wallet EOA // Externally Owned Account *Default } - -/** - * This prompts the wallet to sign a personal message. - * It authorizes the `identity` key to act on behalf of this wallet. - * e.g. "XMTP : Create Identity ..." - * @param identity key to act on behalf of this wallet - * @return AuthorizedIdentity object that contains the `identity` key signed by the wallet, - * together with a `publicKey` and `address` signed by the `identity` key. - */ -fun SigningKey.createIdentity( - identity: PrivateKeyOuterClass.PrivateKey, - preCreateIdentityCallback: PreEventCallback? = null, -): AuthorizedIdentity { - val slimKey = - PublicKeyOuterClass.PublicKey - .newBuilder() - .apply { - timestamp = Date().time - secp256K1Uncompressed = identity.publicKey.secp256K1Uncompressed - }.build() - - preCreateIdentityCallback?.let { - runBlocking { - it.invoke() - } - } - - val signatureClass = Signature.newBuilder().build() - val signatureText = signatureClass.createIdentityText(key = slimKey.toByteArray()) - val digest = signatureClass.ethHash(message = signatureText) - val signature = runBlocking { sign(signatureText) } ?: throw XMTPException("Illegal signature") - - val signatureData = KeyUtil.getSignatureData(signature.rawData.toByteString().toByteArray()) - val publicKey = - Sign.recoverFromSignature( - BigInteger(1, signatureData.v).toInt(), - ECDSASignature(BigInteger(1, signatureData.r), BigInteger(1, signatureData.s)), - digest, - ) - - val authorized = - PublicKey.newBuilder().also { - it.secp256K1Uncompressed = slimKey.secp256K1Uncompressed - it.timestamp = slimKey.timestamp - it.signature = signature - } - return AuthorizedIdentity( - address = Keys.toChecksumAddress(Keys.getAddress(publicKey)), - authorized = authorized.build(), - identity = identity, - ) -} diff --git a/library/src/main/java/org/xmtp/android/library/Util.kt b/library/src/main/java/org/xmtp/android/library/Util.kt index c902fdcaa..fabe36bd4 100644 --- a/library/src/main/java/org/xmtp/android/library/Util.kt +++ b/library/src/main/java/org/xmtp/android/library/Util.kt @@ -1,10 +1,7 @@ package org.xmtp.android.library -import com.google.protobuf.kotlin.toByteString import org.bouncycastle.jcajce.provider.digest.Keccak import org.web3j.utils.Numeric -import org.xmtp.proto.message.api.v1.MessageApiOuterClass -import uniffi.xmtpv3.FfiEnvelope class Util { companion object { @@ -12,14 +9,6 @@ class Util { val digest256 = Keccak.Digest256() return digest256.digest(data) } - - fun envelopeFromFFi(envelope: FfiEnvelope): MessageApiOuterClass.Envelope { - return MessageApiOuterClass.Envelope.newBuilder().also { - it.contentTopic = envelope.contentTopic - it.timestampNs = envelope.timestampNs.toLong() - it.message = envelope.message.toByteString() - }.build() - } } } diff --git a/library/src/main/java/org/xmtp/android/library/frames/FramesClient.kt b/library/src/main/java/org/xmtp/android/library/frames/FramesClient.kt deleted file mode 100644 index d08defdc1..000000000 --- a/library/src/main/java/org/xmtp/android/library/frames/FramesClient.kt +++ /dev/null @@ -1,94 +0,0 @@ -package org.xmtp.android.library.frames - -import android.util.Base64 -import org.xmtp.android.library.Client -import org.xmtp.android.library.XMTPException -import org.xmtp.android.library.frames.FramesConstants.PROTOCOL_VERSION -import org.xmtp.android.library.messages.PrivateKeyBuilder -import org.xmtp.android.library.messages.Signature -import org.xmtp.android.library.messages.getPublicKeyBundle -import org.xmtp.proto.message.contents.PublicKeyOuterClass.SignedPublicKeyBundle -import java.security.MessageDigest -import org.xmtp.proto.message.contents.Frames.FrameActionBody -import org.xmtp.proto.message.contents.Frames.FrameAction -import java.util.Date - -class FramesClient(private val xmtpClient: Client, var proxy: OpenFramesProxy = OpenFramesProxy()) { - - suspend fun signFrameAction(inputs: FrameActionInputs): FramePostPayload { - val opaqueConversationIdentifier = buildOpaqueIdentifier(inputs) - val frameUrl = inputs.frameUrl - val buttonIndex = inputs.buttonIndex - val inputText = inputs.inputText - val state = inputs.state - val now = Date().time * 1_000_000 - val frameActionBuilder = FrameActionBody.newBuilder().also { frame -> - frame.frameUrl = frameUrl - frame.buttonIndex = buttonIndex - frame.opaqueConversationIdentifier = opaqueConversationIdentifier - frame.timestamp = now - frame.unixTimestamp = now.toInt() - if (inputText != null) { - frame.inputText = inputText - } - if (state != null) { - frame.state = state - } - } - - val toSign = frameActionBuilder.build() - val signedAction = Base64.encodeToString(buildSignedFrameAction(toSign), Base64.NO_WRAP) - - val untrustedData = FramePostUntrustedData(frameUrl, now, buttonIndex, inputText, state, xmtpClient.address, opaqueConversationIdentifier, now.toInt()) - val trustedData = FramePostTrustedData(signedAction) - - return FramePostPayload("xmtp@$PROTOCOL_VERSION", untrustedData, trustedData) - } - - private suspend fun signDigest(digest: ByteArray): Signature { - val signedPrivateKey = xmtpClient.keys.identityKey - val privateKey = PrivateKeyBuilder.buildFromSignedPrivateKey(signedPrivateKey) - return PrivateKeyBuilder(privateKey).sign(digest) - } - - private fun getPublicKeyBundle(): SignedPublicKeyBundle { - return xmtpClient.keys.getPublicKeyBundle() - } - - private suspend fun buildSignedFrameAction(actionBodyInputs: FrameActionBody): ByteArray { - val digest = sha256(actionBodyInputs.toByteArray()) - val signature = signDigest(digest) - - val publicKeyBundle = getPublicKeyBundle() - val frameAction = FrameAction.newBuilder().also { - it.actionBody = actionBodyInputs.toByteString() - it.signature = signature - it.signedPublicKeyBundle = publicKeyBundle - }.build() - - return frameAction.toByteArray() - } - - private fun buildOpaqueIdentifier(inputs: FrameActionInputs): String { - return when (inputs.conversationInputs) { - is ConversationActionInputs.Group -> { - val groupInputs = inputs.conversationInputs.inputs - val combined = groupInputs.groupId + groupInputs.groupSecret - val digest = sha256(combined) - Base64.encodeToString(digest, Base64.NO_WRAP) - } - is ConversationActionInputs.Dm -> { - val dmInputs = inputs.conversationInputs.inputs - val conversationTopic = dmInputs.conversationTopic ?: throw XMTPException("No conversation topic") - val combined = (conversationTopic.lowercase() + dmInputs.participantAccountAddresses.map { it.lowercase() }.sorted().joinToString("")).toByteArray() - val digest = sha256(combined) - Base64.encodeToString(digest, Base64.NO_WRAP) - } - } - } - - private fun sha256(input: ByteArray): ByteArray { - val digest = MessageDigest.getInstance("SHA-256") - return digest.digest(input) - } -} diff --git a/library/src/main/java/org/xmtp/android/library/libxmtp/MessageV3.kt b/library/src/main/java/org/xmtp/android/library/libxmtp/Message.kt similarity index 75% rename from library/src/main/java/org/xmtp/android/library/libxmtp/MessageV3.kt rename to library/src/main/java/org/xmtp/android/library/libxmtp/Message.kt index f26ae2fd5..0511e50f5 100644 --- a/library/src/main/java/org/xmtp/android/library/libxmtp/MessageV3.kt +++ b/library/src/main/java/org/xmtp/android/library/libxmtp/Message.kt @@ -6,8 +6,6 @@ import org.xmtp.android.library.DecodedMessage import org.xmtp.android.library.XMTPException import org.xmtp.android.library.codecs.ContentTypeGroupUpdated import org.xmtp.android.library.codecs.EncodedContent -import org.xmtp.android.library.messages.DecryptedMessage -import org.xmtp.android.library.messages.MessageDeliveryStatus import org.xmtp.android.library.messages.Topic import org.xmtp.android.library.toHex import uniffi.xmtpv3.FfiDeliveryStatus @@ -15,7 +13,15 @@ import uniffi.xmtpv3.FfiConversationMessageKind import uniffi.xmtpv3.FfiMessage import java.util.Date -data class MessageV3(val client: Client, private val libXMTPMessage: FfiMessage) { +data class Message(val client: Client, private val libXMTPMessage: FfiMessage) { + enum class MessageDeliveryStatus { + ALL, PUBLISHED, UNPUBLISHED, FAILED + } + + enum class SortDirection { + ASCENDING, + DESCENDING; + } val id: String get() = libXMTPMessage.id.toHex() @@ -67,24 +73,4 @@ data class MessageV3(val client: Client, private val libXMTPMessage: FfiMessage) null } } - - fun decryptOrNull(): DecryptedMessage? { - return try { - decrypt() - } catch (e: Exception) { - Log.d("MESSAGE_V3", "discarding message that failed to decrypt", e) - null - } - } - - fun decrypt(): DecryptedMessage { - return DecryptedMessage( - id = id, - topic = Topic.groupMessage(convoId).description, - encodedContent = decode().encodedContent, - senderAddress = senderInboxId, - sentAt = sentAt, - deliveryStatus = deliveryStatus - ) - } } diff --git a/library/src/main/java/org/xmtp/android/library/messages/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/DecryptedMessage.kt b/library/src/main/java/org/xmtp/android/library/messages/DecryptedMessage.kt deleted file mode 100644 index accd89459..000000000 --- a/library/src/main/java/org/xmtp/android/library/messages/DecryptedMessage.kt +++ /dev/null @@ -1,13 +0,0 @@ -package org.xmtp.android.library.messages - -import org.xmtp.android.library.codecs.EncodedContent -import java.util.Date - -data class DecryptedMessage( - var id: String, - var encodedContent: EncodedContent, - var senderAddress: String, - var sentAt: Date, - var topic: String = "", - var deliveryStatus: MessageDeliveryStatus = MessageDeliveryStatus.PUBLISHED -) diff --git a/library/src/main/java/org/xmtp/android/library/messages/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/Message.kt b/library/src/main/java/org/xmtp/android/library/messages/Message.kt deleted file mode 100644 index 2beb7c8ac..000000000 --- a/library/src/main/java/org/xmtp/android/library/messages/Message.kt +++ /dev/null @@ -1,33 +0,0 @@ -package org.xmtp.android.library.messages - -typealias Message = org.xmtp.proto.message.contents.MessageOuterClass.Message - -enum class MessageDeliveryStatus { - ALL, PUBLISHED, UNPUBLISHED, FAILED -} - -enum class MessageVersion(val rawValue: String) { - V1("v1"), - V2("v2"); - - companion object { - operator fun invoke(rawValue: String) = - MessageVersion.values().firstOrNull { it.rawValue == rawValue } - } -} - -class MessageBuilder { - companion object { - fun buildFromMessageV1(v1: MessageV1): Message { - return Message.newBuilder().also { - it.v1 = v1 - }.build() - } - - fun buildFromMessageV2(v2: MessageV2): Message { - return Message.newBuilder().also { - it.v2 = v2 - }.build() - } - } -} diff --git a/library/src/main/java/org/xmtp/android/library/messages/MessageHeaderV1.kt b/library/src/main/java/org/xmtp/android/library/messages/MessageHeaderV1.kt deleted file mode 100644 index 8090f8914..000000000 --- a/library/src/main/java/org/xmtp/android/library/messages/MessageHeaderV1.kt +++ /dev/null @@ -1,19 +0,0 @@ -package org.xmtp.android.library.messages - -typealias MessageHeaderV1 = org.xmtp.proto.message.contents.MessageOuterClass.MessageHeaderV1 - -class MessageHeaderV1Builder { - companion object { - fun buildFromPublicBundles( - sender: PublicKeyBundle, - recipient: PublicKeyBundle, - timestamp: Long - ): MessageHeaderV1 { - return MessageHeaderV1.newBuilder().also { - it.sender = sender - it.recipient = recipient - it.timestamp = timestamp - }.build() - } - } -} diff --git a/library/src/main/java/org/xmtp/android/library/messages/MessageHeaderv2.kt b/library/src/main/java/org/xmtp/android/library/messages/MessageHeaderv2.kt deleted file mode 100644 index 2e7c2b7e5..000000000 --- a/library/src/main/java/org/xmtp/android/library/messages/MessageHeaderv2.kt +++ /dev/null @@ -1,16 +0,0 @@ -package org.xmtp.android.library.messages - -import java.util.Date - -typealias MessageHeaderV2 = org.xmtp.proto.message.contents.MessageOuterClass.MessageHeaderV2 - -class MessageHeaderV2Builder { - companion object { - fun buildFromTopic(topic: String, created: Date): MessageHeaderV2 { - return MessageHeaderV2.newBuilder().also { - it.topic = topic - it.createdNs = (created.time * 1_000_000) - }.build() - } - } -} diff --git a/library/src/main/java/org/xmtp/android/library/messages/MessageV1.kt b/library/src/main/java/org/xmtp/android/library/messages/MessageV1.kt deleted file mode 100644 index 2b8d7594c..000000000 --- a/library/src/main/java/org/xmtp/android/library/messages/MessageV1.kt +++ /dev/null @@ -1,84 +0,0 @@ -package org.xmtp.android.library.messages - -import com.google.protobuf.kotlin.toByteString -import org.xmtp.android.library.CipherText -import org.xmtp.android.library.Crypto -import org.xmtp.android.library.XMTPException -import org.xmtp.proto.message.contents.MessageOuterClass -import java.util.Date - -typealias MessageV1 = org.xmtp.proto.message.contents.MessageOuterClass.MessageV1 - -class MessageV1Builder { - companion object { - fun buildEncode( - sender: PrivateKeyBundleV1, - recipient: PublicKeyBundle, - message: ByteArray, - timestamp: Date - ): MessageV1 { - val secret = sender.sharedSecret( - peer = recipient, - myPreKey = sender.preKeysList[0].publicKey, - isRecipient = false - ) - val header = MessageHeaderV1Builder.buildFromPublicBundles( - sender = sender.toPublicKeyBundle(), - recipient = recipient, - timestamp = timestamp.time - ) - val headerBytes = header.toByteArray() - val ciphertext = Crypto.encrypt(secret, message, additionalData = headerBytes) - return buildFromCipherText(headerBytes = headerBytes, ciphertext = ciphertext) - } - - fun buildFromBytes(bytes: ByteArray): MessageV1 { - val message = Message.parseFrom(bytes) - val headerBytes: ByteArray - val ciphertext: CipherText - when (message.versionCase) { - MessageOuterClass.Message.VersionCase.V1 -> { - headerBytes = message.v1.headerBytes.toByteArray() - ciphertext = message.v1.ciphertext - } - MessageOuterClass.Message.VersionCase.V2 -> { - headerBytes = message.v2.headerBytes.toByteArray() - ciphertext = message.v2.ciphertext - } - else -> throw XMTPException("Cannot decode from bytes") - } - return buildFromCipherText(headerBytes, ciphertext) - } - - fun buildFromCipherText(headerBytes: ByteArray, ciphertext: CipherText?): MessageV1 { - return MessageV1.newBuilder().also { - it.headerBytes = headerBytes.toByteString() - it.ciphertext = ciphertext - }.build() - } - } -} - -val MessageV1.header: MessageHeaderV1 - get() = MessageHeaderV1.parseFrom(headerBytes) - -val MessageV1.senderAddress: String - get() = header.sender.identityKey.recoverWalletSignerPublicKey().walletAddress - -val MessageV1.sentAt: Date get() = Date(header.timestamp) - -val MessageV1.recipientAddress: String - get() = header.recipient.identityKey.recoverWalletSignerPublicKey().walletAddress - -fun MessageV1.decrypt(viewer: PrivateKeyBundleV1?): ByteArray? { - val header = MessageHeaderV1.parseFrom(headerBytes) - val recipient = header.recipient - val sender = header.sender - val secret: ByteArray = if (viewer?.walletAddress == sender.walletAddress) { - viewer.sharedSecret(peer = recipient, myPreKey = sender.preKey, isRecipient = false) - } else { - viewer?.sharedSecret(peer = sender, myPreKey = recipient.preKey, isRecipient = true) - ?: byteArrayOf() - } - return Crypto.decrypt(secret, ciphertext, additionalData = headerBytes.toByteArray()) -} diff --git a/library/src/main/java/org/xmtp/android/library/messages/MessageV2.kt b/library/src/main/java/org/xmtp/android/library/messages/MessageV2.kt deleted file mode 100644 index c8af884aa..000000000 --- a/library/src/main/java/org/xmtp/android/library/messages/MessageV2.kt +++ /dev/null @@ -1,174 +0,0 @@ -package org.xmtp.android.library.messages - -import com.google.protobuf.kotlin.toByteString -import com.google.protobuf.kotlin.toByteStringUtf8 -import org.web3j.crypto.ECDSASignature -import org.web3j.crypto.Hash -import org.web3j.crypto.Sign -import org.xmtp.android.library.CipherText -import org.xmtp.android.library.Client -import org.xmtp.android.library.Crypto -import org.xmtp.android.library.DecodedMessage -import org.xmtp.android.library.KeyUtil -import org.xmtp.android.library.XMTPException -import org.xmtp.android.library.codecs.ContentCodec -import org.xmtp.android.library.codecs.EncodedContent -import java.math.BigInteger -import java.util.Date - -typealias MessageV2 = org.xmtp.proto.message.contents.MessageOuterClass.MessageV2 - -class MessageV2Builder(val senderHmac: ByteArray? = null, val shouldPush: Boolean = false) { - lateinit var messageV2: MessageV2 - - companion object { - fun buildFromCipherText( - headerBytes: ByteArray, - ciphertext: CipherText?, - senderHmac: ByteArray, - shouldPush: Boolean, - ): MessageV2Builder { - val messageBuilder = MessageV2Builder(senderHmac = senderHmac, shouldPush = shouldPush) - messageBuilder.messageV2 = MessageV2.newBuilder().also { - it.headerBytes = headerBytes.toByteString() - it.ciphertext = ciphertext - it.shouldPush = shouldPush - it.senderHmac = senderHmac.toByteString() - }.build() - return messageBuilder - } - - fun buildDecode( - id: String, - topic: String, - message: MessageV2, - keyMaterial: ByteArray, - client: Client, - ): DecodedMessage { - try { - val decryptedMessage = buildDecrypt(id, topic, message, keyMaterial, client) - - return DecodedMessage( - id = id, - client = client, - topic = decryptedMessage.topic, - encodedContent = decryptedMessage.encodedContent, - senderAddress = decryptedMessage.senderAddress, - sent = decryptedMessage.sentAt, - ) - } catch (e: Exception) { - throw XMTPException("Error decoding message", e) - } - } - - fun buildDecrypt( - id: String, - topic: String, - message: MessageV2, - keyMaterial: ByteArray, - client: Client, - ): DecryptedMessage { - val decrypted = - Crypto.decrypt(keyMaterial, message.ciphertext, message.headerBytes.toByteArray()) - val signed = SignedContent.parseFrom(decrypted) - - if (!signed.sender.hasPreKey() || !signed.sender.hasIdentityKey()) { - throw XMTPException("missing sender pre-key or identity key") - } - - val senderPreKey = PublicKeyBuilder.buildFromSignedPublicKey(signed.sender.preKey) - val senderIdentityKey = - PublicKeyBuilder.buildFromSignedPublicKey(signed.sender.identityKey) - - if (!senderPreKey.signature.verify( - senderIdentityKey, - signed.sender.preKey.keyBytes.toByteArray(), - ) - ) { - throw XMTPException("pre-key not signed by identity key") - } - - // Verify content signature - val digest = - Hash.sha256(message.headerBytes.toByteArray() + signed.payload.toByteArray()) - - val signatureData = - KeyUtil.getSignatureData(signed.signature.rawData.toByteString().toByteArray()) - val publicKey = Sign.recoverFromSignature( - BigInteger(1, signatureData.v).toInt(), - ECDSASignature(BigInteger(1, signatureData.r), BigInteger(1, signatureData.s)), - digest, - ) - - val key = PublicKey.newBuilder().also { - it.secp256K1Uncompressed = it.secp256K1Uncompressed.toBuilder().also { keyBuilder -> - keyBuilder.bytes = - KeyUtil.addUncompressedByte(publicKey.toByteArray()).toByteString() - }.build() - }.build() - - if (key.walletAddress != (PublicKeyBuilder.buildFromSignedPublicKey(signed.sender.preKey).walletAddress)) { - throw XMTPException("Invalid signature") - } - - val encodedMessage = EncodedContent.parseFrom(signed.payload) - val header = MessageHeaderV2.parseFrom(message.headerBytes) - if (header.topic != topic) { - throw XMTPException("Topic mismatch") - } - - return DecryptedMessage( - id = id, - encodedContent = encodedMessage, - senderAddress = signed.sender.walletAddress, - sentAt = Date(header.createdNs / 1_000_000), - topic = topic, - ) - } - - private fun , T> shouldPush(codec: Codec, content: T?): Boolean { - if (content != null) { - return codec.shouldPush(content = content) - } else { - throw XMTPException("Codec invalid content") - } - } - - fun , T> buildEncode( - client: Client, - encodedContent: EncodedContent, - topic: String, - keyMaterial: ByteArray, - codec: Codec, - ): MessageV2Builder { - val payload = encodedContent.toByteArray() - val date = Date() - val header = MessageHeaderV2Builder.buildFromTopic(topic, date) - val headerBytes = header.toByteArray() - val digest = Hash.sha256(headerBytes + payload) - val preKey = client.keys.preKeysList?.get(0) - val signature = preKey?.sign(digest) - val bundle = client.v1keys.toV2().getPublicKeyBundle() - val signedContent = SignedContentBuilder.builderFromPayload(payload, bundle, signature) - val signedBytes = signedContent.toByteArray() - val ciphertext = Crypto.encrypt(keyMaterial, signedBytes, additionalData = headerBytes) - - val thirtyDayPeriodsSinceEpoch = - (Date().time / 1000 / 60 / 60 / 24 / 30).toInt() - val info = "$thirtyDayPeriodsSinceEpoch-${client.address}" - val infoEncoded = info.toByteStringUtf8().toByteArray() - val senderHmacGenerated = - Crypto.calculateMac( - Crypto.deriveKey(keyMaterial, ByteArray(0), infoEncoded), - headerBytes - ) - - return buildFromCipherText( - headerBytes, - ciphertext, - senderHmacGenerated, - shouldPush(codec = codec, content = codec.decode(encodedContent)), - ) - } - } -} 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..b7566b3be 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( @@ -56,24 +16,3 @@ 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())) -} 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..c3c8be764 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 @@ -2,7 +2,6 @@ package org.xmtp.android.library.messages import com.google.protobuf.kotlin.toByteString import org.xmtp.android.library.Util -import org.xmtp.android.library.toHex import org.xmtp.proto.message.contents.SignatureOuterClass import java.text.SimpleDateFormat import java.util.TimeZone @@ -29,26 +28,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()