From 3129752d482c2a420c162589c4e83e27321fa52f Mon Sep 17 00:00:00 2001 From: Alex Risch Date: Tue, 18 Jun 2024 08:06:48 -0700 Subject: [PATCH 01/13] Removed ethers from test Removed ethers from test for much better performance --- example/src/tests.ts | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/example/src/tests.ts b/example/src/tests.ts index 2a74da803..3f1c7ad1e 100644 --- a/example/src/tests.ts +++ b/example/src/tests.ts @@ -2,7 +2,6 @@ import { sha256 } from '@noble/hashes/sha256' import { FramesClient } from '@xmtp/frames-client' import { content, invitation, signature as signatureProto } from '@xmtp/proto' import { createHmac } from 'crypto' -import { ethers } from 'ethers' import ReactNativeBlobUtil from 'react-native-blob-util' import Config from 'react-native-config' import { TextEncoder, TextDecoder } from 'text-encoding' @@ -1678,13 +1677,31 @@ test('can handle complex streaming setup with messages from self', async () => { return true }) +class ViemSigner { + account: PrivateKeyAccount + + constructor(account: PrivateKeyAccount) { + this.account = account + } + + async getAddress() { + return this.account.address + } + + async signMessage(message: string) { + return this.account.signMessage({ message }) + } +} + test('can send and receive consent proofs', async () => { - const alixWallet = await ethers.Wallet.createRandom() - const boWallet = await ethers.Wallet.createRandom() - const bo = await Client.create(boWallet, { env: 'local' }) - await delayToPropogate() - const alix = await Client.create(alixWallet, { env: 'local' }) - await delayToPropogate() + const alixPrivateKey = generatePrivateKey() + const alixAccount = privateKeyToAccount(alixPrivateKey) + const boPrivateKey = generatePrivateKey() + const boAccount = privateKeyToAccount(boPrivateKey) + const alixSigner = new ViemSigner(alixAccount) + const boSigner = new ViemSigner(boAccount) + const alix = await Client.create(alixSigner, { env: 'local' }) + const bo = await Client.create(boSigner, { env: 'local' }) const timestamp = Date.now() const consentMessage = @@ -1694,7 +1711,7 @@ test('can send and receive consent proofs', async () => { `From Address: ${bo.address}\n` + '\n' + 'For more info: https://xmtp.org/signatures/' - const sig = await alixWallet.signMessage(consentMessage) + const sig = await alixSigner.signMessage(consentMessage) const consentProof = invitation.ConsentProofPayload.fromPartial({ payloadVersion: invitation.ConsentProofPayloadVersion.CONSENT_PROOF_PAYLOAD_VERSION_1, From f246dc282fd4f47b2aea7d404d8cb247162fdf03 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Mon, 1 Jul 2024 12:50:11 -0700 Subject: [PATCH 02/13] feat: update the v2 to all be backed by rust --- android/build.gradle | 5 ++--- ios/XMTPReactNative.podspec | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 28ed1806b..b92a17266 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -98,8 +98,7 @@ repositories { dependencies { implementation project(':expo-modules-core') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}" - implementation "org.xmtp:android:0.11.2" + implementation "org.xmtp:android:0.14.0" 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" -} + implementation "com.daveanthonythomas.moshipack:moshipack:1.0.1"} diff --git a/ios/XMTPReactNative.podspec b/ios/XMTPReactNative.podspec index 22a28797f..b16799339 100644 --- a/ios/XMTPReactNative.podspec +++ b/ios/XMTPReactNative.podspec @@ -26,5 +26,5 @@ Pod::Spec.new do |s| s.source_files = "**/*.{h,m,swift}" s.dependency 'secp256k1.swift' s.dependency "MessagePacker" - s.dependency "XMTP", "= 0.10.9" + s.dependency "XMTP", "= 0.13.0" end From 9ea3f6d3f44486d558330f5a4458dbc68938a55a Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Mon, 1 Jul 2024 12:52:12 -0700 Subject: [PATCH 03/13] fix up the coroutine --- .../modules/xmtpreactnativesdk/XMTPModule.kt | 41 +++++++++++-------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt index b5278afbc..d43acaaf8 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -299,20 +299,24 @@ class XMTPModule : Module() { // // Client API - AsyncFunction("canMessage") { clientAddress: String, peerAddress: String -> - logV("canMessage") - val client = clients[clientAddress] ?: throw XMTPException("No client") + AsyncFunction("canMessage") Coroutine { clientAddress: String, peerAddress: String -> + withContext(Dispatchers.IO) { + logV("canMessage") + val client = clients[clientAddress] ?: throw XMTPException("No client") - client.canMessage(peerAddress) + client.canMessage(peerAddress) + } } - AsyncFunction("staticCanMessage") { peerAddress: String, environment: String, appVersion: String? -> - try { - logV("staticCanMessage") - val options = ClientOptions(api = apiEnvironments(environment, appVersion)) - Client.canMessage(peerAddress = peerAddress, options = options) - } catch (e: Exception) { - throw XMTPException("Failed to create client: ${e.message}") + AsyncFunction("staticCanMessage") Coroutine { peerAddress: String, environment: String, appVersion: String? -> + withContext(Dispatchers.IO) { + try { + logV("staticCanMessage") + val options = ClientOptions(api = apiEnvironments(environment, appVersion)) + Client.canMessage(peerAddress = peerAddress, options = options) + } catch (e: Exception) { + throw XMTPException("Failed to create client: ${e.message}") + } } } @@ -583,14 +587,15 @@ class XMTPModule : Module() { var consentProof: ConsentProofPayload? = null if (consentProofPayload.isNotEmpty()) { - val consentProofDataBytes = consentProofPayload.foldIndexed(ByteArray(consentProofPayload.size)) { i, a, v -> - a.apply { - set( - i, - v.toByte() - ) + val consentProofDataBytes = + consentProofPayload.foldIndexed(ByteArray(consentProofPayload.size)) { i, a, v -> + a.apply { + set( + i, + v.toByte() + ) + } } - } consentProof = ConsentProofPayload.parseFrom(consentProofDataBytes) } From eb3a9e2c6f688b6ea74ec47bc47d44930ff2fc6e Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Mon, 1 Jul 2024 13:01:11 -0700 Subject: [PATCH 04/13] bump the ios version --- example/ios/Podfile.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 660d9a938..a313e030e 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -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.4.4-beta5) + - LibXMTP (0.5.4-beta0) - Logging (1.0.0) - MessagePacker (0.4.7) - MMKV (1.3.5): @@ -322,7 +322,7 @@ PODS: - React-Core - react-native-encrypted-storage (4.0.3): - React-Core - - react-native-get-random-values (1.10.0): + - react-native-get-random-values (1.11.0): - React-Core - react-native-mmkv (2.11.0): - MMKV (>= 1.2.13) @@ -445,16 +445,16 @@ PODS: - GenericJSON (~> 2.0) - Logging (~> 1.0.0) - secp256k1.swift (~> 0.1) - - XMTP (0.10.9): + - XMTP (0.13.0): - Connect-Swift (= 0.12.0) - GzipSwift - - LibXMTP (= 0.4.4-beta5) + - LibXMTP (= 0.5.4-beta0) - web3.swift - XMTPReactNative (0.1.0): - ExpoModulesCore - MessagePacker - secp256k1.swift - - XMTP (= 0.10.9) + - XMTP (= 0.13.0) - Yoga (1.14.0) DEPENDENCIES: @@ -701,7 +701,7 @@ SPEC CHECKSUMS: GzipSwift: 893f3e48e597a1a4f62fafcb6514220fcf8287fa hermes-engine: d7cc127932c89c53374452d6f93473f1970d8e88 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 - LibXMTP: e2fb601691981900099551ff3e05621bd73dccf1 + LibXMTP: 549e85c40f00957be3e114a823a2887cdcf5817a Logging: 9ef4ecb546ad3169398d5a723bc9bea1c46bef26 MessagePacker: ab2fe250e86ea7aedd1a9ee47a37083edd41fd02 MMKV: 506311d0494023c2f7e0b62cc1f31b7370fa3cfb @@ -724,7 +724,7 @@ SPEC CHECKSUMS: react-native-blob-util: d8fa1a7f726867907a8e43163fdd8b441d4489ea react-native-config: 86038147314e2e6d10ea9972022aa171e6b1d4d8 react-native-encrypted-storage: db300a3f2f0aba1e818417c1c0a6be549038deb7 - react-native-get-random-values: 384787fd76976f5aec9465aff6fa9e9129af1e74 + react-native-get-random-values: 21325b2244dfa6b58878f51f9aa42821e7ba3d06 react-native-mmkv: e97c0c79403fb94577e5d902ab1ebd42b0715b43 react-native-netinfo: 8a7fd3f7130ef4ad2fb4276d5c9f8d3f28d2df3d react-native-quick-base64: 777057ea4286f806b00259ede65dc79c7c706320 @@ -751,8 +751,8 @@ SPEC CHECKSUMS: secp256k1.swift: a7e7a214f6db6ce5db32cc6b2b45e5c4dd633634 SwiftProtobuf: 407a385e97fd206c4fbe880cc84123989167e0d1 web3.swift: 2263d1e12e121b2c42ffb63a5a7beb1acaf33959 - XMTP: 5efac5f69327cc6a4ad5b1f1fb9624b9e8355efe - XMTPReactNative: 5c4baec31d5ef83e5e5a35c33c31e70524db620e + XMTP: fbcb4e36d906d220778a5a0727cab6fd912bbb1d + XMTPReactNative: 44fd251a95a87f5ced57cce617f0748f2d056940 Yoga: e71803b4c1fff832ccf9b92541e00f9b873119b9 PODFILE CHECKSUM: 95d6ace79946933ecf80684613842ee553dd76a2 From 90c6eae9e4101b97eae986a2a91ba140befb2d1f Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Mon, 1 Jul 2024 14:40:11 -0700 Subject: [PATCH 05/13] fix up swift --- ios/Wrappers/ConversationWrapper.swift | 2 +- ios/XMTPModule.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ios/Wrappers/ConversationWrapper.swift b/ios/Wrappers/ConversationWrapper.swift index 103c6c629..81a4e74e0 100644 --- a/ios/Wrappers/ConversationWrapper.swift +++ b/ios/Wrappers/ConversationWrapper.swift @@ -26,7 +26,7 @@ struct ConversationWrapper { "topic": conversation.topic, "createdAt": UInt64(conversation.createdAt.timeIntervalSince1970 * 1000), "context": context, - "peerAddress": conversation.peerAddress, + "peerAddress": try conversation.peerAddress, "version": conversation.version == .v1 ? "v1" : "v2", "conversationID": conversation.conversationID ?? "", "keyMaterial": conversation.keyMaterial?.base64EncodedString() ?? "", diff --git a/ios/XMTPModule.swift b/ios/XMTPModule.swift index cfb89d939..1b77fc244 100644 --- a/ios/XMTPModule.swift +++ b/ios/XMTPModule.swift @@ -601,7 +601,7 @@ public class XMTPModule: Module { guard let conversation = try await findConversation(clientAddress: clientAddress, topic: conversationTopic) else { throw Error.conversationNotFound(conversationTopic) } - return ConsentWrapper.consentStateToString(state: await conversation.consentState()) + return ConsentWrapper.consentStateToString(state: try await conversation.consentState()) } AsyncFunction("consentList") { (clientAddress: String) -> [String] in @@ -682,7 +682,7 @@ public class XMTPModule: Module { await subscriptionsManager.get(getConversationsKey(clientAddress: clientAddress))?.cancel() await subscriptionsManager.set(getConversationsKey(clientAddress: clientAddress), Task { do { - for try await conversation in await client.conversations.stream() { + for try await conversation in try await client.conversations.stream() { try sendEvent("conversation", [ "clientAddress": clientAddress, "conversation": ConversationWrapper.encodeToObj(conversation, client: client), From 87e0503f65cdeb43f30e6c88bca521a626ff0c93 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Mon, 1 Jul 2024 15:48:49 -0700 Subject: [PATCH 06/13] bump to a working android version --- android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/build.gradle b/android/build.gradle index b92a17266..38d62dd2d 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -98,7 +98,7 @@ repositories { dependencies { implementation project(':expo-modules-core') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}" - implementation "org.xmtp:android:0.14.0" + implementation "org.xmtp:android:0.14.1" 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"} From 4ae83d90977736a315aaf8bce0e91e54ae08cd64 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Wed, 10 Jul 2024 17:10:53 -0600 Subject: [PATCH 07/13] fix up the test --- example/src/tests/groupTests.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/example/src/tests/groupTests.ts b/example/src/tests/groupTests.ts index 09bafb78b..cef98751e 100644 --- a/example/src/tests/groupTests.ts +++ b/example/src/tests/groupTests.ts @@ -591,6 +591,8 @@ test('unpublished messages handling', async () => { if (preparedMessageId !== messages[0].id) { throw new Error(`Message ID should match the prepared message ID`) } + + return true }) test('can add members to a group', async () => { From b8b3d51753cb024c4c1eb061912bfb9c1b568dbe Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Wed, 10 Jul 2024 17:48:00 -0600 Subject: [PATCH 08/13] feat: group chat this version bump gives all new group chat functionality BREAKING CHANGE: This commit introduces several breaking changes potentially for creating group chats. --- example/src/tests/tests.ts | 4 +++- src/lib/Client.ts | 15 +++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/example/src/tests/tests.ts b/example/src/tests/tests.ts index e184c1986..b46ab7270 100644 --- a/example/src/tests/tests.ts +++ b/example/src/tests/tests.ts @@ -175,7 +175,9 @@ function test(name: string, perform: () => Promise) { } test('can make a client', async () => { - const [client] = await createClients(1) + const client = await Client.createRandom({ + env: 'local', + }) client.register(new RemoteAttachmentCodec()) if (Object.keys(client.codecRegistry).length !== 2) { diff --git a/src/lib/Client.ts b/src/lib/Client.ts index 4cc9373b9..7dd46c66f 100644 --- a/src/lib/Client.ts +++ b/src/lib/Client.ts @@ -55,8 +55,9 @@ export class Client< options: ClientOptions & { codecs?: ContentCodecs } ): Promise> { if ( - options.dbEncryptionKey === undefined || - options.dbEncryptionKey.length !== 32 + options.enableV3 === true && + (options.dbEncryptionKey === undefined || + options.dbEncryptionKey.length !== 32) ) { throw new Error('Must pass an encryption key that is exactly 32 bytes.') } @@ -159,8 +160,9 @@ export class Client< options: ClientOptions & { codecs?: ContentTypes } ): Promise> { if ( - options.dbEncryptionKey === undefined || - options.dbEncryptionKey.length !== 32 + options.enableV3 === true && + (options.dbEncryptionKey === undefined || + options.dbEncryptionKey.length !== 32) ) { throw new Error('Must pass an encryption key that is exactly 32 bytes.') } @@ -204,8 +206,9 @@ export class Client< options: ClientOptions & { codecs?: ContentCodecs } ): Promise> { if ( - options.dbEncryptionKey === undefined || - options.dbEncryptionKey.length !== 32 + options.enableV3 === true && + (options.dbEncryptionKey === undefined || + options.dbEncryptionKey.length !== 32) ) { throw new Error('Must pass an encryption key that is exactly 32 bytes.') } From 10e7e73a40ba8b3d36a88bd5ff0b8c95c69c336e Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Tue, 23 Jul 2024 11:33:32 -0600 Subject: [PATCH 09/13] get all 99 tests passing --- android/build.gradle | 2 +- example/ios/Podfile.lock | 8 +- example/src/tests/tests.ts | 509 ++++++------------------------------ ios/XMTPReactNative.podspec | 2 +- 4 files changed, 90 insertions(+), 431 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index fcc62ea94..988c5a454 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -98,7 +98,7 @@ repositories { dependencies { implementation project(':expo-modules-core') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}" - implementation "org.xmtp:android:0.14.8" + implementation "org.xmtp:android:0.14.9" 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" diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index eef599503..03fe186f5 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -449,7 +449,7 @@ PODS: - GenericJSON (~> 2.0) - Logging (~> 1.0.0) - secp256k1.swift (~> 0.1) - - XMTP (0.13.7): + - XMTP (0.13.8): - Connect-Swift (= 0.12.0) - GzipSwift - LibXMTP (= 0.5.6-beta0) @@ -458,7 +458,7 @@ PODS: - ExpoModulesCore - MessagePacker - secp256k1.swift - - XMTP (= 0.13.7) + - XMTP (= 0.13.8) - Yoga (1.14.0) DEPENDENCIES: @@ -763,8 +763,8 @@ SPEC CHECKSUMS: secp256k1.swift: a7e7a214f6db6ce5db32cc6b2b45e5c4dd633634 SwiftProtobuf: 407a385e97fd206c4fbe880cc84123989167e0d1 web3.swift: 2263d1e12e121b2c42ffb63a5a7beb1acaf33959 - XMTP: 16bd630ff61081d3a325619a26ea176ed256d419 - XMTPReactNative: 4716836807cb33c72bde0846ac46b3fe923a3625 + XMTP: 7d0a3f3b22916acfbb0ae67f1ca6bbd3f5956138 + XMTPReactNative: 51e5b1b8669dab2ad5e2d74b518146388f5f425e Yoga: e71803b4c1fff832ccf9b92541e00f9b873119b9 PODFILE CHECKSUM: 95d6ace79946933ecf80684613842ee553dd76a2 diff --git a/example/src/tests/tests.ts b/example/src/tests/tests.ts index b46ab7270..b1250276d 100644 --- a/example/src/tests/tests.ts +++ b/example/src/tests/tests.ts @@ -1,11 +1,12 @@ +import { sha256 } from '@noble/hashes/sha256' import { FramesClient } from '@xmtp/frames-client' -import { content, invitation } from '@xmtp/proto' +import { content, invitation, signature as signatureProto } from '@xmtp/proto' import { createHmac } from 'crypto' import ReactNativeBlobUtil from 'react-native-blob-util' import Config from 'react-native-config' import { TextEncoder, TextDecoder } from 'text-encoding' -import { PrivateKeyAccount } from 'viem' -import { privateKeyToAccount } from 'viem/accounts' +import { createWalletClient, custom, PrivateKeyAccount, toHex } from 'viem' +import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts' import { DecodedMessage } from 'xmtp-react-native-sdk/lib/DecodedMessage' import { Test, assert, createClients, delayToPropogate } from './test-utils' @@ -177,8 +178,8 @@ function test(name: string, perform: () => Promise) { test('can make a client', async () => { const client = await Client.createRandom({ env: 'local', + appVersion: 'Testing/0.0.0', }) - client.register(new RemoteAttachmentCodec()) if (Object.keys(client.codecRegistry).length !== 2) { throw new Error( @@ -215,13 +216,8 @@ test('can load a client from env "2k lens convos" private key', async () => { const signer = convertPrivateKeyAccountToSigner( privateKeyToAccount(privateKeyHex) ) - const key = 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 xmtpClient = await Client.create(signer, { env: 'local', - dbEncryptionKey: key, }) assert( @@ -237,17 +233,12 @@ test('can load 1995 conversations from dev network "2k lens convos" account', as } const privateKeyHex: `0x${string}` = `0x${Config.TEST_PRIVATE_KEY}` - const key = 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 signer = convertPrivateKeyAccountToSigner( privateKeyToAccount(privateKeyHex) ) const xmtpClient = await Client.create(signer, { env: 'dev', - dbEncryptionKey: key, }) assert( @@ -270,7 +261,8 @@ test('can load 1995 conversations from dev network "2k lens convos" account', as test('can pass a custom filter date and receive message objects with expected dates', async () => { try { - const [bob, alice] = await createClients(2) + const bob = await Client.createRandom({ env: 'local' }) + const alice = await Client.createRandom({ env: 'local' }) if (bob.address === alice.address) { throw new Error('bob and alice should be different') @@ -335,38 +327,15 @@ test('can pass a custom filter date and receive message objects with expected da }) test('canMessage', async () => { - const [bo, alix] = await createClients(2) - - const canMessage = await bo.canMessage(alix.address) - if (!canMessage) { - throw new Error('should be able to message v2 client') - } + const bob = await Client.createRandom({ env: 'local' }) + const alice = await Client.createRandom({ env: 'local' }) - 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 caro = await Client.createRandom({ - env: 'local', - enableV3: true, - dbEncryptionKey: keyBytes, - }) - const chux = await Client.createRandom({ - env: 'local', - enableV3: true, - dbEncryptionKey: keyBytes, - }) - - const canMessageV3 = await caro.canGroupMessage([chux.address]) - if (!canMessageV3) { - throw new Error('should be able to message v3 client') - } - return true + const canMessage = await bob.canMessage(alice.address) + return canMessage }) test('fetch a public key bundle and sign a digest', async () => { - const [bob] = await createClients(1) + const bob = await Client.createRandom({ env: 'local' }) const bytes = new Uint8Array([1, 2, 3]) const signature = await bob.sign(bytes, { kind: 'identity' }) if (signature.length === 0) { @@ -380,15 +349,10 @@ test('fetch a public key bundle and sign a digest', async () => { }) test('createFromKeyBundle throws error for non string value', async () => { - const key = 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, - ]) try { const bytes = [1, 2, 3] await Client.createFromKeyBundle(JSON.stringify(bytes), { env: 'local', - dbEncryptionKey: key, }) } catch { return true @@ -397,8 +361,8 @@ test('createFromKeyBundle throws error for non string value', async () => { }) test('canPrepareMessage', async () => { - const [bob, alice] = await createClients(2) - + const bob = await Client.createRandom({ env: 'local' }) + const alice = await Client.createRandom({ env: 'local' }) await delayToPropogate() const bobConversation = await bob.conversations.newConversation(alice.address) @@ -424,7 +388,9 @@ test('canPrepareMessage', async () => { }) test('can list batch messages', async () => { - const [bob, alice] = await createClients(2) + const bob = await Client.createRandom({ env: 'local' }) + await delayToPropogate() + const alice = await Client.createRandom({ env: 'local' }) await delayToPropogate() if (bob.address === alice.address) { throw new Error('bob and alice should be different') @@ -477,7 +443,9 @@ test('can list batch messages', async () => { }) test('can paginate batch messages', async () => { - const [bob, alice] = await createClients(2) + const bob = await Client.createRandom({ env: 'local' }) + await delayToPropogate() + const alice = await Client.createRandom({ env: 'local' }) await delayToPropogate() if (bob.address === alice.address) { throw new Error('bob and alice should be different') @@ -579,7 +547,9 @@ test('can paginate batch messages', async () => { }) test('can stream messages', async () => { - const [bob, alice] = await createClients(2) + const bob = await Client.createRandom({ env: 'local' }) + await delayToPropogate() + const alice = await Client.createRandom({ env: 'local' }) await delayToPropogate() // Record new conversation stream @@ -676,7 +646,9 @@ test('can stream messages', async () => { }) test('can stream conversations with delay', async () => { - const [bo, alix] = await createClients(2) + const bo = await Client.createRandom({ env: 'dev' }) + await delayToPropogate() + const alix = await Client.createRandom({ env: 'dev' }) await delayToPropogate() const allConvos: Conversation[] = [] @@ -716,12 +688,14 @@ test('can stream conversations with delay', async () => { }) test('remote attachments should work', async () => { - const [bob, alice] = await createClients(2) - alice.register(new StaticAttachmentCodec()) - alice.register(new RemoteAttachmentCodec()) - bob.register(new StaticAttachmentCodec()) - bob.register(new RemoteAttachmentCodec()) - + const alice = await Client.createRandom({ + env: 'local', + codecs: [new StaticAttachmentCodec(), new RemoteAttachmentCodec()], + }) + const bob = await Client.createRandom({ + env: 'local', + codecs: [new StaticAttachmentCodec(), new RemoteAttachmentCodec()], + }) const convo = await alice.conversations.newConversation(bob.address) // Alice is sending Bob a file from her phone. @@ -803,7 +777,9 @@ test('remote attachments should work', async () => { }) test('can send read receipts', async () => { - const [bob, alice] = await createClients(2) + const bob = await Client.createRandom({ env: 'local' }) + await delayToPropogate() + const alice = await Client.createRandom({ env: 'local' }) await delayToPropogate() if (bob.address === alice.address) { throw new Error('bob and alice should be different') @@ -837,7 +813,9 @@ test('can send read receipts', async () => { }) test('can stream all messages', async () => { - const [bo, alix, caro] = await createClients(3) + const bo = await Client.createRandom({ env: 'local' }) + await delayToPropogate() + const alix = await Client.createRandom({ env: 'local' }) await delayToPropogate() // Record message stream across all conversations @@ -861,6 +839,7 @@ test('can stream all messages', async () => { } // Starts a new conversation. + const caro = await Client.createRandom({ env: 'local' }) const caroConvo = await caro.conversations.newConversation(alix.address) await delayToPropogate() for (let i = 0; i < 5; i++) { @@ -890,7 +869,9 @@ test('can stream all messages', async () => { }) test('can stream all msgs with delay', async () => { - const [bo, alix, caro] = await createClients(3) + const bo = await Client.createRandom({ env: 'dev' }) + await delayToPropogate() + const alix = await Client.createRandom({ env: 'dev' }) await delayToPropogate() // Record message stream across all conversations @@ -915,6 +896,7 @@ test('can stream all msgs with delay', async () => { await sleep(LONG_STREAM_DELAY) // Starts a new conversation. + const caro = await Client.createRandom({ env: 'dev' }) const caroConvo = await caro.conversations.newConversation(alix.address) await delayToPropogate() @@ -946,7 +928,8 @@ test('can stream all msgs with delay', async () => { }) test('canManagePreferences', async () => { - const [bo, alix] = await createClients(2) + const bo = await Client.createRandom({ env: 'local' }) + const alix = await Client.createRandom({ env: 'local' }) await delayToPropogate() const alixConversation = await bo.conversations.newConversation(alix.address) @@ -1009,20 +992,14 @@ test('canManagePreferences', async () => { }) test('is address on the XMTP network', async () => { - const [alix] = await createClients(1) + const alix = await Client.createRandom({ env: 'local' }) const notOnNetwork = '0x0000000000000000000000000000000000000000' - const key = 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 isAlixAddressAvailable = await Client.canMessage(alix.address, { env: 'local', - dbEncryptionKey: key, }) const isAddressAvailable = await Client.canMessage(notOnNetwork, { env: 'local', - dbEncryptionKey: key, }) if (!isAlixAddressAvailable) { @@ -1037,7 +1014,15 @@ test('is address on the XMTP network', async () => { }) test('register and use custom content types', async () => { - const [bob, alice] = await createClients(2) + const bob = await Client.createRandom({ + env: 'local', + codecs: [new NumberCodec()], + }) + const alice = await Client.createRandom({ + env: 'local', + codecs: [new NumberCodec()], + }) + bob.register(new NumberCodec()) alice.register(new NumberCodec()) @@ -1069,7 +1054,14 @@ test('register and use custom content types', async () => { }) test('register and use custom content types when preparing message', async () => { - const [bob, alice] = await createClients(2) + const bob = await Client.createRandom({ + env: 'local', + codecs: [new NumberCodec()], + }) + const alice = await Client.createRandom({ + env: 'local', + codecs: [new NumberCodec()], + }) bob.register(new NumberCodec()) alice.register(new NumberCodec()) @@ -1105,14 +1097,9 @@ test('calls preCreateIdentityCallback when supplied', async () => { const preCreateIdentityCallback = () => { isCallbackCalled = true } - const key = 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, - ]) await Client.createRandom({ env: 'local', preCreateIdentityCallback, - dbEncryptionKey: key, }) if (!isCallbackCalled) { @@ -1127,14 +1114,9 @@ test('calls preEnableIdentityCallback when supplied', async () => { const preEnableIdentityCallback = () => { isCallbackCalled = true } - const key = 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, - ]) await Client.createRandom({ env: 'local', preEnableIdentityCallback, - dbEncryptionKey: key, }) if (!isCallbackCalled) { @@ -1145,7 +1127,9 @@ test('calls preEnableIdentityCallback when supplied', async () => { }) test('returns keyMaterial for conversations', async () => { - const [bob, alice] = await createClients(2) + const bob = await Client.createRandom({ env: 'local' }) + await delayToPropogate() + const alice = await Client.createRandom({ env: 'local' }) await delayToPropogate() if (bob.address === alice.address) { throw new Error('bob and alice should be different') @@ -1171,7 +1155,9 @@ test('returns keyMaterial for conversations', async () => { }) test('correctly handles lowercase addresses', async () => { - const [bob, alice] = await createClients(2) + const bob = await Client.createRandom({ env: 'local' }) + await delayToPropogate() + const alice = await Client.createRandom({ env: 'local' }) await delayToPropogate() if (bob.address === alice.address) { throw new Error('bob and alice should be different') @@ -1225,10 +1211,18 @@ test('correctly handles lowercase addresses', async () => { }) test('handle fallback types appropriately', async () => { - const [bob, alice] = await await createClients(2) + const bob = await Client.createRandom({ + env: 'local', + codecs: [ + new NumberCodecEmptyFallback(), + new NumberCodecUndefinedFallback(), + ], + }) + const alice = await Client.createRandom({ + env: 'local', + }) bob.register(new NumberCodecEmptyFallback()) bob.register(new NumberCodecUndefinedFallback()) - const bobConvo = await bob.conversations.newConversation(alice.address) const aliceConvo = await alice.conversations.newConversation(bob.address) @@ -1275,7 +1269,7 @@ test('handle fallback types appropriately', async () => { test('instantiate frames client correctly', async () => { const frameUrl = 'https://fc-polls-five.vercel.app/polls/01032f47-e976-42ee-9e3d-3aac1324f4b8' - const [client] = await createClients(1) + const client = await Client.createRandom({ env: 'local' }) const framesClient = new FramesClient(client) const metadata = await framesClient.proxy.readMetadata(frameUrl) if (!metadata) { @@ -1308,127 +1302,6 @@ test('instantiate frames client correctly', async () => { return true }) -// Skipping this test as it's not something supported right now -test('can stream all conversation Messages from multiple clients', async () => { - const [alix, bo, caro] = await createClients(3) - - if (bo.address === alix.address) { - throw Error('Bo and Alix should have different addresses') - } - if (bo.address === caro.address) { - throw Error('Bo and Caro should have different addresses') - } - if (alix.address === caro.address) { - throw Error('Alix and Caro should have different addresses') - } - - // Setup stream - const allAlixMessages: DecodedMessage[] = [] - const allBoMessages: DecodedMessage[] = [] - const alixConvo = await caro.conversations.newConversation(alix.address) - const boConvo = await caro.conversations.newConversation(bo.address) - - await alixConvo.streamMessages(async (message) => { - allAlixMessages.push(message) - }) - await boConvo.streamMessages(async (message) => { - allBoMessages.push(message) - }) - - // Start Caro starts a new conversation. - await delayToPropogate() - await alixConvo.send({ text: `Message` }) - await delayToPropogate() - if (allBoMessages.length !== 0) { - throw Error( - 'Unexpected all conversations count for Bo ' + allBoMessages.length - ) - } - - if (allAlixMessages.length !== 1) { - throw Error( - 'Unexpected all conversations count for Alix ' + allAlixMessages.length - ) - } - - const alixConv = (await alix.conversations.list())[0] - await alixConv.send({ text: `Message` }) - await delayToPropogate() - if (allBoMessages.length !== 0) { - throw Error( - 'Unexpected all conversations count for Bo ' + allBoMessages.length - ) - } - // @ts-ignore-next-line - if (allAlixMessages.length !== 2) { - throw Error( - 'Unexpected all conversations count for Alix ' + allAlixMessages.length - ) - } - - return true -}) - -test('can stream all conversation Messages from multiple clients - swapped', async () => { - const [alix, bo, caro] = await createClients(3) - - if (bo.address === alix.address) { - throw Error('Bo and Alix should have different addresses') - } - if (bo.address === caro.address) { - throw Error('Bo and Caro should have different addresses') - } - if (alix.address === caro.address) { - throw Error('Alix and Caro should have different addresses') - } - - // Setup stream - const allAlixMessages: DecodedMessage[] = [] - const allBoMessages: DecodedMessage[] = [] - const alixConvo = await caro.conversations.newConversation(alix.address) - const boConvo = await caro.conversations.newConversation(bo.address) - - await boConvo.streamMessages(async (message) => { - allBoMessages.push(message) - }) - await alixConvo.streamMessages(async (message) => { - allAlixMessages.push(message) - }) - - // Start Caro starts a new conversation. - await delayToPropogate() - await alixConvo.send({ text: `Message` }) - await delayToPropogate() - if (allBoMessages.length !== 0) { - throw Error( - 'Unexpected all conversations count for Bo ' + allBoMessages.length - ) - } - - if (allAlixMessages.length !== 1) { - throw Error( - 'Unexpected all conversations count for Alix ' + allAlixMessages.length - ) - } - - const alixConv = (await alix.conversations.list())[0] - await alixConv.send({ text: `Message` }) - await delayToPropogate() - if (allBoMessages.length !== 0) { - throw Error( - 'Unexpected all conversations count for Bo ' + allBoMessages.length - ) - } - // @ts-ignore-next-line - if (allAlixMessages.length !== 2) { - throw Error( - 'Unexpected all conversations count for Alix ' + allAlixMessages.length - ) - } - - return true -}) - test('generates and validates HMAC', async () => { const secret = crypto.getRandomValues(new Uint8Array(32)) const info = crypto.getRandomValues(new Uint8Array(32)) @@ -1504,12 +1377,12 @@ test('fails to validate HMAC with wrong key', async () => { }) test('get all HMAC keys', async () => { - const [alice] = await createClients(1) + const alice = await Client.createRandom({ env: 'local' }) const conversations: Conversation[] = [] for (let i = 0; i < 5; i++) { - const [client] = await createClients(1) + const client = await Client.createRandom({ env: 'local' }) const convo = await alice.conversations.newConversation(client.address, { conversationID: `https://example.com/${i}`, metadata: { @@ -1579,220 +1452,6 @@ test('get all HMAC keys', async () => { return true }) -test('can handle complex streaming setup', async () => { - const [bo, alix] = await createClients(2) - await delayToPropogate() - - const allConvos: Conversation[] = [] - await alix.conversations.stream(async (convo) => { - allConvos.push(convo) - }) - const allMessages: DecodedMessage[] = [] - await alix.conversations.streamAllMessages(async (message) => { - allMessages.push(message) - }) - - const conv1 = await bo.conversations.newConversation(alix.address) - await delayToPropogate() - - await bo.conversations.newConversation(alix.address, { - conversationID: 'convo-2', - metadata: {}, - }) - const allConvMessages: DecodedMessage[] = [] - await conv1.streamMessages(async (message) => { - allConvMessages.push(message) - }) - await conv1.send({ text: 'Hello' }) - await delayToPropogate() - - assert( - allConvos.length === 2, - 'Unexpected all convos count1 ' + allConvos.length - ) - - assert( - allMessages.length === 1, - 'Unexpected all messages count2 ' + allMessages.length - ) - - assert( - allConvMessages.length === 1, - 'Unexpected all conv messages count3 ' + allConvMessages.length - ) - - await sleep(LONG_STREAM_DELAY) - const conv3 = await bo.conversations.newConversation(alix.address, { - conversationID: 'convo-3', - metadata: {}, - }) - const allConv3Messages: DecodedMessage[] = [] - await conv3.streamMessages(async (message) => { - allConv3Messages.push(message) - }) - await conv1.send({ text: 'Hello' }) - await conv3.send({ text: 'Hello' }) - await delayToPropogate() - - assert( - allConvos.length === 3, - 'Unexpected all convos count4 ' + allConvos.length - ) - - assert( - allMessages.length === 2, // TODO: should be 3 - 'Unexpected all messages count5 ' + allMessages.length - ) - - assert( - allConvMessages.length === 2, - 'Unexpected all conv messages count6 ' + allConvMessages.length - ) - - assert( - allConv3Messages.length === 1, - 'Unexpected all conv3 messages count7 ' + allConv3Messages.length - ) - - alix.conversations.cancelStream() - alix.conversations.cancelStreamAllMessages() - - await bo.conversations.newConversation(alix.address, { - conversationID: 'convo-4', - metadata: {}, - }) - await conv3.send({ text: 'Hello' }) - - assert( - allConvos.length === 3, - 'Unexpected all convos count8 ' + allConvos.length - ) - - assert( - allMessages.length === 3, - 'Unexpected all messages count9 ' + allMessages.length - ) - - assert( - allConvMessages.length === 2, - 'Unexpected all conv messages count10 ' + allConvMessages.length - ) - - assert( - allConv3Messages.length === 2, - 'Unexpected all conv3 messages count11 ' + allConv3Messages.length - ) - - return true -}) - -test('can handle complex streaming setup with messages from self', async () => { - const [bo, alix] = await createClients(2) - await delayToPropogate() - - const allConvos: Conversation[] = [] - await alix.conversations.stream(async (convo) => { - allConvos.push(convo) - }) - const allMessages: DecodedMessage[] = [] - await alix.conversations.streamAllMessages(async (message) => { - allMessages.push(message) - }) - - const conv1 = await alix.conversations.newConversation(bo.address) - await delayToPropogate() - - await alix.conversations.newConversation(bo.address, { - conversationID: 'convo-2', - metadata: {}, - }) - const allConvMessages: DecodedMessage[] = [] - await conv1.streamMessages(async (message) => { - allConvMessages.push(message) - }) - await conv1.send({ text: 'Hello' }) - await delayToPropogate() - - assert( - allConvos.length === 2, - 'Unexpected all convos count1 ' + allConvos.length - ) - - assert( - allMessages.length === 1, - 'Unexpected all messages count2 ' + allMessages.length - ) - - assert( - allConvMessages.length === 1, - 'Unexpected all conv messages count3 ' + allConvMessages.length - ) - - await sleep(LONG_STREAM_DELAY) - const conv3 = await alix.conversations.newConversation(bo.address, { - conversationID: 'convo-3', - metadata: {}, - }) - const allConv3Messages: DecodedMessage[] = [] - await conv3.streamMessages(async (message) => { - allConv3Messages.push(message) - }) - await conv1.send({ text: 'Hello' }) - await conv3.send({ text: 'Hello' }) - await delayToPropogate() - - assert( - allConvos.length === 3, - 'Unexpected all convos count4 ' + allConvos.length - ) - - assert( - allMessages.length === 2, // TODO: should be 3 - 'Unexpected all messages count5 ' + allMessages.length - ) - - assert( - allConvMessages.length === 3, - 'Unexpected all conv messages count6 ' + allConvMessages.length - ) - - assert( - allConv3Messages.length === 1, - 'Unexpected all conv3 messages count7 ' + allConv3Messages.length - ) - - alix.conversations.cancelStream() - alix.conversations.cancelStreamAllMessages() - - await bo.conversations.newConversation(alix.address, { - conversationID: 'convo-4', - metadata: {}, - }) - await conv3.send({ text: 'Hello' }) - - assert( - allConvos.length === 3, - 'Unexpected all convos count8 ' + allConvos.length - ) - - assert( - allMessages.length === 3, - 'Unexpected all messages count9 ' + allMessages.length - ) - - assert( - allConvMessages.length === 2, - 'Unexpected all conv messages count10 ' + allConvMessages.length - ) - - assert( - allConv3Messages.length === 2, - 'Unexpected all conv3 messages count11 ' + allConv3Messages.length - ) - - return true -}) - class ViemSigner { account: PrivateKeyAccount diff --git a/ios/XMTPReactNative.podspec b/ios/XMTPReactNative.podspec index ea8dbe65d..c7fa69b34 100644 --- a/ios/XMTPReactNative.podspec +++ b/ios/XMTPReactNative.podspec @@ -26,5 +26,5 @@ Pod::Spec.new do |s| s.source_files = "**/*.{h,m,swift}" s.dependency 'secp256k1.swift' s.dependency "MessagePacker" - s.dependency "XMTP", "= 0.13.7" + s.dependency "XMTP", "= 0.13.8" end From abc3d51d35373a0bf25aadccfd00be9d62d80cc1 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Tue, 23 Jul 2024 11:52:52 -0600 Subject: [PATCH 10/13] get all tests passing on both platforms --- .../modules/xmtpreactnativesdk/XMTPModule.kt | 179 +++++++++--------- 1 file changed, 93 insertions(+), 86 deletions(-) diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt index 0d0dc7c29..9c3263a12 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -228,41 +228,44 @@ class XMTPModule : Module() { // // Auth functions // - AsyncFunction("auth") { address: String, hasCreateIdentityCallback: Boolean?, hasEnableIdentityCallback: Boolean?, dbEncryptionKey: List?, authParams: String -> - logV("auth") - val reactSigner = ReactNativeSigner(module = this@XMTPModule, address = address) - signer = reactSigner - val authOptions = AuthParamsWrapper.authParamsFromJson(authParams) - - if (hasCreateIdentityCallback == true) - preCreateIdentityCallbackDeferred = CompletableDeferred() - if (hasEnableIdentityCallback == true) - preEnableIdentityCallbackDeferred = CompletableDeferred() - val preCreateIdentityCallback: PreEventCallback? = - preCreateIdentityCallback.takeIf { hasCreateIdentityCallback == true } - val preEnableIdentityCallback: PreEventCallback? = - preEnableIdentityCallback.takeIf { hasEnableIdentityCallback == true } - val context = if (authOptions.enableV3) context else null - val encryptionKeyBytes = - dbEncryptionKey?.foldIndexed(ByteArray(dbEncryptionKey.size)) { i, a, v -> - a.apply { set(i, v.toByte()) } - } + AsyncFunction("auth") Coroutine { address: String, hasCreateIdentityCallback: Boolean?, hasEnableIdentityCallback: Boolean?, dbEncryptionKey: List?, authParams: String -> + withContext(Dispatchers.IO) { + + logV("auth") + val reactSigner = ReactNativeSigner(module = this@XMTPModule, address = address) + signer = reactSigner + val authOptions = AuthParamsWrapper.authParamsFromJson(authParams) + + if (hasCreateIdentityCallback == true) + preCreateIdentityCallbackDeferred = CompletableDeferred() + if (hasEnableIdentityCallback == true) + preEnableIdentityCallbackDeferred = CompletableDeferred() + val preCreateIdentityCallback: PreEventCallback? = + preCreateIdentityCallback.takeIf { hasCreateIdentityCallback == true } + val preEnableIdentityCallback: PreEventCallback? = + preEnableIdentityCallback.takeIf { hasEnableIdentityCallback == true } + 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(authOptions.environment, authOptions.appVersion), - preCreateIdentityCallback = preCreateIdentityCallback, - preEnableIdentityCallback = preEnableIdentityCallback, - enableV3 = authOptions.enableV3, - appContext = context, - dbEncryptionKey = encryptionKeyBytes, - dbDirectory = authOptions.dbDirectory, - historySyncUrl = authOptions.historySyncUrl - ) - val client = Client().create(account = reactSigner, options = options) - clients[client.inboxId] = client - ContentJson.Companion - signer = null - sendEvent("authed", ClientWrapper.encodeToObj(client)) + val options = ClientOptions( + api = apiEnvironments(authOptions.environment, authOptions.appVersion), + preCreateIdentityCallback = preCreateIdentityCallback, + preEnableIdentityCallback = preEnableIdentityCallback, + enableV3 = authOptions.enableV3, + appContext = context, + dbEncryptionKey = encryptionKeyBytes, + dbDirectory = authOptions.dbDirectory, + historySyncUrl = authOptions.historySyncUrl + ) + val client = Client().create(account = reactSigner, options = options) + clients[client.inboxId] = client + ContentJson.Companion + signer = null + sendEvent("authed", ClientWrapper.encodeToObj(client)) + } } Function("receiveSignature") { requestID: String, signature: String -> @@ -271,74 +274,78 @@ class XMTPModule : Module() { } // Generate a random wallet and set the client to that - AsyncFunction("createRandom") { hasCreateIdentityCallback: Boolean?, hasEnableIdentityCallback: Boolean?, dbEncryptionKey: List?, authParams: String -> - logV("createRandom") - val privateKey = PrivateKeyBuilder() - - if (hasCreateIdentityCallback == true) - preCreateIdentityCallbackDeferred = CompletableDeferred() - if (hasEnableIdentityCallback == true) - preEnableIdentityCallbackDeferred = CompletableDeferred() - val preCreateIdentityCallback: PreEventCallback? = - preCreateIdentityCallback.takeIf { hasCreateIdentityCallback == true } - val preEnableIdentityCallback: PreEventCallback? = - preEnableIdentityCallback.takeIf { hasEnableIdentityCallback == true } - - 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(authOptions.environment, authOptions.appVersion), - preCreateIdentityCallback = preCreateIdentityCallback, - preEnableIdentityCallback = preEnableIdentityCallback, - enableV3 = authOptions.enableV3, - appContext = context, - dbEncryptionKey = encryptionKeyBytes, - dbDirectory = authOptions.dbDirectory, - historySyncUrl = authOptions.historySyncUrl - - ) - val randomClient = Client().create(account = privateKey, options = options) + AsyncFunction("createRandom") Coroutine { hasCreateIdentityCallback: Boolean?, hasEnableIdentityCallback: Boolean?, dbEncryptionKey: List?, authParams: String -> + withContext(Dispatchers.IO) { + logV("createRandom") + val privateKey = PrivateKeyBuilder() - ContentJson.Companion - clients[randomClient.inboxId] = randomClient - ClientWrapper.encodeToObj(randomClient) - } + if (hasCreateIdentityCallback == true) + preCreateIdentityCallbackDeferred = CompletableDeferred() + if (hasEnableIdentityCallback == true) + preEnableIdentityCallbackDeferred = CompletableDeferred() + val preCreateIdentityCallback: PreEventCallback? = + preCreateIdentityCallback.takeIf { hasCreateIdentityCallback == true } + val preEnableIdentityCallback: PreEventCallback? = + preEnableIdentityCallback.takeIf { hasEnableIdentityCallback == true } - AsyncFunction("createFromKeyBundle") { keyBundle: String, dbEncryptionKey: List?, authParams: String -> - logV("createFromKeyBundle") - val authOptions = AuthParamsWrapper.authParamsFromJson(authParams) - try { + 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(authOptions.environment, authOptions.appVersion), + preCreateIdentityCallback = preCreateIdentityCallback, + preEnableIdentityCallback = preEnableIdentityCallback, enableV3 = authOptions.enableV3, appContext = context, dbEncryptionKey = encryptionKeyBytes, dbDirectory = authOptions.dbDirectory, historySyncUrl = authOptions.historySyncUrl + ) - val bundle = - PrivateKeyOuterClass.PrivateKeyBundle.parseFrom( - Base64.decode( - keyBundle, - NO_WRAP - ) - ) - val client = Client().buildFromBundle(bundle = bundle, options = options) + val randomClient = Client().create(account = privateKey, options = options) + ContentJson.Companion - clients[client.inboxId] = client - ClientWrapper.encodeToObj(client) - } catch (e: Exception) { - throw XMTPException("Failed to create client: $e") + clients[randomClient.inboxId] = randomClient + ClientWrapper.encodeToObj(randomClient) + } + } + + AsyncFunction("createFromKeyBundle") Coroutine { keyBundle: String, dbEncryptionKey: List?, authParams: String -> + withContext(Dispatchers.IO) { + logV("createFromKeyBundle") + val authOptions = AuthParamsWrapper.authParamsFromJson(authParams) + try { + 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(authOptions.environment, authOptions.appVersion), + enableV3 = authOptions.enableV3, + appContext = context, + dbEncryptionKey = encryptionKeyBytes, + dbDirectory = authOptions.dbDirectory, + historySyncUrl = authOptions.historySyncUrl + ) + val bundle = + PrivateKeyOuterClass.PrivateKeyBundle.parseFrom( + Base64.decode( + keyBundle, + NO_WRAP + ) + ) + val client = Client().buildFromBundle(bundle = bundle, options = options) + ContentJson.Companion + clients[client.inboxId] = client + ClientWrapper.encodeToObj(client) + } catch (e: Exception) { + throw XMTPException("Failed to create client: $e") + } } } From d352e535f64b1cfca4375dd31577e33ae7886020 Mon Sep 17 00:00:00 2001 From: cameronvoell Date: Wed, 24 Jul 2024 12:01:40 -0700 Subject: [PATCH 11/13] adds newGroupCustomPermissions function --- android/build.gradle | 2 +- .../modules/xmtpreactnativesdk/XMTPModule.kt | 19 ++++ .../wrappers/PermissionPolicySetWrapper.kt | 25 +++++ example/ios/Podfile.lock | 14 +-- example/src/tests/groupPermissionsTests.ts | 95 +++++++++++++++++++ ios/Wrappers/PermissionPolicySetWrapper.swift | 1 + ios/XMTPModule.swift | 28 ++++++ ios/XMTPReactNative.podspec | 2 +- src/index.ts | 30 ++++++ src/lib/Conversations.ts | 30 +++++- src/lib/types/PermissionPolicySet.ts | 9 +- 11 files changed, 241 insertions(+), 14 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 988c5a454..90fc41efd 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -98,7 +98,7 @@ repositories { dependencies { implementation project(':expo-modules-core') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}" - implementation "org.xmtp:android:0.14.9" + implementation "org.xmtp:android:0.14.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" diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt index 9c3263a12..962269af5 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -892,6 +892,25 @@ class XMTPModule : Module() { } } + AsyncFunction("createGroupCustomPermissions") Coroutine { inboxId: String, peerAddresses: List, permissionPolicySetJson: String, groupOptionsJson: String -> + withContext(Dispatchers.IO) { + logV("createGroup") + val client = clients[inboxId] ?: throw XMTPException("No client") + val createGroupParams = + CreateGroupParamsWrapper.createGroupParamsFromJson(groupOptionsJson) + val permissionPolicySet = PermissionPolicySetWrapper.createPermissionPolicySetFromJson(permissionPolicySetJson) + val group = client.conversations.newGroupCustomPermissions( + peerAddresses, + permissionPolicySet, + createGroupParams.groupName, + createGroupParams.groupImageUrlSquare, + createGroupParams.groupDescription, + createGroupParams.groupPinnedFrameUrl + ) + GroupWrapper.encode(client, group) + } + } + AsyncFunction("listMemberInboxIds") Coroutine { inboxId: String, groupId: String -> withContext(Dispatchers.IO) { diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/PermissionPolicySetWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/PermissionPolicySetWrapper.kt index 0922d33a9..a6f1b9783 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/PermissionPolicySetWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/PermissionPolicySetWrapper.kt @@ -1,6 +1,7 @@ package expo.modules.xmtpreactnativesdk.wrappers import com.google.gson.GsonBuilder +import com.google.gson.JsonParser import uniffi.xmtpv3.org.xmtp.android.library.libxmtp.PermissionOption import uniffi.xmtpv3.org.xmtp.android.library.libxmtp.PermissionPolicySet @@ -16,6 +17,16 @@ class PermissionPolicySetWrapper { PermissionOption.Unknown -> "unknown" } } + + fun createPermissionOptionFromString(permissionOptionString: String): PermissionOption { + return when (permissionOptionString) { + "allow" -> PermissionOption.Allow + "deny" -> PermissionOption.Deny + "admin" -> PermissionOption.Admin + "superAdmin" -> PermissionOption.SuperAdmin + else -> PermissionOption.Unknown + } + } fun encodeToObj(policySet: PermissionPolicySet): Map { return mapOf( "addMemberPolicy" to fromPermissionOption(policySet.addMemberPolicy), @@ -29,6 +40,20 @@ class PermissionPolicySetWrapper { ) } + fun createPermissionPolicySetFromJson(permissionPolicySetJson: String): PermissionPolicySet { + val jsonObj = JsonParser.parseString(permissionPolicySetJson).asJsonObject + return PermissionPolicySet( + addMemberPolicy = createPermissionOptionFromString(jsonObj.get("addMemberPolicy").asString), + removeMemberPolicy = createPermissionOptionFromString(jsonObj.get("removeMemberPolicy").asString), + addAdminPolicy = createPermissionOptionFromString(jsonObj.get("addAdminPolicy").asString), + removeAdminPolicy = createPermissionOptionFromString(jsonObj.get("removeAdminPolicy").asString), + updateGroupNamePolicy = createPermissionOptionFromString(jsonObj.get("updateGroupNamePolicy").asString), + updateGroupDescriptionPolicy = createPermissionOptionFromString(jsonObj.get("updateGroupDescriptionPolicy").asString), + updateGroupImagePolicy = createPermissionOptionFromString(jsonObj.get("updateGroupImagePolicy").asString), + updateGroupPinnedFrameUrlPolicy = createPermissionOptionFromString(jsonObj.get("updateGroupPinnedFrameUrlPolicy").asString) + ) + } + fun encodeToJsonString(policySet: PermissionPolicySet): String { val gson = GsonBuilder().create() val obj = encodeToObj(policySet) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 03fe186f5..8fa3073f5 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -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.6-beta0) + - LibXMTP (0.5.6-beta1) - Logging (1.0.0) - MessagePacker (0.4.7) - MMKV (1.3.7): @@ -449,16 +449,16 @@ PODS: - GenericJSON (~> 2.0) - Logging (~> 1.0.0) - secp256k1.swift (~> 0.1) - - XMTP (0.13.8): + - XMTP (0.13.9): - Connect-Swift (= 0.12.0) - GzipSwift - - LibXMTP (= 0.5.6-beta0) + - LibXMTP (= 0.5.6-beta1) - web3.swift - XMTPReactNative (0.1.0): - ExpoModulesCore - MessagePacker - secp256k1.swift - - XMTP (= 0.13.8) + - XMTP (= 0.13.9) - Yoga (1.14.0) DEPENDENCIES: @@ -711,7 +711,7 @@ SPEC CHECKSUMS: GzipSwift: 893f3e48e597a1a4f62fafcb6514220fcf8287fa hermes-engine: d7cc127932c89c53374452d6f93473f1970d8e88 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 - LibXMTP: e7682dedb10e18343c011280d494a8e4a43d9eb7 + LibXMTP: 2205108c6c3a2bcdc405e42d4c718ad87c31a7c2 Logging: 9ef4ecb546ad3169398d5a723bc9bea1c46bef26 MessagePacker: ab2fe250e86ea7aedd1a9ee47a37083edd41fd02 MMKV: 36a22a9ec84c9bb960613a089ddf6f48be9312b0 @@ -763,8 +763,8 @@ SPEC CHECKSUMS: secp256k1.swift: a7e7a214f6db6ce5db32cc6b2b45e5c4dd633634 SwiftProtobuf: 407a385e97fd206c4fbe880cc84123989167e0d1 web3.swift: 2263d1e12e121b2c42ffb63a5a7beb1acaf33959 - XMTP: 7d0a3f3b22916acfbb0ae67f1ca6bbd3f5956138 - XMTPReactNative: 51e5b1b8669dab2ad5e2d74b518146388f5f425e + XMTP: 518a21ff9d2b7235dbf8d79fdc388a576c94f1e2 + XMTPReactNative: e3803ae32fa0dd849c2265b8d9109f682840ee24 Yoga: e71803b4c1fff832ccf9b92541e00f9b873119b9 PODFILE CHECKSUM: 95d6ace79946933ecf80684613842ee553dd76a2 diff --git a/example/src/tests/groupPermissionsTests.ts b/example/src/tests/groupPermissionsTests.ts index 71f3e8406..cbff2c77c 100644 --- a/example/src/tests/groupPermissionsTests.ts +++ b/example/src/tests/groupPermissionsTests.ts @@ -1,4 +1,6 @@ +import { permissionPolicySet } from 'xmtp-react-native-sdk' import { Test, assert, createClients } from './test-utils' +import { PermissionPolicySet } from 'xmtp-react-native-sdk/lib/types/PermissionPolicySet' export const groupPermissionsTests: Test[] = [] let counter = 1 @@ -512,3 +514,96 @@ test('can update group pinned frame', async () => { return true }) + +test('can create a group with custom permissions', async () => { + // Create clients + const [alix, bo, caro] = await createClients(3) + + const customPermissionsPolicySet: PermissionPolicySet = { + addMemberPolicy: 'allow', + removeMemberPolicy: 'deny', + addAdminPolicy: 'admin', + removeAdminPolicy: 'superAdmin', + updateGroupNamePolicy: 'admin', + updateGroupDescriptionPolicy: 'allow', + updateGroupImagePolicy: 'admin', + updateGroupPinnedFrameUrlPolicy: 'deny', + } + + // Bo creates a group with Alix and Caro with custom permissions + const boGroup = await bo.conversations.newGroupCustomPermissions( + [alix.address, caro.address], + customPermissionsPolicySet + ) + + // Verify that bo can read the correct permissions + await alix.conversations.syncGroups() + const alixGroup = (await alix.conversations.listGroups())[0] + const permissions = await alixGroup.permissionPolicySet() + assert(permissions.addMemberPolicy === customPermissionsPolicySet.addMemberPolicy, `permissions.addMemberPolicy should be ${customPermissionsPolicySet.addMemberPolicy} but was ${permissions.addMemberPolicy}`) + assert(permissions.removeMemberPolicy === customPermissionsPolicySet.removeMemberPolicy, `permissions.removeMemberPolicy should be ${customPermissionsPolicySet.removeMemberPolicy} but was ${permissions.removeMemberPolicy}`) + assert(permissions.addAdminPolicy === customPermissionsPolicySet.addAdminPolicy, `permissions.addAdminPolicy should be ${customPermissionsPolicySet.addAdminPolicy} but was ${permissions.addAdminPolicy}`) + assert(permissions.removeAdminPolicy === customPermissionsPolicySet.removeAdminPolicy, `permissions.removeAdminPolicy should be ${customPermissionsPolicySet.removeAdminPolicy} but was ${permissions.removeAdminPolicy}`) + assert(permissions.updateGroupNamePolicy === customPermissionsPolicySet.updateGroupNamePolicy, `permissions.updateGroupNamePolicy should be ${customPermissionsPolicySet.updateGroupNamePolicy} but was ${permissions.updateGroupNamePolicy}`) + assert(permissions.updateGroupDescriptionPolicy === customPermissionsPolicySet.updateGroupDescriptionPolicy, `permissions.updateGroupDescriptionPolicy should be ${customPermissionsPolicySet.updateGroupDescriptionPolicy} but was ${permissions.updateGroupDescriptionPolicy}`) + assert(permissions.updateGroupImagePolicy === customPermissionsPolicySet.updateGroupImagePolicy, `permissions.updateGroupImagePolicy should be ${customPermissionsPolicySet.updateGroupImagePolicy} but was ${permissions.updateGroupImagePolicy}`) + assert(permissions.updateGroupPinnedFrameUrlPolicy === customPermissionsPolicySet.updateGroupPinnedFrameUrlPolicy, `permissions.updateGroupPinnedFrameUrlPolicy should be ${customPermissionsPolicySet.updateGroupPinnedFrameUrlPolicy} but was ${permissions.updateGroupPinnedFrameUrlPolicy}`) + + // Verify that bo can not update the pinned frame even though they are a super admin + try { + await boGroup.updateGroupPinnedFrameUrl('new pinned frame') + assert(false, 'Bo should not be able to update the group pinned frame') + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (error) { + // expected + } + + // Verify that alix can update the group description + await alixGroup.updateGroupDescription('new description') + await alixGroup.sync() + assert( + (await alixGroup.groupDescription()) === 'new description', + `alixGroup.groupDescription should be "new description" but was ${alixGroup.groupDescription}` + ) + + // Verify that alix can not update the group name + try { + await alixGroup.updateGroupName('new name') + assert(false, 'Alix should not be able to update the group name') + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (error) { + // expected + } + + return true +}) + +test('creating a group with invalid permissions should fail', async () => { + // Create clients + const [alix, bo, caro] = await createClients(3) + + // Add/Remove admin must be admin or super admin + const customPermissionsPolicySet: PermissionPolicySet = { + addMemberPolicy: 'allow', + removeMemberPolicy: 'deny', + addAdminPolicy: 'allow', + removeAdminPolicy: 'superAdmin', + updateGroupNamePolicy: 'admin', + updateGroupDescriptionPolicy: 'allow', + updateGroupImagePolicy: 'admin', + updateGroupPinnedFrameUrlPolicy: 'deny', + } + + // Bo creates a group with Alix and Caro + try { + const boGroup = await bo.conversations.newGroupCustomPermissions( + [alix.address, caro.address], + customPermissionsPolicySet + ) + assert(false, 'Group creation should fail') + } catch (error) { + // expected + } + return true +}) + diff --git a/ios/Wrappers/PermissionPolicySetWrapper.swift b/ios/Wrappers/PermissionPolicySetWrapper.swift index 1ca923c84..bac7d5844 100644 --- a/ios/Wrappers/PermissionPolicySetWrapper.swift +++ b/ios/Wrappers/PermissionPolicySetWrapper.swift @@ -25,6 +25,7 @@ class PermissionPolicySetWrapper { } static func encodeToObj(_ policySet: XMTP.PermissionPolicySet) -> [String: Any] { + return [ "addMemberPolicy": fromPermissionOption(policySet.addMemberPolicy), "removeMemberPolicy": fromPermissionOption(policySet.removeMemberPolicy), diff --git a/ios/XMTPModule.swift b/ios/XMTPModule.swift index 977079665..8ad214e10 100644 --- a/ios/XMTPModule.swift +++ b/ios/XMTPModule.swift @@ -744,6 +744,27 @@ public class XMTPModule: Module { throw error } } + +// AsyncFunction("createGroupCustomPermissions") { (inboxId: String, peerAddresses: [String], permissionJson: String, groupOptionsJson: String) -> String in +// guard let client = await clientsManager.getClient(key: inboxId) else { +// throw Error.noClient +// } +// do { +// let createGroupParams = CreateGroupParamsWrapper.createGroupParamsFromJson(groupOptionsJson) +// let group = try await client.conversations.newGroupCustomP( +// with: peerAddresses, +// permissions: permissionLevel, +// name: createGroupParams.groupName, +// imageUrlSquare: createGroupParams.groupImageUrlSquare, +// description: createGroupParams.groupDescription, +// pinnedFrameUrl: createGroupParams.groupPinnedFrameUrl +// ) +// return try GroupWrapper.encode(group, client: client) +// } catch { +// print("ERRRO!: \(error.localizedDescription)") +// throw error +// } +// } AsyncFunction("listMemberInboxIds") { (inboxId: String, groupId: String) -> [String] in guard let client = await clientsManager.getClient(key: inboxId) else { @@ -1431,6 +1452,13 @@ public class XMTPModule: Module { throw Error.invalidPermissionOption } } + +// func createPermissionPolicySetFromJSON(permissionPolicySetJson: String) -> PermissionPolicySet { +// let data = permissionPolicySetJson.data(using: .utf8) ?? Data() +// let jsonObj = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any] ?? [:] +// +// return PermissionPolicySet +// } func createClientConfig(env: String, appVersion: String?, preEnableIdentityCallback: PreEventCallback? = nil, preCreateIdentityCallback: PreEventCallback? = nil, enableV3: Bool = false, dbEncryptionKey: Data? = nil, dbDirectory: String? = nil, historySyncUrl: String? = nil) -> XMTP.ClientOptions { // Ensure that all codecs have been registered. diff --git a/ios/XMTPReactNative.podspec b/ios/XMTPReactNative.podspec index c7fa69b34..6a267bd9f 100644 --- a/ios/XMTPReactNative.podspec +++ b/ios/XMTPReactNative.podspec @@ -26,5 +26,5 @@ Pod::Spec.new do |s| s.source_files = "**/*.{h,m,swift}" s.dependency 'secp256k1.swift' s.dependency "MessagePacker" - s.dependency "XMTP", "= 0.13.8" + s.dependency "XMTP", "= 0.13.10" end diff --git a/src/index.ts b/src/index.ts index 33668b5a6..564421978 100644 --- a/src/index.ts +++ b/src/index.ts @@ -191,6 +191,36 @@ export async function createGroup< ) } +export async function createGroupCustomPermissions< + ContentTypes extends DefaultContentTypes = DefaultContentTypes, +>( + client: Client, + peerAddresses: string[], + permissionPolicySet: PermissionPolicySet, + name: string = '', + imageUrlSquare: string = '', + description: string = '', + pinnedFrameUrl: string = '' +): Promise> { + const options: CreateGroupParams = { + name, + imageUrlSquare, + description, + pinnedFrameUrl, + } + return new Group( + client, + JSON.parse( + await XMTPModule.createGroupCustomPermissions( + client.inboxId, + peerAddresses, + JSON.stringify(permissionPolicySet), + JSON.stringify(options) + ) + ) + ) +} + export async function listGroups< ContentTypes extends DefaultContentTypes = DefaultContentTypes, >(client: Client): Promise[]> { diff --git a/src/lib/Conversations.ts b/src/lib/Conversations.ts index 15f6030fb..7821d1900 100644 --- a/src/lib/Conversations.ts +++ b/src/lib/Conversations.ts @@ -14,6 +14,7 @@ import { ConversationContext } from '../XMTP.types' import * as XMTPModule from '../index' import { ContentCodec } from '../index' import { getAddress } from '../utils/address' +import { PermissionPolicySet } from './types/PermissionPolicySet' export default class Conversations< ContentTypes extends ContentCodec[] = [], @@ -167,9 +168,10 @@ export default class Conversations< /** * Creates a new group. * - * This method creates a new conversation with the specified peer address and context. + * This method creates a new group with the specified peer addresses and options. * * @param {string[]} peerAddresses - The addresses of the peers to create a group with. + * @param {CreateGroupOptions} opts - The options to use for the group. * @returns {Promise>} A Promise that resolves to a Group object. */ async newGroup( @@ -187,6 +189,32 @@ export default class Conversations< ) } + /** + * Creates a new group with custom permissions. + * + * This method creates a new group with the specified peer addresses and options. + * + * @param {string[]} peerAddresses - The addresses of the peers to create a group with. + * @param {PermissionPolicySet} permissionPolicySet - The permission policy set to use for the group. + * @param {CreateGroupOptions} opts - The options to use for the group. + * @returns {Promise>} A Promise that resolves to a Group object. + */ + async newGroupCustomPermissions( + peerAddresses: string[], + permissionPolicySet: PermissionPolicySet, + opts?: CreateGroupOptions | undefined + ): Promise> { + return await XMTPModule.createGroupCustomPermissions( + this.client, + peerAddresses, + permissionPolicySet, + opts?.name, + opts?.imageUrlSquare, + opts?.description, + opts?.pinnedFrameUrl + ) + } + /** * Executes a network request to fetch the latest list of groups assoociated with the client * and save them to the local state. diff --git a/src/lib/types/PermissionPolicySet.ts b/src/lib/types/PermissionPolicySet.ts index 2e7968604..64bc4974f 100644 --- a/src/lib/types/PermissionPolicySet.ts +++ b/src/lib/types/PermissionPolicySet.ts @@ -1,10 +1,11 @@ export type PermissionOption = - | 'allow' - | 'deny' - | 'admin' - | 'superAdmin' + | 'allow' // Any members of the group can perform this action + | 'deny' // No members of the group can perform this action + | 'admin' // Only admins or super admins of the group can perform this action + | 'superAdmin' // Only the super admin of the group can perform this action | 'unknown' +// Add Admin and Remove admin must be set to either 'admin' or 'superAdmin' to be valid export type PermissionPolicySet = { addMemberPolicy: PermissionOption removeMemberPolicy: PermissionOption From 44e224c364fac57b10a560b8b4adb419abd528ba Mon Sep 17 00:00:00 2001 From: cameronvoell Date: Wed, 24 Jul 2024 12:17:52 -0700 Subject: [PATCH 12/13] lint fixes --- example/src/tests/groupPermissionsTests.ts | 62 ++++++++++++++++------ example/src/tests/groupTests.ts | 2 +- example/src/tests/tests.ts | 7 ++- src/lib/Conversations.ts | 6 +-- 4 files changed, 53 insertions(+), 24 deletions(-) diff --git a/example/src/tests/groupPermissionsTests.ts b/example/src/tests/groupPermissionsTests.ts index cbff2c77c..22463a9f7 100644 --- a/example/src/tests/groupPermissionsTests.ts +++ b/example/src/tests/groupPermissionsTests.ts @@ -1,7 +1,7 @@ -import { permissionPolicySet } from 'xmtp-react-native-sdk' -import { Test, assert, createClients } from './test-utils' import { PermissionPolicySet } from 'xmtp-react-native-sdk/lib/types/PermissionPolicySet' +import { Test, assert, createClients } from './test-utils' + export const groupPermissionsTests: Test[] = [] let counter = 1 function test(name: string, perform: () => Promise) { @@ -540,14 +540,44 @@ test('can create a group with custom permissions', async () => { await alix.conversations.syncGroups() const alixGroup = (await alix.conversations.listGroups())[0] const permissions = await alixGroup.permissionPolicySet() - assert(permissions.addMemberPolicy === customPermissionsPolicySet.addMemberPolicy, `permissions.addMemberPolicy should be ${customPermissionsPolicySet.addMemberPolicy} but was ${permissions.addMemberPolicy}`) - assert(permissions.removeMemberPolicy === customPermissionsPolicySet.removeMemberPolicy, `permissions.removeMemberPolicy should be ${customPermissionsPolicySet.removeMemberPolicy} but was ${permissions.removeMemberPolicy}`) - assert(permissions.addAdminPolicy === customPermissionsPolicySet.addAdminPolicy, `permissions.addAdminPolicy should be ${customPermissionsPolicySet.addAdminPolicy} but was ${permissions.addAdminPolicy}`) - assert(permissions.removeAdminPolicy === customPermissionsPolicySet.removeAdminPolicy, `permissions.removeAdminPolicy should be ${customPermissionsPolicySet.removeAdminPolicy} but was ${permissions.removeAdminPolicy}`) - assert(permissions.updateGroupNamePolicy === customPermissionsPolicySet.updateGroupNamePolicy, `permissions.updateGroupNamePolicy should be ${customPermissionsPolicySet.updateGroupNamePolicy} but was ${permissions.updateGroupNamePolicy}`) - assert(permissions.updateGroupDescriptionPolicy === customPermissionsPolicySet.updateGroupDescriptionPolicy, `permissions.updateGroupDescriptionPolicy should be ${customPermissionsPolicySet.updateGroupDescriptionPolicy} but was ${permissions.updateGroupDescriptionPolicy}`) - assert(permissions.updateGroupImagePolicy === customPermissionsPolicySet.updateGroupImagePolicy, `permissions.updateGroupImagePolicy should be ${customPermissionsPolicySet.updateGroupImagePolicy} but was ${permissions.updateGroupImagePolicy}`) - assert(permissions.updateGroupPinnedFrameUrlPolicy === customPermissionsPolicySet.updateGroupPinnedFrameUrlPolicy, `permissions.updateGroupPinnedFrameUrlPolicy should be ${customPermissionsPolicySet.updateGroupPinnedFrameUrlPolicy} but was ${permissions.updateGroupPinnedFrameUrlPolicy}`) + assert( + permissions.addMemberPolicy === customPermissionsPolicySet.addMemberPolicy, + `permissions.addMemberPolicy should be ${customPermissionsPolicySet.addMemberPolicy} but was ${permissions.addMemberPolicy}` + ) + assert( + permissions.removeMemberPolicy === + customPermissionsPolicySet.removeMemberPolicy, + `permissions.removeMemberPolicy should be ${customPermissionsPolicySet.removeMemberPolicy} but was ${permissions.removeMemberPolicy}` + ) + assert( + permissions.addAdminPolicy === customPermissionsPolicySet.addAdminPolicy, + `permissions.addAdminPolicy should be ${customPermissionsPolicySet.addAdminPolicy} but was ${permissions.addAdminPolicy}` + ) + assert( + permissions.removeAdminPolicy === + customPermissionsPolicySet.removeAdminPolicy, + `permissions.removeAdminPolicy should be ${customPermissionsPolicySet.removeAdminPolicy} but was ${permissions.removeAdminPolicy}` + ) + assert( + permissions.updateGroupNamePolicy === + customPermissionsPolicySet.updateGroupNamePolicy, + `permissions.updateGroupNamePolicy should be ${customPermissionsPolicySet.updateGroupNamePolicy} but was ${permissions.updateGroupNamePolicy}` + ) + assert( + permissions.updateGroupDescriptionPolicy === + customPermissionsPolicySet.updateGroupDescriptionPolicy, + `permissions.updateGroupDescriptionPolicy should be ${customPermissionsPolicySet.updateGroupDescriptionPolicy} but was ${permissions.updateGroupDescriptionPolicy}` + ) + assert( + permissions.updateGroupImagePolicy === + customPermissionsPolicySet.updateGroupImagePolicy, + `permissions.updateGroupImagePolicy should be ${customPermissionsPolicySet.updateGroupImagePolicy} but was ${permissions.updateGroupImagePolicy}` + ) + assert( + permissions.updateGroupPinnedFrameUrlPolicy === + customPermissionsPolicySet.updateGroupPinnedFrameUrlPolicy, + `permissions.updateGroupPinnedFrameUrlPolicy should be ${customPermissionsPolicySet.updateGroupPinnedFrameUrlPolicy} but was ${permissions.updateGroupPinnedFrameUrlPolicy}` + ) // Verify that bo can not update the pinned frame even though they are a super admin try { @@ -556,7 +586,7 @@ test('can create a group with custom permissions', async () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { // expected - } + } // Verify that alix can update the group description await alixGroup.updateGroupDescription('new description') @@ -596,14 +626,14 @@ test('creating a group with invalid permissions should fail', async () => { // Bo creates a group with Alix and Caro try { - const boGroup = await bo.conversations.newGroupCustomPermissions( + await bo.conversations.newGroupCustomPermissions( [alix.address, caro.address], - customPermissionsPolicySet - ) - assert(false, 'Group creation should fail') + customPermissionsPolicySet + ) + assert(false, 'Group creation should fail') + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { // expected } return true }) - diff --git a/example/src/tests/groupTests.ts b/example/src/tests/groupTests.ts index 756595a3e..e171c9e08 100644 --- a/example/src/tests/groupTests.ts +++ b/example/src/tests/groupTests.ts @@ -31,7 +31,7 @@ test('can make a MLS V3 client', async () => { 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.createRandom({ + await Client.createRandom({ env: 'local', appVersion: 'Testing/0.0.0', enableV3: true, diff --git a/example/src/tests/tests.ts b/example/src/tests/tests.ts index b1250276d..ad1b6c6c6 100644 --- a/example/src/tests/tests.ts +++ b/example/src/tests/tests.ts @@ -1,15 +1,14 @@ -import { sha256 } from '@noble/hashes/sha256' import { FramesClient } from '@xmtp/frames-client' -import { content, invitation, signature as signatureProto } from '@xmtp/proto' +import { content, invitation } from '@xmtp/proto' import { createHmac } from 'crypto' import ReactNativeBlobUtil from 'react-native-blob-util' import Config from 'react-native-config' import { TextEncoder, TextDecoder } from 'text-encoding' -import { createWalletClient, custom, PrivateKeyAccount, toHex } from 'viem' +import { PrivateKeyAccount } from 'viem' import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts' import { DecodedMessage } from 'xmtp-react-native-sdk/lib/DecodedMessage' -import { Test, assert, createClients, delayToPropogate } from './test-utils' +import { Test, assert, delayToPropogate } from './test-utils' import { Query, JSContentCodec, diff --git a/src/lib/Conversations.ts b/src/lib/Conversations.ts index 7821d1900..c274fdf86 100644 --- a/src/lib/Conversations.ts +++ b/src/lib/Conversations.ts @@ -10,11 +10,11 @@ import { DecodedMessage } from './DecodedMessage' import { Group } from './Group' import { CreateGroupOptions } from './types/CreateGroupOptions' import { EventTypes } from './types/EventTypes' +import { PermissionPolicySet } from './types/PermissionPolicySet' import { ConversationContext } from '../XMTP.types' import * as XMTPModule from '../index' import { ContentCodec } from '../index' import { getAddress } from '../utils/address' -import { PermissionPolicySet } from './types/PermissionPolicySet' export default class Conversations< ContentTypes extends ContentCodec[] = [], @@ -189,7 +189,7 @@ export default class Conversations< ) } - /** + /** * Creates a new group with custom permissions. * * This method creates a new group with the specified peer addresses and options. @@ -199,7 +199,7 @@ export default class Conversations< * @param {CreateGroupOptions} opts - The options to use for the group. * @returns {Promise>} A Promise that resolves to a Group object. */ - async newGroupCustomPermissions( + async newGroupCustomPermissions( peerAddresses: string[], permissionPolicySet: PermissionPolicySet, opts?: CreateGroupOptions | undefined From 7e46566ea68e910e9dcd0c707a26b05753f3fb72 Mon Sep 17 00:00:00 2001 From: cameronvoell Date: Wed, 24 Jul 2024 14:38:34 -0700 Subject: [PATCH 13/13] feat: added ability to set custom permissions on group creation --- example/ios/Podfile.lock | 8 +- example/src/tests/groupPermissionsTests.ts | 30 ++--- ios/Wrappers/PermissionPolicySetWrapper.swift | 110 ++++++++++++------ ios/Wrappers/Wrapper.swift | 1 + ios/XMTPModule.swift | 48 ++++---- src/lib/types/PermissionPolicySet.ts | 2 +- 6 files changed, 118 insertions(+), 81 deletions(-) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 8fa3073f5..f96e127e7 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -449,7 +449,7 @@ PODS: - GenericJSON (~> 2.0) - Logging (~> 1.0.0) - secp256k1.swift (~> 0.1) - - XMTP (0.13.9): + - XMTP (0.13.10): - Connect-Swift (= 0.12.0) - GzipSwift - LibXMTP (= 0.5.6-beta1) @@ -458,7 +458,7 @@ PODS: - ExpoModulesCore - MessagePacker - secp256k1.swift - - XMTP (= 0.13.9) + - XMTP (= 0.13.10) - Yoga (1.14.0) DEPENDENCIES: @@ -763,8 +763,8 @@ SPEC CHECKSUMS: secp256k1.swift: a7e7a214f6db6ce5db32cc6b2b45e5c4dd633634 SwiftProtobuf: 407a385e97fd206c4fbe880cc84123989167e0d1 web3.swift: 2263d1e12e121b2c42ffb63a5a7beb1acaf33959 - XMTP: 518a21ff9d2b7235dbf8d79fdc388a576c94f1e2 - XMTPReactNative: e3803ae32fa0dd849c2265b8d9109f682840ee24 + XMTP: 19f9c073262c44fbe98489208cda7a44d079064d + XMTPReactNative: 296aaa356ea5c67c98779665bcb5e1cad140d135 Yoga: e71803b4c1fff832ccf9b92541e00f9b873119b9 PODFILE CHECKSUM: 95d6ace79946933ecf80684613842ee553dd76a2 diff --git a/example/src/tests/groupPermissionsTests.ts b/example/src/tests/groupPermissionsTests.ts index 22463a9f7..bbdc4e81f 100644 --- a/example/src/tests/groupPermissionsTests.ts +++ b/example/src/tests/groupPermissionsTests.ts @@ -59,9 +59,7 @@ test('super admin can add a new admin', async () => { const boGroup = (await bo.conversations.listGroups())[0] try { await boGroup.addAdmin(caro.inboxId) - throw new Error( - 'Expected exception when non-super admin attempts to add an admin.' - ) + return false // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { // expected @@ -141,6 +139,7 @@ test('in admin only group, members can update group name once they are an admin' const boGroup = (await bo.conversations.listGroups())[0] try { await boGroup.updateGroupName("bo's group") + return false // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { // expected @@ -216,6 +215,7 @@ test('in admin only group, members can not update group name after admin status // Bo can no longer update the group name try { await boGroup.updateGroupName('new name 2') + return false // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { // expected error @@ -258,6 +258,7 @@ test('can not remove a super admin from a group', async () => { // Bo should not be able to remove alix from the group try { await boGroup.removeMembersByInboxId([alix.inboxId]) + return false // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { // expected @@ -284,6 +285,7 @@ test('can not remove a super admin from a group', async () => { // Verify bo can not remove alix bc alix is a super admin try { await boGroup.removeMembersByInboxId([alix.inboxId]) + return false // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { // expected @@ -335,6 +337,7 @@ test('can commit after invalid permissions commit', async () => { ) try { await alixGroup.addAdmin(alix.inboxId) + return false // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { // expected @@ -376,7 +379,7 @@ test('group with All Members policy has remove function that is admin only', asy // Verify that Alix cannot remove a member try { await alixGroup.removeMembers([caro.address]) - assert(false, 'Alix should not be able to remove a member') + return false // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { // expected @@ -429,8 +432,8 @@ test('can update group permissions', async () => { await alix.conversations.syncGroups() const alixGroup = (await alix.conversations.listGroups())[0] try { - await alixGroup.updateGroupDescription('new description') - assert(false, 'Alix should not be able to update the group description') + await alixGroup.updateGroupDescription('new description 2') + return false // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { // expected @@ -439,7 +442,7 @@ test('can update group permissions', async () => { // Verify that alix can not update permissions try { await alixGroup.updateGroupDescriptionPermission('allow') - assert(false, 'Alix should not be able to update the group name permission') + return false // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { // expected @@ -480,7 +483,7 @@ test('can update group pinned frame', async () => { const alixGroup = (await alix.conversations.listGroups())[0] try { await alixGroup.updateGroupPinnedFrameUrl('new pinned frame') - assert(false, 'Alix should not be able to update the group pinned frame') + return false // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { // expected @@ -582,7 +585,7 @@ test('can create a group with custom permissions', async () => { // Verify that bo can not update the pinned frame even though they are a super admin try { await boGroup.updateGroupPinnedFrameUrl('new pinned frame') - assert(false, 'Bo should not be able to update the group pinned frame') + return false // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { // expected @@ -599,7 +602,7 @@ test('can create a group with custom permissions', async () => { // Verify that alix can not update the group name try { await alixGroup.updateGroupName('new name') - assert(false, 'Alix should not be able to update the group name') + return false // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { // expected @@ -612,7 +615,7 @@ test('creating a group with invalid permissions should fail', async () => { // Create clients const [alix, bo, caro] = await createClients(3) - // Add/Remove admin must be admin or super admin + // Add/Remove admin can not be set to allow const customPermissionsPolicySet: PermissionPolicySet = { addMemberPolicy: 'allow', removeMemberPolicy: 'deny', @@ -630,10 +633,11 @@ test('creating a group with invalid permissions should fail', async () => { [alix.address, caro.address], customPermissionsPolicySet ) - assert(false, 'Group creation should fail') + return false // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { // expected + console.log('error', error) + return true } - return true }) diff --git a/ios/Wrappers/PermissionPolicySetWrapper.swift b/ios/Wrappers/PermissionPolicySetWrapper.swift index bac7d5844..f3d81cd7b 100644 --- a/ios/Wrappers/PermissionPolicySetWrapper.swift +++ b/ios/Wrappers/PermissionPolicySetWrapper.swift @@ -9,41 +9,79 @@ import Foundation import XMTP class PermissionPolicySetWrapper { - static func fromPermissionOption(_ permissionOption: XMTP.PermissionOption) -> String { - switch permissionOption { - case .allow: - return "allow" - case .deny: - return "deny" - case .admin: - return "admin" - case .superAdmin: - return "superAdmin" - case .unknown: - return "unknown" - } - } - - static func encodeToObj(_ policySet: XMTP.PermissionPolicySet) -> [String: Any] { + static func fromPermissionOption(_ permissionOption: XMTP.PermissionOption) -> String { + switch permissionOption { + case .allow: + return "allow" + case .deny: + return "deny" + case .admin: + return "admin" + case .superAdmin: + return "superAdmin" + case .unknown: + return "unknown" + } + } + + static func createPermissionOption(from string: String) -> PermissionOption { + switch string { + case "allow": + return .allow + case "deny": + return .deny + case "admin": + return .admin + case "superAdmin": + return .superAdmin + default: + return .unknown + } + } + + + static func encodeToObj(_ policySet: XMTP.PermissionPolicySet) -> [String: Any] { - return [ - "addMemberPolicy": fromPermissionOption(policySet.addMemberPolicy), - "removeMemberPolicy": fromPermissionOption(policySet.removeMemberPolicy), - "addAdminPolicy": fromPermissionOption(policySet.addAdminPolicy), - "removeAdminPolicy": fromPermissionOption(policySet.removeAdminPolicy), - "updateGroupNamePolicy": fromPermissionOption(policySet.updateGroupNamePolicy), - "updateGroupDescriptionPolicy": fromPermissionOption(policySet.updateGroupDescriptionPolicy), - "updateGroupImagePolicy": fromPermissionOption(policySet.updateGroupImagePolicy), - "updateGroupPinnedFrameUrlPolicy": fromPermissionOption(policySet.updateGroupPinnedFrameUrlPolicy) - ] - } - - static func encodeToJsonString(_ policySet: XMTP.PermissionPolicySet) throws -> String { - let obj = encodeToObj(policySet) - let data = try JSONSerialization.data(withJSONObject: obj) - guard let result = String(data: data, encoding: .utf8) else { - throw WrapperError.encodeError("could not encode permission policy") - } - return result - } + return [ + "addMemberPolicy": fromPermissionOption(policySet.addMemberPolicy), + "removeMemberPolicy": fromPermissionOption(policySet.removeMemberPolicy), + "addAdminPolicy": fromPermissionOption(policySet.addAdminPolicy), + "removeAdminPolicy": fromPermissionOption(policySet.removeAdminPolicy), + "updateGroupNamePolicy": fromPermissionOption(policySet.updateGroupNamePolicy), + "updateGroupDescriptionPolicy": fromPermissionOption(policySet.updateGroupDescriptionPolicy), + "updateGroupImagePolicy": fromPermissionOption(policySet.updateGroupImagePolicy), + "updateGroupPinnedFrameUrlPolicy": fromPermissionOption(policySet.updateGroupPinnedFrameUrlPolicy) + ] + } + public static func createPermissionPolicySet(from json: String) throws -> PermissionPolicySet { + guard let data = json.data(using: .utf8) else { + throw WrapperError.decodeError("Failed to convert PermissionPolicySet JSON string to data") + } + + guard let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []), + let jsonDict = jsonObject as? [String: Any] else { + throw WrapperError.decodeError("Failed to parse PermissionPolicySet JSON data") + } + + return PermissionPolicySet( + addMemberPolicy: createPermissionOption(from: jsonDict["addMemberPolicy"] as? String ?? ""), + removeMemberPolicy: createPermissionOption(from: jsonDict["removeMemberPolicy"] as? String ?? ""), + addAdminPolicy: createPermissionOption(from: jsonDict["addAdminPolicy"] as? String ?? ""), + removeAdminPolicy: createPermissionOption(from: jsonDict["removeAdminPolicy"] as? String ?? ""), + updateGroupNamePolicy: createPermissionOption(from: jsonDict["updateGroupNamePolicy"] as? String ?? ""), + updateGroupDescriptionPolicy: createPermissionOption(from: jsonDict["updateGroupDescriptionPolicy"] as? String ?? ""), + updateGroupImagePolicy: createPermissionOption(from: jsonDict["updateGroupImagePolicy"] as? String ?? ""), + updateGroupPinnedFrameUrlPolicy: createPermissionOption(from: jsonDict["updateGroupPinnedFrameUrlPolicy"] as? String ?? "") + ) + } + + static func encodeToJsonString(_ policySet: XMTP.PermissionPolicySet) throws -> String { + let obj = encodeToObj(policySet) + let data = try JSONSerialization.data(withJSONObject: obj) + guard let result = String(data: data, encoding: .utf8) else { + throw WrapperError.encodeError("could not encode permission policy") + } + return result + } + } diff --git a/ios/Wrappers/Wrapper.swift b/ios/Wrappers/Wrapper.swift index 87b6e60b4..cc5a8249b 100644 --- a/ios/Wrappers/Wrapper.swift +++ b/ios/Wrappers/Wrapper.swift @@ -8,6 +8,7 @@ import Foundation enum WrapperError: Swift.Error { case encodeError(String) + case decodeError(String) } protocol Wrapper: Codable { diff --git a/ios/XMTPModule.swift b/ios/XMTPModule.swift index 8ad214e10..a34af7ff2 100644 --- a/ios/XMTPModule.swift +++ b/ios/XMTPModule.swift @@ -745,26 +745,27 @@ public class XMTPModule: Module { } } -// AsyncFunction("createGroupCustomPermissions") { (inboxId: String, peerAddresses: [String], permissionJson: String, groupOptionsJson: String) -> String in -// guard let client = await clientsManager.getClient(key: inboxId) else { -// throw Error.noClient -// } -// do { -// let createGroupParams = CreateGroupParamsWrapper.createGroupParamsFromJson(groupOptionsJson) -// let group = try await client.conversations.newGroupCustomP( -// with: peerAddresses, -// permissions: permissionLevel, -// name: createGroupParams.groupName, -// imageUrlSquare: createGroupParams.groupImageUrlSquare, -// description: createGroupParams.groupDescription, -// pinnedFrameUrl: createGroupParams.groupPinnedFrameUrl -// ) -// return try GroupWrapper.encode(group, client: client) -// } catch { -// print("ERRRO!: \(error.localizedDescription)") -// throw error -// } -// } + AsyncFunction("createGroupCustomPermissions") { (inboxId: String, peerAddresses: [String], permissionPolicySetJson: String, groupOptionsJson: String) -> String in + guard let client = await clientsManager.getClient(key: inboxId) else { + throw Error.noClient + } + do { + let createGroupParams = CreateGroupParamsWrapper.createGroupParamsFromJson(groupOptionsJson) + let permissionPolicySet = try PermissionPolicySetWrapper.createPermissionPolicySet(from: permissionPolicySetJson) + let group = try await client.conversations.newGroupCustomPermissions( + with: peerAddresses, + permissionPolicySet: permissionPolicySet, + name: createGroupParams.groupName, + imageUrlSquare: createGroupParams.groupImageUrlSquare, + description: createGroupParams.groupDescription, + pinnedFrameUrl: createGroupParams.groupPinnedFrameUrl + ) + return try GroupWrapper.encode(group, client: client) + } catch { + print("ERRRO!: \(error.localizedDescription)") + throw error + } + } AsyncFunction("listMemberInboxIds") { (inboxId: String, groupId: String) -> [String] in guard let client = await clientsManager.getClient(key: inboxId) else { @@ -1452,13 +1453,6 @@ public class XMTPModule: Module { throw Error.invalidPermissionOption } } - -// func createPermissionPolicySetFromJSON(permissionPolicySetJson: String) -> PermissionPolicySet { -// let data = permissionPolicySetJson.data(using: .utf8) ?? Data() -// let jsonObj = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any] ?? [:] -// -// return PermissionPolicySet -// } func createClientConfig(env: String, appVersion: String?, preEnableIdentityCallback: PreEventCallback? = nil, preCreateIdentityCallback: PreEventCallback? = nil, enableV3: Bool = false, dbEncryptionKey: Data? = nil, dbDirectory: String? = nil, historySyncUrl: String? = nil) -> XMTP.ClientOptions { // Ensure that all codecs have been registered. diff --git a/src/lib/types/PermissionPolicySet.ts b/src/lib/types/PermissionPolicySet.ts index 64bc4974f..55811c806 100644 --- a/src/lib/types/PermissionPolicySet.ts +++ b/src/lib/types/PermissionPolicySet.ts @@ -5,7 +5,7 @@ export type PermissionOption = | 'superAdmin' // Only the super admin of the group can perform this action | 'unknown' -// Add Admin and Remove admin must be set to either 'admin' or 'superAdmin' to be valid +// Add Admin and Remove admin must be set to either 'admin', 'superAdmin' or 'deny' to be valid export type PermissionPolicySet = { addMemberPolicy: PermissionOption removeMemberPolicy: PermissionOption