UNPKG

@towns-protocol/sdk

Version:

For more details, visit the following resources:

102 lines 7.06 kB
/** * @group with-entitlements */ import { MembershipOp, MembershipReason } from '@towns-protocol/proto'; import { makeUserStreamId } from '../../id'; import { getNftRuleData, linkWallets, unlinkWallet, setupChannelWithCustomRole, expectUserCanJoinChannel, waitFor, setupWalletsAndContexts, everyoneMembershipStruct, createSpaceAndDefaultChannel, } from '../testUtils'; import { dlog } from '@towns-protocol/dlog'; import { TestERC721, AppRegistryDapp, Permission, SpaceAddressFromSpaceId, } from '@towns-protocol/web3'; import { ethers } from 'ethers'; import { makeBaseChainConfig } from '../../riverConfig'; const log = dlog('csb:test:channelsWithEntitlements'); describe('channelScrubbing', () => { test('User who loses entitlements is bounced after a channel scrub is triggered', async () => { const TestNftName = 'TestNFT'; const TestNftAddress = await TestERC721.getContractAddress(TestNftName); const { alice, aliceSpaceDapp, aliceProvider, carol, carolSpaceDapp, carolProvider, spaceId, channelId, } = await setupChannelWithCustomRole([], getNftRuleData(TestNftAddress)); const aliceLinkedWallet = ethers.Wallet.createRandom(); const carolLinkedWallet = ethers.Wallet.createRandom(); // link wallets await Promise.all([ linkWallets(aliceSpaceDapp, aliceProvider.wallet, aliceLinkedWallet), linkWallets(carolSpaceDapp, carolProvider.wallet, carolLinkedWallet), ]); // Mint the needed asset to Alice and Carol's linked wallets log('Minting an NFT to alices linked wallet'); await Promise.all([ TestERC721.publicMint(TestNftName, aliceLinkedWallet.address), TestERC721.publicMint(TestNftName, carolLinkedWallet.address), ]); // Join alice to the channel based on her linked wallet credentials await expectUserCanJoinChannel(alice, aliceSpaceDapp, spaceId, channelId); await unlinkWallet(aliceSpaceDapp, aliceProvider.wallet, aliceLinkedWallet); // Wait 5 seconds so the channel stream will become eligible for scrubbing await new Promise((f) => setTimeout(f, 5000)); // When carol's join event is added to the stream, it should trigger a scrub, and Alice // should be booted from the stream since she unlinked her entitled wallet. await expectUserCanJoinChannel(carol, carolSpaceDapp, spaceId, channelId); const userStreamView = (await alice.waitForStream(makeUserStreamId(alice.userId))).view; // Wait for alice's user stream to have the leave event await waitFor(() => { const membership = userStreamView.userContent.getMembership(channelId); expect(membership?.op).toBe(MembershipOp.SO_LEAVE); expect(membership?.reason).toBe(MembershipReason.MR_NOT_ENTITLED); }); }); test('Bot loses membership and is scrubbed from channel when uninstalled from space', async () => { const { alice: spaceOwner, aliceProvider: spaceOwnerProvider, aliceSpaceDapp: spaceOwnerSpaceDapp, bob: bot, bobsWallet: botWallet, bobProvider: botProvider, } = await setupWalletsAndContexts(); const appRegistryDapp = new AppRegistryDapp(makeBaseChainConfig().chainConfig, spaceOwnerProvider); // Create bot app contract const tx = await appRegistryDapp.createApp(botProvider.signer, 'scrub-test-bot', [Permission.Read, Permission.Write], botWallet.address, ethers.utils.parseEther('0.01').toBigInt(), 31536000n); const receipt = await tx.wait(); const { app: foundAppAddress } = appRegistryDapp.getCreateAppEvent(receipt); expect(foundAppAddress).toBeDefined(); // Create bot user streams await expect(bot.initializeUser({ appAddress: foundAppAddress })).resolves.toBeDefined(); bot.startSync(); // Create a town with channels (everyone can join) const everyoneMembership = await everyoneMembershipStruct(spaceOwnerSpaceDapp, spaceOwner); const { spaceId, defaultChannelId: channelId } = await createSpaceAndDefaultChannel(spaceOwner, spaceOwnerSpaceDapp, spaceOwnerProvider.wallet, "space owner's town", everyoneMembership); // Install the bot to the space (as space owner) const installTx = await appRegistryDapp.installApp(spaceOwnerProvider.signer, foundAppAddress, SpaceAddressFromSpaceId(spaceId), ethers.utils.parseEther('0.02').toBigInt()); const installReceipt = await installTx.wait(); expect(installReceipt.status).toBe(1); // Verify bot is installed const space = spaceOwnerSpaceDapp.getSpace(spaceId); const installedApps = await space.AppAccount.read.getInstalledApps(); expect(installedApps).toContain(foundAppAddress); // Have space owner add bot to space and channel await expect(spaceOwner.joinUser(spaceId, bot.userId)).resolves.toBeDefined(); await expect(spaceOwner.joinUser(channelId, bot.userId)).resolves.toBeDefined(); // Validate bot is a member of both space and channel const botUserStreamView = bot.stream(bot.userStreamId).view; await waitFor(() => { expect(botUserStreamView.userContent.isMember(spaceId, MembershipOp.SO_JOIN)).toBe(true); expect(botUserStreamView.userContent.isMember(channelId, MembershipOp.SO_JOIN)).toBe(true); }); // Uninstall the bot from the space (this should make it lose membership eligibility) // Note: Using removeApp method - in a real implementation this might be a different method // like uninstallApp, but removeApp should serve the same purpose for testing const removeAppTx = await appRegistryDapp.uninstallApp(spaceOwnerProvider.signer, foundAppAddress, space?.Address); const removeAppReceipt = await removeAppTx.wait(); expect(removeAppReceipt.status).toBe(1); // Verify bot is no longer installed const installedAppsAfterRemoval = await space.AppAccount.read.getInstalledApps(); expect(installedAppsAfterRemoval).not.toContain(foundAppAddress); // Wait 5 seconds so the channel stream will become eligible for scrubbing await new Promise((f) => setTimeout(f, 5000)); // Have the bot attempt to post a message to the channel to trigger scrubbing // This should fail with a permission error since the bot is no longer installed/entitled, and trigger a scrub await expect(bot.sendMessage(channelId, 'test message from bot')).rejects.toThrow(/PERMISSION_DENIED/); // Wait for bot's user stream to have the leave event due to being scrubbed await waitFor(() => { const membership = botUserStreamView.userContent.getMembership(channelId); expect(membership?.op).toBe(MembershipOp.SO_LEAVE); expect(membership?.reason).toBe(MembershipReason.MR_NOT_ENTITLED); }); // Cleanup await bot.stopSync(); await spaceOwner.stopSync(); }); }); //# sourceMappingURL=channelScrubbing.test.js.map