Skip to content

Commit

Permalink
feat(buddychain): multi signing and view sharable links (#103)
Browse files Browse the repository at this point in the history
  • Loading branch information
danisharora099 authored Oct 25, 2024
1 parent fc20680 commit 49460a2
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 52 deletions.
12 changes: 9 additions & 3 deletions examples/buddybook/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ 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 { Routes, Route, Navigate, Link, useParams } from 'react-router-dom'
import { BlockPayload, getMessagesFromStore, subscribeToFilter } from './lib/waku'
import TelemetryOptIn from './components/TelemetryOptIn';
import TelemetryPage from './components/TelemetryPage';
import SignSharedChain from './components/Chain/SignSharedChain'

type Status = 'success' | 'in-progress' | 'error';

Expand All @@ -29,6 +30,7 @@ function App() {
store: 'in-progress',
});
const [telemetryOptIn, setTelemetryOptIn] = useState<boolean | null>(null);
const [isLoadingChains, setIsLoadingChains] = useState(true);

useEffect(() => {
const storedOptIn = localStorage.getItem('telemetryOptIn');
Expand Down Expand Up @@ -65,12 +67,15 @@ function App() {
console.log("Starting message listening")
try {
setWakuStatus(prev => ({ ...prev, store: 'in-progress' }));
setIsLoadingChains(true);
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' }));
} finally {
setIsLoadingChains(false);
}

try {
Expand Down Expand Up @@ -115,10 +120,11 @@ function App() {
<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="/view" element={<ChainList chainsData={chainsData} onChainUpdate={handleChainUpdate} isLoading={isLoadingChains} />} />
<Route path="/" element={<Home />} />
<Route path="*" element={<Navigate to="/" replace />} />
<Route path="/sign/:chainUUID/:blockUUID" element={<SignSharedChain chainsData={chainsData} onChainUpdate={handleChainUpdate} />} />
<Route path="/telemetry" element={<TelemetryPage />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</main>
</div>
Expand Down
34 changes: 30 additions & 4 deletions examples/buddybook/src/components/Chain/SignChain.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { useAccount, useSignMessage, useEnsName } from 'wagmi';
import type { LightNode } from '@waku/interfaces';
import { useWaku } from '@waku/react';
Expand All @@ -18,14 +18,30 @@ 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 [alreadySigned, setAlreadySigned] = useState(false);
const { address } = useAccount();
const { data: ensName } = useEnsName({ address });
const { node } = useWaku<LightNode>();

useEffect(() => {
if (address) {
const hasAlreadySigned = block.signatures.some(sig => sig.address.toLowerCase() === address.toLowerCase());
setAlreadySigned(hasAlreadySigned);
}
}, [address, block.signatures]);

const { signMessage } = useSignMessage({
mutation: {
async onSuccess(signature) {
if (!address || !node) return;

// Check if the address has already signed
if (block.signatures.some(sig => sig.address.toLowerCase() === address.toLowerCase())) {
setError('You have already signed this chain.');
setIsSigning(false);
return;
}

const newBlock: BlockPayload = {
chainUUID: block.chainUUID,
blockUUID: uuidv4(),
Expand Down Expand Up @@ -63,6 +79,10 @@ const SignChain: React.FC<SignChainProps> = ({ block, onSuccess }) => {
});

const handleSign = () => {
if (alreadySigned) {
setError('You have already signed this chain.');
return;
}
setIsSigning(true);
setError(null);
const message = `Sign Block:
Expand All @@ -78,25 +98,31 @@ const SignChain: React.FC<SignChainProps> = ({ block, onSuccess }) => {

return (
<>
<Button onClick={() => setIsOpen(true)}>Sign Chain</Button>
<Button onClick={() => setIsOpen(true)} disabled={alreadySigned}>
{alreadySigned ? 'Already Signed' : '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.
{alreadySigned
? 'You have already signed this chain.'
: '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}>
<Button onClick={handleSign} disabled={isSigning || alreadySigned}>
{isSigning ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Signing...
</>
) : alreadySigned ? (
'Already Signed'
) : (
'Sign'
)}
Expand Down
53 changes: 53 additions & 0 deletions examples/buddybook/src/components/Chain/SignSharedChain.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React, { useEffect, useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { BlockPayload } from '@/lib/waku';
import SignChain from './SignChain';
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";

interface SignSharedChainProps {
chainsData: BlockPayload[];
onChainUpdate: (newBlock: BlockPayload) => void;
}

const SignSharedChain: React.FC<SignSharedChainProps> = ({ chainsData, onChainUpdate }) => {
const { chainUUID, blockUUID } = useParams();
const [block, setBlock] = useState<BlockPayload | null>(null);
const navigate = useNavigate();

useEffect(() => {
const foundBlock = chainsData.find(b => b.chainUUID === chainUUID && b.blockUUID === blockUUID);
if (foundBlock) {
setBlock(foundBlock);
}
}, [chainsData, chainUUID, blockUUID]);

if (!block) {
return (
<Card className="w-full max-w-md mx-auto">
<CardHeader>
<CardTitle>Chain Not Found</CardTitle>
</CardHeader>
<CardContent>
<p className="mb-4">The requested chain or block could not be found.</p>
<Button onClick={() => navigate('/view')}>View All Chains</Button>
</CardContent>
</Card>
);
}

return (
<Card className="w-full max-w-2xl mx-auto">
<CardHeader>
<CardTitle>Sign Shared Chain</CardTitle>
</CardHeader>
<CardContent>
<h2 className="text-xl font-semibold mb-2">{block.title}</h2>
<p className="mb-4">{block.description}</p>
<SignChain block={block} onSuccess={onChainUpdate} />
</CardContent>
</Card>
);
};

export default SignSharedChain;
39 changes: 35 additions & 4 deletions examples/buddybook/src/components/Chain/View/ChainList.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import React from 'react';
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
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';
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import QRCode from '@/components/QRCode';
import { Loader2 } from "lucide-react";

interface ChainListProps {
chainsData: BlockPayload[];
onChainUpdate: (newBlock: BlockPayload) => void;
isLoading: boolean;
}

const ChainList: React.FC<ChainListProps> = ({ chainsData, onChainUpdate }) => {
const ChainList: React.FC<ChainListProps> = ({ chainsData, onChainUpdate, isLoading }) => {
const handleChainUpdate = (newBlock: BlockPayload) => {
onChainUpdate(newBlock);
};
Expand All @@ -18,6 +23,8 @@ const ChainList: React.FC<ChainListProps> = ({ chainsData, onChainUpdate }) => {
const childBlocks = chainsData.filter(b => b.parentBlockUUID === block.blockUUID);
const totalSignatures = block.signatures.length + childBlocks.reduce((acc, child) => acc + child.signatures.length, 0);

const shareUrl = `${window.location.origin}/sign/${block.chainUUID ?? block.blockUUID}/${block.blockUUID}`;

return (
<li key={`${block.blockUUID}-${depth}`} className="mb-4">
<div className="flex items-start">
Expand All @@ -42,8 +49,28 @@ const ChainList: React.FC<ChainListProps> = ({ chainsData, onChainUpdate }) => {
<p className="text-sm text-muted-foreground">
Block UUID: {block.blockUUID}
</p>
<div className="mt-2">
<div className="mt-2 space-x-2">
<SignChain block={block} onSuccess={handleChainUpdate} />
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Share</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Share this Chain</DialogTitle>
</DialogHeader>
<div className="flex flex-col items-center space-y-4">
<QRCode text={shareUrl} width={200} height={200} />
<p className="text-sm text-center break-all">{shareUrl}</p>
<Button
onClick={() => navigator.clipboard.writeText(shareUrl)}
variant="outline"
>
Copy Link
</Button>
</div>
</DialogContent>
</Dialog>
</div>
</CardContent>
</Card>
Expand All @@ -70,7 +97,11 @@ const ChainList: React.FC<ChainListProps> = ({ chainsData, onChainUpdate }) => {
<CardTitle>Existing Chains</CardTitle>
</CardHeader>
<CardContent>
{rootBlocks.length === 0 ? (
{isLoading ? (
<div className="flex justify-center items-center h-32">
<Loader2 className="h-8 w-8 animate-spin" />
</div>
) : rootBlocks.length === 0 ? (
<p>No chains found.</p>
) : (
<ul className="space-y-4">
Expand Down
45 changes: 5 additions & 40 deletions examples/buddybook/src/components/QRCode.tsx
Original file line number Diff line number Diff line change
@@ -1,53 +1,18 @@
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;
text: string;
width?: number;
height?: number;
}

const QRCode: React.FC<QRCodeProps> = ({ data, size = 256, onSign }) => {
const shareableLink = `${window.location.origin}/view/${data.chainUUID}/${data.blockUUID}`;

const QRCode: React.FC<QRCodeProps> = ({ text, width = 256, height = 256 }) => {
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>}
<QRCodeSVG value={text} size={Math.min(width, height)} />
</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;
2 changes: 1 addition & 1 deletion examples/buddybook/tasks.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
- [ ] waku connections on header should have green/yellow/red color indicator
- [ x ] 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
Expand Down

0 comments on commit 49460a2

Please sign in to comment.