diff --git a/library/src/main/java/org/xmtp/android/library/ApiClient.kt b/library/src/main/java/org/xmtp/android/library/ApiClient.kt index b3240e706..f8b8d9b9b 100644 --- a/library/src/main/java/org/xmtp/android/library/ApiClient.kt +++ b/library/src/main/java/org/xmtp/android/library/ApiClient.kt @@ -125,6 +125,11 @@ data class GRPCApiClient( return client.query(request, headers = headers) } + /** + * 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 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 def05a3b9..260b8927a 100644 --- a/library/src/main/java/org/xmtp/android/library/Client.kt +++ b/library/src/main/java/org/xmtp/android/library/Client.kt @@ -114,9 +114,9 @@ class Client() { EnvelopeBuilder.buildFromTopic( topic = Topic.userPrivateStoreKeyBundle(v1Key.walletAddress), timestamp = Date(), - message = encryptedKeys.toByteArray() - ) - ) + message = encryptedKeys.toByteArray(), + ), + ), ) } @@ -124,7 +124,7 @@ class Client() { val clientOptions = options ?: ClientOptions() val api = GRPCApiClient( environment = clientOptions.api.env, - secure = clientOptions.api.isSecure + secure = clientOptions.api.isSecure, ) return runBlocking { val topics = api.queryTopic(Topic.contact(peerAddress)).envelopesList @@ -184,6 +184,20 @@ class Client() { return Client(address = address, privateKeyBundleV1 = v1Bundle, apiClient = apiClient) } + /** + * 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, @@ -201,6 +215,11 @@ class Client() { } } + /** + * 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, @@ -291,7 +310,7 @@ class Client() { val authorized = AuthorizedIdentity( address = address, authorized = privateKeyBundleV1.identityKey.publicKey, - identity = privateKeyBundleV1.identityKey + identity = privateKeyBundleV1.identityKey, ) val authToken = authorized.createAuthToken() apiClient.setAuthToken(authToken) @@ -312,14 +331,14 @@ class Client() { val gson = GsonBuilder().create() val v2Export = gson.fromJson( conversationData.toString(StandardCharsets.UTF_8), - ConversationV2Export::class.java + 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 + ConversationV1Export::class.java, ) try { importV1Conversation(export = v1Export) @@ -337,12 +356,12 @@ class Client() { keyMaterial = keyMaterial, context = InvitationV1ContextBuilder.buildFromConversation( conversationId = export.context?.conversationId ?: "", - metadata = export.context?.metadata ?: mapOf() + metadata = export.context?.metadata ?: mapOf(), ), peerAddress = export.peerAddress, client = this, - header = SealedInvitationHeaderV1.newBuilder().build() - ) + header = SealedInvitationHeaderV1.newBuilder().build(), + ), ) } @@ -358,11 +377,18 @@ class Client() { ConversationV1( client = this, peerAddress = export.peerAddress, - sentAt = sentAt - ) + 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). + */ fun canMessage(peerAddress: String): Boolean { return runBlocking { query(Topic.contact(peerAddress)).envelopesList.size > 0 } } 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 af0324840..d4659f21c 100644 --- a/library/src/main/java/org/xmtp/android/library/Conversation.kt +++ b/library/src/main/java/org/xmtp/android/library/Conversation.kt @@ -13,15 +13,20 @@ import org.xmtp.proto.message.contents.Invitation.InvitationV1.Aes256gcmHkdfsha2 import org.xmtp.android.library.messages.DecryptedMessage 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() - enum class Version { - V1, - V2 - } + enum class Version { V1, V2 } + // This indicates whether this a v1 or v2 conversation. val version: Version get() { return when (this) { @@ -30,6 +35,7 @@ sealed class Conversation { } } + // When the conversation was first created. val createdAt: Date get() { return when (this) { @@ -38,6 +44,7 @@ sealed class Conversation { } } + // This is the address of the peer that I am talking to. val peerAddress: String get() { return when (this) { @@ -46,6 +53,8 @@ sealed class Conversation { } } + // This distinctly identifies between two addresses. + // Note: this will be empty for older v1 conversations. val conversationId: String? get() { return when (this) { @@ -70,6 +79,11 @@ sealed class Conversation { return client.contacts.consentList.state(address = peerAddress) } + /** + * 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) @@ -82,8 +96,8 @@ sealed class Conversation { .setContext(conversationV2.context) .setAes256GcmHkdfSha256( Aes256gcmHkdfsha256.newBuilder() - .setKeyMaterial(conversationV2.keyMaterial.toByteString()) - ) + .setKeyMaterial(conversationV2.keyMaterial.toByteString()), + ), ).build() } } @@ -149,6 +163,7 @@ sealed class Conversation { return client.address } + // Is the topic of the conversation depending of the version val topic: String get() { return when (this) { @@ -157,6 +172,19 @@ sealed class Conversation { } } + /** + * 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. + */ fun messages( limit: Int? = null, before: Date? = null, @@ -168,7 +196,7 @@ sealed class Conversation { limit = limit, before = before, after = after, - direction = direction + direction = direction, ) is V2 -> @@ -176,7 +204,7 @@ sealed class Conversation { limit = limit, before = before, after = after, - direction = direction + direction = direction, ) } } @@ -202,6 +230,7 @@ sealed class Conversation { } } + // Get the client according to the version val client: Client get() { return when (this) { @@ -210,6 +239,10 @@ sealed class Conversation { } } + /** + * 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() diff --git a/library/src/main/java/org/xmtp/android/library/ConversationV1.kt b/library/src/main/java/org/xmtp/android/library/ConversationV1.kt index 296f90bb3..f23c73f73 100644 --- a/library/src/main/java/org/xmtp/android/library/ConversationV1.kt +++ b/library/src/main/java/org/xmtp/android/library/ConversationV1.kt @@ -34,12 +34,32 @@ data class ConversationV1( 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 = flow { client.subscribe(listOf(topic.description)).collect { emit(decode(envelope = it)) } } + /** + * 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 + */ fun messages( limit: Int? = null, before: Date? = null, @@ -57,6 +77,19 @@ data class ConversationV1( } } + /** + * 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. + */ fun decryptedMessages( limit: Int? = null, before: Date? = null, @@ -69,13 +102,18 @@ data class ConversationV1( val envelopes = runBlocking { client.apiClient.envelopes( topic = Topic.directMessageV1(client.address, peerAddress).description, - pagination = pagination + 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) @@ -88,13 +126,18 @@ data class ConversationV1( id = generateId(envelope), encodedContent = encodedMessage, senderAddress = header.sender.walletAddress, - sentAt = message.v1.sentAt + 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) @@ -105,7 +148,7 @@ data class ConversationV1( topic = envelope.contentTopic, encodedContent = decryptedMessage.encodedContent, senderAddress = decryptedMessage.senderAddress, - sent = decryptedMessage.sentAt + sent = decryptedMessage.sentAt, ) } catch (e: Exception) { throw XMTPException("Error decoding message", e) @@ -193,7 +236,7 @@ data class ConversationV1( sender = client.privateKeyBundleV1, recipient = recipient, message = encodedContent.toByteArray(), - timestamp = date + timestamp = date, ) val isEphemeral: Boolean = options != null && options.ephemeral @@ -202,7 +245,7 @@ data class ConversationV1( EnvelopeBuilder.buildFromString( topic = if (isEphemeral) ephemeralTopic else topic.description, timestamp = date, - message = MessageBuilder.buildFromMessageV1(v1 = message).toByteArray() + message = MessageBuilder.buildFromMessageV1(v1 = message).toByteArray(), ) val envelopes = mutableListOf(env) @@ -215,7 +258,7 @@ data class ConversationV1( env.toBuilder().apply { contentTopic = Topic.userIntro(client.address).description }.build(), - ) + ), ) client.contacts.hasIntroduced[peerAddress] = true } diff --git a/library/src/main/java/org/xmtp/android/library/ConversationV2.kt b/library/src/main/java/org/xmtp/android/library/ConversationV2.kt index 4ae5bf1b5..3f6e610e4 100644 --- a/library/src/main/java/org/xmtp/android/library/ConversationV2.kt +++ b/library/src/main/java/org/xmtp/android/library/ConversationV2.kt @@ -57,6 +57,20 @@ data class ConversationV2( val createdAt: Date = Date(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 + */ fun messages( limit: Int? = null, before: Date? = null, @@ -77,6 +91,19 @@ data class ConversationV2( } } + /** + * 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. + */ fun decryptedMessages( limit: Int? = null, before: Date? = null, @@ -92,6 +119,11 @@ data class ConversationV2( } } + /** + * 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( @@ -109,6 +141,11 @@ data class ConversationV2( } } + /** + * 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( @@ -120,6 +157,11 @@ data class ConversationV2( ) } + /** + * 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) 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 15234674d..51c897026 100644 --- a/library/src/main/java/org/xmtp/android/library/Conversations.kt +++ b/library/src/main/java/org/xmtp/android/library/Conversations.kt @@ -43,6 +43,12 @@ data class Conversations( private const val TAG = "CONVERSATIONS" } + /** + * 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 { val sealedInvitation = Invitation.SealedInvitation.parseFrom(envelope.message) val unsealed = sealedInvitation.v1.getInvitation(viewer = client.keys) @@ -50,11 +56,17 @@ data class Conversations( ConversationV2.create( client = client, invitation = unsealed, - header = sealedInvitation.v1.header - ) + 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 { val messageV1 = MessageV1Builder.buildFromBytes(envelope.message.toByteArray()) val senderAddress = messageV1.header.sender.walletAddress @@ -64,11 +76,18 @@ data class Conversations( ConversationV1( client = client, peerAddress = peerAddress, - sentAt = messageV1.sentAt - ) + sentAt = messageV1.sentAt, + ), ) } + /** + * 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. + */ fun newConversation( peerAddress: String, context: Invitation.InvitationV1.Context? = null, @@ -93,8 +112,8 @@ data class Conversations( ConversationV1( client = client, peerAddress = peerAddress, - sentAt = peerSeenAt - ) + sentAt = peerSeenAt, + ), ) conversationsByTopic[conversation.topic] = conversation return conversation @@ -107,8 +126,8 @@ data class Conversations( ConversationV1( client = client, peerAddress = peerAddress, - sentAt = Date() - ) + sentAt = Date(), + ), ) conversationsByTopic[conversation.topic] = conversation return conversation @@ -127,8 +146,8 @@ data class Conversations( context = invite.context, peerAddress = peerAddress, client = client, - header = sealedInvitation.v1.header - ) + header = sealedInvitation.v1.header, + ), ) conversationsByTopic[conversation.topic] = conversation return conversation @@ -143,7 +162,7 @@ data class Conversations( val conversationV2 = ConversationV2.create( client = client, invitation = invitation, - header = sealedInvitation.v1.header + header = sealedInvitation.v1.header, ) client.contacts.allow(addresses = listOf(peerAddress)) val conversation = Conversation.V2(conversationV2) @@ -151,6 +170,10 @@ data class Conversations( return conversation } + /** + * Get the list of conversations that current user has + * @return The list of [Conversation] that the current [Client] has. + */ fun list(): List { val newConversations = mutableListOf() val mostRecent = conversationsByTopic.values.maxOfOrNull { it.createdAt } @@ -162,9 +185,9 @@ data class Conversations( ConversationV1( client = client, peerAddress = peerAddress, - sentAt = sentAt - ) - ) + sentAt = sentAt, + ), + ), ) } val invitations = listInvitations(pagination = pagination) @@ -192,8 +215,8 @@ data class Conversations( ConversationV1( client, data.peerAddress, - sentAt - ) + sentAt, + ), ) } else { conversation = Conversation.V2( @@ -203,8 +226,8 @@ data class Conversations( context = data.invitation.context, peerAddress = data.peerAddress, client = client, - header = Invitation.SealedInvitationHeaderV1.getDefaultInstance() - ) + header = Invitation.SealedInvitationHeaderV1.getDefaultInstance(), + ), ) } conversationsByTopic[conversation.topic] = conversation @@ -249,6 +272,11 @@ data class Conversations( return seenPeers } + /** + * 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 fun listInvitations(pagination: Pagination? = null): List { val envelopes = runBlocking { client.apiClient.envelopes(Topic.userInvite(client.address).description, pagination) @@ -267,6 +295,11 @@ data class Conversations( ) } + /** + * @return This lists messages sent to the [Conversation]. + * This pulls messages from multiple conversations in a single call. + * @see Conversation.messages + */ fun listBatchMessages( topics: List>, ): List { @@ -291,13 +324,18 @@ data class Conversations( 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 + */ fun listBatchDecryptedMessages( topics: List>, ): List { @@ -322,13 +360,20 @@ data class Conversations( val msg = conversation.decrypt(envelope) msg } - } + }, ) } } return messages } + /** + * 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. + */ fun sendInvitation( recipient: SignedPublicKeyBundle, invitation: InvitationV1, @@ -339,7 +384,7 @@ data class Conversations( sender = it, recipient = recipient, created = created, - invitation = invitation + invitation = invitation, ) val peerAddress = recipient.walletAddress @@ -348,29 +393,34 @@ data class Conversations( envelopes = listOf( EnvelopeBuilder.buildFromTopic( topic = Topic.userInvite( - client.address + client.address, ), timestamp = created, - message = sealed.toByteArray() + message = sealed.toByteArray(), ), EnvelopeBuilder.buildFromTopic( topic = Topic.userInvite( - peerAddress + peerAddress, ), timestamp = created, - message = sealed.toByteArray() - ) - ) + message = sealed.toByteArray(), + ), + ), ) } return sealed } } + /** + * This subscribes the current [Client] to a topic as userIntro and userInvite and returns a flow + * of the information of those conversations according to the topics + * @return Stream of data information for the conversations + */ fun stream(): Flow = flow { val streamedConversationTopics: MutableSet = mutableSetOf() client.subscribeTopic( - listOf(Topic.userIntro(client.address), Topic.userInvite(client.address)) + listOf(Topic.userIntro(client.address), Topic.userInvite(client.address)), ).collect { envelope -> if (envelope.contentTopic == Topic.userIntro(client.address).description) { @@ -391,10 +441,15 @@ data class Conversations( } } + /** + * 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 + */ fun streamAllMessages(): Flow = flow { val topics = mutableListOf( Topic.userInvite(client.address).description, - Topic.userIntro(client.address).description + Topic.userIntro(client.address).description, ) for (conversation in list()) { @@ -449,7 +504,7 @@ data class Conversations( fun streamAllDecryptedMessages(): Flow = flow { val topics = mutableListOf( Topic.userInvite(client.address).description, - Topic.userIntro(client.address).description + Topic.userIntro(client.address).description, ) for (conversation in list()) { 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 708e79015..34bd986dd 100644 --- a/library/src/main/java/org/xmtp/android/library/SigningKey.kt +++ b/library/src/main/java/org/xmtp/android/library/SigningKey.kt @@ -24,6 +24,14 @@ interface SigningKey { suspend fun sign(message: String): SignatureOuterClass.Signature? } +/** + * 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, 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 index aa9171877..983eafa88 100644 --- a/library/src/main/java/org/xmtp/android/library/messages/ContactBundle.kt +++ b/library/src/main/java/org/xmtp/android/library/messages/ContactBundle.kt @@ -45,6 +45,9 @@ fun ContactBundle.toSignedPublicKeyBundle(): SignedPublicKeyBundle { } } +/** + * Create a wallet address according to the version + */ val ContactBundle.walletAddress: String? get() { when (versionCase) { @@ -54,26 +57,31 @@ val ContactBundle.walletAddress: String? Arrays.copyOfRange( key.secp256K1Uncompressed.bytes.toByteArray(), 1, - key.secp256K1Uncompressed.bytes.toByteArray().size - ) + 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 - ) + 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) { @@ -86,6 +94,7 @@ val ContactBundle.identityAddress: String? } publicKey?.walletAddress } + else -> null } } 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 dc5213710..26679e069 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 @@ -27,9 +27,23 @@ 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/") 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 9927f852d..36f5761f0 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 @@ -9,6 +9,11 @@ sealed class Topic { data class directMessageV2(val addresses: String?) : Topic() data class preferenceList(val identifier: 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) { @@ -30,6 +35,11 @@ sealed class Topic { private fun wrap(value: String): String = "/xmtp/0/$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/")