Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed .DS_Store
Binary file not shown.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,6 @@ typings/

# typescript
*.tsbuildinfo
next-env.d.ts
next-env.d.ts

.DS_Store
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
pnpm install
cd apps/server
cp .env.example .env
# add the PRIVY_APP_SECRET & PRIVY_APP_ID to the apps/server/.env file
pnpm prisma migrate dev
```

Expand Down
1 change: 1 addition & 0 deletions apps/connect/.env.development
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
VITE_HYPERGRAPH_SYNC_SERVER_ORIGIN="http://localhost:3030"
1 change: 1 addition & 0 deletions apps/connect/.env.production
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
VITE_HYPERGRAPH_SYNC_SERVER_ORIGIN="https://syncserver.hypergraph.thegraph.com"
6 changes: 5 additions & 1 deletion apps/connect/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "events",
"name": "connect",
"private": true,
"version": "0.0.0",
"type": "module",
Expand All @@ -9,12 +9,15 @@
},
"dependencies": {
"@graphprotocol/grc-20": "^0.11.5",
"@graphprotocol/hypergraph": "workspace:*",
"@graphprotocol/hypergraph-react": "workspace:*",
"@privy-io/react-auth": "^2.13.0",
"@radix-ui/react-avatar": "^1.1.9",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-slot": "^1.2.2",
"@tanstack/react-query": "^5.75.5",
"@tanstack/react-router": "^1.120.2",
"@xstate/store": "^3.5.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"effect": "^3.16.3",
Expand All @@ -24,6 +27,7 @@
"react-dom": "^19.1.0",
"tailwind-merge": "^3.2.0",
"tailwindcss-animate": "^1.0.7",
"viem": "^2.30.6",
"vite": "^6.3.5"
},
"devDependencies": {
Expand Down
63 changes: 48 additions & 15 deletions apps/connect/src/Boot.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { Connect, StoreConnect } from '@graphprotocol/hypergraph';
import { PrivyProvider } from '@privy-io/react-auth';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { RouterProvider, createRouter } from '@tanstack/react-router';
import { useLayoutEffect, useRef } from 'react';
import { getAddress } from 'viem';
import { routeTree } from './routeTree.gen';

// Create a new router instance
Expand All @@ -12,22 +16,51 @@ declare module '@tanstack/react-router' {
}
}

const queryClient = new QueryClient();

const storage = localStorage;

export function Boot() {
// check if the user is already authenticated on initial render
const initialRenderAuthCheckRef = useRef(false);
// using a layout effect to avoid a re-render
useLayoutEffect(() => {
if (!initialRenderAuthCheckRef.current) {
const accountAddress = Connect.loadAccountAddress(storage);
if (accountAddress) {
const keys = Connect.loadKeys(storage, accountAddress);
if (keys) {
// user is already authenticated, set state
StoreConnect.store.send({
type: 'setAuth',
accountAddress: getAddress(accountAddress),
sessionToken: 'dummy',
keys,
});
}
}
// set render auth check to true so next potential rerender doesn't proc this
initialRenderAuthCheckRef.current = true;
}
}, []);

return (
<PrivyProvider
appId="cm4wx6ziv00ngrmfjf9ik36iu"
config={{
loginMethods: ['email', 'wallet', 'google', 'twitter', 'github'],
appearance: {
theme: 'light',
accentColor: '#676FFF',
},
embeddedWallets: {
createOnLogin: 'users-without-wallets',
},
}}
>
<RouterProvider router={router} />
</PrivyProvider>
<QueryClientProvider client={queryClient}>
<PrivyProvider
appId="cmbhnmo1x000bla0mxudtd8z9"
config={{
loginMethods: ['email', 'google'],
appearance: {
theme: 'light',
accentColor: '#676FFF',
},
embeddedWallets: {
createOnLogin: 'users-without-wallets',
},
}}
>
<RouterProvider router={router} />
</PrivyProvider>
</QueryClientProvider>
);
}
102 changes: 102 additions & 0 deletions apps/connect/src/components/create-space.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { Key, type Messages, SpaceEvents, SpaceInfo, StoreConnect, Utils } from '@graphprotocol/hypergraph';
import { useIdentityToken } from '@privy-io/react-auth';
import { useQueryClient } from '@tanstack/react-query';
import { useSelector } from '@xstate/store/react';
import { Effect } from 'effect';
import { useState } from 'react';
import { Spinner } from './spinner';
import { Button } from './ui/button';
import { Input } from './ui/input';

export function CreateSpace() {
const [isLoading, setIsLoading] = useState(false);
const [spaceName, setSpaceName] = useState('');
const { identityToken } = useIdentityToken();
const accountAddress = useSelector(StoreConnect.store, (state) => state.context.accountAddress);
const keys = useSelector(StoreConnect.store, (state) => state.context.keys);
const queryClient = useQueryClient();

const createSpace = async () => {
setIsLoading(true);
if (!accountAddress || !keys || !identityToken) {
console.error('Missing required fields', { accountAddress, keys, identityToken });
Copy link

Copilot AI Jun 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Early return on missing fields leaves isLoading set to true. Move setIsLoading(false) into this branch or wrap the function body in a try/finally to always reset loading.

Suggested change
console.error('Missing required fields', { accountAddress, keys, identityToken });
console.error('Missing required fields', { accountAddress, keys, identityToken });
setIsLoading(false);

Copilot uses AI. Check for mistakes.

setIsLoading(false);
return;
}

try {
const { encryptionPrivateKey, encryptionPublicKey, signaturePrivateKey, signaturePublicKey } = keys;
const spaceId = Utils.generateId();

const spaceEvent = await Effect.runPromise(
SpaceEvents.createSpace({
author: {
accountAddress,
encryptionPublicKey,
signaturePrivateKey,
signaturePublicKey,
},
spaceId,
}),
);
const result = Key.createKey({
privateKey: Utils.hexToBytes(encryptionPrivateKey),
publicKey: Utils.hexToBytes(encryptionPublicKey),
});

const { infoContent, signature } = SpaceInfo.encryptAndSignSpaceInfo({
accountAddress,
name: spaceName,
secretKey: Utils.bytesToHex(result.key),
signaturePrivateKey,
spaceId,
});

const message: Messages.RequestConnectCreateSpaceEvent = {
type: 'connect-create-space-event',
event: spaceEvent,
spaceId: spaceEvent.transaction.id,
keyBox: {
accountAddress,
ciphertext: Utils.bytesToHex(result.keyBoxCiphertext),
nonce: Utils.bytesToHex(result.keyBoxNonce),
authorPublicKey: encryptionPublicKey,
id: Utils.generateId(),
},
infoContent: Utils.bytesToHex(infoContent),
infoSignature: signature,
name: spaceName,
};

const response = await fetch(`${import.meta.env.VITE_HYPERGRAPH_SYNC_SERVER_ORIGIN}/connect/spaces`, {
headers: {
'privy-id-token': identityToken,
'Content-Type': 'application/json',
},
method: 'POST',
body: JSON.stringify(message),
});
const data = await response.json();
if (data.space) {
queryClient.invalidateQueries({ queryKey: ['spaces'] });
setSpaceName('');
} else {
throw new Error('Failed to create space');
}
} catch (error) {
alert('Failed to create space');
console.error(error);
} finally {
setIsLoading(false);
}
};

return (
<>
<Input value={spaceName} onChange={(e) => setSpaceName(e.target.value)} />
<Button className="home-button" onClick={createSpace} disabled={isLoading}>
Create Space {isLoading ? <Spinner /> : null}
</Button>
</>
);
}
53 changes: 53 additions & 0 deletions apps/connect/src/components/spaces.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { useIdentityToken } from '@privy-io/react-auth';
import { useQuery } from '@tanstack/react-query';

export function Spaces() {
const { identityToken } = useIdentityToken();

const { isPending, error, data } = useQuery<{
spaces: {
id: string;
name: string;
appIdentities: { address: string; appId: string }[];
keyBoxes: {
id: string;
ciphertext: string;
nonce: string;
authorPublicKey: string;
}[];
}[];
}>({
queryKey: ['spaces'],
queryFn: async () => {
if (!identityToken) return;
const response = await fetch(`${import.meta.env.VITE_HYPERGRAPH_SYNC_SERVER_ORIGIN}/connect/spaces`, {
headers: { 'privy-id-token': identityToken },
});
return await response.json();
},
});

if (isPending) return 'Loading spaces …';

if (error) return `An error has occurred: ${error.message}`;

return (
<>
{data.spaces.map((space) => (
<div key={space.id}>
<h2>
{space.name} ({space.id})
<br />
---------
<br />
{space.appIdentities.map((appIdentity) => (
<div key={appIdentity.address}>
{appIdentity.appId} ({appIdentity.address})
</div>
))}
</h2>
</div>
))}
</>
);
}
29 changes: 26 additions & 3 deletions apps/connect/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { createFileRoute } from '@tanstack/react-router'
// Import Routes

import { Route as rootRoute } from './routes/__root'
import { Route as AuthenticateImport } from './routes/authenticate'
import { Route as IndexImport } from './routes/index'

// Create Virtual Routes
Expand All @@ -27,6 +28,12 @@ const LoginLazyRoute = LoginLazyImport.update({
getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/login.lazy').then((d) => d.Route))

const AuthenticateRoute = AuthenticateImport.update({
id: '/authenticate',
path: '/authenticate',
getParentRoute: () => rootRoute,
} as any)

const IndexRoute = IndexImport.update({
id: '/',
path: '/',
Expand All @@ -44,6 +51,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexImport
parentRoute: typeof rootRoute
}
'/authenticate': {
id: '/authenticate'
path: '/authenticate'
fullPath: '/authenticate'
preLoaderRoute: typeof AuthenticateImport
parentRoute: typeof rootRoute
}
'/login': {
id: '/login'
path: '/login'
Expand All @@ -58,36 +72,41 @@ declare module '@tanstack/react-router' {

export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/authenticate': typeof AuthenticateRoute
'/login': typeof LoginLazyRoute
}

export interface FileRoutesByTo {
'/': typeof IndexRoute
'/authenticate': typeof AuthenticateRoute
'/login': typeof LoginLazyRoute
}

export interface FileRoutesById {
__root__: typeof rootRoute
'/': typeof IndexRoute
'/authenticate': typeof AuthenticateRoute
'/login': typeof LoginLazyRoute
}

export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/login'
fullPaths: '/' | '/authenticate' | '/login'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/login'
id: '__root__' | '/' | '/login'
to: '/' | '/authenticate' | '/login'
id: '__root__' | '/' | '/authenticate' | '/login'
fileRoutesById: FileRoutesById
}

export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
AuthenticateRoute: typeof AuthenticateRoute
LoginLazyRoute: typeof LoginLazyRoute
}

const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
AuthenticateRoute: AuthenticateRoute,
LoginLazyRoute: LoginLazyRoute,
}

Expand All @@ -102,12 +121,16 @@ export const routeTree = rootRoute
"filePath": "__root.tsx",
"children": [
"/",
"/authenticate",
"/login"
]
},
"/": {
"filePath": "index.tsx"
},
"/authenticate": {
"filePath": "authenticate.tsx"
},
"/login": {
"filePath": "login.lazy.tsx"
}
Expand Down
Loading
Loading