UNPKG

@river-build/sdk

Version:

For more details, visit the following resources:

410 lines 20.1 kB
import { MembershipOp, WrappedEncryptedDataSchema, } from '@river-build/proto'; import { getEventSignature, makeRemoteTimelineEvent, } from './types'; import { isDefined, logNever } from './check'; import { userIdFromAddress } from './id'; import { StreamStateView_Members_Membership } from './streamStateView_Members_Membership'; import { StreamStateView_Members_Solicitations } from './streamStateView_Members_Solicitations'; import { bin_toHexString, check, dlog } from '@river-build/dlog'; import { StreamStateView_MemberMetadata } from './streamStateView_MemberMetadata'; import { makeParsedEvent } from './sign'; import { StreamStateView_AbstractContent } from './streamStateView_AbstractContent'; import { utils } from 'ethers'; import { create } from '@bufbuild/protobuf'; import { getSpaceReviewEventDataBin } from '@river-build/web3'; const log = dlog('csb:streamStateView_Members'); export class StreamStateView_Members extends StreamStateView_AbstractContent { streamId; joined = new Map(); membership; solicitHelper; memberMetadata; pins = []; tips = {}; encryptionAlgorithm = undefined; spaceReviews = []; tokenTransfers = []; constructor(streamId) { super(); this.streamId = streamId; this.membership = new StreamStateView_Members_Membership(streamId); this.solicitHelper = new StreamStateView_Members_Solicitations(streamId); this.memberMetadata = new StreamStateView_MemberMetadata(streamId); } // initialization applySnapshot(eventId, event, snapshot, cleartexts, encryptionEmitter) { if (!snapshot.members) { return; } for (const member of snapshot.members.joined) { const userId = userIdFromAddress(member.userAddress); this.joined.set(userId, { userId, userAddress: member.userAddress, miniblockNum: member.miniblockNum, eventNum: member.eventNum, solicitations: member.solicitations.map((s) => ({ deviceKey: s.deviceKey, fallbackKey: s.fallbackKey, isNewDevice: s.isNewDevice, sessionIds: [...s.sessionIds], srcEventId: eventId, })), encryptedUsername: member.username, encryptedDisplayName: member.displayName, ensAddress: member.ensAddress, nft: member.nft, }); this.membership.applyMembershipEvent(userId, MembershipOp.SO_JOIN, 'confirmed', undefined); } // user/display names were ported from an older implementation and could be simpler const usernames = Array.from(this.joined.values()) .filter((x) => isDefined(x.encryptedUsername)) .map((member) => ({ userId: member.userId, wrappedEncryptedData: member.encryptedUsername, })); const displayNames = Array.from(this.joined.values()) .filter((x) => isDefined(x.encryptedDisplayName)) .map((member) => ({ userId: member.userId, wrappedEncryptedData: member.encryptedDisplayName, })); const ensAddresses = Array.from(this.joined.values()) .filter((x) => isDefined(x.ensAddress)) .map((member) => ({ userId: member.userId, ensAddress: member.ensAddress, })); const nfts = Array.from(this.joined.values()) .filter((x) => isDefined(x.nft)) .map((member) => ({ userId: member.userId, nft: member.nft, })); this.memberMetadata.applySnapshot(usernames, displayNames, ensAddresses, nfts, cleartexts, encryptionEmitter); const sigBundle = getEventSignature(event); this.solicitHelper.initSolicitations(Array.from(this.joined.values()), sigBundle, encryptionEmitter); snapshot.members?.pins.forEach((snappedPin) => { if (snappedPin.pin?.event) { const parsedEvent = makeParsedEvent(snappedPin.pin.event, snappedPin.pin.eventId, undefined); const remoteEvent = makeRemoteTimelineEvent({ parsedEvent, eventNum: 0n }); const cleartext = cleartexts?.[remoteEvent.hashStr]; this.addPin(userIdFromAddress(snappedPin.creatorAddress), remoteEvent, cleartext, encryptionEmitter, undefined); } }); this.tips = { ...snapshot.members.tips }; this.encryptionAlgorithm = snapshot.members.encryptionAlgorithm?.algorithm; } prependEvent(event, _cleartext, _encryptionEmitter, stateEmitter) { check(event.remoteEvent.event.payload.case === 'memberPayload'); const payload = event.remoteEvent.event.payload.value; switch (payload.content.case) { case 'memberBlockchainTransaction': { const receipt = payload.content.value.transaction?.receipt; const transactionContent = payload.content.value.transaction?.content; switch (transactionContent?.case) { case 'spaceReview': { // space reviews need to be prepended if (!receipt) { return; } if (!transactionContent.value.event) { return; } const review = getSpaceReviewEventDataBin(receipt.logs, transactionContent.value.event.user); const existingReview = this.spaceReviews.find((r) => r.review.user === review.user); // since we're prepending, existing reviews are newer and should be kept if (!existingReview) { this.spaceReviews.unshift({ review: review, createdAtEpochMs: event.createdAtEpochMs, eventHashStr: event.hashStr, }); stateEmitter?.emit('spaceReviewsUpdated', this.streamId, review); } break; } case 'tokenTransfer': { this.addTokenTransfer(payload.content.value, transactionContent.value, event.createdAtEpochMs, stateEmitter, true); break; } default: break; } break; } default: break; } } /** * Places event in a pending queue, to be applied when the event is confirmed in a miniblock header */ appendEvent(event, cleartext, encryptionEmitter, stateEmitter) { check(event.remoteEvent.event.payload.case === 'memberPayload'); const payload = event.remoteEvent.event.payload.value; switch (payload.content.case) { case 'membership': { const membership = payload.content.value; this.membership.pendingMembershipEvents.set(event.hashStr, membership); const userId = userIdFromAddress(membership.userAddress); switch (membership.op) { case MembershipOp.SO_JOIN: if (this.joined.has(userId)) { // aellis 12/24 there is a real bug here, not sure why we // are getting duplicate join events log('user already joined', this.streamId, userId); return; } this.joined.set(userId, { userId, userAddress: membership.userAddress, miniblockNum: event.miniblockNum, eventNum: event.eventNum, solicitations: [], }); break; case MembershipOp.SO_LEAVE: this.joined.delete(userId); break; default: break; } this.membership.applyMembershipEvent(userId, membership.op, 'pending', stateEmitter); } break; case 'keySolicitation': { const stateMember = this.joined.get(event.creatorUserId); check(isDefined(stateMember), 'key solicitation from non-member'); this.solicitHelper.applySolicitation(stateMember, event.hashStr, payload.content.value, getEventSignature(event.remoteEvent), encryptionEmitter); } break; case 'keyFulfillment': { const userId = userIdFromAddress(payload.content.value.userAddress); const stateMember = this.joined.get(userId); check(isDefined(stateMember), 'key fulfillment from non-member'); this.solicitHelper.applyFulfillment(stateMember, payload.content.value, getEventSignature(event.remoteEvent), encryptionEmitter); } break; case 'displayName': { const stateMember = this.joined.get(event.creatorUserId); check(isDefined(stateMember), 'displayName from non-member'); stateMember.encryptedDisplayName = create(WrappedEncryptedDataSchema, { data: payload.content.value, }); this.memberMetadata.appendDisplayName(event.hashStr, payload.content.value, event.creatorUserId, cleartext, encryptionEmitter, stateEmitter); } break; case 'username': { const stateMember = this.joined.get(event.creatorUserId); check(isDefined(stateMember), 'username from non-member'); stateMember.encryptedUsername = create(WrappedEncryptedDataSchema, { data: payload.content.value, }); this.memberMetadata.appendUsername(event.hashStr, payload.content.value, event.creatorUserId, cleartext, encryptionEmitter, stateEmitter); } break; case 'ensAddress': { const stateMember = this.joined.get(event.creatorUserId); check(isDefined(stateMember), 'username from non-member'); this.memberMetadata.appendEnsAddress(event.hashStr, payload.content.value, event.creatorUserId, stateEmitter); break; } case 'nft': { const stateMember = this.joined.get(event.creatorUserId); check(isDefined(stateMember), 'nft from non-member'); this.memberMetadata.appendNft(event.hashStr, payload.content.value, event.creatorUserId, stateEmitter); break; } case 'pin': { const pin = payload.content.value; check(isDefined(pin.event), 'invalid pin event'); const parsedEvent = makeParsedEvent(pin.event, pin.eventId, undefined); const remoteEvent = makeRemoteTimelineEvent({ parsedEvent, eventNum: 0n }); this.addPin(event.creatorUserId, remoteEvent, undefined, encryptionEmitter, stateEmitter); } break; case 'unpin': { const eventId = payload.content.value.eventId; this.removePin(eventId, stateEmitter); } break; case 'memberBlockchainTransaction': { const transactionContent = payload.content.value.transaction?.content; switch (transactionContent?.case) { case undefined: break; case 'tip': { const tipEvent = transactionContent.value.event; if (!tipEvent) { return; } const currency = utils.getAddress(bin_toHexString(tipEvent.currency)); this.tips[currency] = (this.tips[currency] ?? 0n) + tipEvent.amount; stateEmitter?.emit('streamTipped', this.streamId, event.hashStr, transactionContent.value); break; } case 'tokenTransfer': this.addTokenTransfer(payload.content.value, transactionContent.value, event.createdAtEpochMs, stateEmitter); break; case 'spaceReview': { const receipt = payload.content.value.transaction?.receipt; if (!receipt) { return; } if (!transactionContent.value.event) { return; } const review = getSpaceReviewEventDataBin(receipt.logs, transactionContent.value.event.user); const existingReviewIndex = this.spaceReviews.findIndex((r) => r.review.user === review.user); if (existingReviewIndex === -1) { this.spaceReviews.push({ review: review, createdAtEpochMs: event.createdAtEpochMs, eventHashStr: event.hashStr, }); } else { // since we're prepending, existing reviews are newer and should be kept this.spaceReviews[existingReviewIndex] = { review: review, createdAtEpochMs: event.createdAtEpochMs, eventHashStr: event.hashStr, }; } stateEmitter?.emit('spaceReviewsUpdated', this.streamId, review); break; } default: logNever(transactionContent); } break; } case 'encryptionAlgorithm': this.encryptionAlgorithm = payload.content.value.algorithm; stateEmitter?.emit('streamEncryptionAlgorithmUpdated', this.streamId, this.encryptionAlgorithm); break; case undefined: break; default: logNever(payload.content); } } onConfirmedEvent(event, stateEmitter, _) { check(event.remoteEvent.event.payload.case === 'memberPayload'); const payload = event.remoteEvent.event.payload.value; switch (payload.content.case) { case 'membership': { const eventId = event.hashStr; const membership = this.membership.pendingMembershipEvents.get(eventId); if (membership) { this.membership.pendingMembershipEvents.delete(eventId); const userId = userIdFromAddress(membership.userAddress); const streamMember = this.joined.get(userId); if (streamMember) { streamMember.miniblockNum = event.miniblockNum; streamMember.eventNum = event.eventNum; } this.membership.applyMembershipEvent(userId, membership.op, 'confirmed', stateEmitter); } } break; case 'keyFulfillment': break; case 'keySolicitation': break; case 'displayName': case 'username': case 'ensAddress': case 'nft': this.memberMetadata.onConfirmedEvent(event, stateEmitter); break; case 'pin': break; case 'unpin': break; case 'memberBlockchainTransaction': break; case 'encryptionAlgorithm': break; case undefined: break; default: logNever(payload.content); } } onDecryptedContent(eventId, content, stateEmitter) { if (content.kind === 'text') { this.memberMetadata.onDecryptedContent(eventId, content.content, stateEmitter); } const pinIndex = this.pins.findIndex((pin) => pin.event.hashStr === eventId); if (pinIndex !== -1) { this.pins[pinIndex].event.decryptedContent = content; stateEmitter?.emit('channelPinDecrypted', this.streamId, this.pins[pinIndex], pinIndex); } } isMemberJoined(userId) { return this.membership.joinedUsers.has(userId); } isMember(membership, userId) { return this.membership.isMember(membership, userId); } participants() { return this.membership.participants(); } joinedParticipants() { return this.membership.joinedParticipants(); } joinedOrInvitedParticipants() { return this.membership.joinedOrInvitedParticipants(); } addTokenTransfer(payload, transferContent, createdAtEpochMs, stateEmitter, prepend = false) { const receipt = payload.transaction?.receipt; const solanaReceipt = payload.transaction?.solanaReceipt; const transferData = { address: transferContent.address, userId: userIdFromAddress(payload.fromUserAddress), chainId: receipt ? receipt.chainId.toString() : solanaReceipt ? 'solana-mainnet' : 'unknown chain', createdAtEpochMs: createdAtEpochMs, isBuy: transferContent.isBuy, messageId: bin_toHexString(transferContent.messageId), amount: BigInt(transferContent.amount), }; prepend ? this.tokenTransfers.unshift(transferData) : this.tokenTransfers.push(transferData); stateEmitter?.emit('streamTokenTransfer', this.streamId, transferData); } addPin(creatorUserId, event, cleartext, encryptionEmitter, stateEmitter) { const newPin = { creatorUserId, event }; this.pins.push(newPin); if ((event.remoteEvent.event.payload.case === 'channelPayload' && event.remoteEvent.event.payload.value.content.case === 'message') || (event.remoteEvent.event.payload.case === 'dmChannelPayload' && event.remoteEvent.event.payload.value.content.case === 'message') || (event.remoteEvent.event.payload.case === 'gdmChannelPayload' && event.remoteEvent.event.payload.value.content.case === 'message')) { this.decryptEvent('channelMessage', event, event.remoteEvent.event.payload.value.content.value, cleartext, encryptionEmitter); } stateEmitter?.emit('channelPinAdded', this.streamId, newPin); } removePin(eventId, stateEmitter) { const eventIdStr = bin_toHexString(eventId); const index = this.pins.findIndex((pin) => pin.event.hashStr === eventIdStr); if (index !== -1) { const pin = this.pins.splice(index, 1)[0]; stateEmitter?.emit('channelPinRemoved', this.streamId, pin, index); } } } //# sourceMappingURL=streamStateView_Members.js.map