Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create LibXMTP Client - android #231

Merged
merged 17 commits into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,19 @@ repositories {
dependencies {
implementation project(':expo-modules-core')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
implementation "org.xmtp:android:0.7.6"
// implementation "org.xmtp:android:0.7.6"
implementation 'com.google.code.gson:gson:2.10.1'
implementation 'com.facebook.react:react-native:0.71.3'
implementation "com.daveanthonythomas.moshipack:moshipack:1.0.1"
// xmtp-android local testing
implementation files('<PATH TO XMTP-ANDROID>/xmtp-android/library/build/outputs/aar/library-debug.aar')
implementation 'com.google.crypto.tink:tink-android:1.7.0'
implementation 'io.grpc:grpc-kotlin-stub:1.3.0'
implementation 'io.grpc:grpc-okhttp:1.51.1'
implementation 'io.grpc:grpc-protobuf-lite:1.51.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4'
implementation 'org.web3j:crypto:5.0.0'
implementation "net.java.dev.jna:jna:5.13.0@aar"
implementation 'com.google.protobuf:protobuf-kotlin-lite:3.22.3'
implementation 'org.xmtp:proto-kotlin:3.40.1'
}
170 changes: 159 additions & 11 deletions android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package expo.modules.xmtpreactnativesdk

import android.content.Context
import android.net.Uri
import android.util.Base64
import android.util.Base64.NO_WRAP
import android.util.Log
import androidx.core.net.toUri
import com.google.gson.JsonParser
import com.google.protobuf.kotlin.toByteString
import expo.modules.kotlin.exception.Exceptions
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
import expo.modules.xmtpreactnativesdk.wrappers.ConsentWrapper
Expand All @@ -16,17 +18,20 @@ import expo.modules.xmtpreactnativesdk.wrappers.ConversationWrapper
import expo.modules.xmtpreactnativesdk.wrappers.DecodedMessageWrapper
import expo.modules.xmtpreactnativesdk.wrappers.DecryptedLocalAttachment
import expo.modules.xmtpreactnativesdk.wrappers.EncryptedLocalAttachment
import expo.modules.xmtpreactnativesdk.wrappers.GroupWrapper
import expo.modules.xmtpreactnativesdk.wrappers.PreparedLocalMessage
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.suspendCancellableCoroutine
import org.json.JSONObject
import org.xmtp.android.library.Client
import org.xmtp.android.library.ClientOptions
import org.xmtp.android.library.Conversation
import org.xmtp.android.library.Group
import org.xmtp.android.library.PreEventCallback
import org.xmtp.android.library.PreparedMessage
import org.xmtp.android.library.SendOptions
Expand Down Expand Up @@ -76,16 +81,25 @@ class ReactNativeSigner(var module: XMTPModule, override var address: String) :
continuations.remove(id)
}

