UNPKG

@river-build/sdk

Version:

For more details, visit the following resources:

559 lines 25.9 kB
/** * @group main */ import { makeEvent, makeEvents, unpackStreamEnvelopes } from '../../sign'; import { MembershipOp, SyncOp } from '@river-build/proto'; import { bin_equal, dlog } from '@river-build/dlog'; import { makeEvent_test, makeRandomUserContext, makeUserContextFromWallet, makeTestRpcClient, makeUniqueSpaceStreamId, iterableWrapper, TEST_ENCRYPTED_MESSAGE_PROPS, waitForSyncStreams, } from '../testUtils'; import { addressFromUserId, makeUniqueChannelStreamId, makeUserStreamId, streamIdAsString, streamIdToBytes, userIdFromAddress, } from '../../id'; import { make_ChannelPayload_Inception, make_ChannelPayload_Message, make_MemberPayload_Membership2, make_SpacePayload_Inception, make_UserPayload_Inception, make_UserPayload_UserMembership, make_UserPayload_UserMembershipAction, } from '../../types'; import { bobTalksToHimself } from '../bob_testUtils'; import { ethers } from 'ethers'; import { makeSignerContext } from '../../signerContext'; const log = dlog('csb:test:streamRpcClient'); describe('streamRpcClient', () => { let bobsContext; let alicesContext; beforeEach(async () => { bobsContext = await makeRandomUserContext(); alicesContext = await makeRandomUserContext(); }); test('makeStreamRpcClient', async () => { const client = await makeTestRpcClient(); log('makeStreamRpcClient', 'url', client.url); expect(client).toBeDefined(); const result = await client.info({ debug: ['graffiti'] }); expect(result).toBeDefined(); expect(result.graffiti).toEqual('River Node welcomes you!'); }); test('ping', async () => { // ping is a debug endpoint, so it's not available in production const client = await makeTestRpcClient(); log('ping', 'url', client.url); expect(client).toBeDefined(); const result = await client.info({ debug: ['ping'] }); expect(result).toBeDefined(); expect(result.graffiti).toEqual('pong'); }); test('error', async () => { const client = await makeTestRpcClient(); expect(client).toBeDefined(); let err = undefined; try { await client.info({ debug: ['error'] }); } catch (e) { expect(e).toBeInstanceOf(Error); err = e; } log('error', err); expect(err).toBeDefined(); log('error', err.toString()); expect(err.toString()).toContain('Error requested through Info request'); }); test('error_untyped', async () => { const client = await makeTestRpcClient(); expect(client).toBeDefined(); let err = undefined; try { await client.info({ debug: ['error_untyped'] }); } catch (e) { expect(e).toBeInstanceOf(Error); err = e; } log('error_untyped', err); expect(err).toBeDefined(); log('error_untyped', err.toString()); expect(err.toString()).toContain('[unknown] error requested through Info request'); }); test('charlieUsesRegularOldWallet', async () => { const wallet = ethers.Wallet.createRandom(); const charliesContext = await makeUserContextFromWallet(wallet); const charlie = await makeTestRpcClient(); const userId = userIdFromAddress(charliesContext.creatorAddress); const streamIdStr = makeUserStreamId(userId); const streamId = streamIdToBytes(streamIdStr); await charlie.createStream({ events: [ await makeEvent(charliesContext, make_UserPayload_Inception({ streamId: streamId, })), ], streamId: streamId, }); }); test('bobSendsMismatchedPayloadCase', async () => { log('bobSendsMismatchedPayloadCase', 'start'); const bob = await makeTestRpcClient(); const bobsUserId = userIdFromAddress(bobsContext.creatorAddress); const bobsUserStreamIdStr = makeUserStreamId(bobsUserId); const bobsUserStreamId = streamIdToBytes(bobsUserStreamIdStr); const inceptionEvent = await makeEvent(bobsContext, make_UserPayload_Inception({ streamId: bobsUserStreamId, })); await bob.createStream({ events: [inceptionEvent], streamId: bobsUserStreamId, }); const userStream = await bob.getStream({ streamId: bobsUserStreamId }); expect(userStream).toBeDefined(); expect(bin_equal(userStream.stream?.nextSyncCookie?.streamId, bobsUserStreamId)).toBeTruthy(); // try to send a channel message const event = await makeEvent(bobsContext, make_ChannelPayload_Message({ ...TEST_ENCRYPTED_MESSAGE_PROPS, ciphertext: 'hello', }), userStream.stream?.miniblocks.at(-1)?.header?.hash); const promise = bob.addEvent({ streamId: bobsUserStreamId, event, }); await expect(promise).rejects.toThrow('inception type mismatch: *protocol.StreamEvent_ChannelPayload::*protocol.ChannelPayload_Message vs *protocol.UserPayload_Inception'); log('bobSendsMismatchedPayloadCase', 'done'); }); test.each([ ['bobTalksToHimself-noflush-nopresync', false], ['bobTalksToHimself-noflush-presync', true], ])('%s', async (name, presync) => { await bobTalksToHimself(log.extend(name), bobsContext, false, presync); }); test('aliceTalksToBob', async () => { log('bobAndAliceConverse start'); const bob = await makeTestRpcClient(); const bobsUserId = userIdFromAddress(bobsContext.creatorAddress); const bobsUserStreamIdStr = makeUserStreamId(bobsUserId); const bobsUserStreamId = streamIdToBytes(bobsUserStreamIdStr); const alice = await makeTestRpcClient(); const alicesUserId = userIdFromAddress(alicesContext.creatorAddress); const alicesUserStreamIdStr = makeUserStreamId(alicesUserId); const alicesUserStreamId = streamIdToBytes(alicesUserStreamIdStr); // Create accounts for Bob and Alice const bobsStream = await bob.createStream({ events: [ await makeEvent(bobsContext, make_UserPayload_Inception({ streamId: bobsUserStreamId, })), ], streamId: bobsUserStreamId, }); const alicesStream = await alice.createStream({ events: [ await makeEvent(alicesContext, make_UserPayload_Inception({ streamId: alicesUserStreamId, })), ], streamId: alicesUserStreamId, }); // Bob creates space const spaceIdStr = makeUniqueSpaceStreamId(); const spaceId = streamIdToBytes(spaceIdStr); const inceptionEvent = await makeEvent(bobsContext, make_SpacePayload_Inception({ streamId: spaceId, })); const joinEvent = await makeEvent(bobsContext, make_MemberPayload_Membership2({ userId: bobsUserId, op: MembershipOp.SO_JOIN, initiatorId: bobsUserId, })); await bob.createStream({ events: [inceptionEvent, joinEvent], streamId: spaceId, }); // Bob creates channel const channelIdStr = makeUniqueChannelStreamId(spaceIdStr); const channelId = streamIdToBytes(channelIdStr); const channelInceptionEvent = await makeEvent(bobsContext, make_ChannelPayload_Inception({ streamId: channelId, spaceId: spaceId, })); let event = await makeEvent(bobsContext, make_MemberPayload_Membership2({ userId: bobsUserId, op: MembershipOp.SO_JOIN, initiatorId: bobsUserId, streamParentId: spaceIdStr, })); const createChannelResponse = await bob.createStream({ events: [channelInceptionEvent, event], streamId: channelId, }); // Bob succesdfully posts a message event = await makeEvent(bobsContext, make_ChannelPayload_Message({ ...TEST_ENCRYPTED_MESSAGE_PROPS, ciphertext: 'hello', }), createChannelResponse.stream?.miniblocks.at(-1)?.header?.hash); await bob.addEvent({ streamId: channelId, event, }); // Alice fails to post a message if she hasn't joined the channel log("Alice fails to post a message if she hasn't joined the channel"); await expect(alice.addEvent({ streamId: channelId, event: await makeEvent(alicesContext, make_ChannelPayload_Message({ ...TEST_ENCRYPTED_MESSAGE_PROPS, ciphertext: 'hello', }), createChannelResponse.stream?.miniblocks.at(-1)?.header?.hash), })).rejects.toThrow(expect.objectContaining({ message: expect.stringContaining('7:PERMISSION_DENIED'), })); // Alice syncs her user stream waiting for invite const userAlice = await alice.getStream({ streamId: alicesUserStreamId, }); if (!userAlice.stream) throw new Error('userAlice stream not found'); let aliceSyncCookie = userAlice.stream.nextSyncCookie; const aliceSyncStreams = alice.syncStreams({ syncPos: aliceSyncCookie ? [aliceSyncCookie] : [], }, { timeoutMs: -1 }); let syncId; log("Alice waits for Bob's channel creation event"); await expect(waitForSyncStreams(aliceSyncStreams, async (res) => { syncId = res.syncId; return res.syncOp === SyncOp.SYNC_NEW && res.syncId !== undefined; })).resolves.not.toThrow(); // Bob invites Alice to the channel log('Bob invites Alice to the channel'); event = await makeEvent(bobsContext, make_UserPayload_UserMembershipAction({ op: MembershipOp.SO_INVITE, userId: addressFromUserId(alicesUserId), streamId: channelId, streamParentId: spaceId, }), bobsStream.stream?.miniblocks.at(-1)?.header?.hash); await bob.addEvent({ streamId: bobsUserStreamId, event, }); log("Alice waits for Bob's invite event"); aliceSyncCookie = await waitForEvent(aliceSyncStreams, alicesUserStreamIdStr, (e) => { if (e.event.payload?.case === 'userPayload' && e.event.payload?.value.content.case === 'userMembership') { log("Alice's received over sync:", { op: e.event.payload?.value.content.value.op, streamId: streamIdAsString(e.event.payload?.value.content.value.streamId), inviter: e.event.payload?.value.content.value.inviter, inviterId: e.event.payload?.value.content.value.inviter ? userIdFromAddress(e.event.payload?.value.content.value.inviter) : undefined, bob: bobsUserId, bobAddress: addressFromUserId(bobsUserId), bobAddress2: bobsContext.creatorAddress, bobAddress3: userIdFromAddress(bobsContext.creatorAddress), inviterEquals: bin_equal(e.event.payload?.value.content.value.inviter, bobsContext.creatorAddress), channelIdEquals: bin_equal(e.event.payload?.value.content.value.streamId, channelId), inviteEquals: e.event.payload?.value.content.value.op === MembershipOp.SO_INVITE, }); return (e.event.payload?.value.content.value.op === MembershipOp.SO_INVITE && bin_equal(e.event.payload?.value.content.value.streamId, channelId) && bin_equal(e.event.payload?.value.content.value.inviter, bobsContext.creatorAddress)); } return false; }); // Alice joins the channel event = await makeEvent(alicesContext, make_UserPayload_UserMembership({ op: MembershipOp.SO_JOIN, streamId: channelId, streamParentId: spaceId, }), alicesStream.stream?.miniblocks.at(-1)?.header?.hash); await alice.addEvent({ streamId: alicesUserStreamId, event, }); log('Alice waits for join event in her user stream'); // Alice sees derived join event in her user stream aliceSyncCookie = await waitForEvent(aliceSyncStreams, alicesUserStreamIdStr, (e) => e.event.payload?.case === 'userPayload' && e.event.payload?.value.content.case === 'userMembership' && e.event.payload?.value.content.value.op === MembershipOp.SO_JOIN && bin_equal(e.event.payload?.value.content.value.streamId, channelId)); // Alice reads previouse messages from the channel const channel = await alice.getStream({ streamId: channelId }); let messageCount = 0; if (!channel.stream) throw new Error('channel stream not found'); const envelopes = await unpackStreamEnvelopes(channel.stream, undefined); envelopes.forEach((e) => { const p = e.event.payload; if (p?.case === 'channelPayload' && p.value.content.case === 'message') { messageCount++; expect(p.value.content.value.ciphertext).toEqual('hello'); } }); expect(messageCount).toEqual(1); await alice.addStreamToSync({ syncId, syncPos: channel.stream.nextSyncCookie, }); // Bob posts another message event = await makeEvent(bobsContext, make_ChannelPayload_Message({ ...TEST_ENCRYPTED_MESSAGE_PROPS, ciphertext: 'Hello, Alice!', }), channel.stream?.miniblocks.at(-1)?.header?.hash); await bob.addEvent({ streamId: channelId, event, }); log('Alice waits for Bob to post another message'); // Alice sees the message in the channel stream await expect(waitForEvent(aliceSyncStreams, channelIdStr, (e) => e.event.payload?.case === 'channelPayload' && e.event.payload?.value.content.case === 'message' && e.event.payload?.value.content.value.ciphertext === 'Hello, Alice!')).resolves.not.toThrow(); await alice.cancelSync({ syncId }); }); test.each([ [0n, 'never'], [{ days: 2 }, 'in two days'], ])('cantAddOrCreateWithExpiredDelegateSig expiry: %o expires %s', async (goodExpiry, desc) => { log('testing with good expiry of', goodExpiry, 'which expires', desc); const jimmy = await makeTestRpcClient(); const jimmysWallet = ethers.Wallet.createRandom(); const jimmysDelegateWallet = ethers.Wallet.createRandom(); const jimmysGoodContext = await makeSignerContext(jimmysWallet, jimmysDelegateWallet, goodExpiry); const jimmysExpiredContext = await makeSignerContext(jimmysWallet, jimmysDelegateWallet, { days: -2, }); const jimmysUserId = userIdFromAddress(jimmysGoodContext.creatorAddress); const jimmysUserStreamId = streamIdToBytes(makeUserStreamId(jimmysUserId)); const makeUserStreamWith = async (context) => { return jimmy.createStream({ events: [ await makeEvent(context, make_UserPayload_Inception({ streamId: jimmysUserStreamId, })), ], streamId: jimmysUserStreamId, }); }; // test create stream await expect(makeUserStreamWith(jimmysExpiredContext)).rejects.toThrow(expect.objectContaining({ message: expect.stringContaining('7:PERMISSION_DENIED'), })); await expect(makeUserStreamWith(jimmysGoodContext)).resolves.not.toThrow(); // create a space const spacedStreamId = streamIdToBytes(makeUniqueSpaceStreamId()); const spaceEvents = await makeEvents(jimmysGoodContext, [ make_SpacePayload_Inception({ streamId: spacedStreamId, }), make_MemberPayload_Membership2({ userId: jimmysUserId, op: MembershipOp.SO_JOIN, initiatorId: jimmysUserId, }), ]); await jimmy.createStream({ events: spaceEvents, streamId: spacedStreamId, }); // try to leave, first with expired context, then with good context const addEventWith = async (context) => { const lastMiniblockHash = (await jimmy.getLastMiniblockHash({ streamId: jimmysUserStreamId })).hash; const messageEvent = await makeEvent(context, make_UserPayload_UserMembership({ streamId: spacedStreamId, op: MembershipOp.SO_LEAVE, }), lastMiniblockHash); return jimmy.addEvent({ streamId: jimmysUserStreamId, event: messageEvent, }); }; // test add event await expect(addEventWith(jimmysExpiredContext)).rejects.toThrow(expect.objectContaining({ message: expect.stringContaining('7:PERMISSION_DENIED'), })); await expect(addEventWith(jimmysGoodContext)).resolves.not.toThrow(); }); test('cantAddWithBadHash', async () => { const bob = await makeTestRpcClient(); const bobsUserId = userIdFromAddress(bobsContext.creatorAddress); const bobsUserStreamIdStr = makeUserStreamId(bobsUserId); const bobsUserStreamId = streamIdToBytes(bobsUserStreamIdStr); await expect(bob.createStream({ events: [ await makeEvent(bobsContext, make_UserPayload_Inception({ streamId: bobsUserStreamId, })), ], streamId: bobsUserStreamId, })).resolves.not.toThrow(); log('Bob created user, about to create space'); // Bob creates space and channel const spacedStreamIdStr = makeUniqueSpaceStreamId(); const spacedStreamId = streamIdToBytes(spacedStreamIdStr); const spaceEvents = await makeEvents(bobsContext, [ make_SpacePayload_Inception({ streamId: spacedStreamId, }), make_MemberPayload_Membership2({ userId: bobsUserId, op: MembershipOp.SO_JOIN, initiatorId: bobsUserId, }), ]); await bob.createStream({ events: spaceEvents, streamId: spacedStreamId, }); log('Bob created space, about to create channel'); const channelIdStr = makeUniqueChannelStreamId(spacedStreamIdStr); const channelId = streamIdToBytes(channelIdStr); const channelEvents = await makeEvents(bobsContext, [ make_ChannelPayload_Inception({ streamId: channelId, spaceId: spacedStreamId, }), make_MemberPayload_Membership2({ userId: bobsUserId, op: MembershipOp.SO_JOIN, initiatorId: bobsUserId, streamParentId: spacedStreamIdStr, }), ]); await bob.createStream({ events: channelEvents, streamId: channelId, }); log('Bob created channel'); log('Bob fails to create channel with badly chained initial events, hash empty'); const channelId2Str = makeUniqueChannelStreamId(spacedStreamIdStr); const channelId2 = streamIdToBytes(channelId2Str); const channelEvent2_0 = await makeEvent(bobsContext, make_ChannelPayload_Inception({ streamId: channelId2, spaceId: spacedStreamId, })); log('Bob fails to create channel with badly chained initial events, wrong hash value'); const channelEvent2_2 = await makeEvent(bobsContext, make_MemberPayload_Membership2({ userId: bobsUserId, op: MembershipOp.SO_JOIN, initiatorId: bobsUserId, }), Uint8Array.from(Array(32).fill('1'))); // TODO: fix up error codes Err.BAD_PREV_EVENTS await expect(bob.createStream({ events: [channelEvent2_0, channelEvent2_2], streamId: channelId2, })).rejects.toThrow(expect.objectContaining({ message: expect.stringContaining('19:BAD_STREAM_CREATION_PARAMS'), })); log('Bob adds event with correct hash'); const lastMiniblockHash = (await bob.getLastMiniblockHash({ streamId: channelId })).hash; const messageEvent = await makeEvent(bobsContext, make_ChannelPayload_Message({ ...TEST_ENCRYPTED_MESSAGE_PROPS, ciphertext: 'Hello, World!', }), lastMiniblockHash); await expect(bob.addEvent({ streamId: channelId, event: messageEvent, })).resolves.not.toThrow(); log('Bob fails to add event with empty hash'); await expect(bob.addEvent({ streamId: channelId, event: await makeEvent_test(bobsContext, make_ChannelPayload_Message({ ...TEST_ENCRYPTED_MESSAGE_PROPS, ciphertext: 'Hello, World!', })), })).rejects.toThrow(expect.objectContaining({ message: expect.stringContaining('3:INVALID_ARGUMENT'), })); }); test('cantAddWithBadSignature', async () => { const bob = await makeTestRpcClient(); const bobsUserId = userIdFromAddress(bobsContext.creatorAddress); const bobsUserStreamIdStr = makeUserStreamId(bobsUserId); const bobsUserStreamId = streamIdToBytes(bobsUserStreamIdStr); await expect(bob.createStream({ events: [ await makeEvent(bobsContext, make_UserPayload_Inception({ streamId: bobsUserStreamId, })), ], streamId: bobsUserStreamId, })).resolves.not.toThrow(); log('Bob created user, about to create space'); // Bob creates space and channel const spacedStreamIdStr = makeUniqueSpaceStreamId(); const spacedStreamId = streamIdToBytes(spacedStreamIdStr); const spaceEvents = await makeEvents(bobsContext, [ make_SpacePayload_Inception({ streamId: spacedStreamId, }), make_MemberPayload_Membership2({ userId: bobsUserId, op: MembershipOp.SO_JOIN, initiatorId: bobsUserId, }), ]); await bob.createStream({ events: spaceEvents, streamId: spacedStreamId, }); log('Bob created space, about to create channel'); const channelIdStr = makeUniqueChannelStreamId(spacedStreamIdStr); const channelId = streamIdToBytes(channelIdStr); const channelEvents = await makeEvents(bobsContext, [ make_ChannelPayload_Inception({ streamId: channelId, spaceId: spacedStreamId, }), make_MemberPayload_Membership2({ userId: bobsUserId, op: MembershipOp.SO_JOIN, initiatorId: bobsUserId, }), ]); await bob.createStream({ events: channelEvents, streamId: channelId, }); log('Bob created channel'); log('Bob adds event with correct signature'); const lastMiniblockHash = (await bob.getLastMiniblockHash({ streamId: channelId })).hash; const messageEvent = await makeEvent(bobsContext, make_ChannelPayload_Message({ ...TEST_ENCRYPTED_MESSAGE_PROPS, ciphertext: 'Hello, World!', }), lastMiniblockHash); channelEvents.push(messageEvent); await expect(bob.addEvent({ streamId: channelId, event: messageEvent, })).resolves.not.toThrow(); log('Adding event again succeeds (noop)'); await expect(bob.addEvent({ streamId: channelId, event: messageEvent, })).resolves.not.toThrow(); log('Bob failes to add event with bad signature'); const badEvent = await makeEvent(bobsContext, make_ChannelPayload_Message({ ...TEST_ENCRYPTED_MESSAGE_PROPS, ciphertext: 'Nah, not really', }), lastMiniblockHash); badEvent.signature = messageEvent.signature; await expect(bob.addEvent({ streamId: channelId, event: badEvent, })).rejects.toThrow('22:BAD_EVENT_SIGNATURE'); log('Bob fails with outdated prev minibloc hash'); const expiredEvent = await makeEvent(bobsContext, make_ChannelPayload_Message({ ...TEST_ENCRYPTED_MESSAGE_PROPS, ciphertext: 'Nah, not really', }), Uint8Array.from(Array(32).fill('1'))); await expect(bob.addEvent({ streamId: channelId, event: expiredEvent, })).rejects.toThrow('24:BAD_PREV_MINIBLOCK_HASH'); }); }); const waitForEvent = async (syncStream, streamId, matcher) => { for await (const res of iterableWrapper(syncStream)) { const stream = res.stream; if (stream?.nextSyncCookie?.streamId && bin_equal(stream.nextSyncCookie.streamId, streamIdToBytes(streamId))) { const events = await unpackStreamEnvelopes(stream, undefined); for (const e of events) { if (matcher(e)) { return stream.nextSyncCookie; } } } } throw new Error('unreachable'); }; //# sourceMappingURL=streamRpcClient.test.js.map