Skip to content

Commit

Permalink
Merge pull request #499 from xmtp/np/v3-only-client
Browse files Browse the repository at this point in the history
Create a pure v3 client
  • Loading branch information
nplasterer authored Sep 24, 2024
2 parents 83a352d + 23f496e commit a209215
Show file tree
Hide file tree
Showing 11 changed files with 545 additions and 40 deletions.
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.15.9"
implementation "org.xmtp:android:0.15.10"
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
100 changes: 76 additions & 24 deletions android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ class XMTPModule : Module() {
// Auth
"sign",
"authed",
"authedV3",
"preCreateIdentityCallback",
"preEnableIdentityCallback",
"preAuthenticateToInboxCallback",
Expand Down Expand Up @@ -371,6 +372,45 @@ class XMTPModule : Module() {
}
}

AsyncFunction("createOrBuild") Coroutine { address: String, hasCreateIdentityCallback: Boolean?, hasEnableIdentityCallback: Boolean?, hasAuthInboxCallback: Boolean?, dbEncryptionKey: List<Int>?, authParams: String ->
withContext(Dispatchers.IO) {
logV("createOrBuild")
val reactSigner = ReactNativeSigner(module = this@XMTPModule, address = address)
signer = reactSigner
val options = clientOptions(
dbEncryptionKey,
authParams,
hasCreateIdentityCallback,
hasEnableIdentityCallback,
hasAuthInboxCallback,
)
val client = Client().createOrBuild(account = reactSigner, options = options)
clients[client.inboxId] = client
ContentJson.Companion
signer = null
sendEvent("authedV3", ClientWrapper.encodeToObj(client))
}
}

AsyncFunction("createRandomV3") Coroutine { hasCreateIdentityCallback: Boolean?, hasEnableIdentityCallback: Boolean?, hasPreAuthenticateToInboxCallback: Boolean?, dbEncryptionKey: List<Int>?, authParams: String ->
withContext(Dispatchers.IO) {
logV("createRandomV3")
val privateKey = PrivateKeyBuilder()
val options = clientOptions(
dbEncryptionKey,
authParams,
hasCreateIdentityCallback,
hasEnableIdentityCallback,
hasPreAuthenticateToInboxCallback,
)
val randomClient = Client().createOrBuild(account = privateKey, options = options)

ContentJson.Companion
clients[randomClient.inboxId] = randomClient
ClientWrapper.encodeToObj(randomClient)
}
}

