mirror of
https://github.com/logos-messaging/lab.waku.org.git
synced 2026-01-10 01:33:09 +00:00
234 lines
8.1 KiB
TypeScript
234 lines
8.1 KiB
TypeScript
import React, { useState, useEffect } 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 { useWalletPrompt } from '@/hooks/useWalletPrompt';
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
import { fromLightPush, Telemetry } from '@/lib/telemetry';
|
|
|
|
interface SignChainProps {
|
|
block: BlockPayload;
|
|
chainsData: BlockPayload[];
|
|
onSuccess: (newBlock: BlockPayload) => void;
|
|
}
|
|
|
|
const SignChain: React.FC<SignChainProps> = ({ block, chainsData, onSuccess }) => {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const [isSigning, setIsSigning] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [alreadySigned, setAlreadySigned] = useState(false);
|
|
const [isWalletPrompt, setIsWalletPrompt] = useState(false);
|
|
|
|
const { address } = useAccount();
|
|
const { data: ensName } = useEnsName({ address });
|
|
const { node } = useWaku<LightNode>();
|
|
const { ensureWalletConnected } = useWalletPrompt();
|
|
|
|
const checkSignatures = (blockToCheck: BlockPayload, visitedBlocks: Set<string>): boolean => {
|
|
if (visitedBlocks.has(blockToCheck.blockUUID)) return false;
|
|
visitedBlocks.add(blockToCheck.blockUUID);
|
|
|
|
// Check current block signatures
|
|
if (blockToCheck.signatures?.some(sig => sig?.address?.toLowerCase() === address?.toLowerCase())) {
|
|
return true;
|
|
}
|
|
|
|
// Check parent block
|
|
const parentBlock = chainsData.find(b => b.blockUUID === blockToCheck.parentBlockUUID);
|
|
if (parentBlock && checkSignatures(parentBlock, visitedBlocks)) {
|
|
return true;
|
|
}
|
|
|
|
// Check immediate child blocks
|
|
return chainsData
|
|
.filter(b => b.parentBlockUUID === blockToCheck.blockUUID)
|
|
.some(childBlock => checkSignatures(childBlock, visitedBlocks));
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (!address) return;
|
|
|
|
try {
|
|
const visitedBlocks = new Set<string>();
|
|
setAlreadySigned(checkSignatures(block, visitedBlocks));
|
|
} catch (error) {
|
|
console.error('Error in signature check:', error);
|
|
setAlreadySigned(false);
|
|
}
|
|
}, [address, block, chainsData]);
|
|
|
|
const { signMessage } = useSignMessage({
|
|
mutation: {
|
|
onMutate() {
|
|
setError(null);
|
|
setIsSigning(true);
|
|
},
|
|
async onSuccess(signature) {
|
|
if (!address || !node) return;
|
|
|
|
try {
|
|
if (block.signatures.some(sig => sig.address.toLowerCase() === address.toLowerCase())) {
|
|
setError('You have already signed this chain.');
|
|
return;
|
|
}
|
|
|
|
const timestamp = Date.now();
|
|
const newBlock: BlockPayload = {
|
|
chainUUID: block.chainUUID,
|
|
blockUUID: uuidv4(),
|
|
title: block.title,
|
|
description: block.description,
|
|
signedMessage: signature,
|
|
timestamp,
|
|
signatures: [{ address, signature }],
|
|
parentBlockUUID: block.blockUUID
|
|
};
|
|
|
|
const wakuMessage = createMessage(newBlock);
|
|
const result = await node.lightPush.send(encoder, wakuMessage);
|
|
|
|
Telemetry.push(fromLightPush({
|
|
result,
|
|
node,
|
|
encoder,
|
|
timestamp,
|
|
bookId: block.chainUUID,
|
|
wallet: address,
|
|
}));
|
|
|
|
const { failures, successes } = result;
|
|
|
|
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.');
|
|
}
|
|
},
|
|
onError(error) {
|
|
console.error('Error signing message:', error);
|
|
setError('Error signing message. Please try again. If using a mobile wallet, please ensure your wallet app is open.');
|
|
},
|
|
onSettled() {
|
|
setIsSigning(false);
|
|
}
|
|
}
|
|
});
|
|
|
|
const handleSign = async () => {
|
|
try {
|
|
if (!address) {
|
|
setIsWalletPrompt(true);
|
|
const connected = await ensureWalletConnected();
|
|
if (!connected) {
|
|
setError('Please ensure your wallet is connected and the app is open.');
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (alreadySigned) {
|
|
setError('You have already signed this chain.');
|
|
return;
|
|
}
|
|
|
|
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
|
|
if (isMobile && typeof window.ethereum === 'undefined') {
|
|
setError('Please ensure your wallet app is installed and open before signing.');
|
|
window.location.href = 'metamask:///';
|
|
return;
|
|
}
|
|
|
|
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}`
|
|
].join('\n');
|
|
|
|
signMessage({ message });
|
|
} catch (error) {
|
|
console.error('Error in sign flow:', error);
|
|
setError('Failed to initiate signing. Please ensure your wallet app is open and try again.');
|
|
setIsSigning(false);
|
|
} finally {
|
|
setIsWalletPrompt(false);
|
|
}
|
|
};
|
|
|
|
const getButtonText = () => {
|
|
if (isSigning) return 'Signing...';
|
|
if (isWalletPrompt) return 'Connecting...';
|
|
if (alreadySigned) return 'Already Signed';
|
|
if (!address) return 'Connect Wallet';
|
|
return 'Sign';
|
|
};
|
|
|
|
const showLoadingSpinner = isSigning || isWalletPrompt;
|
|
|
|
return (
|
|
<>
|
|
<Button onClick={() => setIsOpen(true)} disabled={alreadySigned}>
|
|
{alreadySigned ? 'Already Signed' : !address ? 'Connect Wallet' : 'Sign Book'}
|
|
</Button>
|
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
<DialogContent className="sm:max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle>Sign Book</DialogTitle>
|
|
<DialogDescription>
|
|
{alreadySigned
|
|
? 'You have already signed this book.'
|
|
: 'Review the block details and sign to add your signature to the book.'}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="flex flex-col space-y-4">
|
|
<div className="space-y-2">
|
|
<h4 className="font-medium">Block Details</h4>
|
|
<p className="text-sm text-muted-foreground">{block.title}</p>
|
|
<p className="text-sm text-muted-foreground">{block.description}</p>
|
|
</div>
|
|
<QRCode text={`${window.location.origin}/sign/${block.chainUUID}/${block.blockUUID}`} />
|
|
</div>
|
|
{(error || isWalletPrompt) && (
|
|
<div className="space-y-2">
|
|
{error && <p className="text-sm text-destructive">{error}</p>}
|
|
{isWalletPrompt && (
|
|
<div className="rounded-md bg-blue-50 p-4">
|
|
<p className="text-sm text-blue-700">Attempting to connect to your wallet...</p>
|
|
<p className="text-xs text-blue-600 mt-1">
|
|
If your wallet doesn't open automatically, please open it manually to approve the connection.
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
<DialogFooter>
|
|
<Button variant="secondary" onClick={() => setIsOpen(false)}>Cancel</Button>
|
|
<Button
|
|
onClick={handleSign}
|
|
disabled={isSigning || alreadySigned || isWalletPrompt}
|
|
>
|
|
{showLoadingSpinner && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
{getButtonText()}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default SignChain;
|