diff --git a/.changeset/fork-fix-filter-adjustment.md b/.changeset/fork-fix-filter-adjustment.md new file mode 100644 index 00000000..3e8027fe --- /dev/null +++ b/.changeset/fork-fix-filter-adjustment.md @@ -0,0 +1,5 @@ +--- +"@xmtp/react-native-sdk": patch +--- + +Fix for forked groups via intent filter adjustment diff --git a/android/build.gradle b/android/build.gradle index 5a7d0dd1..297784f9 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:3.0.15" + implementation "org.xmtp:android:3.0.16" 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 1d6094c8..69a69e06 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -55,7 +55,7 @@ PODS: - hermes-engine/Pre-built (= 0.71.14) - hermes-engine/Pre-built (0.71.14) - libevent (2.1.12) - - LibXMTP (3.0.10) + - LibXMTP (3.0.12) - MessagePacker (0.4.7) - MMKV (2.0.0): - MMKVCore (~> 2.0.0) @@ -448,18 +448,18 @@ PODS: - SQLCipher/standard (4.5.7): - SQLCipher/common - SwiftProtobuf (1.28.2) - - XMTP (3.0.16): + - XMTP (3.0.17): - Connect-Swift (= 1.0.0) - CryptoSwift (= 1.8.3) - CSecp256k1 (~> 0.2) - - LibXMTP (= 3.0.10) + - LibXMTP (= 3.0.12) - SQLCipher (= 4.5.7) - XMTPReactNative (3.1.2): - CSecp256k1 (~> 0.2) - ExpoModulesCore - MessagePacker - SQLCipher (= 4.5.7) - - XMTP (= 3.0.16) + - XMTP (= 3.0.17) - Yoga (1.14.0) DEPENDENCIES: @@ -711,7 +711,7 @@ SPEC CHECKSUMS: glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b hermes-engine: d7cc127932c89c53374452d6f93473f1970d8e88 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 - LibXMTP: e550ccb4565023eb77f5a2eddd9d5a71cfb9d2b3 + LibXMTP: 7e7607786ccc82c0230960964aa00843a5d47094 MessagePacker: ab2fe250e86ea7aedd1a9ee47a37083edd41fd02 MMKV: f7d1d5945c8765f97f39c3d121f353d46735d801 MMKVCore: c04b296010fcb1d1638f2c69405096aac12f6390 @@ -762,8 +762,8 @@ SPEC CHECKSUMS: RNSVG: d00c8f91c3cbf6d476451313a18f04d220d4f396 SQLCipher: 5e6bfb47323635c8b657b1b27d25c5f1baf63bf5 SwiftProtobuf: 4dbaffec76a39a8dc5da23b40af1a5dc01a4c02d - XMTP: ce70e4a8e71db02af15bf4a0c230f5990c619281 - XMTPReactNative: 00f79e4244439587ade2f7d65900e0dc9bd2634f + XMTP: 5be6a8212c789e828b6eeef9edae84a227c61f22 + XMTPReactNative: 72e92330c1d9883a93c4a1be62ac4342e95dd80d Yoga: e71803b4c1fff832ccf9b92541e00f9b873119b9 PODFILE CHECKSUM: 0e6fe50018f34e575d38dc6a1fdf1f99c9596cdd diff --git a/example/src/tests/groupTests.ts b/example/src/tests/groupTests.ts index fa0b2dcb..95585c89 100644 --- a/example/src/tests/groupTests.ts +++ b/example/src/tests/groupTests.ts @@ -16,6 +16,7 @@ import { DecodedMessage, ConsentRecord, } from '../../../src/index' +import { DefaultContentTypes } from 'xmtp-react-native-sdk/lib/types/DefaultContentType' export const groupTests: Test[] = [] let counter = 1 @@ -23,6 +24,277 @@ function test(name: string, perform: () => Promise) { groupTests.push({ name: String(counter++) + '. ' + name, run: perform }) } +test('groups cannot fork', async () => { + const [alix, bo, caro] = await createClients(3) + // Create group with 3 users + const { id: groupId } = await alix.conversations.newGroup([ + bo.address, + caro.address, + ]) + + const getGroupForClient = async (client: Client) => { + // Always sync the client before getting the group + await client.conversations.sync() + const group = await client.conversations.findGroup(groupId) + assert(group !== undefined, `Group not found for ${client.address}`) + return group as Group + } + + const syncClientAndGroup = async (client: Client) => { + const group = await getGroupForClient(client) + await group.sync() + } + + const addMemberToGroup = async (fromClient: Client, addresses: string[]) => { + await syncClientAndGroup(fromClient) + const group = await getGroupForClient(fromClient) + await group.addMembers(addresses) + await delayToPropogate(500) + } + + const removeMemberFromGroup = async ( + fromClient: Client, + addresses: string[] + ) => { + await syncClientAndGroup(fromClient) + const group = await getGroupForClient(fromClient) + await group.removeMembers(addresses) + await delayToPropogate(500) + } + + // Helper to send a message from a bunch of senders and make sure it is received by all receivers + const testMessageSending = async (senderClient: Client, receiver: Client) => { + // for (const senderClient of senders) { + const messageContent = Math.random().toString(36) + await syncClientAndGroup(senderClient) + const senderGroup = await getGroupForClient(senderClient) + await senderGroup.send(messageContent) + + await delayToPropogate(500) + await senderGroup.sync() + + await syncClientAndGroup(receiver) + + const receiverGroupToCheck = await getGroupForClient(receiver) + await receiverGroupToCheck.sync() + + const messages = await receiverGroupToCheck.messages({ + direction: 'DESCENDING', + }) + const lastMessage = messages[0] + // console.log(lastMessage); + console.log( + `${receiverGroupToCheck.client.address} sees ${messages.length} messages in group` + ) + assert( + lastMessage !== undefined && + lastMessage.nativeContent.text === messageContent, + `${receiverGroupToCheck.client.address} should have received the message, FORK? ${lastMessage?.nativeContent.text} !== ${messageContent}` + ) + // } + } + + console.log('Testing that messages sent by alix are received by bo') + await testMessageSending(alix, bo) + console.log('Alix & Bo are not forked at the beginning') + + // Test adding members one by one + // console.log('Testing adding members one by one...') + const newClients = await createClients(2) + + // Add back several members + console.log('Adding new members to the group...') + for (const client of newClients) { + console.log(`Adding member ${client.address}...`) + await addMemberToGroup(alix, [client.address]) + } + await delayToPropogate() + + await alix.conversations.sync() + await syncClientAndGroup(alix) + + // NB => if we don't use Promise.all but a loop, we don't get a fork + const REMOVE_MEMBERS_IN_PARALLEL = true + if (REMOVE_MEMBERS_IN_PARALLEL) { + console.log('Removing members in parallel') + + await Promise.all( + newClients.map((client) => { + console.log(`Removing member ${client.address}...`) + return removeMemberFromGroup(alix, [client.address]) + }) + ) + } else { + console.log('Removing members one by one') + + for (const client of newClients) { + console.log(`Removing member ${client.address}...`) + await removeMemberFromGroup(alix, [client.address]) + } + } + + await delayToPropogate(1000) + + // When forked, it stays forked even if we try 5 times + // but sometimes it is not forked and works 5/5 times + let forkCount = 0 + const tryCount = 5 + for (let i = 0; i < tryCount; i++) { + console.log(`Checking fork status ${i+1}/${tryCount}`) + try { + await syncClientAndGroup(alix) + await syncClientAndGroup(bo) + await delayToPropogate(500) + await testMessageSending(alix, bo) + console.log('Not forked!') + } catch (e: any) { + console.log('Forked!') + console.log(e) + forkCount++ + } + } + + assert(forkCount === 0, `Forked ${forkCount}/${tryCount} times`) + + return true +}) + +test('groups cannot fork short version', async () => { + const [alix, bo, new_one, new_two] = await createClients(4) + // Create group with 2 users + const alixGroup = await alix.conversations.newGroup([ + bo.address, + new_one.address, + new_two.address, + ]) + + // sync clients + await alix.conversations.sync() + await bo.conversations.sync() + const boGroup: Group = (await bo.conversations.findGroup(alixGroup.id))! + + // Remove two members in parallel + // NB => if we don't use Promise.all but a loop, we don't get a fork + console.log('*************libxmtp*********************: Removing members in parallel') + await Promise.all([ + alixGroup.removeMembers([new_one.address]), + alixGroup.removeMembers([new_two.address]) + ]) + + // Helper to send a message from a bunch of senders and make sure it is received by all receivers + const testMessageSending = async (senderGroup: Group, receiverGroup: Group) => { + const messageContent = Math.random().toString(36) + await senderGroup.sync() + await alixGroup.send(messageContent) + + await delayToPropogate(500) + await alixGroup.sync() + await receiverGroup.sync() + + const messages = await receiverGroup.messages({ + direction: 'DESCENDING', + }) + const lastMessage = messages[0] + console.log( + `${receiverGroup.client.address} sees ${messages.length} messages in group` + ) + assert( + lastMessage !== undefined && + lastMessage.nativeContent.text === messageContent, + `${receiverGroup.client.address} should have received the message, FORK? ${lastMessage?.nativeContent.text} !== ${messageContent}` + ) + } + // When forked, it stays forked even if we try 5 times + // but sometimes it is not forked and works 5/5 times + let forkCount = 0 + const tryCount = 5 + for (let i = 0; i < tryCount; i++) { + console.log(`Checking fork status ${i+1}/${tryCount}`) + try { + await alixGroup.sync() + await boGroup.sync() + await delayToPropogate(500) + await testMessageSending(alixGroup, boGroup) + console.log('Not forked!') + } catch (e: any) { + console.log('Forked!') + console.log(e) + forkCount++ + } + } + assert(forkCount === 0, `Forked ${forkCount}/${tryCount} times`) + + return true +}) + +test('groups cannot fork short version - update metadata', async () => { + const [alix, bo, new_one, new_two, new_three, new_four] = await createClients(6) + // Create group with 2 users + const alixGroup = await alix.conversations.newGroup([ + bo.address, + new_one.address, + new_two.address, + ]) + + // sync clients + await alix.conversations.sync() + await bo.conversations.sync() + const boGroup: Group = (await bo.conversations.findGroup(alixGroup.id))! + + // Remove two members in parallel + // NB => if we don't use Promise.all but a loop, we don't get a fork + console.log('*************libxmtp*********************: Updating metadata in parallel') + await Promise.all([ + alixGroup.updateGroupName('new name'), + alixGroup.updateGroupName('new name 2') + ]) + + // Helper to send a message from a bunch of senders and make sure it is received by all receivers + const testMessageSending = async (senderGroup: Group, receiverGroup: Group) => { + const messageContent = Math.random().toString(36) + await senderGroup.sync() + await alixGroup.send(messageContent) + + await delayToPropogate(500) + await alixGroup.sync() + await receiverGroup.sync() + + const messages = await receiverGroup.messages({ + direction: 'DESCENDING', + }) + const lastMessage = messages[0] + console.log( + `${receiverGroup.client.address} sees ${messages.length} messages in group` + ) + assert( + lastMessage !== undefined && + lastMessage.nativeContent.text === messageContent, + `${receiverGroup.client.address} should have received the message, FORK? ${lastMessage?.nativeContent.text} !== ${messageContent}` + ) + } + // When forked, it stays forked even if we try 5 times + // but sometimes it is not forked and works 5/5 times + let forkCount = 0 + const tryCount = 5 + for (let i = 0; i < tryCount; i++) { + console.log(`Checking fork status ${i+1}/${tryCount}`) + try { + await alixGroup.sync() + await boGroup.sync() + await delayToPropogate(500) + await testMessageSending(alixGroup, boGroup) + console.log('Not forked!') + } catch (e: any) { + console.log('Forked!') + console.log(e) + forkCount++ + } + } + assert(forkCount === 0, `Forked ${forkCount}/${tryCount} times`) + + return true +}) + test('can cancel streams', async () => { const [alix, bo] = await createClients(2) let messageCallbacks = 0 diff --git a/example/yarn.lock b/example/yarn.lock index 5bcc21b5..aebad0f2 100644 --- a/example/yarn.lock +++ b/example/yarn.lock @@ -6283,7 +6283,7 @@ resolved "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz" integrity sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw== -"@types/node@^20.14.9": +"@types/node@^20.15.0": version "20.17.10" resolved "https://registry.npmjs.org/@types/node/-/node-20.17.10.tgz#3f7166190aece19a0d1d364d75c8b0b5778c1e18" integrity sha512-/jrvh5h6NXhEauFFexRin69nA0uHJ5gwk4iDivp/DeoEua3uwCUto6PC86IpRITBOs4+6i2I56K5x5b6WYGXHA== diff --git a/ios/XMTPReactNative.podspec b/ios/XMTPReactNative.podspec index 6e267100..80ef7bee 100644 --- a/ios/XMTPReactNative.podspec +++ b/ios/XMTPReactNative.podspec @@ -26,7 +26,7 @@ Pod::Spec.new do |s| s.source_files = "**/*.{h,m,swift}" s.dependency "MessagePacker" - s.dependency "XMTP", "= 3.0.16" + s.dependency "XMTP", "= 3.0.17" s.dependency 'CSecp256k1', '~> 0.2' s.dependency "SQLCipher", "= 4.5.7" end