feat: buddychain (#101)

This commit is contained in:
Danish Arora 2024-10-25 05:41:04 +05:30 committed by GitHub
parent 49f3a436d3
commit e823496abf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
50 changed files with 22601 additions and 19 deletions

View File

@ -21,7 +21,8 @@ jobs:
experimental/rln-identity, experimental/rln-identity,
dogfooding, dogfooding,
message-monitor, message-monitor,
flush-notes flush-notes,
buddybook
] ]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -40,7 +41,12 @@ jobs:
working-directory: "examples/${{ matrix.example }}" working-directory: "examples/${{ matrix.example }}"
- name: build - name: build
run: npm run build run: |
if [ "${{ matrix.example }}" == "buddybook" ]; then
npm run build:ci
else
npm run build
fi
working-directory: "examples/${{ matrix.example }}" working-directory: "examples/${{ matrix.example }}"
- name: test - name: test

View File

@ -57,6 +57,11 @@ This example uses Waku Relay to send and receive simple text messages.
- [website](https://lab.waku.org/experimental/relay-direct-rtc) - [website](https://lab.waku.org/experimental/relay-direct-rtc)
- Demonstrates: Relay over WebRTC. - Demonstrates: Relay over WebRTC.
### Buddychain App
- [code](examples/buddychain)
- [website](https://lab.waku.org/buddychain)
- Demonstrates: [Add a brief description of what the Buddychain app demonstrates]
# Continuous Integration # Continuous Integration

10
ci/Jenkinsfile vendored
View File

@ -44,6 +44,16 @@ pipeline {
stage('dogfooding') { steps { script { buildExample() } } } stage('dogfooding') { steps { script { buildExample() } } }
stage('message-monitor') { steps { script { buildExample() } } } stage('message-monitor') { steps { script { buildExample() } } }
stage('flush-notes') { steps { script { buildNextJSExample() } } } stage('flush-notes') { steps { script { buildNextJSExample() } } }
stage('buddybook') {
steps {
script {
dir('examples/buddybook') {
sh 'npm install'
sh 'npm run build:ci'
}
}
}
}
} }
} }

17
examples/buddybook/.gitignore vendored Normal file
View File

@ -0,0 +1,17 @@
node_modules
dist
dist-ssr
*.local
instructions
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.env

View File