AsyncFunction("dropClient") Coroutine { inboxId: String ->
withContext(Dispatchers.IO) {
logV("dropClient")
Expand Down Expand Up @@ -1487,16 +1527,20 @@ class XMTPModule : Module() {
}
}

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

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

AsyncFunction("denyContacts") Coroutine { inboxId: String, addresses: List<String> ->
Expand All @@ -1514,16 +1558,20 @@ class XMTPModule : Module() {
}
}

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

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

AsyncFunction("denyInboxes") Coroutine { inboxId: String, inboxIds: List<String> ->
Expand Down Expand Up @@ -1602,15 +1650,19 @@ class XMTPModule : Module() {
}
}

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

AsyncFunction("exportNativeLogs") Coroutine { ->
Expand Down
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.8-beta5)
- LibXMTP (0.5.8-beta6)
- Logging (1.0.0)
- MessagePacker (0.4.7)
- MMKV (1.3.9):
Expand Down Expand Up @@ -449,16 +449,16 @@ PODS:
- GenericJSON (~> 2.0)
- Logging (~> 1.0.0)
- secp256k1.swift (~> 0.1)
- XMTP (0.14.13):
- XMTP (0.14.14):
- Connect-Swift (= 0.12.0)
- GzipSwift
- LibXMTP (= 0.5.8-beta5)
- LibXMTP (= 0.5.8-beta6)
- web3.swift
- XMTPReactNative (0.1.0):
- ExpoModulesCore
- MessagePacker
- secp256k1.swift
- XMTP (= 0.14.13)
- XMTP (= 0.14.14)
- Yoga (1.14.0)

DEPENDENCIES:
Expand Down Expand Up @@ -711,7 +711,7 @@ SPEC CHECKSUMS:
GzipSwift: 893f3e48e597a1a4f62fafcb6514220fcf8287fa
hermes-engine: d7cc127932c89c53374452d6f93473f1970d8e88
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
LibXMTP: ee1591fdb51bc6cc690c1a9ba10792ccc2104328
LibXMTP: c7338cace222bed90f950579300725325a2c0bfd
Logging: 9ef4ecb546ad3169398d5a723bc9bea1c46bef26
MessagePacker: ab2fe250e86ea7aedd1a9ee47a37083edd41fd02
MMKV: 817ba1eea17421547e01e087285606eb270a8dcb
Expand Down Expand Up @@ -763,8 +763,8 @@ SPEC CHECKSUMS:
secp256k1.swift: a7e7a214f6db6ce5db32cc6b2b45e5c4dd633634
SwiftProtobuf: 407a385e97fd206c4fbe880cc84123989167e0d1
web3.swift: 2263d1e12e121b2c42ffb63a5a7beb1acaf33959
XMTP: a9e7382ec5b57eeda3df7b177f034d061e3c9b61
XMTPReactNative: c4c859a489af84b225508567a16f7cf977f85c8a
XMTP: 37621f1258b12629af305e6697414ccb2fbd4ea8
XMTPReactNative: 7bec275ed26997e6a73f06a678c328e6ba852cd5
Yoga: e71803b4c1fff832ccf9b92541e00f9b873119b9

PODFILE CHECKSUM: 0e6fe50018f34e575d38dc6a1fdf1f99c9596cdd
Expand Down
7 changes: 7 additions & 0 deletions example/src/TestScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { groupTests } from './tests/groupTests'
import { restartStreamTests } from './tests/restartStreamsTests'
import { Test } from './tests/test-utils'
import { tests } from './tests/tests'
import { v3OnlyTests } from './tests/v3OnlyTests'

type Result = 'waiting' | 'running' | 'success' | 'failure' | 'error'

Expand Down Expand Up @@ -107,6 +108,7 @@ export enum TestCategory {
all = 'all',
tests = 'tests',
group = 'group',
v3Only = 'v3Only',
restartStreans = 'restartStreams',
groupPermissions = 'groupPermissions',
groupPerformance = 'groupPerformance',
Expand All @@ -121,6 +123,7 @@ export default function TestScreen(): JSX.Element {
const allTests = [
...tests,
...groupTests,
...v3OnlyTests,
...restartStreamTests,
...groupPermissionsTests,
...groupPerformanceTests,
Expand All @@ -139,6 +142,10 @@ export default function TestScreen(): JSX.Element {
activeTests = groupTests
title = 'Group Unit Tests'
break
case TestCategory.v3Only:
activeTests = v3OnlyTests
title = 'V3 Only Tests'
break
case TestCategory.restartStreans:
activeTests = restartStreamTests
title = 'Restart Streams Unit Tests'
Expand Down
26 changes: 26 additions & 0 deletions example/src/tests/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,29 @@ export async function createClients(numClients: number): Promise<Client[]> {
}
return clients
}

export async function createV3TestingClients(): Promise<Client[]> {
const clients = []
const keyBytes = new Uint8Array([
233, 120, 198, 96, 154, 65, 132, 17, 132, 96, 250, 40, 103, 35, 125, 64,
166, 83, 208, 224, 254, 44, 205, 227, 175, 49, 234, 129, 74, 252, 135, 145,
])
const alix = await Client.createRandom({
env: 'local',
})
const bo = await Client.createRandomV3({
env: 'local',
enableV3: true,
dbEncryptionKey: keyBytes,
})
const caro = await Client.createRandom({
env: 'local',
enableV3: true,
dbEncryptionKey: keyBytes,
})
bo.register(new GroupUpdatedCodec())
caro.register(new GroupUpdatedCodec())

clients.push(alix, bo, caro)
return clients
}
135 changes: 135 additions & 0 deletions example/src/tests/v3OnlyTests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/* eslint-disable @typescript-eslint/no-extra-non-null-assertion */
import { Client } from 'xmtp-react-native-sdk'

import {
Test,
assert,
createV3TestingClients,
delayToPropogate,
} from './test-utils'

export const v3OnlyTests: Test[] = []
let counter = 1
function test(name: string, perform: () => Promise<boolean>) {
v3OnlyTests.push({
name: String(counter++) + '. ' + name,
run: perform,
})
}

test('can make a V3 only client', async () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const keyBytes = new Uint8Array([
233, 120, 198, 96, 154, 65, 132, 17, 132, 96, 250, 40, 103, 35, 125, 64,
166, 83, 208, 224, 254, 44, 205, 227, 175, 49, 234, 129, 74, 252, 135, 145,
])
const client = await Client.createRandomV3({
env: 'local',
appVersion: 'Testing/0.0.0',
enableV3: true,
dbEncryptionKey: keyBytes,
})

const inboxId = await Client.getOrCreateInboxId(client.address, {
env: 'local',
})

assert(
client.inboxId === inboxId,
`inboxIds should match but were ${client.inboxId} and ${inboxId}`
)
const canMessageV3 = await client.canGroupMessage([client.address])

assert(
canMessageV3[client.address.toLowerCase()] === true,
`canMessageV3 should be true`
)
try {
await client.canMessage(client.address)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) {
return true
}

throw new Error('should throw error when hitting V2 api')
})

test('can create group', async () => {
const [alixV2, boV3, caroV2V3] = await createV3TestingClients()
const group = await boV3.conversations.newGroup([caroV2V3.address])
assert(group?.members?.length === 2, `group should have 2 members`)

try {
await boV3.conversations.newGroup([alixV2.address])
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) {
return true
}
throw new Error(
'should throw error when trying to add a V2 only client to a group'
)
})

test('can send message', async () => {
const [alixV2, boV3, caroV2V3] = await createV3TestingClients()

Check warning on line 74 in example/src/tests/v3OnlyTests.ts

View workflow job for this annotation

GitHub Actions / lint

'alixV2' is assigned a value but never used
const group = await boV3.conversations.newGroup([caroV2V3.address])
await group.send('gm')
await group.sync()
const groupMessages = await group.messages()
assert(
groupMessages[0].content() === 'gm',
`first should be gm but was ${groupMessages[0].content()}`
)

await caroV2V3.conversations.syncGroups()
const sameGroups = await caroV2V3.conversations.listGroups()
await sameGroups[0].sync()

const sameGroupMessages = await sameGroups[0].messages()
assert(
sameGroupMessages[0].content() === 'gm',
`second should be gm but was ${sameGroupMessages[0].content()}`
)
return true
})

test('can stream all messages', async () => {
const [alixV2, boV3, caroV2V3] = await createV3TestingClients()
const conversation = await alixV2.conversations.newConversation(
caroV2V3.address
)
const group = await boV3.conversations.newGroup([caroV2V3.address])
await caroV2V3.conversations.syncGroups()

const allMessages: any[] = []

await caroV2V3.conversations.streamAllMessages(async (conversation) => {
allMessages.push(conversation)
}, true)

await conversation.send('hi')
await group.send('hi')

assert(allMessages.length === 2, '2 messages should have been streamed')

return true
})

test('can stream groups and conversations', async () => {
const [alixV2, boV3, caroV2V3] = await createV3TestingClients()

const allConvos: any[] = []

await caroV2V3.conversations.streamAll(async (conversation) => {
allConvos.push(conversation)
})

await alixV2.conversations.newConversation(caroV2V3.address)
await boV3.conversations.newGroup([caroV2V3.address])

await delayToPropogate()

assert(allConvos.length === 2, '2 convos should have been streamed')

return true
})
Loading

0 comments on commit a209215

Please sign in to comment.