@river-build/sdk
Version:
For more details, visit the following resources:
1,025 lines • 42.2 kB
JavaScript
/* 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