From 150710b4c740b04f9041017423c79523aec74217 Mon Sep 17 00:00:00 2001 From: Danish Arora Date: Thu, 24 Apr 2025 16:30:50 +0530 Subject: [PATCH] feat: signatures + time-based key delegation for interactions --- .gitignore | 4 + package-lock.json | 25 +- package.json | 2 + src/components/Header.tsx | 65 ++++- src/components/ui/badge.tsx | 17 +- src/components/ui/wallet-dialog.tsx | 67 +++++ src/contexts/AuthContext.tsx | 155 +++++++++++- src/contexts/ForumContext.tsx | 153 +++++++++--- src/contexts/forum/actions.ts | 231 +++++++++++------- src/lib/identity/signatures/key-delegation.ts | 197 +++++++++++++++ .../identity/signatures/message-signing.ts | 55 +++++ src/lib/identity/signatures/types.ts | 10 + src/lib/identity/wallets/index.ts | 172 +++++++++++++ src/lib/identity/wallets/phantom.ts | 138 +++++++++++ src/lib/identity/wallets/types.ts | 34 +++ src/lib/utils.ts | 15 ++ src/lib/waku/constants.ts | 11 +- src/lib/waku/types.ts | 2 + src/types/index.ts | 13 + 19 files changed, 1232 insertions(+), 134 deletions(-) create mode 100644 src/components/ui/wallet-dialog.tsx create mode 100644 src/lib/identity/signatures/key-delegation.ts create mode 100644 src/lib/identity/signatures/message-signing.ts create mode 100644 src/lib/identity/signatures/types.ts create mode 100644 src/lib/identity/wallets/index.ts create mode 100644 src/lib/identity/wallets/phantom.ts create mode 100644 src/lib/identity/wallets/types.ts diff --git a/.gitignore b/.gitignore index 0fad054..327d05a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +.cursorrules +comparison.md +.giga/ + README-task-master.md .cursor scripts diff --git a/package-lock.json b/package-lock.json index e5684ec..2cef02c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "0.0.0", "dependencies": { "@hookform/resolvers": "^3.9.0", + "@noble/ed25519": "^2.2.3", + "@noble/hashes": "^1.8.0", "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-alert-dialog": "^1.1.1", "@radix-ui/react-aspect-ratio": "^1.1.0", @@ -1374,7 +1376,7 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@noble/hashes": { + "node_modules/@noble/curves/node_modules/@noble/hashes": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.2.tgz", "integrity": "sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ==", @@ -1386,6 +1388,27 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@noble/ed25519": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-2.2.3.tgz", + "integrity": "sha512-iHV8eI2mRcUmOx159QNrU8vTpQ/Xm70yJ2cTk3Trc86++02usfqFoNl6x0p3JN81ZDS/1gx6xiK0OwrgqCT43g==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@noble/secp256k1": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.2.tgz", diff --git a/package.json b/package.json index 389b6e7..99b958e 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,8 @@ }, "dependencies": { "@hookform/resolvers": "^3.9.0", + "@noble/ed25519": "^2.2.3", + "@noble/hashes": "^1.8.0", "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-alert-dialog": "^1.1.1", "@radix-ui/react-aspect-ratio": "^1.1.0", diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 752f512..ad70212 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -4,11 +4,21 @@ import { useAuth } from '@/contexts/AuthContext'; import { useForum } from '@/contexts/ForumContext'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; -import { ShieldCheck, LogOut, Terminal, Wifi, WifiOff, Eye, MessageSquare, RefreshCw } from 'lucide-react'; +import { ShieldCheck, LogOut, Terminal, Wifi, WifiOff, Eye, MessageSquare, RefreshCw, Key } from 'lucide-react'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; const Header = () => { - const { currentUser, isAuthenticated, verificationStatus, connectWallet, disconnectWallet, verifyOrdinal } = useAuth(); + const { + currentUser, + isAuthenticated, + verificationStatus, + connectWallet, + disconnectWallet, + verifyOrdinal, + delegateKey, + isDelegationValid, + delegationTimeRemaining + } = useAuth(); const { isNetworkConnected, isRefreshing } = useForum(); const handleConnect = async () => { @@ -23,6 +33,56 @@ const Header = () => { await verifyOrdinal(); }; + const handleDelegateKey = async () => { + await delegateKey(); + }; + + // Format delegation time remaining for display + const formatDelegationTime = () => { + if (!isDelegationValid()) return null; + + const timeRemaining = delegationTimeRemaining(); + const hours = Math.floor(timeRemaining / (1000 * 60 * 60)); + const minutes = Math.floor((timeRemaining % (1000 * 60 * 60)) / (1000 * 60)); + + return `${hours}h ${minutes}m`; + }; + + const renderDelegationButton = () => { + // Only show delegation button for verified Ordinal owners + if (verificationStatus !== 'verified-owner') return null; + + const hasValidDelegation = isDelegationValid(); + const timeRemaining = formatDelegationTime(); + + return ( + + + + + + {hasValidDelegation ? ( +

You have a delegated browser key active for {timeRemaining}. + You won't need to sign messages with your wallet for most actions.

+ ) : ( +

Delegate a browser key to avoid signing every action with your wallet. + Improves UX by reducing wallet popups for 24 hours.

+ )} +
+
+ ); + }; + const renderAccessBadge = () => { if (verificationStatus === 'unverified') { return ( @@ -182,6 +242,7 @@ const Header = () => { ) : ( <> {renderAccessBadge()} + {renderDelegationButton()} {currentUser.address.slice(0, 6)}...{currentUser.address.slice(-4)} diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx index f000e3e..c6d8208 100644 --- a/src/components/ui/badge.tsx +++ b/src/components/ui/badge.tsx @@ -27,10 +27,17 @@ export interface BadgeProps extends React.HTMLAttributes, VariantProps {} -function Badge({ className, variant, ...props }: BadgeProps) { - return ( -
- ) -} +const Badge = React.forwardRef( + ({ className, variant, ...props }, ref) => { + return ( +
+ ) + } +) +Badge.displayName = "Badge" export { Badge, badgeVariants } diff --git a/src/components/ui/wallet-dialog.tsx b/src/components/ui/wallet-dialog.tsx new file mode 100644 index 0000000..256b701 --- /dev/null +++ b/src/components/ui/wallet-dialog.tsx @@ -0,0 +1,67 @@ +import * as React from "react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { WalletConnectionStatus } from "@/lib/identity/wallets/phantom"; + +interface WalletDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onConnectPhantom: () => void; + onInstallPhantom: () => void; + status: WalletConnectionStatus; + isAuthenticating: boolean; +} + +export function WalletConnectionDialog({ + open, + onOpenChange, + onConnectPhantom, + onInstallPhantom, + status, + isAuthenticating, +}: WalletDialogProps) { + return ( + + + + Connect Wallet + + {status === WalletConnectionStatus.NotDetected + ? "Phantom wallet not detected. Please install it to continue." + : "Choose a wallet connection method to continue"} + + + +
+ {status !== WalletConnectionStatus.NotDetected ? ( + + ) : ( + + )} +
+ + +

Phantom wallet is required to use OpChan's features

+
+
+
+ ); +} \ No newline at end of file diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx index 72d057f..5ad524f 100644 --- a/src/contexts/AuthContext.tsx +++ b/src/contexts/AuthContext.tsx @@ -1,7 +1,10 @@ -import React, { createContext, useContext, useState, useEffect } from 'react'; +import React, { createContext, useContext, useState, useEffect, useRef } from 'react'; import { useToast } from '@/components/ui/use-toast'; import { User } from '@/types'; import { OrdinalAPI } from '@/lib/identity/ordinal'; +import { KeyDelegation } from '@/lib/identity/signatures/key-delegation'; +import { PhantomWalletAdapter } from '@/lib/identity/wallets/phantom'; +import { MessageSigning } from '@/lib/identity/signatures/message-signing'; export type VerificationStatus = 'unverified' | 'verified-none' | 'verified-owner' | 'verifying'; @@ -13,6 +16,10 @@ interface AuthContextType { connectWallet: () => Promise; disconnectWallet: () => void; verifyOrdinal: () => Promise; + delegateKey: () => Promise; + isDelegationValid: () => boolean; + delegationTimeRemaining: () => number; + messageSigning: MessageSigning; } const AuthContext = createContext(undefined); @@ -24,6 +31,11 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const { toast } = useToast(); const ordinalApi = new OrdinalAPI(); + // Create refs for our services so they persist between renders + const phantomWalletRef = useRef(new PhantomWalletAdapter()); + const keyDelegationRef = useRef(new KeyDelegation()); + const messageSigningRef = useRef(new MessageSigning(keyDelegationRef.current)); + useEffect(() => { const storedUser = localStorage.getItem('opchan-user'); if (storedUser) { @@ -52,16 +64,25 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { } }, []); - // Mock wallet connection for development const connectWallet = async () => { setIsAuthenticating(true); try { - //TODO: replace with actual wallet connection - const mockAddress = "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh"; + // Check if Phantom wallet is installed + if (!phantomWalletRef.current.isInstalled()) { + toast({ + title: "Wallet Not Found", + description: "Please install Phantom wallet to continue.", + variant: "destructive", + }); + throw new Error("Phantom wallet not installed"); + } + + // Connect to wallet + const address = await phantomWalletRef.current.connect(); // Create a new user object const newUser: User = { - address: mockAddress, + address, lastChecked: Date.now(), }; @@ -72,10 +93,15 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { toast({ title: "Wallet Connected", - description: `Connected with address ${mockAddress.slice(0, 6)}...${mockAddress.slice(-4)}`, + description: `Connected with address ${address.slice(0, 6)}...${address.slice(-4)}`, + }); + + // Prompt the user to verify ordinal ownership and delegate key + toast({ + title: "Action Required", + description: "Please verify your Ordinal ownership and delegate a signing key for better UX.", }); - // Don't return the address anymore to match the Promise return type } catch (error) { console.error("Error connecting wallet:", error); toast({ @@ -90,9 +116,15 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { }; const disconnectWallet = () => { + // Disconnect from Phantom wallet + phantomWalletRef.current.disconnect(); + + // Clear user data and delegation setCurrentUser(null); localStorage.removeItem('opchan-user'); + keyDelegationRef.current.clearDelegation(); setVerificationStatus('unverified'); + toast({ title: "Disconnected", description: "Your wallet has been disconnected.", @@ -136,7 +168,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { if (hasOperators) { toast({ title: "Ordinal Verified", - description: "You now have full access to post and interact with the forum.", + description: "You now have full access. We recommend delegating a key for better UX.", }); } else { toast({ @@ -168,6 +200,109 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { } }; + /** + * Creates a key delegation by generating a browser keypair, having the + * wallet sign a delegation message, and storing the delegation + */ + const delegateKey = async (): Promise => { + if (!currentUser || !currentUser.address) { + toast({ + title: "Not Connected", + description: "Please connect your wallet first.", + variant: "destructive", + }); + return false; + } + + setIsAuthenticating(true); + + try { + toast({ + title: "Delegating Key", + description: "Generating a browser keypair for quick signing...", + }); + + // Generate a browser keypair + const keypair = await keyDelegationRef.current.generateKeypair(); + + // Calculate expiry time (24 hours from now) + const expiryHours = 24; + const expiryTimestamp = Date.now() + (expiryHours * 60 * 60 * 1000); + + // Create delegation message + const delegationMessage = keyDelegationRef.current.createDelegationMessage( + keypair.publicKey, + currentUser.address, + expiryTimestamp + ); + + toast({ + title: "Signing Required", + description: "Please sign the delegation message with your wallet...", + }); + + const signature = await phantomWalletRef.current.signMessage(delegationMessage); + + const delegationInfo = keyDelegationRef.current.createDelegation( + currentUser.address, + signature, + keypair.publicKey, + keypair.privateKey, + expiryHours + ); + + keyDelegationRef.current.storeDelegation(delegationInfo); + + const updatedUser = { + ...currentUser, + browserPubKey: keypair.publicKey, + delegationSignature: signature, + delegationExpiry: expiryTimestamp, + }; + + setCurrentUser(updatedUser); + localStorage.setItem('opchan-user', JSON.stringify(updatedUser)); + + toast({ + title: "Key Delegated", + description: `You won't need to sign every action for the next ${expiryHours} hours.`, + }); + + return true; + } catch (error) { + console.error("Error delegating key:", error); + + let errorMessage = "Failed to delegate key. Please try again."; + if (error instanceof Error) { + errorMessage = error.message; + } + + toast({ + title: "Delegation Error", + description: errorMessage, + variant: "destructive", + }); + + return false; + } finally { + setIsAuthenticating(false); + } + }; + + /** + * Checks if the current delegation is valid + */ + const isDelegationValid = (): boolean => { + return keyDelegationRef.current.isDelegationValid(); + }; + + /** + * Returns the time remaining on the current delegation in milliseconds + */ + const delegationTimeRemaining = (): number => { + return keyDelegationRef.current.getDelegationTimeRemaining(); + }; + return ( {children} diff --git a/src/contexts/ForumContext.tsx b/src/contexts/ForumContext.tsx index a403754..fe3b578 100644 --- a/src/contexts/ForumContext.tsx +++ b/src/contexts/ForumContext.tsx @@ -1,19 +1,45 @@ import React, { createContext, useContext, useState, useEffect } from 'react'; -import { useToast } from '@/components/ui/use-toast'; import { Cell, Post, Comment } from '@/types'; -import { useAuth } from './AuthContext'; +import { useToast } from '@/components/ui/use-toast'; +import { useAuth } from '@/contexts/AuthContext'; import { - getDataFromCache, createPost, createComment, vote, - createCell, - refreshData as refreshNetworkData, - initializeNetwork, + createCell +} from './forum/actions'; +import { setupPeriodicQueries, - monitorNetworkHealth, - ForumContextType -} from './forum'; + monitorNetworkHealth, + initializeNetwork +} from './forum/network'; +import messageManager from '@/lib/waku'; +import { transformCell, transformComment, transformPost } from './forum/transformers'; + +interface ForumContextType { + cells: Cell[]; + posts: Post[]; + comments: Comment[]; + // Granular loading states + isInitialLoading: boolean; + isPostingCell: boolean; + isPostingPost: boolean; + isPostingComment: boolean; + isVoting: boolean; + isRefreshing: boolean; + // Network status + isNetworkConnected: boolean; + error: string | null; + getCellById: (id: string) => Cell | undefined; + getPostsByCell: (cellId: string) => Post[]; + getCommentsByPost: (postId: string) => Comment[]; + createPost: (cellId: string, title: string, content: string) => Promise; + createComment: (postId: string, content: string) => Promise; + votePost: (postId: string, isUpvote: boolean) => Promise; + voteComment: (commentId: string, isUpvote: boolean) => Promise; + createCell: (name: string, description: string, icon: string) => Promise; + refreshData: () => Promise; +} const ForumContext = createContext(undefined); @@ -21,35 +47,58 @@ export function ForumProvider({ children }: { children: React.ReactNode }) { const [cells, setCells] = useState([]); const [posts, setPosts] = useState([]); const [comments, setComments] = useState([]); - -// Loading states const [isInitialLoading, setIsInitialLoading] = useState(true); const [isPostingCell, setIsPostingCell] = useState(false); const [isPostingPost, setIsPostingPost] = useState(false); const [isPostingComment, setIsPostingComment] = useState(false); const [isVoting, setIsVoting] = useState(false); const [isRefreshing, setIsRefreshing] = useState(false); - - // Network connection status const [isNetworkConnected, setIsNetworkConnected] = useState(false); - const [error, setError] = useState(null); - const { currentUser, isAuthenticated } = useAuth(); + const { toast } = useToast(); - - // Function to update UI state from message cache + const { currentUser, isAuthenticated, messageSigning } = useAuth(); + + // Transform message cache data to the expected types const updateStateFromCache = () => { - const data = getDataFromCache(); - setCells(data.cells); - setPosts(data.posts); - setComments(data.comments); + // Transform cells + setCells( + Object.values(messageManager.messageCache.cells).map(cell => + transformCell(cell) + ) + ); + + // Transform posts + setPosts( + Object.values(messageManager.messageCache.posts).map(post => + transformPost(post) + ) + ); + + // Transform comments + setComments( + Object.values(messageManager.messageCache.comments).map(comment => + transformComment(comment) + ) + ); }; - - // Function to refresh data from the network + const handleRefreshData = async () => { setIsRefreshing(true); - await refreshNetworkData(isNetworkConnected, toast, updateStateFromCache, setError); - setIsRefreshing(false); + try { + // Manually query the network for updates + await messageManager.queryStore(); + updateStateFromCache(); + } catch (error) { + console.error("Error refreshing data:", error); + toast({ + title: "Refresh Failed", + description: "Could not fetch the latest data. Please try again.", + variant: "destructive", + }); + } finally { + setIsRefreshing(false); + } }; // Monitor network connection status @@ -89,35 +138,77 @@ export function ForumProvider({ children }: { children: React.ReactNode }) { const handleCreatePost = async (cellId: string, title: string, content: string): Promise => { setIsPostingPost(true); - const result = await createPost(cellId, title, content, currentUser, isAuthenticated, toast, updateStateFromCache); + const result = await createPost( + cellId, + title, + content, + currentUser, + isAuthenticated, + toast, + updateStateFromCache, + messageSigning + ); setIsPostingPost(false); return result; }; const handleCreateComment = async (postId: string, content: string): Promise => { setIsPostingComment(true); - const result = await createComment(postId, content, currentUser, isAuthenticated, toast, updateStateFromCache); + const result = await createComment( + postId, + content, + currentUser, + isAuthenticated, + toast, + updateStateFromCache, + messageSigning + ); setIsPostingComment(false); return result; }; const handleVotePost = async (postId: string, isUpvote: boolean): Promise => { setIsVoting(true); - const result = await vote(postId, isUpvote, currentUser, isAuthenticated, toast, updateStateFromCache); + const result = await vote( + postId, + isUpvote, + currentUser, + isAuthenticated, + toast, + updateStateFromCache, + messageSigning + ); setIsVoting(false); return result; }; const handleVoteComment = async (commentId: string, isUpvote: boolean): Promise => { setIsVoting(true); - const result = await vote(commentId, isUpvote, currentUser, isAuthenticated, toast, updateStateFromCache); + const result = await vote( + commentId, + isUpvote, + currentUser, + isAuthenticated, + toast, + updateStateFromCache, + messageSigning + ); setIsVoting(false); return result; }; const handleCreateCell = async (name: string, description: string, icon: string): Promise => { setIsPostingCell(true); - const result = await createCell(name, description, icon, currentUser, isAuthenticated, toast, updateStateFromCache); + const result = await createCell( + name, + description, + icon, + currentUser, + isAuthenticated, + toast, + updateStateFromCache, + messageSigning + ); setIsPostingCell(false); return result; }; @@ -155,7 +246,7 @@ export function ForumProvider({ children }: { children: React.ReactNode }) { export const useForum = () => { const context = useContext(ForumContext); if (context === undefined) { - throw new Error('useForum must be used within a ForumProvider'); + throw new Error("useForum must be used within a ForumProvider"); } return context; }; diff --git a/src/contexts/forum/actions.ts b/src/contexts/forum/actions.ts index 7befffa..41b1c85 100644 --- a/src/contexts/forum/actions.ts +++ b/src/contexts/forum/actions.ts @@ -3,6 +3,7 @@ import { CellMessage, CommentMessage, MessageType, PostMessage, VoteMessage } fr import messageManager from '@/lib/waku'; import { Cell, Comment, Post, User } from '@/types'; import { transformCell, transformComment, transformPost } from './transformers'; +import { MessageSigning } from '@/lib/identity/signatures/message-signing'; type ToastFunction = (props: { title: string; @@ -10,7 +11,52 @@ type ToastFunction = (props: { variant?: "default" | "destructive"; }) => void; -// Create a post +async function signAndSendMessage( + message: T, + currentUser: User | null, + messageSigning: MessageSigning, + toast: ToastFunction +): Promise { + if (!currentUser) { + toast({ + title: "Authentication Required", + description: "You need to be authenticated to perform this action.", + variant: "destructive", + }); + return null; + } + + try { + let signedMessage: T | null = null; + + if (messageSigning) { + signedMessage = await messageSigning.signMessage(message); + + if (!signedMessage) { + toast({ + title: "Key Delegation Required", + description: "Please delegate a signing key for better UX.", + variant: "destructive", + }); + return null; + } + } else { + signedMessage = message; + } + + await messageManager.sendMessage(signedMessage); + return signedMessage; + } catch (error) { + console.error("Error signing and sending message:", error); + toast({ + title: "Message Error", + description: "Failed to sign and send message. Please try again.", + variant: "destructive", + }); + return null; + } +} + export const createPost = async ( cellId: string, title: string, @@ -18,7 +64,8 @@ export const createPost = async ( currentUser: User | null, isAuthenticated: boolean, toast: ToastFunction, - updateStateFromCache: () => void + updateStateFromCache: () => void, + messageSigning?: MessageSigning ): Promise => { if (!isAuthenticated || !currentUser) { toast({ @@ -47,10 +94,15 @@ export const createPost = async ( author: currentUser.address }; - // Send the message to the network - await messageManager.sendMessage(postMessage); + const sentMessage = await signAndSendMessage( + postMessage, + currentUser, + messageSigning!, + toast + ); + + if (!sentMessage) return null; - // Update UI (the cache is already updated in sendMessage) updateStateFromCache(); toast({ @@ -58,8 +110,7 @@ export const createPost = async ( description: "Your post has been published successfully.", }); - // Return the transformed post - return transformPost(postMessage); + return transformPost(sentMessage); } catch (error) { console.error("Error creating post:", error); toast({ @@ -71,14 +122,14 @@ export const createPost = async ( } }; -// Create a comment export const createComment = async ( postId: string, content: string, currentUser: User | null, isAuthenticated: boolean, toast: ToastFunction, - updateStateFromCache: () => void + updateStateFromCache: () => void, + messageSigning?: MessageSigning ): Promise => { if (!isAuthenticated || !currentUser) { toast({ @@ -106,10 +157,15 @@ export const createComment = async ( author: currentUser.address }; - // Send the message to the network - await messageManager.sendMessage(commentMessage); + const sentMessage = await signAndSendMessage( + commentMessage, + currentUser, + messageSigning!, + toast + ); + + if (!sentMessage) return null; - // Update UI (the cache is already updated in sendMessage) updateStateFromCache(); toast({ @@ -117,8 +173,7 @@ export const createComment = async ( description: "Your comment has been published.", }); - // Return the transformed comment - return transformComment(commentMessage); + return transformComment(sentMessage); } catch (error) { console.error("Error creating comment:", error); toast({ @@ -130,74 +185,15 @@ export const createComment = async ( } }; -// Vote on a post or comment -export const vote = async ( - targetId: string, - isUpvote: boolean, - currentUser: User | null, - isAuthenticated: boolean, - toast: ToastFunction, - updateStateFromCache: () => void -): Promise => { - if (!isAuthenticated || !currentUser) { - toast({ - title: "Authentication Required", - description: "You need to verify Ordinal ownership to vote.", - variant: "destructive", - }); - return false; - } - - try { - const voteType = isUpvote ? "upvote" : "downvote"; - toast({ - title: `Sending ${voteType}`, - description: "Recording your vote on the network...", - }); - - const voteId = uuidv4(); - - const voteMessage: VoteMessage = { - type: MessageType.VOTE, - id: voteId, - targetId, - value: isUpvote ? 1 : -1, - timestamp: Date.now(), - author: currentUser.address - }; - - // Send the vote message to the network - await messageManager.sendMessage(voteMessage); - - // Update UI (the cache is already updated in sendMessage) - updateStateFromCache(); - - toast({ - title: "Vote Recorded", - description: `Your ${voteType} has been registered.`, - }); - - return true; - } catch (error) { - console.error("Error voting:", error); - toast({ - title: "Vote Failed", - description: "Failed to register your vote. Please try again.", - variant: "destructive", - }); - return false; - } -}; - -// Create a cell export const createCell = async ( - name: string, - description: string, + name: string, + description: string, icon: string, currentUser: User | null, isAuthenticated: boolean, toast: ToastFunction, - updateStateFromCache: () => void + updateStateFromCache: () => void, + messageSigning?: MessageSigning ): Promise => { if (!isAuthenticated || !currentUser) { toast({ @@ -226,25 +222,94 @@ export const createCell = async ( author: currentUser.address }; - // Send the cell message to the network - await messageManager.sendMessage(cellMessage); + const sentMessage = await signAndSendMessage( + cellMessage, + currentUser, + messageSigning!, + toast + ); + + if (!sentMessage) return null; - // Update UI (the cache is already updated in sendMessage) updateStateFromCache(); toast({ title: "Cell Created", - description: "Your cell has been created successfully.", + description: "Your cell has been published.", }); - return transformCell(cellMessage); + return transformCell(sentMessage); } catch (error) { console.error("Error creating cell:", error); toast({ - title: "Cell Creation Failed", + title: "Cell Failed", description: "Failed to create cell. Please try again.", variant: "destructive", }); return null; } +}; + +export const vote = async ( + targetId: string, + isUpvote: boolean, + currentUser: User | null, + isAuthenticated: boolean, + toast: ToastFunction, + updateStateFromCache: () => void, + messageSigning?: MessageSigning +): Promise => { + if (!isAuthenticated || !currentUser) { + toast({ + title: "Authentication Required", + description: "You need to verify Ordinal ownership to vote.", + variant: "destructive", + }); + return false; + } + + try { + const voteType = isUpvote ? "upvote" : "downvote"; + toast({ + title: `Sending ${voteType}`, + description: "Recording your vote on the network...", + }); + + const voteId = uuidv4(); + + const voteMessage: VoteMessage = { + type: MessageType.VOTE, + id: voteId, + targetId, + value: isUpvote ? 1 : -1, + timestamp: Date.now(), + author: currentUser.address + }; + + const sentMessage = await signAndSendMessage( + voteMessage, + currentUser, + messageSigning!, + toast + ); + + if (!sentMessage) return false; + + updateStateFromCache(); + + toast({ + title: "Vote Recorded", + description: `Your ${voteType} has been registered.`, + }); + + return true; + } catch (error) { + console.error("Error voting:", error); + toast({ + title: "Vote Failed", + description: "Failed to register your vote. Please try again.", + variant: "destructive", + }); + return false; + } }; \ No newline at end of file diff --git a/src/lib/identity/signatures/key-delegation.ts b/src/lib/identity/signatures/key-delegation.ts new file mode 100644 index 0000000..daa1393 --- /dev/null +++ b/src/lib/identity/signatures/key-delegation.ts @@ -0,0 +1,197 @@ +/** + * Key delegation for Bitcoin wallets + * + * This module handles the creation of browser-based keypairs and + * delegation of signing authority from Bitcoin wallets to these keypairs. + */ + +import * as ed from '@noble/ed25519'; +import { bytesToHex, hexToBytes } from '@/lib/utils'; +import { LOCAL_STORAGE_KEYS } from '@/lib/waku/constants'; +import { DelegationInfo } from './types'; + + +export class KeyDelegation { + private static readonly DEFAULT_EXPIRY_HOURS = 24; + private static readonly STORAGE_KEY = LOCAL_STORAGE_KEYS.KEY_DELEGATION; + + /** + * Generates a new browser-based keypair for signing messages + * @returns Promise with keypair object containing hex-encoded public and private keys + */ + generateKeypair(): { publicKey: string; privateKey: string } { + const privateKey = ed.utils.randomPrivateKey(); + const privateKeyHex = bytesToHex(privateKey); + + const publicKey = ed.getPublicKey(privateKey); + const publicKeyHex = bytesToHex(publicKey); + + return { + privateKey: privateKeyHex, + publicKey: publicKeyHex + }; + } + + /** + * Creates a delegation message to be signed by the Bitcoin wallet + * @param browserPublicKey The browser-generated public key + * @param bitcoinAddress The user's Bitcoin address + * @param expiryTimestamp When the delegation will expire + * @returns The message to be signed + */ + createDelegationMessage( + browserPublicKey: string, + bitcoinAddress: string, + expiryTimestamp: number + ): string { + return `I, ${bitcoinAddress}, delegate authority to this pubkey: ${browserPublicKey} until ${expiryTimestamp}`; + } + + /** + * Creates a delegation with the specified expiry time in hours + * @param bitcoinAddress The Bitcoin wallet address + * @param signature The signature from the Bitcoin wallet + * @param browserPublicKey The browser public key + * @param browserPrivateKey The browser private key + * @param expiryHours How many hours the delegation should be valid (default: 24) + * @returns The created delegation info + */ + createDelegation( + bitcoinAddress: string, + signature: string, + browserPublicKey: string, + browserPrivateKey: string, + expiryHours: number = KeyDelegation.DEFAULT_EXPIRY_HOURS + ): DelegationInfo { + const now = Date.now(); + const expiryTimestamp = now + (expiryHours * 60 * 60 * 1000); + + return { + signature, + expiryTimestamp, + browserPublicKey, + browserPrivateKey, + bitcoinAddress + }; + } + + /** + * Stores delegation information in local storage + * @param delegationInfo The delegation information to store + */ + storeDelegation(delegationInfo: DelegationInfo): void { + localStorage.setItem(KeyDelegation.STORAGE_KEY, JSON.stringify(delegationInfo)); + } + + /** + * Retrieves delegation information from local storage + * @returns The stored delegation information or null if not found + */ + retrieveDelegation(): DelegationInfo | null { + const delegationJson = localStorage.getItem(KeyDelegation.STORAGE_KEY); + if (!delegationJson) return null; + + try { + return JSON.parse(delegationJson); + } catch (e) { + console.error('Failed to parse delegation information', e); + return null; + } + } + + /** + * Checks if a delegation is valid (exists and not expired) + * @returns boolean indicating if the delegation is valid + */ + isDelegationValid(): boolean { + const delegation = this.retrieveDelegation(); + if (!delegation) return false; + + const now = Date.now(); + return now < delegation.expiryTimestamp; + } + + /** + * Signs a message using the browser-generated private key + * @param message The message to sign + * @returns Promise resolving to the signature as a hex string, or null if no valid delegation + */ + signMessage(message: string): string | null { + const delegation = this.retrieveDelegation(); + if (!delegation || !this.isDelegationValid()) return null; + + try { + const privateKeyBytes = hexToBytes(delegation.browserPrivateKey); + const messageBytes = new TextEncoder().encode(message); + + const signature = ed.sign(messageBytes, privateKeyBytes); + return bytesToHex(signature); + } catch (error) { + console.error('Error signing with browser key:', error); + return null; + } + } + + /** + * Verifies a signature made with the browser key + * @param message The original message + * @param signature The signature to verify (hex string) + * @param publicKey The public key to verify against (hex string) + * @returns Promise resolving to a boolean indicating if the signature is valid + */ + verifySignature( + message: string, + signature: string, + publicKey: string + ): boolean { + try { + const messageBytes = new TextEncoder().encode(message); + const signatureBytes = hexToBytes(signature); + const publicKeyBytes = hexToBytes(publicKey); + + return ed.verify(signatureBytes, messageBytes, publicKeyBytes); + } catch (error) { + console.error('Error verifying signature:', error); + return false; + } + } + + /** + * Gets the current delegation's Bitcoin address, if available + * @returns The Bitcoin address or null if no valid delegation exists + */ + getDelegatingAddress(): string | null { + const delegation = this.retrieveDelegation(); + if (!delegation || !this.isDelegationValid()) return null; + return delegation.bitcoinAddress; + } + + /** + * Gets the browser public key from the current delegation + * @returns The browser public key or null if no valid delegation exists + */ + getBrowserPublicKey(): string | null { + const delegation = this.retrieveDelegation(); + if (!delegation) return null; + return delegation.browserPublicKey; + } + + /** + * Clears the stored delegation + */ + clearDelegation(): void { + localStorage.removeItem(KeyDelegation.STORAGE_KEY); + } + + /** + * Gets the time remaining on the current delegation + * @returns Time remaining in milliseconds, or 0 if expired/no delegation + */ + getDelegationTimeRemaining(): number { + const delegation = this.retrieveDelegation(); + if (!delegation) return 0; + + const now = Date.now(); + return Math.max(0, delegation.expiryTimestamp - now); + } +} \ No newline at end of file diff --git a/src/lib/identity/signatures/message-signing.ts b/src/lib/identity/signatures/message-signing.ts new file mode 100644 index 0000000..0a40932 --- /dev/null +++ b/src/lib/identity/signatures/message-signing.ts @@ -0,0 +1,55 @@ +import { OpchanMessage } from '@/types'; +import { KeyDelegation } from './key-delegation'; + + +export class MessageSigning { + private keyDelegation: KeyDelegation; + + constructor(keyDelegation: KeyDelegation) { + this.keyDelegation = keyDelegation; + } + + signMessage(message: T): T | null { + if (!this.keyDelegation.isDelegationValid()) { + console.error('No valid key delegation found. Cannot sign message.'); + return null; + } + + const delegation = this.keyDelegation.retrieveDelegation(); + if (!delegation) return null; + + const messageToSign = JSON.stringify({ + ...message, + signature: undefined, + browserPubKey: undefined + }); + + const signature = this.keyDelegation.signMessage(messageToSign); + if (!signature) return null; + + return { + ...message, + signature, + browserPubKey: delegation.browserPublicKey + }; + } + + verifyMessage(message: OpchanMessage): boolean { + if (!message.signature || !message.browserPubKey) { + console.warn('Message is missing signature information'); + return false; + } + + const signedContent = JSON.stringify({ + ...message, + signature: undefined, + browserPubKey: undefined + }); + + return this.keyDelegation.verifySignature( + signedContent, + message.signature, + message.browserPubKey + ); + } +} \ No newline at end of file diff --git a/src/lib/identity/signatures/types.ts b/src/lib/identity/signatures/types.ts new file mode 100644 index 0000000..e73e3f3 --- /dev/null +++ b/src/lib/identity/signatures/types.ts @@ -0,0 +1,10 @@ +export interface DelegationSignature { + signature: string; // Signature from Bitcoin wallet + expiryTimestamp: number; // When this delegation expires + browserPublicKey: string; // Browser-generated public key that was delegated to + bitcoinAddress: string; // Bitcoin address that signed the delegation +} + +export interface DelegationInfo extends DelegationSignature { + browserPrivateKey: string; +} \ No newline at end of file diff --git a/src/lib/identity/wallets/index.ts b/src/lib/identity/wallets/index.ts new file mode 100644 index 0000000..113beab --- /dev/null +++ b/src/lib/identity/wallets/index.ts @@ -0,0 +1,172 @@ +import { PhantomWalletAdapter } from './phantom'; +import { KeyDelegation } from '../signatures/key-delegation'; +import { DelegationInfo } from '../signatures/types'; + +export type WalletType = 'phantom'; + +export interface WalletInfo { + address: string; + type: WalletType; + delegated: boolean; + delegationExpiry?: number; +} + +/** + * Service for managing wallet connections and key delegation + */ +export class WalletService { + // Default delegation validity period: 24 hours + private static readonly DEFAULT_DELEGATION_PERIOD = 24 * 60 * 60 * 1000; + + private keyDelegation: KeyDelegation; + private phantomAdapter: PhantomWalletAdapter; + + constructor() { + this.keyDelegation = new KeyDelegation(); + this.phantomAdapter = new PhantomWalletAdapter(); + } + + /** + * Checks if a specific wallet type is available in the browser + */ + public isWalletAvailable(type: WalletType): boolean { + return this.phantomAdapter.isInstalled(); + } + + /** + * Connect to a specific wallet type + * @param type The wallet type to connect to + * @returns Promise resolving to the wallet's address + */ + public async connectWallet(type: WalletType = 'phantom'): Promise { + return await this.phantomAdapter.connect(); + } + + /** + * Disconnect the current wallet + * @param type The wallet type to disconnect + */ + public async disconnectWallet(type: WalletType): Promise { + this.keyDelegation.clearDelegation(); // Clear any delegation + await this.phantomAdapter.disconnect(); + } + + /** + * Get the current wallet information from local storage + * @returns The current wallet info or null if not connected + */ + public getWalletInfo(): WalletInfo | null { + const userJson = localStorage.getItem('opchan-user'); + if (!userJson) return null; + + try { + const user = JSON.parse(userJson); + const delegation = this.keyDelegation.retrieveDelegation(); + + return { + address: user.address, + type: 'phantom', + delegated: !!delegation && this.keyDelegation.isDelegationValid(), + delegationExpiry: delegation?.expiryTimestamp + }; + } catch (e) { + console.error('Failed to parse user data', e); + return null; + } + } + + /** + * Set up key delegation for the connected wallet + * @param bitcoinAddress The Bitcoin address to delegate from + * @param walletType The wallet type + * @param validityPeriod Milliseconds the delegation should be valid for + * @returns Promise resolving to the delegation info + */ + public async setupKeyDelegation( + bitcoinAddress: string, + walletType: WalletType, + validityPeriod: number = WalletService.DEFAULT_DELEGATION_PERIOD + ): Promise> { + // Generate browser keypair + const keypair = this.keyDelegation.generateKeypair(); + + // Calculate expiry in hours + const expiryHours = validityPeriod / (60 * 60 * 1000); + + // Create delegation message + const delegationMessage = this.keyDelegation.createDelegationMessage( + keypair.publicKey, + bitcoinAddress, + Date.now() + validityPeriod + ); + + // Sign the delegation message with the Bitcoin wallet + const signature = await this.phantomAdapter.signMessage(delegationMessage); + + // Create and store the delegation + const delegationInfo = this.keyDelegation.createDelegation( + bitcoinAddress, + signature, + keypair.publicKey, + keypair.privateKey, + expiryHours + ); + + this.keyDelegation.storeDelegation(delegationInfo); + + // Return delegation info (excluding private key) + return { + signature, + expiryTimestamp: delegationInfo.expiryTimestamp, + browserPublicKey: keypair.publicKey, + bitcoinAddress + }; + } + + /** + * Signs a message using the delegated browser key + * @param message The message to sign + * @returns Promise resolving to the signature or null if no valid delegation + */ + public async signMessage(message: string): Promise { + return this.keyDelegation.signMessage(message); + } + + /** + * Verifies a message signature against a public key + * @param message The original message + * @param signature The signature to verify + * @param publicKey The public key to verify against + * @returns Promise resolving to a boolean indicating if the signature is valid + */ + public async verifySignature( + message: string, + signature: string, + publicKey: string + ): Promise { + return this.keyDelegation.verifySignature(message, signature, publicKey); + } + + /** + * Checks if the current key delegation is valid + * @returns boolean indicating if the delegation is valid + */ + public isDelegationValid(): boolean { + return this.keyDelegation.isDelegationValid(); + } + + /** + * Gets the time remaining on the current delegation + * @returns Time remaining in milliseconds, or 0 if expired/no delegation + */ + public getDelegationTimeRemaining(): number { + return this.keyDelegation.getDelegationTimeRemaining(); + } + + /** + * Clears the stored delegation + */ + public clearDelegation(): void { + this.keyDelegation.clearDelegation(); + } +} \ No newline at end of file diff --git a/src/lib/identity/wallets/phantom.ts b/src/lib/identity/wallets/phantom.ts new file mode 100644 index 0000000..bf29d4b --- /dev/null +++ b/src/lib/identity/wallets/phantom.ts @@ -0,0 +1,138 @@ +import { sha512 } from '@noble/hashes/sha2'; +import * as ed from '@noble/ed25519'; +import { PhantomBitcoinProvider, PhantomWallet, WalletConnectionStatus } from './types'; + +ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m)); + +/** + * PhantomWalletAdapter provides methods for connecting to and interacting with + * the Phantom wallet for Bitcoin operations. + */ +export class PhantomWalletAdapter { + private provider: PhantomWallet | null = null; + private btcProvider: PhantomBitcoinProvider | null = null; + private connectionStatus: WalletConnectionStatus = WalletConnectionStatus.Disconnected; + private currentAccount: string | null = null; + + constructor() { + this.checkWalletAvailability(); + } + + + public getStatus(): WalletConnectionStatus { + return this.connectionStatus; + } + + public isInstalled(): boolean { + if (typeof window === 'undefined') { + return false; + } + return !!(window?.phantom?.bitcoin?.isPhantom); + } + + async connect(): Promise { + this.connectionStatus = WalletConnectionStatus.Connecting; + + try { + if (!window?.phantom?.bitcoin) { + this.connectionStatus = WalletConnectionStatus.NotDetected; + return Promise.reject(new Error('Phantom wallet not detected. Please install Phantom wallet.')); + } + + this.provider = window.phantom; + this.btcProvider = window.phantom.bitcoin; + + if (this.btcProvider?.connect) { + await this.btcProvider.connect(); + } + + if (this.btcProvider?.requestAccounts) { + const btcAccounts = await this.btcProvider.requestAccounts(); + + if (!btcAccounts || btcAccounts.length === 0) { + this.connectionStatus = WalletConnectionStatus.Disconnected; + throw new Error('No accounts found'); + } + + const ordinalAccount = btcAccounts.find(acc => acc.purpose === 'ordinals'); + const account = ordinalAccount || btcAccounts[0]; + + this.currentAccount = account.address; + this.connectionStatus = WalletConnectionStatus.Connected; + return account.address; + } else { + throw new Error('requestAccounts method not available on wallet provider'); + } + } catch (error) { + this.connectionStatus = window?.phantom?.bitcoin + ? WalletConnectionStatus.Disconnected + : WalletConnectionStatus.NotDetected; + + throw error; + } + } + + async disconnect(): Promise { + if (this.btcProvider && this.btcProvider.disconnect) { + try { + await this.btcProvider.disconnect(); + } catch (error) { + console.error('Error disconnecting from Phantom wallet:', error); + } + } + + this.provider = null; + this.btcProvider = null; + this.currentAccount = null; + this.connectionStatus = WalletConnectionStatus.Disconnected; + } + + async signMessage(message: string): Promise { + if (!this.btcProvider) { + throw new Error('Wallet is not connected'); + } + + if (!this.currentAccount) { + throw new Error('No active account to sign with'); + } + + try { + if (!this.btcProvider.signMessage) { + throw new Error('signMessage method not available on wallet provider'); + } + + const messageBytes = new TextEncoder().encode(message); + + const { signature } = await this.btcProvider.signMessage( + this.currentAccount, + messageBytes + ); + + if (signature instanceof Uint8Array) { + const binString = String.fromCodePoint(...signature); + return btoa(binString); + } + + return signature as unknown as string; + } catch (error) { + console.error('Error signing message:', error); + throw error; + } + } + + private checkWalletAvailability(): void { + if (typeof window === 'undefined') { + this.connectionStatus = WalletConnectionStatus.NotDetected; + return; + } + + const isPhantomInstalled = window?.phantom?.bitcoin || window?.phantom; + + if (!isPhantomInstalled) { + this.connectionStatus = WalletConnectionStatus.NotDetected; + } else { + this.connectionStatus = WalletConnectionStatus.Disconnected; + } + } + +} diff --git a/src/lib/identity/wallets/types.ts b/src/lib/identity/wallets/types.ts new file mode 100644 index 0000000..3060d29 --- /dev/null +++ b/src/lib/identity/wallets/types.ts @@ -0,0 +1,34 @@ +export enum WalletConnectionStatus { + Connected = 'connected', + Disconnected = 'disconnected', + NotDetected = 'not-detected', + Connecting = 'connecting' + } + +export interface BtcAccount { + address: string; + addressType: "p2tr" | "p2wpkh" | "p2sh" | "p2pkh"; + publicKey: string; + purpose: "payment" | "ordinals"; + } + + export interface PhantomBitcoinProvider { + isPhantom?: boolean; + signMessage?: (address: string, message: Uint8Array) => Promise<{ signature: Uint8Array }>; + connect?: () => Promise<{ publicKey: string }>; + disconnect?: () => Promise; + on?: (event: string, callback: (arg: unknown) => void) => void; + off?: (event: string, callback: (arg: unknown) => void) => void; + publicKey?: string; + requestAccounts?: () => Promise; + } + + export interface PhantomWallet { + bitcoin?: PhantomBitcoinProvider; + } + + declare global { + interface Window { + phantom?: PhantomWallet; + } + } \ No newline at end of file diff --git a/src/lib/utils.ts b/src/lib/utils.ts index bd0c391..cacd391 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -4,3 +4,18 @@ import { twMerge } from "tailwind-merge" export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } + +export function bytesToHex(bytes: Uint8Array): string { + return Array.from(bytes) + .map(b => b.toString(16).padStart(2, '0')) + .join(''); +} + + +export function hexToBytes(hex: string): Uint8Array { + const bytes = new Uint8Array(hex.length / 2); + for (let i = 0; i < hex.length; i += 2) { + bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16); + } + return bytes; +} \ No newline at end of file diff --git a/src/lib/waku/constants.ts b/src/lib/waku/constants.ts index eb9b1e3..27e4103 100644 --- a/src/lib/waku/constants.ts +++ b/src/lib/waku/constants.ts @@ -24,8 +24,11 @@ export const NETWORK_CONFIG: NetworkConfig = { export const BOOTSTRAP_NODES = { "42": [ "/dns4/waku-test.bloxy.one/tcp/8095/wss/p2p/16Uiu2HAmSZbDB7CusdRhgkD81VssRjQV5ZH13FbzCGcdnbbh6VwZ", - "/dns4/node-01.do-ams3.waku.sandbox.status.im/tcp/30303/p2p/16Uiu2HAmNaeL4p3WEYzC9mgXBmBWSgWjPHRvatZTXnp8Jgv3iKsb", - // "/dns4/waku.fryorcraken.xyz/tcp/8000/wss/p2p/16Uiu2HAmMRvhDHrtiHft1FTUYnn6cVA8AWVrTyLUayJJ3MWpUZDB", - // "/dns4/vps-aaa00d52.vps.ovh.ca/tcp/8000/wss/p2p/16Uiu2HAm9PftGgHZwWE3wzdMde4m3kT2eYJFXLZfGoSED3gysofk" + // "/dns4/node-01.do-ams3.waku.sandbox.status.im/tcp/30303/p2p/16Uiu2HAmNaeL4p3WEYzC9mgXBmBWSgWjPHRvatZTXnp8Jgv3iKsb", + // "/dns4/vps-aaa00d52.vps.ovh.ca/tcp/8000/wss/p2p/16Uiu2HAm9PftGgHZwWE3wzdMde4m3kT2eYJFXLZfGoSED3gysofk" ] - }; \ No newline at end of file + }; + + export const LOCAL_STORAGE_KEYS = { + "KEY_DELEGATION": "opchan-key-delegation", + } \ No newline at end of file diff --git a/src/lib/waku/types.ts b/src/lib/waku/types.ts index f8e2eb7..8d2c153 100644 --- a/src/lib/waku/types.ts +++ b/src/lib/waku/types.ts @@ -15,6 +15,8 @@ export interface BaseMessage { type: MessageType; timestamp: number; author: string; + signature?: string; // Message signature for verification + browserPubKey?: string; // Public key that signed the message } /** diff --git a/src/types/index.ts b/src/types/index.ts index 048f081..70aabfe 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -7,6 +7,9 @@ export interface User { ordinalOwnership?: boolean | { id: string; details: string }; signature?: string; lastChecked?: number; + browserPubKey?: string; // Browser-generated public key for key delegation + delegationSignature?: string; // Signature from Bitcoin wallet for delegation + delegationExpiry?: number; // When the delegation expires } export interface Cell { @@ -25,6 +28,8 @@ export interface Post { timestamp: number; upvotes: VoteMessage[]; downvotes: VoteMessage[]; + signature?: string; // Message signature + browserPubKey?: string; // Public key that signed the message } export interface Comment { @@ -35,4 +40,12 @@ export interface Comment { timestamp: number; upvotes: VoteMessage[]; downvotes: VoteMessage[]; + signature?: string; // Message signature + browserPubKey?: string; // Public key that signed the message +} + +// Extended message types for verification +export interface SignedMessage { + signature?: string; // Signature of the message + browserPubKey?: string; // Public key that signed the message }