@ -0,0 +1,50 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
- Configure the top-level `parserOptions` property like this:
```js
export default tseslint.config({
languageOptions: {
// other options...
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
},
})
```
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
- Optionally add `...tseslint.configs.stylisticTypeChecked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
```js
// eslint.config.js
import react from 'eslint-plugin-react'
export default tseslint.config({
// Set the react version
settings: { react: { version: '18.3' } },
plugins: {
// Add the react plugin
react,
},
rules: {
// other rules...
// Enable its recommended rules
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
},
})
```

View File

@ -0,0 +1,20 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}

View File

@ -0,0 +1,28 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)

View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Buddychain Dogfood</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

20578
examples/buddybook/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,60 @@
{
"name": "buddybook",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"build:ci": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-scroll-area": "^1.2.0",
"@radix-ui/react-slot": "^1.1.0",
"@t3-oss/env-core": "^0.9.2",
"@t3-oss/env-nextjs": "^0.9.2",
"@tailwindcss/typography": "^0.5.15",
"@tanstack/react-query": "^5.59.15",
"@waku/react": "0.0.7-0adebab",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"connectkit": "^1.8.2",
"lucide-react": "^0.453.0",
"protobufjs": "^7.4.0",
"qrcode.react": "^4.0.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-markdown": "^9.0.1",
"react-router-dom": "^6.27.0",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
"uuid": "^10.0.0",
"viem": "^2.21.28",
"wagmi": "^2.12.19",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/node": "^22.7.6",
"@types/react": "^18.3.10",
"@types/react-dom": "^18.3.0",
"autoprefixer": "^10.4.20",
"copy-webpack-plugin": "^11.0.0",
"eslint": "^8.56.0",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.12",
"postcss": "^8.4.47",
"tailwindcss": "^3.4.14",
"ts-loader": "^9.5.1",
"typescript": "^5.5.3",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.11.1",
"@vitejs/plugin-react": "^4.3.2",
"vite": "^5.4.8"
}
}

View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,42 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

View File

@ -0,0 +1,149 @@
import React, { useEffect, useState } from 'react'
import './App.css'
import Header from './components/Header'
import ChainCreationForm from './components/Chain/Create/CreationPreview'
import ChainList from './components/Chain/View/ChainList'
import { Button } from "@/components/ui/button"
import { type LightNode } from "@waku/sdk"
import { useWaku } from "@waku/react"
import { Loader2 } from "lucide-react"
import { Routes, Route, Navigate, Link } from 'react-router-dom'
import { BlockPayload, getMessagesFromStore, subscribeToFilter } from './lib/waku'
import TelemetryOptIn from './components/TelemetryOptIn';
import TelemetryPage from './components/TelemetryPage';
type Status = 'success' | 'in-progress' | 'error';
interface WakuStatus {
filter: Status;
store: Status;
}
function App() {
const [isListening, setIsListening] = useState(false);
const [chainsData, setChainsData] = useState<BlockPayload[]>([])
const { isLoading: isWakuLoading, error: wakuError, node } = useWaku();
// Add this new state
const [wakuStatus, setWakuStatus] = useState<WakuStatus>({
filter: 'in-progress',
store: 'in-progress',
});
const [telemetryOptIn, setTelemetryOptIn] = useState<boolean | null>(null);
useEffect(() => {
const storedOptIn = localStorage.getItem('telemetryOptIn');
if (storedOptIn !== null) {
setTelemetryOptIn(storedOptIn === 'true');
}
}, []);
useEffect(() => {
if (isWakuLoading || !node || node.libp2p.getConnections().length === 0 || chainsData.length > 0 || isListening) return;
setTimeout(() => {
setIsListening(true);
startMessageListening();
}, 3000);
}, [node, isWakuLoading, wakuStatus])
const handleTelemetryOptIn = (optIn: boolean) => {
setTelemetryOptIn(optIn);
localStorage.setItem('telemetryOptIn', optIn.toString());
};
if (isWakuLoading) {
return (
<div className="min-h-screen bg-background text-foreground flex justify-center items-center">
<Loader2 className="h-8 w-8 animate-spin" />
</div>
);
}
const startMessageListening = async () => {
console.log("Starting message listening")
try {
setWakuStatus(prev => ({ ...prev, store: 'in-progress' }));
const storeMessages = await getMessagesFromStore(node as LightNode)
setChainsData(storeMessages)
setWakuStatus(prev => ({ ...prev, store: 'success' }));
} catch (error) {
console.error("Error fetching messages from store:", error);
setWakuStatus(prev => ({ ...prev, store: 'error' }));
}
try {
setWakuStatus(prev => ({ ...prev, filter: 'in-progress' }));
await subscribeToFilter(node as LightNode, (message) => {
handleChainUpdate(message); // Use the same function for both updates
})
setWakuStatus(prev => ({ ...prev, filter: 'success' }));
} catch (error) {
console.error("Error subscribing to filter:", error);
setWakuStatus(prev => ({ ...prev, filter: 'error' }));
}
}
if (wakuError) {
return (
<div className="min-h-screen bg-background text-foreground flex flex-col justify-center items-center">
<p className="text-red-500">Error connecting to Waku network</p>
<p className="text-sm text-muted-foreground">{wakuError.toString()}</p>
</div>
);
}
const handleChainUpdate = (newBlock: BlockPayload) => {
setChainsData(prevChains => {
// Check if the block already exists
const blockExists = prevChains.some(block => block.blockUUID === newBlock.blockUUID);
if (blockExists) {
return prevChains; // Don't add duplicate blocks
}
return [...prevChains, newBlock];
});
};
if (telemetryOptIn === null) {
return <TelemetryOptIn onOptIn={handleTelemetryOptIn} />;
}
return (
<div className="min-h-screen bg-background text-foreground">
<Header wakuStatus={wakuStatus} />
<main className="container mx-auto px-4 py-8">
<Routes>
<Route path="/create" element={<ChainCreationForm />} />
<Route path="/view" element={<ChainList chainsData={chainsData} onChainUpdate={handleChainUpdate} />} />
<Route path="/" element={<Home />} />
<Route path="*" element={<Navigate to="/" replace />} />
<Route path="/telemetry" element={<TelemetryPage />} />
</Routes>
</main>
</div>
)
}
const Home: React.FC = () => (
<div className="space-y-6 text-center">
<h1 className="text-4xl font-bold">BuddyChain</h1>
<div className="max-w-md mx-auto p-6 bg-card rounded-lg shadow-md">
<Link to="/create">
<Button
className="w-full mb-4"
>
Create New Chain
</Button>
</Link>
<p className="text-muted-foreground">
Click the button above to start creating a new chain.
</p>
</div>
<p className="text-sm text-muted-foreground">
Welcome to BuddyChain - Create and share your chains!
</p>
</div>
)
export default App

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -0,0 +1,208 @@
import React, { useState } from 'react';
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import { Button } from "@/components/ui/button"
import { Label } from "@/components/ui/label"
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"
import { useAccount, useSignMessage } from 'wagmi'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"
import { Loader2 } from "lucide-react"
import QRCode from '@/components/QRCode';
import { v4 as uuidv4 } from 'uuid';
import { useWaku } from '@waku/react';
import { LightNode } from '@waku/sdk';
import { createMessage, encoder } from '@/lib/waku';
interface FormData {
title: string;
description: string;
uuid: string;
}
const DEFAULT_FORM_DATA: FormData = {
title: 'Devcon24 DeFi Dynamo',
description: 'A revolutionary blockchain for Devcon 24, focusing on scalable DeFi solutions and cross-chain interoperability.',
uuid: uuidv4(),
}
const ChainCreationForm: React.FC = () => {
const [formData, setFormData] = useState<FormData>(DEFAULT_FORM_DATA);
const [errors, setErrors] = useState<Partial<FormData>>({});
const [showModal, setShowModal] = useState<boolean>(false);
const [isSigning, setIsSigning] = useState<boolean>(false);
const [isSuccess, setIsSuccess] = useState<boolean>(false);
const [sendError, setSendError] = useState<string | null>(null);
const [signedMessage, setSignedMessage] = useState<string | null>(null);
const { node } = useWaku<LightNode>();
const { address } = useAccount();
const { signMessage } = useSignMessage({
mutation: {
async onSuccess(signature: string) {
if (!address || !node) return;
setSignedMessage(signature);
const message = createMessage({
chainUUID: formData.uuid,
blockUUID: uuidv4(),
title: formData.title,
description: formData.description,
signedMessage: signature,
timestamp: Date.now(),
signatures: [{address, signature}],
parentBlockUUID: null
});
await node?.lightPush.send(encoder, message)
setIsSuccess(true);
setIsSigning(false);
},
onError(error: Error) {
console.error('Error signing message:', error);
setIsSigning(false);
setSendError('Error signing message. Please try again.');
}
}
});
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormData(prevData => ({
...prevData,
[name]: value,
}));
if (errors[name as keyof FormData]) {
setErrors(prevErrors => ({
...prevErrors,
[name]: undefined,
}));
}
};
const validateForm = (): boolean => {
const newErrors: Partial<FormData> = {};
if (!formData.title.trim()) {
newErrors.title = 'Title is required';
}
if (!formData.description.trim()) {
newErrors.description = 'Description is required';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleCreateChain = (e: React.FormEvent) => {
e.preventDefault();
if (validateForm()) {
setShowModal(true);
}
};
const handleSubmit = async () => {
setIsSigning(true);
setSendError(null);
const message = `Create Chain:
Chain UUID: ${formData.uuid}
Title: ${formData.title}
Description: ${formData.description}
Timestamp: ${new Date().getTime()}
Signed by: ${address}`;
signMessage({ message });
};
const handleCloseModal = () => {
setShowModal(false);
setIsSuccess(false);
setIsSigning(false);
setSendError(null);
};
return (
<Card className="w-full max-w-2xl mx-auto">
<CardHeader>
<CardTitle>Create a New Chain</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleCreateChain} className="space-y-6">
<div className="space-y-2">
<Label htmlFor="title">Chain Title</Label>
<Input
type="text"
id="title"
name="title"
value={formData.title}
onChange={handleInputChange}
maxLength={50}
/>
{errors.title && <p className="text-sm text-destructive">{errors.title}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="description">Chain Description</Label>
<Textarea
id="description"
name="description"
value={formData.description}
onChange={handleInputChange}
maxLength={500}
/>
{errors.description && <p className="text-sm text-destructive">{errors.description}</p>}
</div>
<Button type="submit" className="w-full">Create Chain</Button>
</form>
</CardContent>
<Dialog open={showModal} onOpenChange={handleCloseModal}>
<DialogContent>
<DialogHeader>
<DialogTitle>{isSuccess ? "Chain Created" : "Chain Preview"}</DialogTitle>
</DialogHeader>
{!isSuccess ? (
<>
<div className="space-y-4">
<h4 className="text-xl font-semibold">{formData.title}</h4>
<p className="text-muted-foreground">{formData.description}</p>
{sendError && <p className="text-sm text-destructive">{sendError}</p>}
</div>
<DialogFooter className="sm:justify-start">
<Button type="button" variant="secondary" onClick={handleCloseModal}>
Edit
</Button>
<Button type="button" onClick={handleSubmit} disabled={isSigning}>
{isSigning ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Signing...
</>
) : (
'Sign'
)}
</Button>
</DialogFooter>
</>
) : (
<>
{signedMessage && (
<QRCode
data={{
chainUUID: formData.uuid,
blockUUID: uuidv4(),
title: formData.title,
description: formData.description,
signedMessage: signedMessage,
timestamp: Date.now(),
signatures: [{address: address!, signature: signedMessage}],
parentBlockUUID: null
}}
/>
)}
</>
)}
</DialogContent>
</Dialog>
</Card>
);
};
export default ChainCreationForm;

View File

@ -0,0 +1,111 @@
import React, { useState } from 'react';
import { useAccount, useSignMessage, useEnsName } from 'wagmi';
import type { LightNode } from '@waku/interfaces';
import { useWaku } from '@waku/react';
import { createMessage, encoder, BlockPayload } from '@/lib/waku';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Loader2 } from "lucide-react";
import QRCode from '@/components/QRCode';
import { v4 as uuidv4 } from 'uuid';
interface SignChainProps {
block: BlockPayload;
onSuccess: (newBlock: BlockPayload) => void;
}
const SignChain: React.FC<SignChainProps> = ({ block, onSuccess }) => {
const [isOpen, setIsOpen] = useState(false);
const [isSigning, setIsSigning] = useState(false);
const [error, setError] = useState<string | null>(null);
const { address } = useAccount();
const { data: ensName } = useEnsName({ address });
const { node } = useWaku<LightNode>();
const { signMessage } = useSignMessage({
mutation: {
async onSuccess(signature) {
if (!address || !node) return;
const newBlock: BlockPayload = {
chainUUID: block.chainUUID,
blockUUID: uuidv4(),
title: block.title,
description: block.description,
signedMessage: signature,
timestamp: Date.now(),
signatures: [{ address, signature }],
parentBlockUUID: block.blockUUID
};
try {
const wakuMessage = createMessage(newBlock);
const { failures, successes } = await node.lightPush.send(encoder, wakuMessage);
if (failures.length > 0 || successes.length === 0) {
throw new Error('Failed to send message to Waku network');
}
onSuccess(newBlock);
setIsOpen(false);
} catch (error) {
console.error('Error creating new block:', error);
setError('Failed to create new block. Please try again.');
} finally {
setIsSigning(false);
}
},
onError(error) {
console.error('Error signing message:', error);
setError('Error signing message. Please try again.');
setIsSigning(false);
}
}
});
const handleSign = () => {
setIsSigning(true);
setError(null);
const message = `Sign Block:
Chain UUID: ${block.chainUUID}
Block UUID: ${block.blockUUID}
Title: ${block.title}
Description: ${block.description}
Timestamp: ${new Date().getTime()}
Parent Block UUID: ${block.parentBlockUUID}
Signed by: ${ensName || address}`;
signMessage({ message });
};
return (
<>
<Button onClick={() => setIsOpen(true)}>Sign Chain</Button>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Sign Chain</DialogTitle>
<DialogDescription>
Review the block details and sign to add your signature to the chain.
</DialogDescription>
</DialogHeader>
<QRCode data={block} />
{error && <p className="text-sm text-destructive">{error}</p>}
<DialogFooter>
<Button variant="secondary" onClick={() => setIsOpen(false)}>Cancel</Button>
<Button onClick={handleSign} disabled={isSigning}>
{isSigning ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Signing...
</>
) : (
'Sign'
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
};
export default SignChain;

View File

@ -0,0 +1,95 @@
import React from 'react';
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { type BlockPayload } from '@/lib/waku';
import SignChain from '@/components/Chain/SignChain';
import { useEnsName } from 'wagmi';
interface ChainListProps {
chainsData: BlockPayload[];
onChainUpdate: (newBlock: BlockPayload) => void;
}
const ChainList: React.FC<ChainListProps> = ({ chainsData, onChainUpdate }) => {
const handleChainUpdate = (newBlock: BlockPayload) => {
onChainUpdate(newBlock);
};
const renderBlock = (block: BlockPayload, depth: number = 0) => {
const childBlocks = chainsData.filter(b => b.parentBlockUUID === block.blockUUID);
const totalSignatures = block.signatures.length + childBlocks.reduce((acc, child) => acc + child.signatures.length, 0);
return (
<li key={`${block.blockUUID}-${depth}`} className="mb-4">
<div className="flex items-start">
<div className="mr-4 mt-2">
{depth > 0 && (
<div className="w-6 h-6 border-l-2 border-b-2 border-gray-300"></div>
)}
</div>
{depth === 0 ? (
<Card className="flex-grow">
<CardHeader>
<CardTitle>{block.title}</CardTitle>
</CardHeader>
<CardContent>
<p>{block.description}</p>
<p className="text-sm text-muted-foreground mt-2">
Created at: {new Date(block.timestamp).toLocaleString()}
</p>
<p className="text-sm text-muted-foreground">
Total Signatures: {totalSignatures}
</p>
<p className="text-sm text-muted-foreground">
Block UUID: {block.blockUUID}
</p>
<div className="mt-2">
<SignChain block={block} onSuccess={handleChainUpdate} />
</div>
</CardContent>
</Card>
) : (
<div className="flex-grow">
<SignerName address={block.signatures[0].address} />
</div>
)}
</div>
{childBlocks.length > 0 && (
<ul className="ml-8 mt-2">
{childBlocks.map((childBlock) => renderBlock(childBlock, depth + 1))}
</ul>
)}
</li>
);
};
const rootBlocks = chainsData.filter(block => !block.parentBlockUUID);
return (
<Card className="w-full max-w-4xl mx-auto">
<CardHeader>
<CardTitle>Existing Chains</CardTitle>
</CardHeader>
<CardContent>
{rootBlocks.length === 0 ? (
<p>No chains found.</p>
) : (
<ul className="space-y-4">
{rootBlocks.map((block) => renderBlock(block, 0))}
</ul>
)}
</CardContent>
</Card>
);
};
const SignerName: React.FC<{ address: `0x${string}` }> = ({ address }) => {
const { data: ensName } = useEnsName({ address })
return (
<p className="text-sm">
Signed by: {ensName || `${address.slice(0, 6)}...${address.slice(-4)}`}
</p>
);
};
export default ChainList;

View File

@ -0,0 +1,128 @@
import React, { useState, useEffect } from 'react';
import { useAccount, useDisconnect, useEnsName } from 'wagmi';
import { ConnectKitButton } from 'connectkit';
import { Button } from "@/components/ui/button";
import { useWaku } from "@waku/react";
import { Loader2 } from "lucide-react";
import { Link, useLocation } from 'react-router-dom';
// Add these new types
type Status = 'success' | 'in-progress' | 'error';
interface WakuStatus {
filter: Status;
store: Status;
}
interface HeaderProps {
wakuStatus: WakuStatus;
}
const Header: React.FC<HeaderProps> = ({ wakuStatus }) => {
const { address, isConnected } = useAccount();
const { disconnect } = useDisconnect();
const { isLoading: isWakuLoading, error: wakuError, node: waku } = useWaku();
const [connections, setConnections] = useState(0);
const location = useLocation();
const { data: ensName } = useEnsName({ address });
useEffect(() => {
if (waku) {
const updateConnections = () => {
setConnections(waku.libp2p.getConnections().length);
};
updateConnections();
waku.libp2p.addEventListener("peer:connect", updateConnections);
waku.libp2p.addEventListener("peer:disconnect", updateConnections);
return () => {
waku.libp2p.removeEventListener("peer:connect", updateConnections);
waku.libp2p.removeEventListener("peer:disconnect", updateConnections);
};
}
}, [waku]);
const getStatusColor = (status: Status) => {
switch (status) {
case 'success':
return 'bg-green-500';
case 'in-progress':
return 'bg-yellow-500';
case 'error':
return 'bg-red-500';
}
};
return (
<header className="bg-background border-b border-border">
<div className="container mx-auto px-4 py-4 flex justify-between items-center">
<div className="flex items-center space-x-4">
<h1 className="text-2xl font-bold">BuddyBook</h1>
<nav>
<ul className="flex space-x-4">
<li>
<Link
to="/create"
className={`text-sm ${location.pathname === '/create' ? 'text-primary font-semibold' : 'text-muted-foreground'}`}
>
Create Chain
</Link>
</li>
<li>
<Link
to="/view"
className={`text-sm ${location.pathname === '/view' ? 'text-primary font-semibold' : 'text-muted-foreground'}`}
>
View Existing Chains
</Link>
</li>
<li>
<Link
to="/telemetry"
className={`text-sm ${location.pathname === '/telemetry' ? 'text-primary font-semibold' : 'text-muted-foreground'}`}
>
Telemetry
</Link>
</li>
</ul>
</nav>
</div>
<div className="flex items-center space-x-2">
<div className="flex items-center space-x-1">
<span className="text-sm text-muted-foreground">Filter:</span>
<div className={`w-3 h-3 rounded-full ${getStatusColor(wakuStatus.filter)}`}></div>
</div>
<div className="flex items-center space-x-1">
<span className="text-sm text-muted-foreground">Store:</span>
<div className={`w-3 h-3 rounded-full ${getStatusColor(wakuStatus.store)}`}></div>
</div>
{isWakuLoading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : wakuError ? (
<span className="text-sm text-red-500">Waku Error</span>
) : (
<span className="text-sm text-muted-foreground">
Waku Connections: {connections}
</span>
)}
{isConnected ? (
<>
<span className="text-sm text-muted-foreground">
{ensName || (address ? `${address.slice(0, 6)}...${address.slice(-4)}` : '')}
</span>
<Button variant="outline" size="sm" onClick={() => disconnect()}>
Logout
</Button>
</>
) : (
<ConnectKitButton />
)}
</div>
</div>
</header>
);
};
export default Header;

View File

@ -0,0 +1,53 @@
import React from 'react';
import { QRCodeSVG } from 'qrcode.react';
import { BlockPayload } from '@/lib/waku';
import { Button } from '@/components/ui/button';
import { useEnsName } from 'wagmi';
interface QRCodeProps {
data: BlockPayload;
size?: number;
onSign?: () => void;
}
const QRCode: React.FC<QRCodeProps> = ({ data, size = 256, onSign }) => {
const shareableLink = `${window.location.origin}/view/${data.chainUUID}/${data.blockUUID}`;
return (
<div className="flex flex-col items-center space-y-4">
<QRCodeSVG value={shareableLink} size={size} />
<div className="text-sm text-muted-foreground">
<p><strong>Title:</strong> {data.title}</p>
<p><strong>Description:</strong> {data.description}</p>
<p><strong>Timestamp:</strong> {new Date(data.timestamp).toLocaleString()}</p>
<p><strong>Signed Message:</strong> {`0x${data.signedMessage.slice(2, 6)}....${data.signedMessage.slice(-6)}`}</p>
<p><strong>Parent Block:</strong> {data.parentBlockUUID || 'Root'}</p>
<p><strong>Signatures:</strong></p>
<ul>
{data.signatures.map((sig, index) => (
<SignatureItem key={index} address={sig.address} />
))}
</ul>
</div>
<input
type="text"
value={shareableLink}
readOnly
className="w-full p-2 border rounded"
/>
{onSign && <Button onClick={onSign}>Sign This Block</Button>}
</div>
);
};
const SignatureItem: React.FC<{ address: `0x${string}` }> = ({ address }) => {
const { data: ensName } = useEnsName({ address });
return (
<li>
{ensName || `${address.slice(0, 6)}...${address.slice(-4)}`}
</li>
);
};
export default QRCode;

View File

@ -0,0 +1,54 @@
import React, { useState } from 'react';
import { Button } from "@/components/ui/button";
import { Card, CardHeader, CardTitle, CardContent, CardFooter } from "@/components/ui/card";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
import { ScrollArea } from "@/components/ui/scroll-area";
import { privacyPolicy } from '@/lib/privacyPolicy';
import ReactMarkdown from 'react-markdown';
interface TelemetryOptInProps {
onOptIn: (optIn: boolean) => void;
}
const TelemetryOptIn: React.FC<TelemetryOptInProps> = ({ onOptIn }) => {
const [showFullPolicy, setShowFullPolicy] = useState(false);
return (
<div className="min-h-screen flex items-center justify-center bg-background">
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle>Telemetry Data Collection</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground mb-4">
We collect telemetry data to improve our services. This data is anonymous and helps us understand how our application is used. You can opt-in or opt-out of this data collection.
</p>
<Button variant="link" onClick={() => setShowFullPolicy(true)}>
View Full Privacy Policy
</Button>
</CardContent>
<CardFooter className="flex justify-between">
<Button variant="outline" onClick={() => onOptIn(false)}>Opt Out</Button>
<Button onClick={() => onOptIn(true)}>Opt In</Button>
</CardFooter>
</Card>
<Dialog open={showFullPolicy} onOpenChange={setShowFullPolicy}>
<DialogContent className="max-w-4xl max-h-[80vh]">
<DialogHeader>
<DialogTitle>Privacy Policy</DialogTitle>
</DialogHeader>
<ScrollArea className="mt-4 h-[60vh]">
<DialogDescription className="space-y-4">
<ReactMarkdown className="prose dark:prose-invert max-w-none">
{privacyPolicy}
</ReactMarkdown>
</DialogDescription>
</ScrollArea>
</DialogContent>
</Dialog>
</div>
);
};
export default TelemetryOptIn;

View File

@ -0,0 +1,57 @@
import React, { useState, useEffect } from 'react';
import { Button } from "@/components/ui/button";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { ScrollArea } from "@/components/ui/scroll-area";
import { privacyPolicy } from '@/lib/privacyPolicy';
import ReactMarkdown from 'react-markdown';
const TelemetryPage: React.FC = () => {
const [telemetryOptIn, setTelemetryOptIn] = useState<boolean>(false);
useEffect(() => {
const storedOptIn = localStorage.getItem('telemetryOptIn');
if (storedOptIn !== null) {
setTelemetryOptIn(storedOptIn === 'true');
}
}, []);
const handleToggleTelemetry = () => {
const newOptIn = !telemetryOptIn;
setTelemetryOptIn(newOptIn);
localStorage.setItem('telemetryOptIn', newOptIn.toString());
};
return (
<Card className="w-full max-w-4xl mx-auto">
<CardHeader>
<CardTitle>Telemetry Settings</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-6">
<div>
<p className="text-sm text-muted-foreground mb-2">
We collect telemetry data to improve our services. This data is anonymous and helps us understand how our application is used.
</p>
<p className="font-semibold mb-2">
Current status: {telemetryOptIn ? 'Opted In' : 'Opted Out'}
</p>
<Button onClick={handleToggleTelemetry}>
{telemetryOptIn ? 'Opt Out' : 'Opt In'}
</Button>
</div>
<div>
<h3 className="text-lg font-semibold mb-4">Privacy Policy</h3>
<ScrollArea className="h-[60vh] border rounded-md p-4">
<ReactMarkdown className="prose dark:prose-invert max-w-none">
{privacyPolicy}
</ReactMarkdown>
</ScrollArea>
</div>
</div>
</CardContent>
</Card>
);
};
export default TelemetryPage;

View File

@ -0,0 +1,57 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@ -0,0 +1,76 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@ -0,0 +1,120 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { Cross2Icon } from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View File

@ -0,0 +1,25 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

View File

@ -0,0 +1,24 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

View File

@ -0,0 +1,46 @@
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar }

View File

@ -0,0 +1,24 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = "Textarea"
export { Textarea }

View File

@ -0,0 +1,10 @@
import { createEnv } from "@t3-oss/env-core";
import { z } from "zod";
export const env = createEnv({
clientPrefix: "VITE_",
client: {
VITE_WALLETCONNECT_PROJECT_ID: z.string().min(1),
},
runtimeEnv: import.meta.env,
});

View File

@ -0,0 +1,66 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

View File

@ -0,0 +1,84 @@
export const privacyPolicy = `
# Waku - Dogfooding Website
*Last updated: 22 August 2024*
This Privacy Policy is intended to inform users of our approach to privacy in respect of this website ("Website"). In this regard, if you are visiting or interacting with our Website, this Privacy Policy applies to you.
This Website has been set-up by us for the purposes of gathering telemetry data from users of the Website about the reliability protocols utilised by Waku (such activity and related activity, being referred to as "Dogfooding").
Waku is a family of robust, censorship-resistant communication protocols designed to enable privacy-focused messaging for web3 apps. You can read more about Waku here: https://waku.org/.
## Who we are
For the purposes of this Privacy Policy and the collection and processing of personal data as a controller, the relevant entity is the Logos Collective Association, which has its registered office in Zug and its legal domicile address at
Logos Collective Association
c/o PST Consulting GmbH
Baarerstrasse 10
6300 Zug
Switzerland
Whenever we refer to "Logos", "we" or other similar references, we are referring to the Logos Collective Association.
## We limit the collection and processing of personal data from your use of the Website and for "Dogfooding" purposes
We aim to limit the collection and processing of personal data from users of the Website and your participation in Dogfooding. We only collect and process certain personal data for specific purposes and where we have the legal basis to do so under applicable privacy legislation. We will not collect or process any personal data that we don't need and where we do store any personal data, we will only store it for the least amount of time needed for the indicated purpose and in any event no longer than thirty (30) days.
In this regard, we collect and process the following personal data from your use of the Website and your participation in Dogfooding:
**IP address**: As part of such use of the Website, we briefly process your IP address. We however have a legitimate interest in processing such IP addresses to ensure the technical functionality and enhance the security measures of the Website. This IP address is not stored by us over time.
**Telemetry data**: We collect certain technical information (also referred to as telemetry data) to primarily determine the reliability (including, the performance and usage patterns) of the Waku reliability protocols for light clients. This information is only collected once you have accepted the prompt on the Website to participate in Dogfooding. Once you've accepted this prompt, a Waku node will start operating on your browser and then the technical information will start being collected by us.
The information collected includes:
- timestamps of receiving and sending messages;
- size of message packets;
- content topics/pubsub topics of the messages;
- code logs of errors and warnings; and
- a randomly generated temporary peer ID that represents your Waku node.
The temporary peer ID is a seeded identifier used for the duration of your participation in Dogfooding and allows us to correlate the obtained technical information with a particular session and with the additional information collected, could be potentially considered personal data. We process such data based on our legitimate interest to improve our software and the user's experience in respect of Waku.
If you do not agree to this data collection and processing, please do not interact with the Website or proceed with your participation in Dogfooding.
## Personal data sharing with third party service providers
We may share personal data with third party service providers in order for us to fulfil the above purposes. Such third party service providers act as data processors on our behalf and are only permitted to process personal data in accordance with our instructions and for the purposes specified above.
## Security measures we take in respect of the Website
As a general approach, we take data security seriously and we have implemented a variety of security measures on the Website that are reasonably designed to maintain the safety of your personal data when you submit such information to us.
In addition to the security measures of the Website, personal data is also protected by Waku (protocol) itself. You can read more about the security features implemented by the Waku protocol here.
## Exporting data outside the European Union and Switzerland
We are obliged to protect the privacy of personal data that you may have submitted in the unlikely event that we export your personal data to places outside the European Union or Switzerland. This means that personal data will only be processed in countries or by parties that provide an adequate level of protection as deemed by Switzerland or the European Commission. Otherwise, we will use other forms of protections, such as specific forms of contractual clauses to ensure such personal data is provided the same protection as required in Switzerland or Europe. In any event, the transmission of personal data outside the European Union and Switzerland will always occur in conformity with applicable privacy legislation.
## Your choices and rights
As explained in this Privacy Policy, we limit our collection and processing of your personal data wherever possible. Nonetheless, you still have certain choices and rights in respect of the personal data which we do collect and process. As laid out in relevant privacy legislation, you have the right to:
- Ask us to correct or update your personal data (where reasonably possible);
- Ask us to remove your personal data from our systems;
- Ask us for a copy of your personal data, which may also be transferred to another data controller at your request;
- Withdraw your consent to process your personal data (only if consent was asked for a processing activity), which only affects processing activities that are based on your consent and doesn't affect the validity of such processing activities before you have withdrawn your consent;
- Object to the processing of your personal data; and
- File a complaint with the Federal Data Protection and Information Commissioner (FDPIC), if you believe that your personal data has been processed unlawfully.
## Third party links
On this Website, you may come across links to third party websites. These third party sites have separate and independent privacy policies. We therefore have no responsibility or liability for the content and activities of these third party websites.
## This Privacy Policy might change
We may modify or replace any part of this Privacy Policy at any time and without notice. Please check the Website periodically for any changes. The new Privacy Policy will be effective immediately upon its posting on our Website.
## Contact information
To the extent that you have any questions about the Privacy Policy, please contact us at legal@free.technology.
This document is licensed under CC-BY-SA.
`;

View File

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@ -0,0 +1,101 @@
import { createEncoder, createDecoder, type LightNode, type CreateWakuNodeOptions } from "@waku/sdk";
import protobuf from 'protobufjs';
export const WAKU_NODE_OPTIONS: CreateWakuNodeOptions = { defaultBootstrap: true, nodeToUse: {
store: "/dns4/store-02.ac-cn-hongkong-c.status.staging.status.im/tcp/443/wss/p2p/16Uiu2HAmU7xtcwytXpGpeDrfyhJkiFvTkQbLB9upL5MXPLGceG9K"
} };
export type Signature = {
address: `0x${string}`;
signature: string;
};
export type BlockPayload = {
chainUUID: string;
blockUUID: string;
title: string;
description: string;
signedMessage: string;
timestamp: number;
signatures: Signature[];
parentBlockUUID: string | null;
}
const contentTopic = "/buddychain-dogfood/1/chain/proto";
export const encoder = createEncoder({
contentTopic: contentTopic,
ephemeral: false
});
export const decoder = createDecoder(contentTopic);
export const block = new protobuf.Type("block")
.add(new protobuf.Field("chainUUID", 1, "string"))
.add(new protobuf.Field("blockUUID", 2, "string"))
.add(new protobuf.Field("title", 3, "string"))
.add(new protobuf.Field("description", 4, "string"))
.add(new protobuf.Field("signedMessage", 5, "string"))
.add(new protobuf.Field("timestamp", 6, "uint64"))
.add(new protobuf.Field("signatures", 7, "string", "repeated"))
.add(new protobuf.Field("parentBlockUUID", 8, "string"));
export function createMessage({
chainUUID,
blockUUID,
title,
description,
signedMessage,
timestamp,
signatures,
parentBlockUUID
}: BlockPayload) {
const protoMessage = block.create({
chainUUID,
blockUUID,
title,
description,
signedMessage,
timestamp,
signatures: signatures.map(s => JSON.stringify(s)),
parentBlockUUID
});
const payload = block.encode(protoMessage).finish();
return { payload: payload };
}
export async function getMessagesFromStore(node: LightNode) {
console.time("getMessagesFromStore")
const messages: BlockPayload[] = [];
await node.store.queryWithOrderedCallback([decoder], async (message) => {
console.log(message)
if (!message.payload) return;
const blockPayload = block.decode(message.payload) as unknown as BlockPayload;
blockPayload.signatures = blockPayload.signatures.map(s => JSON.parse(s as unknown as string) as Signature);
messages.push(blockPayload);
})
console.timeEnd("getMessagesFromStore")
return messages;
}
export async function subscribeToFilter(node: LightNode, callback: (message: BlockPayload) => void) {
const {error, subscription, results} = await node.filter.subscribe([decoder], (message) => {
console.log('message received from filter', message)
if (message.payload) {
const blockPayload = block.decode(message.payload) as unknown as BlockPayload;
blockPayload.signatures = blockPayload.signatures.map(s => JSON.parse(s as unknown as string) as Signature);
callback(blockPayload);
}
}, {forceUseAllPeers: true});
console.log("results", results)
if (error) {
console.log("Error subscribing to filter", error)
}
if (!subscription || error || results.successes.length === 0 ||results.failures.length >0) {
throw new Error("Failed to subscribe to filter")
}
}

View File

@ -0,0 +1,15 @@
import { mainnet } from 'wagmi/chains'
import { createConfig, http } from 'wagmi'
import { getDefaultConfig } from 'connectkit'
import { env } from '@/env'
export const config = createConfig(
getDefaultConfig({
appName: 'BuddyBook',
walletConnectProjectId: env.VITE_WALLETCONNECT_PROJECT_ID,
chains: [mainnet],
transports: {
[mainnet.id]: http(),
},
}),
)

View File

@ -0,0 +1,30 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { WagmiProvider } from 'wagmi'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ConnectKitProvider} from 'connectkit'
import { BrowserRouter as Router } from 'react-router-dom'
import App from './App.tsx'
import './index.css'
import { LightNodeProvider } from "@waku/react";
import { config } from './lib/walletConnect.ts'
import { WAKU_NODE_OPTIONS } from './lib/waku.ts'
const queryClient = new QueryClient()
createRoot(document.getElementById('root')!).render(
<StrictMode>
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<ConnectKitProvider>
<LightNodeProvider options={WAKU_NODE_OPTIONS}>
<Router>
<App />
</Router>
</LightNodeProvider>
</ConnectKitProvider>
</QueryClientProvider>
</WagmiProvider>
</StrictMode>,
)

1
examples/buddybook/src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@ -0,0 +1,63 @@
/** @type {import('tailwindcss').Config} */
export default {
darkMode: ["class"],
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
},
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
},
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
chart: {
'1': 'hsl(var(--chart-1))',
'2': 'hsl(var(--chart-2))',
'3': 'hsl(var(--chart-3))',
'4': 'hsl(var(--chart-4))',
'5': 'hsl(var(--chart-5))'
}
}
}
},
plugins: [
require('@tailwindcss/typography'),
require("tailwindcss-animate")
],
}

View File

@ -0,0 +1,8 @@
- [ ] waku connections on header should have green/yellow/red color indicator
- [ ] clicking on the indicator should show a list of peers
- [ ] chains can't be signed twice by an address
- [ ] generate waku peer id using the wallet address
- [ ] telemetry
- [ x ] disclaimer
- [ ] functionality
- [ ] landing page

View File

@ -0,0 +1,33 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
/* Path aliases */
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@ -0,0 +1 @@
{"root":["./src/app.tsx","./src/env.ts","./src/main.tsx","./src/vite-env.d.ts","./src/components/header.tsx","./src/components/chain/creation/chaincreationform.tsx","./src/components/chain/preview/chainpreview.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/textarea.tsx","./src/lib/utils.ts"],"errors":true,"version":"5.6.3"}

View File

@ -0,0 +1,13 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}

View File

@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}

View File

@ -0,0 +1 @@
{"root":["./vite.config.ts"],"version":"5.6.3"}

View File

@ -0,0 +1,12 @@
import path from "path"
import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
})

View File

@ -0,0 +1,34 @@
const CopyWebpackPlugin = require("copy-webpack-plugin");
const path = require("path");
module.exports = {
entry: "./src/main.tsx",
output: {
path: path.resolve(__dirname, "build"),
filename: "index.js",
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx'],
},
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
experiments: {
asyncWebAssembly: true,
},
mode: "development",
plugins: [
new CopyWebpackPlugin({
patterns: [
{ from: "index.html" },
{ from: "public", to: "public" }
],
}),
],
};

View File

@ -10,7 +10,7 @@
"@types/node": "^16.18.111", "@types/node": "^16.18.111",
"@types/react": "^18.3.9", "@types/react": "^18.3.9",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"@waku/sdk": "^0.0.29-338250a.0", "@waku/sdk": "0.0.29-de10ff4.0",
"protobufjs": "^7.4.0", "protobufjs": "^7.4.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",

View File

@ -99,7 +99,7 @@ function App() {
<h1 className="text-4xl font-bold text-gray-800 mb-8">Waku Message Monitor</h1> <h1 className="text-4xl font-bold text-gray-800 mb-8">Waku Message Monitor</h1>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8"> <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div className="space-y-8"> <div className="space-y-8">
<MessageList title="Sent Messages" messages={sentMessages} showSequence={true} /> <MessageList title="Sent Messages" messages={sentMessages.reverse()} showSequence={true} />
<MessageList title="Received Messages" messages={receivedMessages} showSequence={false} /> <MessageList title="Received Messages" messages={receivedMessages} showSequence={false} />
</div> </div>
<div className="space-y-8"> <div className="space-y-8">

View File

@ -30,10 +30,14 @@ const MessageList: React.FC<MessageListProps> = ({ title, messages, showSequence
if (!acc[message.sequenceId]) { if (!acc[message.sequenceId]) {
acc[message.sequenceId] = []; acc[message.sequenceId] = [];
} }
acc[message.sequenceId].unshift(message); // Use unshift instead of push acc[message.sequenceId].push(message);
return acc; return acc;
}, {} as Record<number, Message[]>) }, {} as Record<number, Message[]>)
: { 0: messages.slice().reverse() }; // Reverse the messages array : { 0: messages };
// Sort sequences in descending order
const sortedSequences = Object.entries(groupedMessages)
.sort((a, b) => Number(b[0]) - Number(a[0]));
if (messages.length === 0) { if (messages.length === 0) {
return ( return (
@ -50,14 +54,16 @@ const MessageList: React.FC<MessageListProps> = ({ title, messages, showSequence
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<h2 className="text-xl font-semibold mb-4 text-gray-700">{title}</h2> <h2 className="text-xl font-semibold mb-4 text-gray-700">{title}</h2>
<div className="bg-gray-50 rounded-lg p-4 h-96 overflow-y-auto border border-gray-200"> <div className="bg-gray-50 rounded-lg p-4 h-96 overflow-y-auto border border-gray-200">
{Object.entries(groupedMessages).map(([sequenceId, sequenceMessages]) => ( {sortedSequences.map(([sequenceId, sequenceMessages]) => (
<div key={sequenceId} className={`mb-4 p-2 rounded-lg ${showSequence ? getSequenceColor(Number(sequenceId)) : ''}`}> <div key={sequenceId} className={`mb-4 p-2 rounded-lg ${showSequence ? getSequenceColor(Number(sequenceId)) : ''}`}>
{showSequence && <div className="text-xs font-semibold mb-2">Sequence {sequenceId}</div>} {showSequence && <div className="text-xs font-semibold mb-2">Sequence {sequenceId}</div>}
{sequenceMessages.map((message: Message, index: number) => ( {sequenceMessages
.sort((a, b) => b.timestamp - a.timestamp)
.map((message: Message, index: number) => (
<div key={index} className="text-sm mb-2 font-mono bg-white p-2 rounded shadow-sm"> <div key={index} className="text-sm mb-2 font-mono bg-white p-2 rounded shadow-sm">
{shortenMessage(message.content)} {shortenMessage(message.content)}
</div> </div>
)).reverse()} ))}
</div> </div>
))} ))}
</div> </div>

View File

@ -92,11 +92,13 @@ export async function startLightPushSequence(
if (success) { if (success) {
const messageText = JSON.stringify({ content: `${reportingHash} - ${sequenceIndex + 1} of ${sequenceTotal}`, success: true }); const messageText = JSON.stringify({ content: `${reportingHash} - ${sequenceIndex + 1} of ${sequenceTotal}`, success: true });
onMessageSent(messageText, true); onMessageSent(messageText, true);
sequenceIndex++;
} else { } else {
onMessageSent(JSON.stringify({ content: `Failed to send message ${sequenceIndex + 1} of ${sequenceTotal}`, success: false }), false); const messageText = JSON.stringify({ content: `Failed to send message ${sequenceIndex + 1} of ${sequenceTotal}`, success: false });
onMessageSent(messageText, false);
} }
sequenceIndex++;
if (sequenceIndex < sequenceTotal) { if (sequenceIndex < sequenceTotal) {
let countdown = period / 1000; let countdown = period / 1000;
const countdownInterval = setInterval(() => { const countdownInterval = setInterval(() => {
@ -113,7 +115,7 @@ export async function startLightPushSequence(
}, period); }, period);
} else { } else {
console.info("Lightpush sequence completed"); console.info("Lightpush sequence completed");
updateSequenceId(sequenceIndex + 1); updateSequenceId(sequenceIndex);
updateCountdown(null); updateCountdown(null);
isRunning.current = false; isRunning.current = false;
// Start a new sequence after a delay // Start a new sequence after a delay
@ -121,11 +123,21 @@ export async function startLightPushSequence(
} }
} catch (error) { } catch (error) {
console.error("Error sending message", error); console.error("Error sending message", error);
onMessageSent(JSON.stringify({ content: `Error sending message ${sequenceIndex + 1} of ${sequenceTotal}`, success: false }), false); const messageText = JSON.stringify({ content: `Error sending message ${sequenceIndex + 1} of ${sequenceTotal}`, success: false });
onMessageSent(messageText, false);
sequenceIndex++;
if (sequenceIndex < sequenceTotal) {
setTimeout(() => sendMessage(), period);
} else {
console.info("Lightpush sequence completed with errors");
updateSequenceId(sequenceIndex);
updateCountdown(null);
isRunning.current = false; isRunning.current = false;
// Retry starting a new sequence after a delay // Start a new sequence after a delay
setTimeout(() => startLightPushSequence(waku, telemetryClient, onMessageSent, updateSequenceId, updateCountdown, isRunning, numMessages, period), period); setTimeout(() => startLightPushSequence(waku, telemetryClient, onMessageSent, updateSequenceId, updateCountdown, isRunning, numMessages, period), period);
} }
}
}; };
sendMessage(); sendMessage();