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 8 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.31.0'
}
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 @@ -22,6 +24,7 @@ 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
Expand Down Expand Up @@ -76,16 +79,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 @@ -98,6 +110,10 @@ fun Conversation.cacheKey(clientAddress: String): String {
}

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 @@ -150,7 +166,7 @@ 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")
val reactSigner = ReactNativeSigner(module = this@XMTPModule, address = address)
signer = reactSigner
Expand All @@ -163,10 +179,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,7 +200,7 @@ 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")
val privateKey = PrivateKeyBuilder()

Expand All @@ -192,22 +212,30 @@ 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? ->
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 +252,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
54 changes: 54 additions & 0 deletions example/src/LaunchScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,24 @@ export default function LaunchScreen({
}}
/>
</View>
<View key="connected-groups-local" style={{ margin: 16 }}>
<Button
title="Use Connected Wallet with Groups (local)"
color="purple"
onPress={() => {
configureWallet(
'local',
XMTP.Client.create(signer, {
env: 'local',
appVersion,
preCreateIdentityCallback,
preEnableIdentityCallback,
enableAlphaMls: true,
})
)
}}
/>
</View>
</>
)}
<Divider key="divider-generated" />
Expand Down Expand Up @@ -189,6 +207,25 @@ export default function LaunchScreen({
}}
/>
</View>
<View key="generated-groups-local" style={{ margin: 16 }}>
<Button
title="Use Generated Wallet with Groups (local)"
color="purple"
onPress={() => {
configureWallet(
'local',
XMTP.Client.createRandom({
enableAlphaMls: true,
env: 'local',
appVersion,
codecs: supportedCodecs,
preCreateIdentityCallback,
preEnableIdentityCallback,
})
)
}}
/>
</View>
{!!savedKeys.keyBundle && (
<>
<Divider key="divider-saved" />
Expand Down Expand Up @@ -236,6 +273,23 @@ export default function LaunchScreen({
}}
/>
</View>
<View key="saved-groups-local" style={{ margin: 16 }}>
<Button
title="Use Saved Wallet with Groups (local)"
color="purple"
onPress={() => {
configureWallet(
'local',
XMTP.Client.createFromKeyBundle(savedKeys.keyBundle!, {
enableAlphaMls: true,
env: 'local',
appVersion,
codecs: supportedCodecs,
})
)
}}
/>
</View>
<View key="saved-clear" style={{ margin: 16 }}>
<Button
title="Clear Saved Wallet"
Expand Down
14 changes: 14 additions & 0 deletions example/src/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,20 @@
return client.address.length > 0
})

test('can make a MLS V3 client', async () => {
const client = await Client.createRandom({
env: 'local',
appVersion: 'Testing/0.0.0',
enableAlphaMls: true

Check warning on line 105 in example/src/tests.ts

View workflow job for this annotation

GitHub Actions / lint

Insert `,`
})
const libXMTPClient = client.libXMTPClient
if (client.address.toLowerCase() != libXMTPClient?.accountAddress().toLowerCase()) {

Check warning on line 108 in example/src/tests.ts

View workflow job for this annotation

GitHub Actions / lint

Replace `client.address.toLowerCase()·!=·libXMTPClient?.accountAddress().toLowerCase()` with `⏎····client.address.toLowerCase()·!=⏎····libXMTPClient?.accountAddress().toLowerCase()⏎··`

Check warning on line 108 in example/src/tests.ts

View workflow job for this annotation

GitHub Actions / lint

Expected '!==' and instead saw '!='
throw new Error('did not receive correct address from libXMTPClient')
}

return true
})

test('can pass a custom filter date and receive message objects with expected dates', async () => {
try {
const bob = await Client.createRandom({ env: 'local' })
Expand Down
10 changes: 8 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export function address(): string {
return XMTPModule.address()
}

export function getLibXMTPClientAccountAddress(address: string): string {
return XMTPModule.getLibXMTPClientAccountAddress(address)
}

export async function auth(
address: string,
environment: 'local' | 'dev' | 'production',
Expand All @@ -54,13 +58,15 @@ export async function createRandom(
environment: 'local' | 'dev' | 'production',
appVersion?: string | undefined,
hasCreateIdentityCallback?: boolean | undefined,
hasEnableIdentityCallback?: boolean | undefined
hasEnableIdentityCallback?: boolean | undefined,
enableAlphaMls?: boolean | undefined
): Promise<string> {
return await XMTPModule.createRandom(
environment,
appVersion,
hasCreateIdentityCallback,
hasEnableIdentityCallback
hasEnableIdentityCallback,
enableAlphaMls
)
}

Expand Down
Loading
Loading