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

Add new fetch methods for V3 local DB #430

Merged
merged 13 commits into from
Jun 25, 2024
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ repositories {
dependencies {
implementation project(':expo-modules-core')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
implementation "org.xmtp:android:0.13.7"
implementation "org.xmtp:android:0.13.8"
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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,25 @@ import android.util.Base64
import android.util.Base64.NO_WRAP
import android.util.Log
import androidx.core.net.toUri
import com.facebook.common.util.Hex
import com.google.gson.JsonParser
import com.google.protobuf.kotlin.toByteString
import expo.modules.kotlin.exception.Exceptions
import expo.modules.kotlin.functions.Coroutine
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
import expo.modules.xmtpreactnativesdk.wrappers.AuthParamsWrapper
import expo.modules.xmtpreactnativesdk.wrappers.ClientWrapper
import expo.modules.xmtpreactnativesdk.wrappers.ConsentWrapper
import expo.modules.xmtpreactnativesdk.wrappers.ConsentWrapper.Companion.consentStateToString
import expo.modules.xmtpreactnativesdk.wrappers.ContentJson
import expo.modules.xmtpreactnativesdk.wrappers.ConversationContainerWrapper
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.ConversationContainerWrapper
import expo.modules.xmtpreactnativesdk.wrappers.MemberWrapper
import expo.modules.xmtpreactnativesdk.wrappers.PreparedLocalMessage
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
Expand Down Expand Up @@ -49,10 +53,12 @@ import org.xmtp.android.library.codecs.RemoteAttachment
import org.xmtp.android.library.codecs.decoded
import org.xmtp.android.library.messages.EnvelopeBuilder
import org.xmtp.android.library.messages.InvitationV1ContextBuilder
import org.xmtp.android.library.messages.MessageDeliveryStatus
import org.xmtp.android.library.messages.Pagination
import org.xmtp.android.library.messages.PrivateKeyBuilder
import org.xmtp.android.library.messages.Signature
import org.xmtp.android.library.messages.getPublicKeyBundle
import org.xmtp.android.library.push.Service
import org.xmtp.android.library.push.XMTPPush
import org.xmtp.android.library.toHex
import org.xmtp.proto.keystore.api.v1.Keystore.TopicMap.TopicData
Expand All @@ -66,12 +72,6 @@ import java.util.UUID
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import com.facebook.common.util.Hex
import expo.modules.xmtpreactnativesdk.wrappers.ClientWrapper
import expo.modules.xmtpreactnativesdk.wrappers.MemberWrapper
import org.xmtp.android.library.messages.MessageDeliveryStatus
import org.xmtp.android.library.messages.Topic
import org.xmtp.android.library.push.Service

class ReactNativeSigner(var module: XMTPModule, override var address: String) : SigningKey {
private val continuations: MutableMap<String, Continuation<Signature>> = mutableMapOf()
Expand Down Expand Up @@ -190,6 +190,14 @@ class XMTPModule : Module() {
client?.inboxId ?: "No Client."
}

AsyncFunction("findInboxIdFromAddress") Coroutine { inboxId: String, address: String ->
withContext(Dispatchers.IO) {
logV("findInboxIdFromAddress")
val client = clients[inboxId] ?: throw XMTPException("No client")
client.inboxIdFromAddress(address)
}
}

AsyncFunction("deleteLocalDatabase") { inboxId: String ->
logV(inboxId)
logV(clients.toString())
Expand All @@ -209,13 +217,21 @@ class XMTPModule : Module() {
}
}

AsyncFunction("requestMessageHistorySync") Coroutine { inboxId: String ->
withContext(Dispatchers.IO) {
val client = clients[inboxId] ?: throw XMTPException("No client")
client.requestMessageHistorySync()
}
}

//
// Auth functions
//
AsyncFunction("auth") { address: String, environment: String, appVersion: String?, hasCreateIdentityCallback: Boolean?, hasEnableIdentityCallback: Boolean?, enableV3: Boolean?, dbEncryptionKey: List<Int>?, dbDirectory: String? ->
AsyncFunction("auth") { address: String, hasCreateIdentityCallback: Boolean?, hasEnableIdentityCallback: Boolean?, dbEncryptionKey: List<Int>?, authParams: String ->
logV("auth")
val reactSigner = ReactNativeSigner(module = this@XMTPModule, address = address)
signer = reactSigner
val authOptions = AuthParamsWrapper.authParamsFromJson(authParams)

if (hasCreateIdentityCallback == true)
preCreateIdentityCallbackDeferred = CompletableDeferred()
Expand All @@ -225,20 +241,21 @@ class XMTPModule : Module() {
preCreateIdentityCallback.takeIf { hasCreateIdentityCallback == true }
val preEnableIdentityCallback: PreEventCallback? =
preEnableIdentityCallback.takeIf { hasEnableIdentityCallback == true }
val context = if (enableV3 == true) context else null
val context = if (authOptions.enableV3) context else null
val encryptionKeyBytes =
dbEncryptionKey?.foldIndexed(ByteArray(dbEncryptionKey.size)) { i, a, v ->
a.apply { set(i, v.toByte()) }
}

val options = ClientOptions(
api = apiEnvironments(environment, appVersion),
api = apiEnvironments(authOptions.environment, authOptions.appVersion),
preCreateIdentityCallback = preCreateIdentityCallback,
preEnableIdentityCallback = preEnableIdentityCallback,
enableV3 = enableV3 == true,
enableV3 = authOptions.enableV3,
appContext = context,
dbEncryptionKey = encryptionKeyBytes,
dbDirectory = dbDirectory
dbDirectory = authOptions.dbDirectory,
historySyncUrl = authOptions.historySyncUrl
)
val client = Client().create(account = reactSigner, options = options)
clients[client.inboxId] = client
Expand All @@ -253,7 +270,7 @@ class XMTPModule : Module() {
}

// Generate a random wallet and set the client to that
AsyncFunction("createRandom") { environment: String, appVersion: String?, hasCreateIdentityCallback: Boolean?, hasEnableIdentityCallback: Boolean?, enableV3: Boolean?, dbEncryptionKey: List<Int>?, dbDirectory: String? ->
AsyncFunction("createRandom") { hasCreateIdentityCallback: Boolean?, hasEnableIdentityCallback: Boolean?, dbEncryptionKey: List<Int>?, authParams: String ->
logV("createRandom")
val privateKey = PrivateKeyBuilder()

Expand All @@ -265,20 +282,24 @@ class XMTPModule : Module() {
preCreateIdentityCallback.takeIf { hasCreateIdentityCallback == true }
val preEnableIdentityCallback: PreEventCallback? =
preEnableIdentityCallback.takeIf { hasEnableIdentityCallback == true }
val context = if (enableV3 == true) context else null

val authOptions = AuthParamsWrapper.authParamsFromJson(authParams)
val context = if (authOptions.enableV3) context else null
val encryptionKeyBytes =
dbEncryptionKey?.foldIndexed(ByteArray(dbEncryptionKey.size)) { i, a, v ->
a.apply { set(i, v.toByte()) }
}

val options = ClientOptions(
api = apiEnvironments(environment, appVersion),
api = apiEnvironments(authOptions.environment, authOptions.appVersion),
preCreateIdentityCallback = preCreateIdentityCallback,
preEnableIdentityCallback = preEnableIdentityCallback,
enableV3 = enableV3 == true,
enableV3 = authOptions.enableV3,
appContext = context,
dbEncryptionKey = encryptionKeyBytes,
dbDirectory = dbDirectory
dbDirectory = authOptions.dbDirectory,
historySyncUrl = authOptions.historySyncUrl

)
val randomClient = Client().create(account = privateKey, options = options)

Expand All @@ -287,21 +308,22 @@ class XMTPModule : Module() {
ClientWrapper.encodeToObj(randomClient)
}

AsyncFunction("createFromKeyBundle") { keyBundle: String, environment: String, appVersion: String?, enableV3: Boolean?, dbEncryptionKey: List<Int>?, dbDirectory: String? ->
AsyncFunction("createFromKeyBundle") { keyBundle: String, dbEncryptionKey: List<Int>?, authParams: String ->
logV("createFromKeyBundle")

val authOptions = AuthParamsWrapper.authParamsFromJson(authParams)
try {
val context = if (enableV3 == true) context else null
val context = if (authOptions.enableV3) context else null
val encryptionKeyBytes =
dbEncryptionKey?.foldIndexed(ByteArray(dbEncryptionKey.size)) { i, a, v ->
a.apply { set(i, v.toByte()) }
}
val options = ClientOptions(
api = apiEnvironments(environment, appVersion),
enableV3 = enableV3 == true,
api = apiEnvironments(authOptions.environment, authOptions.appVersion),
enableV3 = authOptions.enableV3,
appContext = context,
dbEncryptionKey = encryptionKeyBytes,
dbDirectory = dbDirectory
dbDirectory = authOptions.dbDirectory,
historySyncUrl = authOptions.historySyncUrl
)
val bundle =
PrivateKeyOuterClass.PrivateKeyBundle.parseFrom(
Expand Down Expand Up @@ -573,6 +595,28 @@ class XMTPModule : Module() {
}
}

AsyncFunction("findV3Message") Coroutine { inboxId: String, messageId: String ->
withContext(Dispatchers.IO) {
logV("findV3Message")
val client = clients[inboxId] ?: throw XMTPException("No client")
val message = client.findMessage(Hex.hexStringToByteArray(messageId))
message?.let {
DecodedMessageWrapper.encode(it.decrypt())
}
}
}

AsyncFunction("findGroup") Coroutine { inboxId: String, groupId: String ->
withContext(Dispatchers.IO) {
logV("findGroup")
val client = clients[inboxId] ?: throw XMTPException("No client")
val group = client.findGroup(Hex.hexStringToByteArray(groupId))
group?.let {
GroupWrapper.encode(client, it)
}
}
}

AsyncFunction("loadBatchMessages") Coroutine { inboxId: String, topics: List<String> ->
withContext(Dispatchers.IO) {
logV("loadBatchMessages")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package expo.modules.xmtpreactnativesdk.wrappers

import com.google.gson.JsonParser

class AuthParamsWrapper(
val environment: String,
val appVersion: String?,
val enableV3: Boolean = false,
val dbDirectory: String?,
val historySyncUrl: String?,
) {
companion object {
fun authParamsFromJson(authParams: String): AuthParamsWrapper {
val jsonOptions = JsonParser.parseString(authParams).asJsonObject
return AuthParamsWrapper(
jsonOptions.get("environment").asString,
if (jsonOptions.has("appVersion")) jsonOptions.get("appVersion").asString else null,
if (jsonOptions.has("enableV3")) jsonOptions.get("enableV3").asBoolean else false,
if (jsonOptions.has("dbDirectory")) jsonOptions.get("dbDirectory").asString else null,
if (jsonOptions.has("historySyncUrl")) jsonOptions.get("historySyncUrl").asString else null
)
}
}
}
14 changes: 7 additions & 7 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ PODS:
- hermes-engine/Pre-built (= 0.71.14)
- hermes-engine/Pre-built (0.71.14)
- libevent (2.1.12)
- LibXMTP (0.5.3-beta0)
- LibXMTP (0.5.3-beta1)
- Logging (1.0.0)
- MessagePacker (0.4.7)
- MMKV (1.3.5):
Expand Down Expand Up @@ -449,16 +449,16 @@ PODS:
- GenericJSON (~> 2.0)
- Logging (~> 1.0.0)
- secp256k1.swift (~> 0.1)
- XMTP (0.12.2):
- XMTP (0.12.3):
- Connect-Swift (= 0.12.0)
- GzipSwift
- LibXMTP (= 0.5.3-beta0)
- LibXMTP (= 0.5.3-beta1)
- web3.swift
- XMTPReactNative (0.1.0):
- ExpoModulesCore
- MessagePacker
- secp256k1.swift
- XMTP (= 0.12.2)
- XMTP (= 0.12.3)
- Yoga (1.14.0)

DEPENDENCIES:
Expand Down Expand Up @@ -711,7 +711,7 @@ SPEC CHECKSUMS:
GzipSwift: 893f3e48e597a1a4f62fafcb6514220fcf8287fa
hermes-engine: d7cc127932c89c53374452d6f93473f1970d8e88
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
LibXMTP: c3d7b5cfa4b8df5c57ef01f09358c87dea299a13
LibXMTP: a4e1c78fd1b174c56b764e96eff70e39c46c2499
Logging: 9ef4ecb546ad3169398d5a723bc9bea1c46bef26
MessagePacker: ab2fe250e86ea7aedd1a9ee47a37083edd41fd02
MMKV: 506311d0494023c2f7e0b62cc1f31b7370fa3cfb
Expand Down Expand Up @@ -763,8 +763,8 @@ SPEC CHECKSUMS:
secp256k1.swift: a7e7a214f6db6ce5db32cc6b2b45e5c4dd633634
SwiftProtobuf: 407a385e97fd206c4fbe880cc84123989167e0d1
web3.swift: 2263d1e12e121b2c42ffb63a5a7beb1acaf33959
XMTP: 1a25f62cf29a9ba87a879b8c85062c497c9c813f
XMTPReactNative: dbf769e050ca4214b13c096cb873feb25f0c4429
XMTP: 5cf6c97a5cfc7295226b8e14dc079f975ea3a4be
XMTPReactNative: 520f9714e30d2909b14718654cc400473ab8d302
Yoga: e71803b4c1fff832ccf9b92541e00f9b873119b9

PODFILE CHECKSUM: 95d6ace79946933ecf80684613842ee553dd76a2
Expand Down
39 changes: 39 additions & 0 deletions example/src/tests/groupTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,14 @@
throw new Error('should throw when local database not connected')
})

test('can get a inboxId from an address', async () => {
const [alix, bo] = await createClients(2)

const boInboxId = await alix.findInboxIdFromAddress(bo.address)
assert(boInboxId === bo.inboxId, `${boInboxId} should match ${bo.inboxId}`)
return true
})

test('can make a MLS V3 client from bundle', async () => {
const key = new Uint8Array([
233, 120, 198, 96, 154, 65, 132, 17, 132, 96, 250, 40, 103, 35, 125, 64,
Expand Down Expand Up @@ -293,6 +301,37 @@
return true
})

test('can find a group by id', async () => {
const [alixClient, boClient] = await createClients(2)
const alixGroup = await alixClient.conversations.newGroup([boClient.address])

await boClient.conversations.syncGroups()
const boGroup = await boClient.conversations.findGroup(alixGroup.id)

assert(
boGroup?.id === alixGroup.id,
`bo ${boGroup?.id} does not match alix ${alixGroup.id}`
)
return true
})

test('can find a message by id', async () => {
const [alixClient, boClient] = await createClients(2)
const alixGroup = await alixClient.conversations.newGroup([boClient.address])
const alixMessageId = await alixGroup.send("Hello")

Check warning on line 321 in example/src/tests/groupTests.ts

View workflow job for this annotation

GitHub Actions / lint

Replace `"Hello"` with `'Hello'`

await boClient.conversations.syncGroups()
const boGroup = await boClient.conversations.findGroup(alixGroup.id)
await boGroup?.sync()
const boMessage = await boClient.conversations.findV3Message(alixMessageId)

assert(
boMessage?.id === alixMessageId,
`bo message ${boMessage?.id} does not match ${alixMessageId}`
)
return true
})

test('who added me to a group', async () => {
const [alixClient, boClient] = await createClients(2)
await alixClient.conversations.newGroup([boClient.address])
Expand Down Expand Up @@ -1729,7 +1768,7 @@
assert(groupCallbacks === 1, 'group stream should have received 1 group')

return true
})

Check warning on line 1771 in example/src/tests/groupTests.ts

View workflow job for this annotation

GitHub Actions / lint

Delete `⏎`


// Commenting this out so it doesn't block people, but nice to have?
Expand Down
46 changes: 46 additions & 0 deletions ios/Wrappers/AuthParamsWrapper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//
// AuthParamsWrapper.swift
// XMTPReactNative
//
// Created by Naomi Plasterer on 6/24/24.
//

import Foundation
import XMTP

struct AuthParamsWrapper {
let environment: String
let appVersion: String?
let enableV3: Bool
let dbDirectory: String?
let historySyncUrl: String?

init(environment: String, appVersion: String?, enableV3: Bool, dbDirectory: String?, historySyncUrl: String?) {
self.environment = environment
self.appVersion = appVersion
self.enableV3 = enableV3
self.dbDirectory = dbDirectory
self.historySyncUrl = historySyncUrl
}

static func authParamsFromJson(_ authParams: String) -> AuthParamsWrapper {
guard let data = authParams.data(using: .utf8),
let jsonOptions = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
return AuthParamsWrapper(environment: "dev", appVersion: nil, enableV3: false, dbDirectory: nil, historySyncUrl: nil)
}

let environment = jsonOptions["environment"] as? String ?? "dev"
let appVersion = jsonOptions["appVersion"] as? String
let enableV3 = jsonOptions["enableV3"] as? Bool ?? false
let dbDirectory = jsonOptions["dbDirectory"] as? String
let historySyncUrl = jsonOptions["historySyncUrl"] as? String

return AuthParamsWrapper(
environment: environment,
appVersion: appVersion,
enableV3: enableV3,
dbDirectory: dbDirectory,
historySyncUrl: historySyncUrl
)
}
}
Loading
Loading