UNPKG

@river-build/sdk

Version:

For more details, visit the following resources:

1,025 lines 42.2 kB
/* eslint-disable @typescript-eslint/no-redundant-type-constituents */ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ import { _impl_makeEvent_impl_, publicKeyToAddress, unpackStreamEnvelopes } from '../sign'; import { MembershipOp, SyncOp, EncryptedDataVersion, } from '@river-build/proto'; import { Entitlements } from '../sync-agent/entitlements/entitlements'; import { Client } from '../client'; import { makeBaseChainConfig, makeRiverChainConfig, makeRiverConfig, useLegacySpaces, } from '../riverConfig'; import { genId, makeSpaceStreamId, makeDefaultChannelStreamId, makeUniqueChannelStreamId, makeUserStreamId, userIdFromAddress, } from '../id'; import { getPublicKey, utils } from 'ethereum-cryptography/secp256k1'; import { bin_fromHexString, check, dlog } from '@river-build/dlog'; import { ethers } from 'ethers'; import { RiverDbManager } from '../riverDbManager'; import { makeStreamRpcClient } from '../makeStreamRpcClient'; import assert from 'assert'; import _ from 'lodash'; import { MockEntitlementsDelegate } from '../utils'; import { makeSignerContext } from '../signerContext'; import { LocalhostWeb3Provider, createExternalNFTStruct, createRiverRegistry, createSpaceDapp, Permission, isLegacyMembershipType, ETH_ADDRESS, NoopRuleData, CheckOperationType, LogicalOperationType, OperationType, treeToRuleData, TestERC20, TestERC1155, TestCrossChainEntitlement, isCreateLegacySpaceParams, convertRuleDataV1ToV2, encodeRuleDataV2, decodeRuleDataV2, isRuleDataV1, encodeThresholdParams, encodeERC1155Params, convertRuleDataV2ToV1, getFixedPricingModule, getDynamicPricingModule, } from '@river-build/web3'; import { RiverTimelineEvent, } from '../sync-agent/timeline/models/timeline-types'; import { SyncState } from '../syncedStreamsLoop'; import { isDefined } from '../check'; const log = dlog('csb:test:util'); const initTestUrls = async () => { const config = makeRiverChainConfig(); const provider = new LocalhostWeb3Provider(config.rpcUrl); const riverRegistry = createRiverRegistry(provider, config.chainConfig); const urls = await riverRegistry.getOperationalNodeUrls(); const refreshNodeUrl = () => riverRegistry.getOperationalNodeUrls(); log('initTestUrls, RIVER_TEST_CONNECT=', config, 'testUrls=', urls); return { testUrls: urls.split(','), refreshNodeUrl }; }; let curTestUrl = -1; const getNextTestUrl = async () => { const { testUrls, refreshNodeUrl } = await initTestUrls(); if (testUrls.length === 1) { log('getNextTestUrl, url=', testUrls[0]); return { urls: testUrls[0], refreshNodeUrl }; } else if (testUrls.length > 1) { if (curTestUrl < 0) { const seed = expect.getState()?.currentTestName; if (seed === undefined) { curTestUrl = Math.floor(Math.random() * testUrls.length); log('getNextTestUrl, setting to random, index=', curTestUrl); } else { curTestUrl = seed .split('') .map((v) => v.charCodeAt(0)) .reduce((a, v) => ((a + ((a << 7) + (a << 3))) ^ v) & 0xffff) % testUrls.length; log('getNextTestUrl, setting based on test name=', seed, ' index=', curTestUrl); } } curTestUrl = (curTestUrl + 1) % testUrls.length; log('getNextTestUrl, url=', testUrls[curTestUrl], 'index=', curTestUrl); return { urls: testUrls[curTestUrl], refreshNodeUrl }; } else { throw new Error('no test urls'); } }; export const makeTestRpcClient = async (opts) => { const { urls: url, refreshNodeUrl } = await getNextTestUrl(); return makeStreamRpcClient(url, refreshNodeUrl, opts); }; export const makeEvent_test = async (context, payload, prevMiniblockHash) => { return _impl_makeEvent_impl_(context, payload, prevMiniblockHash); }; export const TEST_ENCRYPTED_MESSAGE_PROPS = { sessionId: '', sessionIdBytes: new Uint8Array(0), ciphertext: '', algorithm: '', senderKey: '', ciphertextBytes: new Uint8Array(0), ivBytes: new Uint8Array(0), version: EncryptedDataVersion.ENCRYPTED_DATA_VERSION_1, }; export const getXchainConfigForTesting = () => { // TODO: generate this for test environment and read from it return { supportedRpcUrls: { 31337: 'http://127.0.0.1:8545', 31338: 'http://127.0.0.1:8546', }, etherBasedChains: [31337, 31338], }; }; export async function erc1155CheckOp(contractName, tokenId, threshold) { const contractAddress = await TestERC1155.getContractAddress(contractName); return { opType: OperationType.CHECK, checkType: CheckOperationType.ERC1155, chainId: 31337n, contractAddress, params: encodeERC1155Params({ threshold, tokenId }), }; } export async function erc20CheckOp(contractName, threshold) { const contractAddress = await TestERC20.getContractAddress(contractName); return { opType: OperationType.CHECK, checkType: CheckOperationType.ERC20, chainId: 31337n, contractAddress, params: encodeThresholdParams({ threshold }), }; } export async function mockCrossChainCheckOp(contractName, id) { const contractAddress = await TestCrossChainEntitlement.getContractAddress(contractName); return { opType: OperationType.CHECK, checkType: CheckOperationType.ISENTITLED, chainId: 31337n, contractAddress, params: TestCrossChainEntitlement.encodeIdParameter(id), }; } export const twoEth = BigInt(2e18); export const oneEth = BigInt(1e18); export const threeEth = BigInt(3e18); export const oneHalfEth = BigInt(5e17); export function ethBalanceCheckOp(threshold) { return { opType: OperationType.CHECK, checkType: CheckOperationType.ETH_BALANCE, chainId: 31337n, contractAddress: ethers.constants.AddressZero, params: encodeThresholdParams({ threshold }), }; } /** * makeUniqueSpaceStreamId - space stream ids are derived from the contract * in tests without entitlements there are no contracts, so we use a random id */ export const makeUniqueSpaceStreamId = () => { return makeSpaceStreamId(genId(40)); }; /** * * @returns a random user context * Done using a worker thread to avoid blocking the main thread */ export const makeRandomUserContext = async () => { const wallet = ethers.Wallet.createRandom(); log('makeRandomUserContext', wallet.address); return await makeUserContextFromWallet(wallet); }; export const makeRandomUserAddress = () => { return publicKeyToAddress(getPublicKey(utils.randomPrivateKey(), false)); }; export const makeUserContextFromWallet = async (wallet) => { const userPrimaryWallet = wallet; const delegateWallet = ethers.Wallet.createRandom(); const creatorAddress = publicKeyToAddress(bin_fromHexString(userPrimaryWallet.publicKey)); log('makeRandomUserContext', userIdFromAddress(creatorAddress)); return { ...(await makeSignerContext(userPrimaryWallet, delegateWallet, { days: 1 })), wallet }; }; export const cloneTestClient = async (client) => { return makeTestClient({ ...client.opts, context: { ...client.signerContext, wallet: client.wallet, }, deviceId: client.deviceId, }); }; export const makeTestClient = async (opts) => { const context = opts?.context ?? (await makeRandomUserContext()); const entitlementsDelegate = opts?.entitlementsDelegate ?? new MockEntitlementsDelegate(); const deviceId = opts?.deviceId ? `-${opts.deviceId}` : `-${genId(5)}`; const userId = userIdFromAddress(context.creatorAddress); const dbName = `database-${userId}${deviceId}`; const persistenceDbName = `persistence-${userId}${deviceId}`; // create a new client with store(s) const cryptoStore = RiverDbManager.getCryptoDb(userId, dbName); const rpcClient = await makeTestRpcClient(); const client = new Client(context, rpcClient, cryptoStore, entitlementsDelegate, { ...opts, persistenceStoreName: persistenceDbName, }); client.wallet = context.wallet; client.deviceId = deviceId; return client; }; export async function setupWalletsAndContexts() { const baseConfig = makeBaseChainConfig(); const [alicesWallet, bobsWallet, carolsWallet] = await Promise.all([ ethers.Wallet.createRandom(), ethers.Wallet.createRandom(), ethers.Wallet.createRandom(), ]); const [alicesContext, bobsContext, carolsContext] = await Promise.all([ makeUserContextFromWallet(alicesWallet), makeUserContextFromWallet(bobsWallet), makeUserContextFromWallet(carolsWallet), ]); const aliceProvider = new LocalhostWeb3Provider(baseConfig.rpcUrl, alicesWallet); const bobProvider = new LocalhostWeb3Provider(baseConfig.rpcUrl, bobsWallet); const carolProvider = new LocalhostWeb3Provider(baseConfig.rpcUrl, carolsWallet); await Promise.all([ aliceProvider.fundWallet(), bobProvider.fundWallet(), carolProvider.fundWallet(), ]); const bobSpaceDapp = createSpaceDapp(bobProvider, baseConfig.chainConfig); const aliceSpaceDapp = createSpaceDapp(aliceProvider, baseConfig.chainConfig); const carolSpaceDapp = createSpaceDapp(carolProvider, baseConfig.chainConfig); // create a user const riverConfig = makeRiverConfig(); const [alice, bob, carol] = await Promise.all([ makeTestClient({ context: alicesContext, deviceId: 'alice', entitlementsDelegate: new Entitlements(riverConfig, aliceSpaceDapp), }), makeTestClient({ context: bobsContext, entitlementsDelegate: new Entitlements(riverConfig, bobSpaceDapp), }), makeTestClient({ context: carolsContext, entitlementsDelegate: new Entitlements(riverConfig, carolSpaceDapp), }), ]); return { alice, bob, carol, alicesWallet, bobsWallet, carolsWallet, alicesContext, bobsContext, carolsContext, aliceProvider, bobProvider, carolProvider, aliceSpaceDapp, bobSpaceDapp, carolSpaceDapp, }; } class DonePromise { promise; // @ts-ignore: Promise body is executed immediately, so vars are assigned before constructor returns resolve; // @ts-ignore: Promise body is executed immediately, so vars are assigned before constructor returns reject; constructor() { this.promise = new Promise((resolve, reject) => { this.resolve = resolve; this.reject = reject; }); } done() { this.resolve('done'); } async wait() { return this.promise; } async expectToSucceed() { await expect(this.promise).resolves.toBe('done'); } async expectToFail() { await expect(this.promise).rejects.toThrow(); } run(fn) { try { fn(); } catch (err) { this.reject(err); } } runAndDone(fn) { try { fn(); this.done(); } catch (err) { this.reject(err); } } } export const makeDonePromise = () => { return new DonePromise(); }; export const sendFlush = async (client) => { const r = await client.info({ debug: ['flush_cache'] }); assert(r.graffiti === 'cache flushed'); }; export async function* iterableWrapper(iterable) { const iterator = iterable[Symbol.asyncIterator](); while (true) { const result = await iterator.next(); if (typeof result === 'string') { return; } yield result.value; } } // For example, use like this: // // joinPayload = lastEventFiltered( // unpackStreamEnvelopes(userResponse.stream!), // getUserPayload_Membership, // ) // // to get user memebrship payload from a last event containing it, or undefined if not found. export const lastEventFiltered = (events, f) => { let ret = undefined; _.forEachRight(events, (v) => { const r = f(v); if (r !== undefined) { ret = r; return false; } return true; }); return ret; }; // createSpaceAndDefaultChannel creates a space and default channel for a given // client, on the spaceDapp and the stream node. It creates a user stream, joins // the user to the space, and starts syncing the client. export async function createSpaceAndDefaultChannel(client, spaceDapp, wallet, name, membership) { const transaction = await createVersionedSpaceFromMembership(client, spaceDapp, wallet, name, membership); const receipt = await transaction.wait(); expect(receipt.status).toEqual(1); const spaceAddress = spaceDapp.getSpaceAddress(receipt, wallet.address); expect(spaceAddress).toBeDefined(); const spaceId = makeSpaceStreamId(spaceAddress); const channelId = makeDefaultChannelStreamId(spaceAddress); await client.initializeUser({ spaceId }); client.startSync(); const userStreamId = makeUserStreamId(client.userId); const userStreamView = client.stream(userStreamId).view; expect(userStreamView).toBeDefined(); const returnVal = await client.createSpace(spaceId); expect(returnVal.streamId).toEqual(spaceId); expect(userStreamView.userContent.isMember(spaceId, MembershipOp.SO_JOIN)).toBe(true); const channelReturnVal = await client.createChannel(spaceId, 'general', `${name} general channel properties`, channelId); expect(channelReturnVal.streamId).toEqual(channelId); expect(userStreamView.userContent.isMember(channelId, MembershipOp.SO_JOIN)).toBe(true); return { spaceId, defaultChannelId: channelId, userStreamView, }; } export const DefaultFreeAllocation = 1000; export async function createVersionedSpaceFromMembership(client, spaceDapp, wallet, name, membership) { if (useLegacySpaces()) { if (isLegacyMembershipType(membership)) { return await spaceDapp.createLegacySpace({ spaceName: `${name}-space`, uri: `${name}-space-metadata`, channelName: 'general', membership, }, wallet); } else { // Convert space params to legacy space params const legacyMembership = { settings: membership.settings, permissions: membership.permissions, requirements: { everyone: membership.requirements.everyone, users: membership.requirements.users, syncEntitlements: membership.requirements.syncEntitlements, ruleData: convertRuleDataV2ToV1(decodeRuleDataV2(membership.requirements.ruleData)), }, }; return await spaceDapp.createLegacySpace({ spaceName: `${name}-space`, uri: `${name}-space-metadata`, channelName: 'general', membership: legacyMembership, }, wallet); } } else { if (isLegacyMembershipType(membership)) { // Convert legacy space params to current space params membership = { settings: membership.settings, permissions: membership.permissions, requirements: { everyone: membership.requirements.everyone, users: [], syncEntitlements: false, ruleData: encodeRuleDataV2(convertRuleDataV1ToV2(membership.requirements.ruleData)), }, }; } return await spaceDapp.createSpace({ spaceName: `${name}-space`, uri: `${name}-space-metadata`, channelName: 'general', membership, }, wallet); } } // createVersionedSpace accepts either legacy or current space creation parameters and will // fall backto the legacy space creation endpoint on the spaceDapp if the appropriate flag is set. // If a user does not pass in a legacy space creation parameter, the function will not use // the legacy space creation endpoint, because the updated parameters are not backwards // compatible - we don't attempt conversion here. export async function createVersionedSpace(spaceDapp, createSpaceParams, signer) { if (useLegacySpaces() && isCreateLegacySpaceParams(createSpaceParams)) { return await spaceDapp.createLegacySpace(createSpaceParams, signer); } else { if (isCreateLegacySpaceParams(createSpaceParams)) { // Convert legacy space params to current space params createSpaceParams = { spaceName: createSpaceParams.spaceName, uri: createSpaceParams.uri, channelName: createSpaceParams.channelName, membership: { settings: createSpaceParams.membership.settings, permissions: createSpaceParams.membership.permissions, requirements: { everyone: createSpaceParams.membership.requirements.everyone, users: [], syncEntitlements: false, ruleData: encodeRuleDataV2(convertRuleDataV1ToV2(createSpaceParams.membership.requirements .ruleData)), }, }, }; } return await spaceDapp.createSpace(createSpaceParams, signer); } } // createUserStreamAndSyncClient creates a user stream for a given client that // uses a newly created space as the hint for the user stream, since the stream // node will not allow the creation of a user stream without a space id. // // If the membership info is a legacy membership struct and the legacy space flag // is set, the function will create a legacy space. Otherwise, it will convert the // legacy membership struct to a current membership struct if needed and use the // latest space creation endpoint. export async function createUserStreamAndSyncClient(client, spaceDapp, name, membershipInfo, wallet) { let createSpaceParams; if (isLegacyMembershipType(membershipInfo)) { createSpaceParams = { spaceName: `${name}-space`, uri: `${name}-space-metadata`, channelName: 'general', membership: membershipInfo, }; } else { createSpaceParams = { spaceName: `${name}-space`, uri: `${name}-space-metadata`, channelName: 'general', membership: membershipInfo, }; } const transaction = await createVersionedSpace(spaceDapp, createSpaceParams, wallet); const receipt = await transaction.wait(); expect(receipt.status).toEqual(1); const spaceAddress = spaceDapp.getSpaceAddress(receipt, wallet.address); expect(spaceAddress).toBeDefined(); const spaceId = makeSpaceStreamId(spaceAddress); await client.initializeUser({ spaceId }); } export async function expectUserCanJoin(spaceId, channelId, name, client, spaceDapp, address, wallet) { const joinStart = Date.now(); // Check that the local evaluation of the user's entitlements for joining the space // passes. const entitledWallet = await spaceDapp.getEntitledWalletForJoiningSpace(spaceId, address, getXchainConfigForTesting()); expect(entitledWallet).toBeDefined(); const { issued } = await spaceDapp.joinSpace(spaceId, address, wallet); expect(issued).toBe(true); log(`${name} joined space ${spaceId}`, Date.now() - joinStart); await client.initializeUser({ spaceId }); client.startSync(); await waitFor(() => expect(client.streams.syncState).toBe(SyncState.Syncing)); await expect(client.joinStream(spaceId)).resolves.not.toThrow(); await expect(client.joinStream(channelId)).resolves.not.toThrow(); const userStreamView = client.stream(client.userStreamId).view; await waitFor(() => { expect(userStreamView.userContent.isMember(spaceId, MembershipOp.SO_JOIN)).toBe(true); expect(userStreamView.userContent.isMember(channelId, MembershipOp.SO_JOIN)).toBe(true); }); } export async function everyoneMembershipStruct(spaceDapp, client) { const { fixedPricingModuleAddress, freeAllocation, price } = await getFreeSpacePricingSetup(spaceDapp); return { settings: { name: 'Everyone', symbol: 'MEMBER', price, maxSupply: 1000, duration: 0, currency: ETH_ADDRESS, feeRecipient: client.userId, freeAllocation, pricingModule: fixedPricingModuleAddress, }, permissions: [Permission.Read, Permission.Write], requirements: { everyone: true, users: [], ruleData: NoopRuleData, syncEntitlements: false, }, }; } // should start charging after the first member joins export async function zeroPriceWithLimitedAllocationMembershipStruct(spaceDapp, client, opts) { const { fixedPricingModuleAddress, price } = await getFreeSpacePricingSetup(spaceDapp); const { freeAllocation } = opts; const settings = { settings: { name: 'Everyone', symbol: 'MEMBER', price, maxSupply: 1000, duration: 0, currency: ETH_ADDRESS, feeRecipient: client.userId, freeAllocation, pricingModule: fixedPricingModuleAddress, }, permissions: [Permission.Read, Permission.Write], requirements: { everyone: true, users: [], ruleData: NoopRuleData, syncEntitlements: false, }, }; return settings; } // should start charing for the first member export async function dynamicMembershipStruct(spaceDapp, client) { const dynamicPricingModule = await getDynamicPricingModule(spaceDapp); expect(dynamicPricingModule).toBeDefined(); return { settings: { name: 'Everyone', symbol: 'MEMBER', price: 0, maxSupply: 1000, duration: 0, currency: ETH_ADDRESS, feeRecipient: client.userId, freeAllocation: 0, pricingModule: await dynamicPricingModule.module, }, permissions: [Permission.Read, Permission.Write], requirements: { everyone: true, users: [], ruleData: NoopRuleData, syncEntitlements: false, }, }; } // should start charging after the first member joins export async function fixedPriceMembershipStruct(spaceDapp, client, opts = { price: 1 }) { const fixedPricingModule = await getFixedPricingModule(spaceDapp); expect(fixedPricingModule).toBeDefined(); const { price } = opts; const settings = { settings: { name: 'Everyone', symbol: 'MEMBER', price: ethers.utils.parseEther(price.toString()), maxSupply: 1000, duration: 0, currency: ETH_ADDRESS, feeRecipient: client.userId, freeAllocation: 0, pricingModule: fixedPricingModule.module, }, permissions: [Permission.Read, Permission.Write], requirements: { everyone: true, users: [], ruleData: NoopRuleData, syncEntitlements: false, }, }; return settings; } export async function getFreeSpacePricingSetup(spaceDapp) { const fixedPricingModule = await getFixedPricingModule(spaceDapp); expect(fixedPricingModule).toBeDefined(); return { price: 0, fixedPricingModuleAddress: await fixedPricingModule.module, freeAllocation: DefaultFreeAllocation, }; } export function twoNftRuleData(nft1Address, nft2Address, logOpType = LogicalOperationType.AND) { const leftOperation = { opType: OperationType.CHECK, checkType: CheckOperationType.ERC721, chainId: 31337n, contractAddress: nft1Address, params: encodeThresholdParams({ threshold: 1n }), }; const rightOperation = { opType: OperationType.CHECK, checkType: CheckOperationType.ERC721, chainId: 31337n, contractAddress: nft2Address, params: encodeThresholdParams({ threshold: 1n }), }; const root = { opType: OperationType.LOGICAL, logicalType: logOpType, leftOperation, rightOperation, }; return treeToRuleData(root); } export async function unlinkCaller(rootSpaceDapp, rootWallet, caller) { const walletLink = rootSpaceDapp.getWalletLink(); let txn; try { txn = await walletLink.removeCallerLink(caller); } catch (err) { const parsedError = walletLink.parseError(err); log('linkWallets error', parsedError); } expect(txn).toBeDefined(); const receipt = await txn?.wait(); expect(receipt.status).toEqual(1); const linkedWallets = await walletLink.getLinkedWallets(rootWallet.address); expect(linkedWallets).not.toContain(caller.address); } export async function unlinkWallet(rootSpaceDapp, rootWallet, linkedWallet) { const walletLink = rootSpaceDapp.getWalletLink(); let txn; try { txn = await walletLink.removeLink(rootWallet, linkedWallet.address); } catch (err) { const parsedError = walletLink.parseError(err); log('linkWallets error', parsedError); } expect(txn).toBeDefined(); const receipt = await txn?.wait(); expect(receipt.status).toEqual(1); const linkedWallets = await walletLink.getLinkedWallets(rootWallet.address); expect(linkedWallets).not.toContain(linkedWallet.address); } // Hint: pass in the wallets attached to the providers. export async function linkWallets(rootSpaceDapp, rootWallet, linkedWallet) { const walletLink = rootSpaceDapp.getWalletLink(); let txn; try { txn = await walletLink.linkWalletToRootKey(rootWallet, linkedWallet); } catch (err) { const parsedError = walletLink.parseError(err); log('linkWallets error', parsedError); } expect(txn).toBeDefined(); const receipt = await txn?.wait(); expect(receipt.status).toEqual(1); const linkedWallets = await walletLink.getLinkedWallets(rootWallet.address); expect(linkedWallets).toContain(linkedWallet.address); } export function waitFor(callback, options = { timeoutMS: 5000 }) { const timeoutContext = new Error('waitFor timed out after ' + options.timeoutMS.toString() + 'ms'); return new Promise((resolve, reject) => { const timeoutMS = options.timeoutMS; const pollIntervalMS = Math.min(timeoutMS / 2, 100); let lastError = undefined; let promiseStatus = 'none'; const intervalId = setInterval(checkCallback, pollIntervalMS); const timeoutId = setInterval(onTimeout, timeoutMS); function onDone(result) { clearInterval(intervalId); clearInterval(timeoutId); if (result) { resolve(result); } else if (result === undefined && promiseStatus === 'resolved') { resolve(undefined); } else { reject(lastError); } } function onTimeout() { lastError = lastError ?? timeoutContext; onDone(); } function checkCallback() { if (promiseStatus === 'pending') return; try { const result = callback(); if (result && result instanceof Promise) { promiseStatus = 'pending'; result.then((res) => { promiseStatus = 'resolved'; onDone(res); }, (err) => { promiseStatus = 'rejected'; // splat the error to get a stack trace, i don't know why this works lastError = { ...err, }; }); } else { promiseStatus = 'resolved'; if (result) { // if result is not truthy, resolve resolve(result); } // otherwise let the polling continue } } catch (err) { lastError = err; } } }); } export async function waitForSyncStreams(syncStreams, matcher) { for await (const res of iterableWrapper(syncStreams)) { if (await matcher(res)) { return res; } } throw new Error('waitFor: timeout'); } export async function waitForSyncStreamsMessage(syncStreams, message) { return waitForSyncStreams(syncStreams, async (res) => { if (res.syncOp === SyncOp.SYNC_UPDATE) { const stream = res.stream; if (stream) { const env = await unpackStreamEnvelopes(stream, undefined); for (const e of env) { if (e.event.payload.case === 'channelPayload') { const p = e.event.payload.value.content; if (p.case === 'message' && p.value.ciphertext === message) { return true; } } } } } return false; }); } export function getChannelMessagePayload(event) { if (event?.payload?.case === 'post') { if (event.payload.value.content.case === 'text') { return event.payload.value.content.value?.body; } } return undefined; } export function createEventDecryptedPromise(client, expectedMessageText) { const recipientReceivesMessageWithoutError = makeDonePromise(); client.on('eventDecrypted', (streamId, contentKind, event) => { recipientReceivesMessageWithoutError.runAndDone(() => { const content = event.decryptedContent; expect(content).toBeDefined(); check(content.kind === 'channelMessage'); expect(getChannelMessagePayload(content?.content)).toEqual(expectedMessageText); }); }); return recipientReceivesMessageWithoutError.promise; } export function isValidEthAddress(address) { const ethAddressRegex = /^(0x)?[0-9a-fA-F]{40}$/; return ethAddressRegex.test(address); } export function getNftRuleData(testNftAddress) { return createExternalNFTStruct([testNftAddress]); } // createRole creates a role on the spaceDapp with the given parameters, using the legacy endpoint // if the USE_LEGACY_SPACES environment variable is set and converting the ruleData into the correct // format as necessary. Be aware, though, that the legacy endpoint does not support erc1155 checks. export async function createRole(spaceDapp, provider, spaceId, roleName, permissions, users, ruleData, signer) { let txn = undefined; let error = undefined; if (useLegacySpaces()) { try { if (!isRuleDataV1(ruleData)) { ruleData = convertRuleDataV2ToV1(ruleData); } txn = await spaceDapp.legacyCreateRole(spaceId, roleName, permissions, users, ruleData, signer); } catch (err) { error = spaceDapp.parseSpaceError(spaceId, err); return { roleId: undefined, error }; } } else { if (isRuleDataV1(ruleData)) { ruleData = convertRuleDataV1ToV2(ruleData); } try { txn = await spaceDapp.createRole(spaceId, roleName, permissions, users, ruleData, signer); } catch (err) { error = spaceDapp.parseSpaceError(spaceId, err); return { roleId: undefined, error }; } } const { roleId, error: roleError } = await spaceDapp.waitForRoleCreated(spaceId, txn); return { roleId, error: roleError }; } export async function updateRole(spaceDapp, provider, params, signer) { let txn = undefined; let error = undefined; if (useLegacySpaces()) { throw new Error('updateRole is v2 only'); } try { txn = await spaceDapp.updateRole(params, signer); } catch (err) { error = spaceDapp.parseSpaceError(params.spaceNetworkId, err); return { error }; } const receipt = await provider.waitForTransaction(txn.hash); if (receipt.status === 0) { return { error: new Error('Transaction failed') }; } return { error: undefined }; } export async function createChannel(spaceDapp, provider, spaceId, channelName, roleIds, signer) { let txn = undefined; let error = undefined; const channelId = makeUniqueChannelStreamId(spaceId); try { txn = await spaceDapp.createChannel(spaceId, channelName, '', channelId, roleIds, signer); } catch (err) { error = spaceDapp.parseSpaceError(spaceId, err); return { channelId: undefined, error }; } const receipt = await provider.waitForTransaction(txn.hash); if (receipt.status === 0) { return { channelId: undefined, error: new Error('Transaction failed') }; } return { channelId, error: undefined }; } // Type guard function based on field checks export function isEncryptedData(obj) { if (typeof obj !== 'object' || obj === null) { return false; } const data = obj; return (typeof data.ciphertext === 'string' && typeof data.algorithm === 'string' && typeof data.senderKey === 'string' && typeof data.sessionId === 'string' && (typeof data.checksum === 'string' || data.checksum === undefined) && (typeof data.refEventId === 'string' || data.refEventId === undefined)); } // Users need to be mapped from 'alice', 'bob', etc to their wallet addresses, // because the wallets are created within this helper method. export async function createTownWithRequirements(requirements) { const { alice, bob, carol, aliceSpaceDapp, bobSpaceDapp, carolSpaceDapp, aliceProvider, bobProvider, carolProvider, alicesWallet, bobsWallet, carolsWallet, } = await setupWalletsAndContexts(); const { fixedPricingModuleAddress, freeAllocation, price } = await getFreeSpacePricingSetup(bobSpaceDapp); const userNameToWallet = { alice: alicesWallet.address, bob: bobsWallet.address, carol: carolsWallet.address, }; requirements.users = requirements.users.map((user) => userNameToWallet[user]); const membershipInfo = { settings: { name: 'Everyone', symbol: 'MEMBER', price, maxSupply: 1000, duration: 0, currency: ETH_ADDRESS, feeRecipient: bob.userId, freeAllocation, pricingModule: fixedPricingModuleAddress, }, permissions: [Permission.Read, Permission.Write], requirements: { everyone: requirements.everyone, users: requirements.users, ruleData: encodeRuleDataV2(requirements.ruleData), syncEntitlements: false, }, }; // This helper method validates that the owner can join the space and default channel. const { spaceId, defaultChannelId: channelId, userStreamView: bobUserStreamView, } = await createSpaceAndDefaultChannel(bob, bobSpaceDapp, bobProvider.wallet, 'bobs', membershipInfo); // Validate that owner passes entitlement check const entitledWallet = await bobSpaceDapp.getEntitledWalletForJoiningSpace(spaceId, bobsWallet.address, getXchainConfigForTesting()); expect(entitledWallet).toBeDefined(); return { alice, bob, carol, aliceSpaceDapp, bobSpaceDapp, carolSpaceDapp, aliceProvider, bobProvider, carolProvider, alicesWallet, bobsWallet, carolsWallet, spaceId, channelId, bobUserStreamView, }; } export async function expectUserCannotJoinSpace(spaceId, client, spaceDapp, address) { // Check that the local evaluation of the user's entitlements for joining the space // fails. const entitledWallet = await spaceDapp.getEntitledWalletForJoiningSpace(spaceId, address, getXchainConfigForTesting()); expect(entitledWallet).toBeUndefined(); await expect(client.joinStream(spaceId)).rejects.toThrow(/PERMISSION_DENIED/); } // pass in users as 'alice', 'bob', 'carol' - b/c their wallets are created here export async function setupChannelWithCustomRole(userNames, ruleData, permissions = [Permission.Read]) { const { alice, bob, carol, alicesWallet, bobsWallet, carolsWallet, aliceProvider, bobProvider, carolProvider, aliceSpaceDapp, bobSpaceDapp, carolSpaceDapp, } = await setupWalletsAndContexts(); const userNameToWallet = { alice: alicesWallet.address, bob: bobsWallet.address, carol: carolsWallet.address, }; const users = userNames.map((user) => userNameToWallet[user]); const { spaceId, defaultChannelId } = await createSpaceAndDefaultChannel(bob, bobSpaceDapp, bobProvider.wallet, 'bob', await everyoneMembershipStruct(bobSpaceDapp, bob)); const { roleId, error: roleError } = await createRole(bobSpaceDapp, bobProvider, spaceId, 'gated role', permissions, users, ruleData, bobProvider.wallet); expect(roleError).toBeUndefined(); log('roleId', roleId); // Create a channel gated by the above role in the space contract. const { channelId, error: channelError } = await createChannel(bobSpaceDapp, bobProvider, spaceId, 'custom-role-gated-channel', [roleId.valueOf()], bobProvider.wallet); expect(channelError).toBeUndefined(); log('channelId', channelId); // Then, establish a stream for the channel on the river node. const { streamId: channelStreamId } = await bob.createChannel(spaceId, 'nft-gated-channel', 'talk about nfts here', channelId); expect(channelStreamId).toEqual(channelId); // As the space owner, Bob should always be able to join the channel regardless of the custom role. await expect(bob.joinStream(channelId)).resolves.not.toThrow(); // Join alice to the town so she can attempt to join the role-gated channel. // Alice should have no issue joining the space and default channel for an "everyone" town. await expectUserCanJoin(spaceId, defaultChannelId, 'alice', alice, aliceSpaceDapp, alicesWallet.address, aliceProvider.wallet); // Add carol to the space also so she can attempt to join role-gated channels. await expectUserCanJoin(spaceId, defaultChannelId, 'carol', carol, carolSpaceDapp, carolsWallet.address, carolProvider.wallet); return { alice, bob, carol, alicesWallet, bobsWallet, carolsWallet, aliceProvider, bobProvider, carolProvider, aliceSpaceDapp, bobSpaceDapp, carolSpaceDapp, spaceId, defaultChannelId, channelId, roleId, }; } export async function expectUserCanJoinChannel(client, spaceDapp, spaceId, channelId) { // Space dapp should evaluate the user as entitled to the channel await expect(spaceDapp.isEntitledToChannel(spaceId, channelId, client.userId, Permission.Read, getXchainConfigForTesting())).resolves.toBeTruthy(); // Stream node should allow the join await expect(client.joinStream(channelId)).resolves.not.toThrow(); const userStreamView = (await client.waitForStream(makeUserStreamId(client.userId))).view; // Wait for alice's user stream to have the join await waitFor(() => userStreamView.userContent.isMember(channelId, MembershipOp.SO_JOIN)); } export async function expectUserCannotJoinChannel(client, spaceDapp, spaceId, channelId) { // Space dapp should evaluate the user as not entitled to the channel await expect(spaceDapp.isEntitledToChannel(spaceId, channelId, client.userId, Permission.Read, getXchainConfigForTesting())).resolves.toBeFalsy(); // Stream node should not allow the join await expect(client.joinStream(channelId)).rejects.toThrow(/7:PERMISSION_DENIED/); } export const findMessageByText = (events, text) => { return events.find((event) => event.content?.kind === RiverTimelineEvent.ChannelMessage && event.content.body === text); }; export function extractBlockchainTransactionTransferEvents(timeline) { return timeline .map((e) => { if (e.remoteEvent?.event.payload.case === 'userPayload' && e.remoteEvent?.event.payload.value.content.case === 'blockchainTransaction' && e.remoteEvent?.event.payload.value.content.value.content.case === 'tokenTransfer') { return e.remoteEvent?.event.payload.value.content.value.content.value; } return undefined; }) .filter(isDefined); } export function extractMemberBlockchainTransactions(client, channelId) { const stream = client.streams.get(channelId); if (!stream) throw new Error('no stream found'); return stream.view.timeline .map((e) => { if (e.remoteEvent?.event.payload.case === 'memberPayload' && e.remoteEvent?.event.payload.value.content.case === 'memberBlockchainTransaction' && e.remoteEvent.event.payload.value.content.value.transaction?.content.case === 'tokenTransfer') { return e.remoteEvent.event.payload.value.content.value.transaction.content.value; } return undefined; }) .filter(isDefined); } //# sourceMappingURL=testUtils.js.map