diff --git a/android/build.gradle b/android/build.gradle index fcc62ea94..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.8" + 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 0d0dc7c29..962269af5 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") + } } } @@ -885,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 eef599503..f96e127e7 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.7): + - XMTP (0.13.10): - 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.7) + - XMTP (= 0.13.10) - 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: 16bd630ff61081d3a325619a26ea176ed256d419 - XMTPReactNative: 4716836807cb33c72bde0846ac46b3fe923a3625 + XMTP: 19f9c073262c44fbe98489208cda7a44d079064d + XMTPReactNative: 296aaa356ea5c67c98779665bcb5e1cad140d135 Yoga: e71803b4c1fff832ccf9b92541e00f9b873119b9 PODFILE CHECKSUM: 95d6ace79946933ecf80684613842ee553dd76a2 diff --git a/example/src/tests/groupPermissionsTests.ts b/example/src/tests/groupPermissionsTests.ts index 71f3e8406..bbdc4e81f 100644 --- a/example/src/tests/groupPermissionsTests.ts +++ b/example/src/tests/groupPermissionsTests.ts @@ -1,3 +1,5 @@ +import { PermissionPolicySet } from 'xmtp-react-native-sdk/lib/types/PermissionPolicySet' + import { Test, assert, createClients } from './test-utils' export const groupPermissionsTests: Test[] = [] @@ -57,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 @@ -139,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 @@ -214,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 @@ -256,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 @@ -282,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 @@ -333,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 @@ -374,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 @@ -427,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 @@ -437,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 @@ -478,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 @@ -512,3 +517,127 @@ 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') + return false + // 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') + return false + // 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 can not be set to allow + 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 { + await bo.conversations.newGroupCustomPermissions( + [alix.address, caro.address], + customPermissionsPolicySet + ) + return false + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (error) { + // expected + console.log('error', error) + 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 641ba65d7..ad1b6c6c6 100644 --- a/example/src/tests/tests.ts +++ b/example/src/tests/tests.ts @@ -1,15 +1,14 @@ import { FramesClient } from '@xmtp/frames-client' import { content, invitation } 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' import { PrivateKeyAccount } from 'viem' -import { privateKeyToAccount } from 'viem/accounts' +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, @@ -176,8 +175,10 @@ function test(name: string, perform: () => Promise) { } test('can make a client', async () => { - const [client] = await createClients(1) - + 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( @@ -214,13 +215,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( @@ -236,17 +232,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( @@ -269,7 +260,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') @@ -334,38 +326,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 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 bob = await Client.createRandom({ env: 'local' }) + const alice = await Client.createRandom({ env: 'local' }) - 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) { @@ -379,15 +348,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 @@ -396,8 +360,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) @@ -423,7 +387,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') @@ -476,7 +442,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') @@ -578,7 +546,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 @@ -675,7 +645,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[] = [] @@ -715,12 +687,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. @@ -802,7 +776,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') @@ -836,7 +812,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 @@ -860,6 +838,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++) { @@ -889,7 +868,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 @@ -914,6 +895,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() @@ -945,7 +927,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) @@ -1008,20 +991,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) { @@ -1036,7 +1013,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()) @@ -1068,7 +1053,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()) @@ -1104,14 +1096,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) { @@ -1126,14 +1113,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) { @@ -1144,7 +1126,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') @@ -1170,7 +1154,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') @@ -1224,10 +1210,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) @@ -1274,7 +1268,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) { @@ -1307,127 +1301,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)) @@ -1503,12 +1376,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: { @@ -1578,227 +1451,31 @@ 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() +class ViemSigner { + account: PrivateKeyAccount - 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 - ) + constructor(account: PrivateKeyAccount) { + this.account = account + } - assert( - allConv3Messages.length === 2, - 'Unexpected all conv3 messages count11 ' + allConv3Messages.length - ) + async getAddress() { + return this.account.address + } - return true -}) + 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 = @@ -1808,7 +1485,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, diff --git a/ios/Wrappers/PermissionPolicySetWrapper.swift b/ios/Wrappers/PermissionPolicySetWrapper.swift index 1ca923c84..f3d81cd7b 100644 --- a/ios/Wrappers/PermissionPolicySetWrapper.swift +++ b/ios/Wrappers/PermissionPolicySetWrapper.swift @@ -9,40 +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] { - 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 - } + 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) + ] + } + 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 977079665..a34af7ff2 100644 --- a/ios/XMTPModule.swift +++ b/ios/XMTPModule.swift @@ -744,6 +744,28 @@ public class XMTPModule: Module { 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 { diff --git a/ios/XMTPReactNative.podspec b/ios/XMTPReactNative.podspec index ea8dbe65d..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.7" + 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/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.') } diff --git a/src/lib/Conversations.ts b/src/lib/Conversations.ts index 15f6030fb..c274fdf86 100644 --- a/src/lib/Conversations.ts +++ b/src/lib/Conversations.ts @@ -10,6 +10,7 @@ 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' @@ -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..55811c806 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', 'superAdmin' or 'deny' to be valid export type PermissionPolicySet = { addMemberPolicy: PermissionOption removeMemberPolicy: PermissionOption