override suspend fun sign(data: ByteArray): Signature {
val request = SignatureRequest(message = String(data, Charsets.UTF_8))
override suspend fun sign(data: ByteArray): Signature? {
val message = String(data, Charsets.UTF_8)
return signLegacy(message)
}

override suspend fun signLegacy(message: String): Signature {
val request = SignatureRequest(message = message)
module.sendEvent("sign", mapOf("id" to request.id, "message" to request.message))
return suspendCancellableCoroutine { continuation ->
continuations[request.id] = continuation
}
}

override suspend fun sign(message: String): Signature =
sign(message.toByteArray())
override fun sign(message: String): ByteArray {
return runBlocking {
signLegacy(message).toByteArray()
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 I wonder if this will work for sure worth trying but I almost think that would just give me the bytearray of the signature object

When I think we want something more like this
https://github.com/xmtp/xmtp-android/pull/156/files#diff-2be5e4bd7739dcb1debb7023943ee5544ebef6355405115cf89aa0e0a3ee09edR97-R102

Maybe this would do to get the bytes out of the signature 🤔

Suggested change
return runBlocking {
signLegacy(message).toByteArray()
}
return runBlocking {
signLegacy(message).ecdsaCompact.bytes.toByteArray()
}

}

}

data class SignatureRequest(
Expand All @@ -97,7 +111,15 @@ fun Conversation.cacheKey(clientAddress: String): String {
return "${clientAddress}:${topic}"
}

fun Group.cacheKey(clientAddress: String): String {
return "${clientAddress}:${id}"
}

class XMTPModule : Module() {

val context: Context
get() = appContext.reactContext ?: throw Exceptions.ReactContextLost()

private fun apiEnvironments(env: String, appVersion: String?): ClientOptions.Api {
return when (env) {
"local" -> ClientOptions.Api(
Expand Down Expand Up @@ -125,6 +147,7 @@ class XMTPModule : Module() {
private var signer: ReactNativeSigner? = null
private val isDebugEnabled = BuildConfig.DEBUG // TODO: consider making this configurable
private val conversations: MutableMap<String, Conversation> = mutableMapOf()
private val groups: MutableMap<String, Group> = mutableMapOf()
private val subscriptions: MutableMap<String, Job> = mutableMapOf()
private var preEnableIdentityCallbackDeferred: CompletableDeferred<Unit>? = null
private var preCreateIdentityCallbackDeferred: CompletableDeferred<Unit>? = null
Expand All @@ -150,8 +173,9 @@ class XMTPModule : Module() {
//
// Auth functions
//
AsyncFunction("auth") { address: String, environment: String, appVersion: String?, hasCreateIdentityCallback: Boolean?, hasEnableIdentityCallback: Boolean? ->
AsyncFunction("auth") { address: String, environment: String, appVersion: String?, hasCreateIdentityCallback: Boolean?, hasEnableIdentityCallback: Boolean?, enableAlphaMls: Boolean? ->
logV("auth")
requireLocalEnvForAlphaMLS(enableAlphaMls, environment)
val reactSigner = ReactNativeSigner(module = this@XMTPModule, address = address)
signer = reactSigner

Expand All @@ -163,10 +187,14 @@ class XMTPModule : Module() {
preCreateIdentityCallback.takeIf { hasCreateIdentityCallback == true }
val preEnableIdentityCallback: PreEventCallback? =
preEnableIdentityCallback.takeIf { hasEnableIdentityCallback == true }
val context = if (enableAlphaMls == true) context else null
nplasterer marked this conversation as resolved.
Show resolved Hide resolved

val options = ClientOptions(
api = apiEnvironments(environment, appVersion),
preCreateIdentityCallback = preCreateIdentityCallback,
preEnableIdentityCallback = preEnableIdentityCallback
preEnableIdentityCallback = preEnableIdentityCallback,
enableAlphaMls = enableAlphaMls == true,
appContext = context
)
clients[address] = Client().create(account = reactSigner, options = options)
ContentJson.Companion
Expand All @@ -180,8 +208,9 @@ class XMTPModule : Module() {
}

// Generate a random wallet and set the client to that
AsyncFunction("createRandom") { environment: String, appVersion: String?, hasCreateIdentityCallback: Boolean?, hasEnableIdentityCallback: Boolean? ->
AsyncFunction("createRandom") { environment: String, appVersion: String?, hasCreateIdentityCallback: Boolean?, hasEnableIdentityCallback: Boolean?, enableAlphaMls: Boolean? ->
nplasterer marked this conversation as resolved.
Show resolved Hide resolved
logV("createRandom")
requireLocalEnvForAlphaMLS(enableAlphaMls, environment)
val privateKey = PrivateKeyBuilder()

if (hasCreateIdentityCallback == true)
Expand All @@ -192,22 +221,31 @@ class XMTPModule : Module() {
preCreateIdentityCallback.takeIf { hasCreateIdentityCallback == true }
val preEnableIdentityCallback: PreEventCallback? =
preEnableIdentityCallback.takeIf { hasEnableIdentityCallback == true }
val context = if (enableAlphaMls == true) context else null

val options = ClientOptions(
api = apiEnvironments(environment, appVersion),
preCreateIdentityCallback = preCreateIdentityCallback,
preEnableIdentityCallback = preEnableIdentityCallback
preEnableIdentityCallback = preEnableIdentityCallback,
enableAlphaMls = enableAlphaMls == true,
appContext = context
)
val randomClient = Client().create(account = privateKey, options = options)
ContentJson.Companion
clients[randomClient.address] = randomClient
randomClient.address
}

AsyncFunction("createFromKeyBundle") { keyBundle: String, environment: String, appVersion: String? ->
AsyncFunction("createFromKeyBundle") { keyBundle: String, environment: String, appVersion: String?, enableAlphaMls: Boolean? ->
logV("createFromKeyBundle")
requireLocalEnvForAlphaMLS(enableAlphaMls, environment)
try {
logV("createFromKeyBundle")
val options = ClientOptions(api = apiEnvironments(environment, appVersion))
nplasterer marked this conversation as resolved.
Show resolved Hide resolved
val context = if (enableAlphaMls == true) context else null
val options = ClientOptions(
api = apiEnvironments(environment, appVersion),
enableAlphaMls = enableAlphaMls == true,
appContext = context
)
val bundle =
PrivateKeyOuterClass.PrivateKeyBundle.parseFrom(
Base64.decode(
Expand All @@ -224,6 +262,13 @@ class XMTPModule : Module() {
}
}

Function("getLibXMTPClientAccountAddress") { clientAddress: String -> String
logV("getLibXMTPClientAccountAddress")
val client = clients[clientAddress] ?: throw XMTPException("No client")
val libXMTPClient = client.libXMTPClient ?: throw XMTPException("No libxmtp client")
return@Function libXMTPClient.accountAddress()
}
cameronvoell marked this conversation as resolved.
Show resolved Hide resolved

AsyncFunction("exportKeyBundle") { clientAddress: String ->
logV("exportKeyBundle")
val client = clients[clientAddress] ?: throw XMTPException("No client")
Expand Down Expand Up @@ -352,6 +397,16 @@ class XMTPModule : Module() {
}
}

AsyncFunction("listGroups") { clientAddress: String ->
logV("listGroups")
val client = clients[clientAddress] ?: throw XMTPException("No client")
val groupList = client.conversations.listGroups()
groupList.map { group ->
groups[group.cacheKey(clientAddress)] = group
GroupWrapper.encode(client, group)
}
}

AsyncFunction("loadMessages") { clientAddress: String, topic: String, limit: Int?, before: Long?, after: Long?, direction: String? ->
logV("loadMessages")
val conversation =
Expand All @@ -373,6 +428,16 @@ class XMTPModule : Module() {
.map { DecodedMessageWrapper.encode(it) }
}

AsyncFunction("groupMessages") { clientAddress: String, id: String ->
logV("groupMessages")
val client = clients[clientAddress] ?: throw XMTPException("No client")
if (client.libXMTPClient == null) {
throw XMTPException("Create client with enableAlphaMLS true in order to synch groups")
}
val group = findGroup(clientAddress, id)
group?.decryptedMessages()?.map { DecodedMessageWrapper.encode(it) }
}

AsyncFunction("loadBatchMessages") { clientAddress: String, topics: List<String> ->
logV("loadBatchMessages")
val client = clients[clientAddress] ?: throw XMTPException("No client")
Expand Down Expand Up @@ -433,6 +498,21 @@ class XMTPModule : Module() {
)
}

AsyncFunction("sendMessageToGroup") { clientAddress: String, idString: String, contentJson: String ->
logV("sendMessageToGroup")
val group =
findGroup(
clientAddress = clientAddress,
idString = idString
)
?: throw XMTPException("no group found for $idString")
val sending = ContentJson.fromJson(contentJson)
group.send(
content = sending.content,
options = SendOptions(contentType = sending.type)
)
}

AsyncFunction("prepareMessage") { clientAddress: String, conversationTopic: String, contentJson: String ->
logV("prepareMessage")
val conversation =
Expand Down Expand Up @@ -531,6 +611,47 @@ class XMTPModule : Module() {
ConversationWrapper.encode(client, conversation)
}

AsyncFunction("createGroup") { clientAddress: String, peerAddresses: List<String> ->
logV("createGroup")
val client = clients[clientAddress] ?: throw XMTPException("No client")
if (client.libXMTPClient == null) {
throw XMTPException("Create client with enableAlphaMLS true in order to create a group")
}
val group = client.conversations.newGroup(peerAddresses)
logV("id after creating group: " + Base64.encodeToString(group.id, NO_WRAP))
val encodedGroup = GroupWrapper.encode(client, group)
return@AsyncFunction encodedGroup
}

AsyncFunction("listMembers") { clientAddress: String, groupId: String ->
cameronvoell marked this conversation as resolved.
Show resolved Hide resolved
logV("listMembers")
val client = clients[clientAddress] ?: throw XMTPException("No client")
if (client.libXMTPClient == null) {
throw XMTPException("Create client with enableAlphaMLS true in order to create a group")
}
val group = findGroup(clientAddress, groupId)
return@AsyncFunction group?.memberAddresses()
}

AsyncFunction("syncGroups") { clientAddress: String ->
logV("syncGroups")
val client = clients[clientAddress] ?: throw XMTPException("No client")
if (client.libXMTPClient == null) {
throw XMTPException("Create client with enableAlphaMLS true in order to synch groups")
}
runBlocking { client.conversations.syncGroups() }
}

AsyncFunction("syncGroup") { clientAddress: String, id: String ->
logV("syncGroup")
val client = clients[clientAddress] ?: throw XMTPException("No client")
if (client.libXMTPClient == null) {
throw XMTPException("Create client with enableAlphaMLS true in order to synch groups")
}
val group = findGroup(clientAddress, id)
runBlocking { group?.sync() }
}

Function("subscribeToConversations") { clientAddress: String ->
logV("subscribeToConversations")
subscribeToConversations(clientAddress = clientAddress)
Expand Down Expand Up @@ -673,6 +794,27 @@ class XMTPModule : Module() {
return null
}

private fun findGroup(
clientAddress: String,
idString: String,
): Group? {
val client = clients[clientAddress] ?: throw XMTPException("No client")

val cacheKey = "${clientAddress}:${idString}"
val cacheGroup = groups[cacheKey]
if (cacheGroup != null) {
return cacheGroup
} else {
val group = client.conversations.listGroups()
.firstOrNull { Base64.encodeToString(it.id, NO_WRAP) == idString }
if (group != null) {
groups[group.cacheKey(clientAddress)] = group
return group
}
}
return null
}

private fun subscribeToConversations(clientAddress: String) {
val client = clients[clientAddress] ?: throw XMTPException("No client")

Expand Down Expand Up @@ -780,6 +922,12 @@ class XMTPModule : Module() {
preCreateIdentityCallbackDeferred?.await()
preCreateIdentityCallbackDeferred = null
}

private fun requireLocalEnvForAlphaMLS(enableAlphaMls: Boolean?, environment: String) {
if (enableAlphaMls == true && environment != "local") {
throw XMTPException("Environment must be \"local\" to enable alpha MLS")
}
}
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package expo.modules.xmtpreactnativesdk.wrappers

import android.util.Base64
import android.util.Base64.NO_WRAP
import com.google.gson.GsonBuilder
import org.xmtp.android.library.Client
import org.xmtp.android.library.Group

class GroupWrapper {

companion object {
private fun encodeToObj(client: Client, group: Group, idString: String): Map<String, Any> {
return mapOf(
"clientAddress" to client.address,
"id" to idString,
"createdAt" to group.createdAt.time,
"peerAddresses" to group.memberAddresses(),

)
}

fun encode(client: Client, group: Group): String {
val gson = GsonBuilder().create()
val obj = encodeToObj(client, group, Base64.encodeToString(group.id, NO_WRAP))
return gson.toJson(obj)
}
}
}
Loading
Loading