diff --git a/apps/events/src/components/debug-space-events.tsx b/apps/events/src/components/debug-space-events.tsx
new file mode 100644
index 00000000..6be09304
--- /dev/null
+++ b/apps/events/src/components/debug-space-events.tsx
@@ -0,0 +1,15 @@
+import type { SpaceEvent } from 'graph-framework';
+
+export function DebugSpaceEvents({ events }: { events: SpaceEvent[] }) {
+ return (
+
+ );
+}
diff --git a/apps/events/src/components/debug-space-state.tsx b/apps/events/src/components/debug-space-state.tsx
new file mode 100644
index 00000000..9a62c213
--- /dev/null
+++ b/apps/events/src/components/debug-space-state.tsx
@@ -0,0 +1,9 @@
+import type { SpaceState } from 'graph-framework';
+
+export function DebugSpaceState(props: { state: SpaceState | undefined }) {
+ return (
+
+
{JSON.stringify(props, null, 2)}
+
+ );
+}
diff --git a/apps/events/src/routes/playground.tsx b/apps/events/src/routes/playground.tsx
index f82e394f..eb2b79a0 100644
--- a/apps/events/src/routes/playground.tsx
+++ b/apps/events/src/routes/playground.tsx
@@ -1,12 +1,42 @@
+import { DebugSpaceEvents } from '@/components/debug-space-events';
+import { DebugSpaceState } from '@/components/debug-space-state';
import { Button } from '@/components/ui/button';
import { assertExhaustive } from '@/lib/assertExhaustive';
import { createFileRoute } from '@tanstack/react-router';
-import { Effect } from 'effect';
+import { Effect, Exit } from 'effect';
import * as Schema from 'effect/Schema';
-import type { EventMessage, RequestListSpaces, RequestSubscribeToSpace } from 'graph-framework';
-import { ResponseMessage, createSpace } from 'graph-framework';
+import type {
+ EventMessage,
+ RequestListInvitations,
+ RequestListSpaces,
+ RequestSubscribeToSpace,
+ SpaceEvent,
+ SpaceState,
+} from 'graph-framework';
+import { ResponseMessage, applyEvent, createInvitation, createSpace } from 'graph-framework';
import { useEffect, useState } from 'react';
+const availableAccounts = [
+ {
+ accountId: '0262701b2eb1b6b37ad03e24445dfcad1b91309199e43017b657ce2604417c12f5',
+ signaturePrivateKey: '88bb6f20de8dc1787c722dc847f4cf3d00285b8955445f23c483d1237fe85366',
+ },
+ {
+ accountId: '03bf5d2a1badf15387b08a007d1a9a13a9bfd6e1c56f681e251514d9ba10b57462',
+ signaturePrivateKey: '1eee32d3bc202dcb5d17c3b1454fb541d2290cb941860735408f1bfe39e7bc15',
+ },
+ {
+ accountId: '0351460706cf386282d9b6ebee2ccdcb9ba61194fd024345e53037f3036242e6a2',
+ signaturePrivateKey: '434518a2c9a665a7c20da086232c818b6c1592e2edfeecab29a40cf5925ca8fe',
+ },
+];
+
+type SpaceStorageEntry = {
+ id: string;
+ events: SpaceEvent[];
+ state: SpaceState | undefined;
+};
+
const decodeResponseMessage = Schema.decodeUnknownEither(ResponseMessage);
export const Route = createFileRoute('/playground')({
@@ -15,14 +45,14 @@ export const Route = createFileRoute('/playground')({
const App = ({ accountId, signaturePrivateKey }: { accountId: string; signaturePrivateKey: string }) => {
const [websocketConnection, setWebsocketConnection] = useState();
- const [spaces, setSpaces] = useState<{ id: string }[]>([]);
+ const [spaces, setSpaces] = useState([]);
useEffect(() => {
// temporary until we have a way to create accounts and authenticate them
const websocketConnection = new WebSocket(`ws://localhost:3030/?accountId=${accountId}`);
setWebsocketConnection(websocketConnection);
- const onMessage = (event: MessageEvent) => {
+ const onMessage = async (event: MessageEvent) => {
console.log('message received', event.data);
const data = JSON.parse(event.data);
const message = decodeResponseMessage(data);
@@ -30,17 +60,58 @@ const App = ({ accountId, signaturePrivateKey }: { accountId: string; signatureP
const response = message.right;
switch (response.type) {
case 'list-spaces': {
- setSpaces(response.spaces.map((space) => ({ id: space.id })));
+ setSpaces((existingSpaces) => {
+ return response.spaces.map((space) => {
+ const existingSpace = existingSpaces.find((s) => s.id === space.id);
+ return { id: space.id, events: existingSpace?.events ?? [], state: existingSpace?.state };
+ });
+ });
+ // fetch all spaces (for debugging purposes)
+ for (const space of response.spaces) {
+ const message: RequestSubscribeToSpace = { type: 'subscribe-space', id: space.id };
+ websocketConnection?.send(JSON.stringify(message));
+ }
break;
}
case 'space': {
- console.log('space', response);
+ let state: SpaceState | undefined = undefined;
+
+ // TODO fix typing
+ for (const event of response.events) {
+ if (state === undefined) {
+ const applyEventResult = await Effect.runPromiseExit(applyEvent({ event }));
+ if (Exit.isSuccess(applyEventResult)) {
+ state = applyEventResult.value;
+ }
+ } else {
+ const applyEventResult = await Effect.runPromiseExit(applyEvent({ event, state }));
+ if (Exit.isSuccess(applyEventResult)) {
+ state = applyEventResult.value;
+ }
+ }
+ }
+
+ const newState = state as SpaceState;
+
+ setSpaces((spaces) =>
+ spaces.map((space) => {
+ if (space.id === response.id) {
+ // TODO fix readonly type issue
+ return { ...space, events: response.events as SpaceEvent[], state: newState };
+ }
+ return space;
+ }),
+ );
break;
}
case 'event': {
console.log('event', response);
break;
}
+ case 'list-invitations': {
+ console.log('list-invitations', response);
+ break;
+ }
default:
assertExhaustive(response);
}
@@ -86,7 +157,7 @@ const App = ({ accountId, signaturePrivateKey }: { accountId: string; signatureP
},
}),
);
- const message: EventMessage = { type: 'event', event: spaceEvent };
+ const message: EventMessage = { type: 'event', event: spaceEvent, spaceId: spaceEvent.transaction.id };
websocketConnection?.send(JSON.stringify(message));
}}
>
@@ -101,13 +172,22 @@ const App = ({ accountId, signaturePrivateKey }: { accountId: string; signatureP
>
List Spaces
+
+
Spaces
{spaces.map((space) => {
return (
-
-
{space.id}
+ Space id: {space.id}
+
+ {availableAccounts.map((invitee) => {
+ return (
+
+ );
+ })}
+ State
+
+ Events
+
+
);
})}
@@ -132,33 +253,24 @@ export const ChooseAccount = () => {
Choose account
Account: {account?.accountId ? account.accountId : 'none'}
diff --git a/apps/server/prisma/migrations/20241114170708_add_invitation/migration.sql b/apps/server/prisma/migrations/20241114170708_add_invitation/migration.sql
new file mode 100644
index 00000000..51e2d5e1
--- /dev/null
+++ b/apps/server/prisma/migrations/20241114170708_add_invitation/migration.sql
@@ -0,0 +1,9 @@
+-- CreateTable
+CREATE TABLE "Invitation" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "spaceId" TEXT NOT NULL,
+ "accountId" TEXT NOT NULL,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ CONSTRAINT "Invitation_spaceId_fkey" FOREIGN KEY ("spaceId") REFERENCES "Space" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
+ CONSTRAINT "Invitation_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "Account" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
+);
diff --git a/apps/server/prisma/schema.prisma b/apps/server/prisma/schema.prisma
index 5d29b900..bd2a4ebe 100644
--- a/apps/server/prisma/schema.prisma
+++ b/apps/server/prisma/schema.prisma
@@ -23,12 +23,23 @@ model SpaceEvent {
}
model Space {
- id String @id
- events SpaceEvent[]
- members Account[]
+ id String @id
+ events SpaceEvent[]
+ members Account[]
+ invitations Invitation[]
}
model Account {
- id String @id
- spaces Space[]
+ id String @id
+ spaces Space[]
+ invitations Invitation[]
+}
+
+model Invitation {
+ id String @id
+ space Space @relation(fields: [spaceId], references: [id])
+ spaceId String
+ account Account @relation(fields: [accountId], references: [id])
+ accountId String
+ createdAt DateTime @default(now())
}
diff --git a/apps/server/src/handlers/applySpaceEvent.ts b/apps/server/src/handlers/applySpaceEvent.ts
index 4dfec7fa..3a4fc542 100644
--- a/apps/server/src/handlers/applySpaceEvent.ts
+++ b/apps/server/src/handlers/applySpaceEvent.ts
@@ -10,11 +10,11 @@ type Params = {
};
export async function applySpaceEvent({ accountId, spaceId, event }: Params) {
- return await prisma.$transaction(async (transaction) => {
- if (event.transaction.type === 'create-space') {
- throw new Error('applySpaceEvent does not support create-space events.');
- }
+ if (event.transaction.type === 'create-space') {
+ throw new Error('applySpaceEvent does not support create-space events.');
+ }
+ return await prisma.$transaction(async (transaction) => {
// verify that the account is a member of the space
// TODO verify that the account is a admin of the space
await transaction.space.findUniqueOrThrow({
@@ -26,11 +26,22 @@ export async function applySpaceEvent({ accountId, spaceId, event }: Params) {
orderBy: { counter: 'desc' },
});
- const result = await Effect.runPromiseExit(applyEvent({ event }));
+ const result = await Effect.runPromiseExit(applyEvent({ event, state: JSON.parse(lastEvent.state) }));
if (Exit.isFailure(result)) {
+ console.log('Failed to apply event', result);
throw new Error('Invalid event');
}
+ if (event.transaction.type === 'create-invitation') {
+ await transaction.invitation.create({
+ data: {
+ id: event.transaction.id,
+ spaceId,
+ accountId: event.transaction.signaturePublicKey,
+ },
+ });
+ }
+
return await transaction.spaceEvent.create({
data: {
spaceId,
diff --git a/apps/server/src/handlers/listInvitations.ts b/apps/server/src/handlers/listInvitations.ts
new file mode 100644
index 00000000..e4b17bf5
--- /dev/null
+++ b/apps/server/src/handlers/listInvitations.ts
@@ -0,0 +1,45 @@
+import { Schema } from 'effect';
+import { SpaceState } from 'graph-framework-space-events';
+import { prisma } from '../prisma.js';
+
+type Params = {
+ accountId: string;
+};
+
+const decodeSpaceState = Schema.decodeUnknownEither(SpaceState);
+
+export const listInvitations = async ({ accountId }: Params) => {
+ const result = await prisma.invitation.findMany({
+ where: {
+ accountId,
+ },
+ include: {
+ space: {
+ include: {
+ events: {
+ orderBy: {
+ counter: 'asc',
+ },
+ take: 1,
+ },
+ },
+ },
+ },
+ });
+
+ return result
+ .map((invitation) => {
+ const result = decodeSpaceState(JSON.parse(invitation.space.events[0].state));
+ if (result._tag === 'Right') {
+ const state = result.right;
+ return {
+ id: invitation.id,
+ previousEventHash: state.lastEventHash,
+ spaceId: invitation.spaceId,
+ };
+ }
+ console.error('Invalid space state from the DB', result.left);
+ return null;
+ })
+ .filter((invitation) => invitation !== null);
+};
diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts
index ae101014..5627da16 100755
--- a/apps/server/src/index.ts
+++ b/apps/server/src/index.ts
@@ -3,13 +3,15 @@ import 'dotenv/config';
import { parse } from 'node:url';
import { Effect, Exit, Schema } from 'effect';
import express from 'express';
-import type { ResponseListSpaces, ResponseSpace } from 'graph-framework-messages';
+import type { ResponseListInvitations, ResponseListSpaces, ResponseSpace } from 'graph-framework-messages';
import { RequestMessage } from 'graph-framework-messages';
import { type CreateSpaceEvent, applyEvent } from 'graph-framework-space-events';
import type WebSocket from 'ws';
import { WebSocketServer } from 'ws';
+import { applySpaceEvent } from './handlers/applySpaceEvent.js';
import { createSpace } from './handlers/createSpace.js';
import { getSpace } from './handlers/getSpace.js';
+import { listInvitations } from './handlers/listInvitations.js';
import { listSpaces } from './handlers/listSpaces.js';
import { tmpInitAccount } from './handlers/tmpInitAccount.js';
import { assertExhaustive } from './utils/assertExhaustive.js';
@@ -67,6 +69,12 @@ webSocketServer.on('connection', async (webSocket: WebSocket, request: Request)
webSocket.send(JSON.stringify(outgoingMessage));
break;
}
+ case 'list-invitations': {
+ const invitations = await listInvitations({ accountId });
+ const outgoingMessage: ResponseListInvitations = { type: 'list-invitations', invitations: invitations };
+ webSocket.send(JSON.stringify(outgoingMessage));
+ break;
+ }
case 'event': {
switch (data.event.transaction.type) {
case 'create-space': {
@@ -88,6 +96,14 @@ webSocketServer.on('connection', async (webSocket: WebSocket, request: Request)
break;
}
case 'create-invitation': {
+ await applySpaceEvent({ accountId, spaceId: data.spaceId, event: data.event });
+ const spaceWithEvents = await getSpace({ accountId, spaceId: data.spaceId });
+ const outgoingMessage: ResponseSpace = {
+ type: 'space',
+ id: data.spaceId,
+ events: spaceWithEvents.events.map((wrapper) => JSON.parse(wrapper.event)),
+ };
+ webSocket.send(JSON.stringify(outgoingMessage));
break;
}
}
diff --git a/packages/graph-framework-messages/src/types.ts b/packages/graph-framework-messages/src/types.ts
index 150c318e..82553440 100644
--- a/packages/graph-framework-messages/src/types.ts
+++ b/packages/graph-framework-messages/src/types.ts
@@ -3,6 +3,7 @@ import { SpaceEvent } from 'graph-framework-space-events';
export const EventMessage = Schema.Struct({
type: Schema.Literal('event'),
+ spaceId: Schema.String,
event: SpaceEvent,
});
@@ -21,7 +22,18 @@ export const RequestListSpaces = Schema.Struct({
export type RequestListSpaces = Schema.Schema.Type;
-export const RequestMessage = Schema.Union(EventMessage, RequestSubscribeToSpace, RequestListSpaces);
+export const RequestListInvitations = Schema.Struct({
+ type: Schema.Literal('list-invitations'),
+});
+
+export type RequestListInvitations = Schema.Schema.Type;
+
+export const RequestMessage = Schema.Union(
+ EventMessage,
+ RequestSubscribeToSpace,
+ RequestListSpaces,
+ RequestListInvitations,
+);
export type RequestMessage = Schema.Schema.Type;
@@ -36,6 +48,19 @@ export const ResponseListSpaces = Schema.Struct({
export type ResponseListSpaces = Schema.Schema.Type;
+export const ResponseListInvitations = Schema.Struct({
+ type: Schema.Literal('list-invitations'),
+ invitations: Schema.Array(
+ Schema.Struct({
+ id: Schema.String,
+ previousEventHash: Schema.String,
+ spaceId: Schema.String,
+ }),
+ ),
+});
+
+export type ResponseListInvitations = Schema.Schema.Type;
+
export const ResponseSpace = Schema.Struct({
type: Schema.Literal('space'),
id: Schema.String,
@@ -44,6 +69,6 @@ export const ResponseSpace = Schema.Struct({
export type ResponseSpace = Schema.Schema.Type;
-export const ResponseMessage = Schema.Union(EventMessage, ResponseListSpaces, ResponseSpace);
+export const ResponseMessage = Schema.Union(EventMessage, ResponseListSpaces, ResponseListInvitations, ResponseSpace);
export type ResponseMessage = Schema.Schema.Type;
diff --git a/packages/graph-framework-space-events/src/apply-event.test.ts b/packages/graph-framework-space-events/src/apply-event.test.ts
index 19a9d9bf..0b50873d 100644
--- a/packages/graph-framework-space-events/src/apply-event.test.ts
+++ b/packages/graph-framework-space-events/src/apply-event.test.ts
@@ -7,13 +7,19 @@ import { createInvitation } from './create-invitation.js';
import { createSpace } from './create-space.js';
import { InvalidEventError, VerifySignatureError } from './types.js';
-it('should fail in case of an invalid signature', async () => {
- const author = {
- signaturePublicKey: '03594161eed61407084114a142d1ce05ef4c5a5279479fdd73a2b16944fbff003b',
- signaturePrivateKey: '76b78f644c19d6133018a97a3bc2d5038be0af5a2858b9e640ff3e2f2db63a0b',
- encryptionPublicKey: 'encryption',
- };
+const author = {
+ signaturePublicKey: '03594161eed61407084114a142d1ce05ef4c5a5279479fdd73a2b16944fbff003b',
+ signaturePrivateKey: '76b78f644c19d6133018a97a3bc2d5038be0af5a2858b9e640ff3e2f2db63a0b',
+ encryptionPublicKey: 'encryption',
+};
+
+const invitee = {
+ signaturePublicKey: '03bf5d2a1badf15387b08a007d1a9a13a9bfd6e1c56f681e251514d9ba10b57462',
+ signaturePrivateKey: '1eee32d3bc202dcb5d17c3b1454fb541d2290cb941860735408f1bfe39e7bc15',
+ encryptionPublicKey: 'encryption',
+};
+it('should fail in case of an invalid signature', async () => {
const result = await Effect.runPromiseExit(
Effect.gen(function* () {
const spaceEvent = yield* createSpace({ author });
@@ -37,18 +43,12 @@ it('should fail in case of an invalid signature', async () => {
});
it('should fail in case state is not provided for an event other than createSpace', async () => {
- const author = {
- signaturePublicKey: '03594161eed61407084114a142d1ce05ef4c5a5279479fdd73a2b16944fbff003b',
- signaturePrivateKey: '76b78f644c19d6133018a97a3bc2d5038be0af5a2858b9e640ff3e2f2db63a0b',
- encryptionPublicKey: 'encryption',
- };
-
const result = await Effect.runPromiseExit(
Effect.gen(function* () {
const spaceEvent = yield* createSpace({ author });
const state = yield* applyEvent({ event: spaceEvent });
- const spaceEvent2 = yield* createInvitation({ author, id: state.id, previousEventHash: state.lastEventHash });
+ const spaceEvent2 = yield* createInvitation({ author, previousEventHash: state.lastEventHash, invitee });
return yield* applyEvent({ event: spaceEvent2 });
}),
);
@@ -63,12 +63,6 @@ it('should fail in case state is not provided for an event other than createSpac
});
it('should fail in case of an event is applied that is not based on the previous event', async () => {
- const author = {
- signaturePublicKey: '03594161eed61407084114a142d1ce05ef4c5a5279479fdd73a2b16944fbff003b',
- signaturePrivateKey: '76b78f644c19d6133018a97a3bc2d5038be0af5a2858b9e640ff3e2f2db63a0b',
- encryptionPublicKey: 'encryption',
- };
-
const result = await Effect.runPromiseExit(
Effect.gen(function* () {
const spaceEvent = yield* createSpace({ author });
@@ -77,7 +71,7 @@ it('should fail in case of an event is applied that is not based on the previous
const spaceEvent2 = yield* createSpace({ author });
const state2 = yield* applyEvent({ state, event: spaceEvent2 });
- const spaceEvent3 = yield* createInvitation({ author, id: state.id, previousEventHash: state.lastEventHash });
+ const spaceEvent3 = yield* createInvitation({ author, previousEventHash: state.lastEventHash, invitee });
return yield* applyEvent({ state: state2, event: spaceEvent3 });
}),
);
diff --git a/packages/graph-framework-space-events/src/apply-event.ts b/packages/graph-framework-space-events/src/apply-event.ts
index 8c899503..df2310d0 100644
--- a/packages/graph-framework-space-events/src/apply-event.ts
+++ b/packages/graph-framework-space-events/src/apply-event.ts
@@ -70,6 +70,15 @@ export const applyEvent = ({
members = {};
invitations = {};
} else if (event.transaction.type === 'create-invitation') {
+ if (members[event.transaction.signaturePublicKey] !== undefined) {
+ return Effect.fail(new InvalidEventError());
+ }
+ for (const invitation of Object.values(invitations)) {
+ if (invitation.signaturePublicKey === event.transaction.signaturePublicKey) {
+ return Effect.fail(new InvalidEventError());
+ }
+ }
+
invitations[event.transaction.id] = {
signaturePublicKey: event.transaction.signaturePublicKey,
encryptionPublicKey: event.transaction.encryptionPublicKey,
diff --git a/packages/graph-framework-space-events/src/create-invitation.test.ts b/packages/graph-framework-space-events/src/create-invitation.test.ts
index e983cc8a..07e1760d 100644
--- a/packages/graph-framework-space-events/src/create-invitation.test.ts
+++ b/packages/graph-framework-space-events/src/create-invitation.test.ts
@@ -1,22 +1,32 @@
import { expect, it } from 'vitest';
-import { Effect } from 'effect';
+import { Effect, Exit } from 'effect';
import { applyEvent } from './apply-event.js';
import { createInvitation } from './create-invitation.js';
import { createSpace } from './create-space.js';
-it('should create an invitation', async () => {
- const author = {
- signaturePublicKey: '03594161eed61407084114a142d1ce05ef4c5a5279479fdd73a2b16944fbff003b',
- signaturePrivateKey: '76b78f644c19d6133018a97a3bc2d5038be0af5a2858b9e640ff3e2f2db63a0b',
- encryptionPublicKey: 'encryption',
- };
+const author = {
+ signaturePublicKey: '03594161eed61407084114a142d1ce05ef4c5a5279479fdd73a2b16944fbff003b',
+ signaturePrivateKey: '76b78f644c19d6133018a97a3bc2d5038be0af5a2858b9e640ff3e2f2db63a0b',
+ encryptionPublicKey: 'encryption',
+};
+
+const invitee = {
+ signaturePublicKey: '03bf5d2a1badf15387b08a007d1a9a13a9bfd6e1c56f681e251514d9ba10b57462',
+ signaturePrivateKey: '1eee32d3bc202dcb5d17c3b1454fb541d2290cb941860735408f1bfe39e7bc15',
+ encryptionPublicKey: 'encryption',
+};
+it('should create an invitation', async () => {
const { spaceEvent2, state2 } = await Effect.runPromise(
Effect.gen(function* () {
const spaceEvent = yield* createSpace({ author });
const state = yield* applyEvent({ event: spaceEvent });
- const spaceEvent2 = yield* createInvitation({ author, id: state.id, previousEventHash: state.lastEventHash });
+ const spaceEvent2 = yield* createInvitation({
+ author,
+ previousEventHash: state.lastEventHash,
+ invitee,
+ });
const state2 = yield* applyEvent({ state, event: spaceEvent2 });
return {
state2,
@@ -28,8 +38,8 @@ it('should create an invitation', async () => {
expect(state2.id).toBeTypeOf('string');
expect(state2.invitations).toEqual({
[spaceEvent2.transaction.id]: {
- signaturePublicKey: '',
- encryptionPublicKey: '',
+ signaturePublicKey: '03bf5d2a1badf15387b08a007d1a9a13a9bfd6e1c56f681e251514d9ba10b57462',
+ encryptionPublicKey: 'encryption',
},
});
expect(state2.members).toEqual({
@@ -42,3 +52,43 @@ it('should create an invitation', async () => {
expect(state2.removedMembers).toEqual({});
expect(state2.lastEventHash).toBeTypeOf('string');
});
+
+it('should fail to invite the account twice', async () => {
+ const result = await Effect.runPromiseExit(
+ Effect.gen(function* () {
+ const spaceEvent = yield* createSpace({ author });
+ const state = yield* applyEvent({ event: spaceEvent });
+ const spaceEvent2 = yield* createInvitation({
+ author,
+ previousEventHash: state.lastEventHash,
+ invitee,
+ });
+ const state2 = yield* applyEvent({ state, event: spaceEvent2 });
+ const spaceEvent3 = yield* createInvitation({
+ author,
+ previousEventHash: state.lastEventHash,
+ invitee,
+ });
+ return yield* applyEvent({ state: state2, event: spaceEvent3 });
+ }),
+ );
+
+ expect(Exit.isFailure(result)).toBe(true);
+});
+
+it('should fail to invite an account that is already a member', async () => {
+ const result = await Effect.runPromiseExit(
+ Effect.gen(function* () {
+ const spaceEvent = yield* createSpace({ author });
+ const state = yield* applyEvent({ event: spaceEvent });
+ const spaceEvent2 = yield* createInvitation({
+ author,
+ previousEventHash: state.lastEventHash,
+ invitee: author, // inviting the author
+ });
+ yield* applyEvent({ state, event: spaceEvent2 });
+ }),
+ );
+
+ expect(Exit.isFailure(result)).toBe(true);
+});
diff --git a/packages/graph-framework-space-events/src/create-invitation.ts b/packages/graph-framework-space-events/src/create-invitation.ts
index c6c5519a..8f59ae38 100644
--- a/packages/graph-framework-space-events/src/create-invitation.ts
+++ b/packages/graph-framework-space-events/src/create-invitation.ts
@@ -1,22 +1,29 @@
import { secp256k1 } from '@noble/curves/secp256k1';
import { Effect } from 'effect';
-import { canonicalize, stringToUint8Array } from 'graph-framework-utils';
+import { canonicalize, generateId, stringToUint8Array } from 'graph-framework-utils';
import type { Author, SpaceEvent } from './types.js';
type Params = {
author: Author;
- id: string;
previousEventHash: string;
+ invitee: {
+ signaturePublicKey: string;
+ encryptionPublicKey: string;
+ };
};
-export const createInvitation = ({ author, id, previousEventHash }: Params): Effect.Effect => {
+export const createInvitation = ({
+ author,
+ previousEventHash,
+ invitee,
+}: Params): Effect.Effect => {
const transaction = {
+ id: generateId(),
type: 'create-invitation' as const,
- id,
ciphertext: '',
nonce: '',
- signaturePublicKey: '',
- encryptionPublicKey: '',
+ signaturePublicKey: invitee.signaturePublicKey,
+ encryptionPublicKey: invitee.encryptionPublicKey,
previousEventHash,
};
const encodedTransaction = stringToUint8Array(canonicalize(transaction));