From dc4468078e3c712ebbb576e22e6b316894dbf2cf Mon Sep 17 00:00:00 2001 From: Danish Arora Date: Tue, 2 Sep 2025 10:48:49 +0530 Subject: [PATCH] chore: move out of services into hooks + simplified classes --- src/App.tsx | 4 +- src/components/ui/author-display.tsx | 2 +- src/components/ui/delegation-step.tsx | 2 +- src/contexts/AuthContext.tsx | 232 +++++++++--- src/contexts/ForumContext.tsx | 16 +- src/hooks/useDelegation.ts | 70 ++++ src/hooks/useMessageSigning.ts | 48 +++ src/hooks/useWallet.ts | 54 +++ .../CryptoService.ts => delegation/index.ts} | 145 +++---- src/lib/delegation/storage.ts | 38 ++ src/lib/delegation/types.ts | 19 + src/lib/forum/ForumActions.ts | 11 +- src/lib/services/AuthService.ts | 358 ------------------ src/lib/services/MessageService.ts | 15 +- src/lib/services/WalletService/index.ts | 252 ------------ src/lib/services/index.ts | 10 - src/lib/utils/MessageValidator.ts | 16 +- .../WalletService => wallet}/config.ts | 0 src/lib/wallet/index.ts | 196 ++++++++++ src/lib/wallet/types.ts | 12 + 20 files changed, 702 insertions(+), 798 deletions(-) create mode 100644 src/hooks/useDelegation.ts create mode 100644 src/hooks/useMessageSigning.ts create mode 100644 src/hooks/useWallet.ts rename src/lib/{services/CryptoService.ts => delegation/index.ts} (63%) create mode 100644 src/lib/delegation/storage.ts create mode 100644 src/lib/delegation/types.ts delete mode 100644 src/lib/services/AuthService.ts delete mode 100644 src/lib/services/WalletService/index.ts rename src/lib/{services/WalletService => wallet}/config.ts (100%) create mode 100644 src/lib/wallet/index.ts create mode 100644 src/lib/wallet/types.ts diff --git a/src/App.tsx b/src/App.tsx index 3f46068..6026da9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -23,9 +23,9 @@ import PostPage from './pages/PostPage'; import NotFound from './pages/NotFound'; import Dashboard from './pages/Dashboard'; import Index from './pages/Index'; -import { appkitConfig } from './lib/services/WalletService/config'; +import { appkitConfig } from './lib/wallet/config'; import { WagmiProvider } from 'wagmi'; -import { config } from './lib/services/WalletService/config'; +import { config } from './lib/wallet/config'; import { AppKitProvider } from '@reown/appkit/react'; // Create a client diff --git a/src/components/ui/author-display.tsx b/src/components/ui/author-display.tsx index b398609..c7ecc1a 100644 --- a/src/components/ui/author-display.tsx +++ b/src/components/ui/author-display.tsx @@ -3,7 +3,7 @@ import { Badge } from '@/components/ui/badge'; import { Shield, Crown } from 'lucide-react'; import { UserVerificationStatus } from '@/types/forum'; import { getEnsName } from '@wagmi/core'; -import { config } from '@/lib/services/WalletService/config'; +import { config } from '@/lib/wallet/config'; import { OrdinalAPI } from '@/lib/services/Ordinal'; interface AuthorDisplayProps { diff --git a/src/components/ui/delegation-step.tsx b/src/components/ui/delegation-step.tsx index 9a55396..3473a01 100644 --- a/src/components/ui/delegation-step.tsx +++ b/src/components/ui/delegation-step.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { Button } from './button'; import { useAuth } from '@/contexts/useAuth'; import { CheckCircle, AlertCircle, Trash2 } from 'lucide-react'; -import { DelegationDuration } from '@/lib/services/CryptoService'; +import { DelegationDuration } from '@/lib/delegation'; interface DelegationStepProps { onComplete: () => void; diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx index 090e71b..f6fd0b3 100644 --- a/src/contexts/AuthContext.tsx +++ b/src/contexts/AuthContext.tsx @@ -1,9 +1,9 @@ -import React, { createContext, useState, useEffect, useRef } from 'react'; +import React, { createContext, useState, useEffect, useMemo } from 'react'; import { useToast } from '@/components/ui/use-toast'; import { OpchanMessage } from '@/types/forum'; import { User, EVerificationStatus, DisplayPreference } from '@/types/identity'; -import { AuthService, CryptoService, DelegationDuration } from '@/lib/services'; -import { AuthResult } from '@/lib/services/AuthService'; +import { WalletManager } from '@/lib/wallet'; +import { DelegationManager, DelegationDuration } from '@/lib/delegation'; import { useAppKitAccount, useDisconnect, modal } from '@reown/appkit/react'; export type VerificationStatus = @@ -53,26 +53,152 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const activeAccount = isBitcoinConnected ? bitcoinAccount : ethereumAccount; const address = activeAccount.address; - // Create service instances that persist between renders - const cryptoServiceRef = useRef(new CryptoService()); - const authServiceRef = useRef(new AuthService(cryptoServiceRef.current)); + // Create manager instances that persist between renders + const walletManager = useMemo(() => new WalletManager(), []); + const delegationManager = useMemo(() => new DelegationManager(), []); - // Set AppKit instance and accounts in AuthService + // Set AppKit instance and accounts in WalletManager useEffect(() => { if (modal) { - authServiceRef.current.setAppKit(modal); + walletManager.setAppKit(modal); } - }, []); + }, [walletManager]); useEffect(() => { - authServiceRef.current.setAccounts(bitcoinAccount, ethereumAccount); - }, [bitcoinAccount, ethereumAccount]); + walletManager.setAccounts(bitcoinAccount, ethereumAccount); + }, [bitcoinAccount, ethereumAccount, walletManager]); + + // Helper functions for user persistence + const loadStoredUser = (): User | null => { + const storedUser = localStorage.getItem('opchan-user'); + if (!storedUser) return null; + + try { + const user = JSON.parse(storedUser); + const lastChecked = user.lastChecked || 0; + const expiryTime = 24 * 60 * 60 * 1000; + + if (Date.now() - lastChecked < expiryTime) { + return user; + } else { + localStorage.removeItem('opchan-user'); + return null; + } + } catch (e) { + console.error('Failed to parse stored user data', e); + localStorage.removeItem('opchan-user'); + return null; + } + }; + + const saveUser = (user: User): void => { + localStorage.setItem('opchan-user', JSON.stringify(user)); + }; + + // Helper function for ownership verification + const verifyUserOwnership = async (user: User): Promise => { + if (user.walletType === 'bitcoin') { + // TODO: revert when the API is ready + // const response = await ordinalApi.getOperatorDetails(user.address); + // const hasOperators = response.has_operators; + const hasOperators = true; + + return { + ...user, + ordinalDetails: hasOperators + ? { ordinalId: 'mock', ordinalDetails: 'Mock ordinal for testing' } + : undefined, + verificationStatus: hasOperators + ? EVerificationStatus.VERIFIED_OWNER + : EVerificationStatus.VERIFIED_BASIC, + lastChecked: Date.now(), + }; + } else if (user.walletType === 'ethereum') { + try { + const walletInfo = await walletManager.getWalletInfo(); + const hasENS = !!walletInfo?.ensName; + const ensName = walletInfo?.ensName; + + return { + ...user, + ensDetails: hasENS && ensName ? { ensName } : undefined, + verificationStatus: hasENS + ? EVerificationStatus.VERIFIED_OWNER + : EVerificationStatus.VERIFIED_BASIC, + lastChecked: Date.now(), + }; + } catch (error) { + console.error('Error verifying ENS ownership:', error); + return { + ...user, + ensDetails: undefined, + verificationStatus: EVerificationStatus.VERIFIED_BASIC, + lastChecked: Date.now(), + }; + } + } else { + throw new Error('Unknown wallet type'); + } + }; + + // Helper function for key delegation + const createUserDelegation = async ( + user: User, + duration: DelegationDuration = '7days' + ): Promise => { + try { + const walletType = user.walletType; + const isAvailable = walletManager.isWalletConnected(walletType); + + if (!isAvailable) { + throw new Error( + `${walletType} wallet is not available or connected. Please ensure it is connected.` + ); + } + + // Generate new keypair + const keypair = delegationManager.generateKeypair(); + + // Create delegation message with expiry + const expiryHours = DelegationManager.getDurationHours(duration); + const expiryTimestamp = Date.now() + expiryHours * 60 * 60 * 1000; + const delegationMessage = delegationManager.createDelegationMessage( + keypair.publicKey, + user.address, + expiryTimestamp + ); + + // Sign the delegation message with wallet + const signature = await walletManager.signMessage( + delegationMessage, + walletType + ); + + // Create and store the delegation + delegationManager.createDelegation( + user.address, + signature, + keypair.publicKey, + keypair.privateKey, + duration, + walletType + ); + + return true; + } catch (error) { + console.error( + `Error creating key delegation for ${user.walletType}:`, + error + ); + return false; + } + }; // Sync with AppKit wallet state useEffect(() => { if (isConnected && address) { // Check if we have a stored user for this address - const storedUser = authServiceRef.current.loadStoredUser(); + const storedUser = loadStoredUser(); if (storedUser && storedUser.address === address) { // Use stored user data @@ -90,35 +216,34 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { // For Ethereum wallets, try to check ENS ownership immediately if (isEthereumConnected) { - authServiceRef.current + walletManager .getWalletInfo() .then(walletInfo => { if (walletInfo?.ensName) { const updatedUser = { ...newUser, - ensOwnership: true, - ensName: walletInfo.ensName, + ensDetails: { ensName: walletInfo.ensName }, verificationStatus: EVerificationStatus.VERIFIED_OWNER, }; setCurrentUser(updatedUser); setVerificationStatus('verified-owner'); - authServiceRef.current.saveUser(updatedUser); + saveUser(updatedUser); } else { setCurrentUser(newUser); setVerificationStatus('verified-basic'); - authServiceRef.current.saveUser(newUser); + saveUser(newUser); } }) .catch(() => { // Fallback to basic verification if ENS check fails setCurrentUser(newUser); setVerificationStatus('verified-basic'); - authServiceRef.current.saveUser(newUser); + saveUser(newUser); }); } else { setCurrentUser(newUser); setVerificationStatus('verified-basic'); - authServiceRef.current.saveUser(newUser); + saveUser(newUser); } const chainName = isBitcoinConnected ? 'Bitcoin' : 'Ethereum'; @@ -142,7 +267,14 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { setCurrentUser(null); setVerificationStatus('unverified'); } - }, [isConnected, address, isBitcoinConnected, isEthereumConnected, toast]); + }, [ + isConnected, + address, + isBitcoinConnected, + isEthereumConnected, + toast, + walletManager, + ]); const { disconnect } = useDisconnect(); @@ -165,13 +297,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const getVerificationStatus = (user: User): VerificationStatus => { if (user.walletType === 'bitcoin') { - return user.ordinalDetails - ? EVerificationStatus.VERIFIED_OWNER - : EVerificationStatus.VERIFIED_BASIC; + return user.ordinalDetails ? 'verified-owner' : 'verified-basic'; } else if (user.walletType === 'ethereum') { - return user.ensDetails - ? EVerificationStatus.VERIFIED_OWNER - : EVerificationStatus.VERIFIED_BASIC; + return user.ensDetails ? 'verified-owner' : 'verified-basic'; } return 'unverified'; }; @@ -197,16 +325,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { description: `Checking your wallet for ${verificationType} ownership...`, }); - const result: AuthResult = - await authServiceRef.current.verifyOwnership(currentUser); - - if (!result.success) { - throw new Error(result.error); - } - - const updatedUser = result.user!; + const updatedUser = await verifyUserOwnership(currentUser); setCurrentUser(updatedUser); - authServiceRef.current.saveUser(updatedUser); + saveUser(updatedUser); // Update verification status setVerificationStatus(getVerificationStatus(updatedUser)); @@ -284,18 +405,29 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { description: `This will let you post, comment, and vote without approving each action for ${durationText}.`, }); - const result: AuthResult = await authServiceRef.current.delegateKey( - currentUser, - duration - ); - - if (!result.success) { - throw new Error(result.error); + const success = await createUserDelegation(currentUser, duration); + if (!success) { + throw new Error('Failed to create key delegation'); } - const updatedUser = result.user!; + // Update user with delegation info + const browserPublicKey = delegationManager.getBrowserPublicKey(); + const delegationStatus = delegationManager.getDelegationStatus( + currentUser.address, + currentUser.walletType + ); + + const updatedUser = { + ...currentUser, + browserPubKey: browserPublicKey || undefined, + delegationSignature: delegationStatus.isValid ? 'valid' : undefined, + delegationExpiry: delegationStatus.timeRemaining + ? Date.now() + delegationStatus.timeRemaining + : undefined, + }; + setCurrentUser(updatedUser); - authServiceRef.current.saveUser(updatedUser); + saveUser(updatedUser); // Format date for user-friendly display const expiryDate = new Date(updatedUser.delegationExpiry!); @@ -328,15 +460,15 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { }; const isDelegationValid = (): boolean => { - return cryptoServiceRef.current.isDelegationValid(); + return delegationManager.isDelegationValid(); }; const delegationTimeRemaining = (): number => { - return cryptoServiceRef.current.getDelegationTimeRemaining(); + return delegationManager.getDelegationTimeRemaining(); }; const clearDelegation = (): void => { - cryptoServiceRef.current.clearDelegation(); + delegationManager.clearDelegation(); // Update the current user to remove delegation info if (currentUser) { @@ -346,7 +478,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { browserPublicKey: undefined, }; setCurrentUser(updatedUser); - authServiceRef.current.saveUser(updatedUser); + saveUser(updatedUser); } toast({ @@ -360,10 +492,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { signMessage: async ( message: OpchanMessage ): Promise => { - return cryptoServiceRef.current.signMessage(message); + return delegationManager.signMessageWithDelegatedKey(message); }, verifyMessage: (message: OpchanMessage): boolean => { - return cryptoServiceRef.current.verifyMessage(message); + return delegationManager.verifyMessage(message); }, }; diff --git a/src/contexts/ForumContext.tsx b/src/contexts/ForumContext.tsx index 21d469c..c6a64ad 100644 --- a/src/contexts/ForumContext.tsx +++ b/src/contexts/ForumContext.tsx @@ -19,9 +19,9 @@ import messageManager from '@/lib/waku'; import { getDataFromCache } from '@/lib/forum/transformers'; import { RelevanceCalculator } from '@/lib/forum/RelevanceCalculator'; import { UserVerificationStatus } from '@/types/forum'; -import { CryptoService } from '@/lib/services'; +import { DelegationManager } from '@/lib/delegation'; import { getEnsName } from '@wagmi/core'; -import { config } from '@/lib/services/WalletService/config'; +import { config } from '@/lib/wallet/config'; interface ForumContextType { cells: Cell[]; @@ -98,17 +98,17 @@ export function ForumProvider({ children }: { children: React.ReactNode }) { const { toast } = useToast(); const { currentUser, isAuthenticated } = useAuth(); - const cryptoService = useMemo(() => new CryptoService(), []); + const delegationManager = useMemo(() => new DelegationManager(), []); const forumActions = useMemo( - () => new ForumActions(cryptoService), - [cryptoService] + () => new ForumActions(delegationManager), + [delegationManager] ); // Transform message cache data to the expected types const updateStateFromCache = useCallback(() => { - // Use the verifyMessage function from cryptoService if available + // Use the verifyMessage function from delegationManager if available const verifyFn = isAuthenticated - ? (message: OpchanMessage) => cryptoService.verifyMessage(message) + ? (message: OpchanMessage) => delegationManager.verifyMessage(message) : undefined; // Build user verification status for relevance calculation @@ -214,7 +214,7 @@ export function ForumProvider({ children }: { children: React.ReactNode }) { setComments(transformed.comments); setUserVerificationStatus(enrichedStatus); })(); - }, [cryptoService, isAuthenticated, currentUser]); + }, [delegationManager, isAuthenticated, currentUser]); const handleRefreshData = async () => { setIsRefreshing(true); diff --git a/src/hooks/useDelegation.ts b/src/hooks/useDelegation.ts new file mode 100644 index 0000000..0161084 --- /dev/null +++ b/src/hooks/useDelegation.ts @@ -0,0 +1,70 @@ +import { useCallback, useContext, useMemo } from 'react'; +import { AuthContext } from '@/contexts/AuthContext'; +import { DelegationDuration } from '@/lib/delegation'; + +export const useDelegation = () => { + const context = useContext(AuthContext); + + if (!context) { + throw new Error('useDelegation must be used within an AuthProvider'); + } + + const { + delegateKey: contextDelegateKey, + isDelegationValid: contextIsDelegationValid, + delegationTimeRemaining: contextDelegationTimeRemaining, + clearDelegation: contextClearDelegation, + isAuthenticating, + } = context; + + const createDelegation = useCallback( + async (duration?: DelegationDuration): Promise => { + return contextDelegateKey(duration); + }, + [contextDelegateKey] + ); + + const clearDelegation = useCallback((): void => { + contextClearDelegation(); + }, [contextClearDelegation]); + + const delegationStatus = useMemo(() => { + const isValid = contextIsDelegationValid(); + const timeRemaining = contextDelegationTimeRemaining(); + + return { + hasDelegation: timeRemaining > 0, + isValid, + timeRemaining: timeRemaining > 0 ? timeRemaining : undefined, + expiresAt: + timeRemaining > 0 ? new Date(Date.now() + timeRemaining) : undefined, + }; + }, [contextIsDelegationValid, contextDelegationTimeRemaining]); + + const formatTimeRemaining = useCallback((timeMs: number): string => { + const hours = Math.floor(timeMs / (1000 * 60 * 60)); + const minutes = Math.floor((timeMs % (1000 * 60 * 60)) / (1000 * 60)); + + if (hours > 24) { + const days = Math.floor(hours / 24); + return `${days} day${days === 1 ? '' : 's'}`; + } else if (hours > 0) { + return `${hours}h ${minutes}m`; + } else { + return `${minutes}m`; + } + }, []); + + return { + // Delegation status + delegationStatus, + isCreatingDelegation: isAuthenticating, + + // Delegation actions + createDelegation, + clearDelegation, + + // Utilities + formatTimeRemaining, + }; +}; diff --git a/src/hooks/useMessageSigning.ts b/src/hooks/useMessageSigning.ts new file mode 100644 index 0000000..c211edd --- /dev/null +++ b/src/hooks/useMessageSigning.ts @@ -0,0 +1,48 @@ +import { useCallback, useContext } from 'react'; +import { AuthContext } from '@/contexts/AuthContext'; +import { OpchanMessage } from '@/types/forum'; + +export const useMessageSigning = () => { + const context = useContext(AuthContext); + + if (!context) { + throw new Error('useMessageSigning must be used within an AuthProvider'); + } + + const { + signMessage: contextSignMessage, + verifyMessage: contextVerifyMessage, + isDelegationValid, + } = context; + + const signMessage = useCallback( + async (message: OpchanMessage): Promise => { + // Check if we have a valid delegation before attempting to sign + if (!isDelegationValid()) { + console.warn('No valid delegation found. Cannot sign message.'); + return null; + } + + return contextSignMessage(message); + }, + [contextSignMessage, isDelegationValid] + ); + + const verifyMessage = useCallback( + (message: OpchanMessage): boolean => { + return contextVerifyMessage(message); + }, + [contextVerifyMessage] + ); + + const canSignMessages = useCallback((): boolean => { + return isDelegationValid(); + }, [isDelegationValid]); + + return { + // Message signing + signMessage, + verifyMessage, + canSignMessages, + }; +}; diff --git a/src/hooks/useWallet.ts b/src/hooks/useWallet.ts new file mode 100644 index 0000000..ff28f6d --- /dev/null +++ b/src/hooks/useWallet.ts @@ -0,0 +1,54 @@ +import { useCallback, useContext } from 'react'; +import { AuthContext } from '@/contexts/AuthContext'; +import { modal } from '@reown/appkit/react'; + +export const useWallet = () => { + const context = useContext(AuthContext); + + if (!context) { + throw new Error('useWallet must be used within an AuthProvider'); + } + + const { + currentUser, + isAuthenticated, + verificationStatus, + connectWallet: contextConnectWallet, + disconnectWallet: contextDisconnectWallet, + } = context; + + const connect = useCallback(async (): Promise => { + return contextConnectWallet(); + }, [contextConnectWallet]); + + const disconnect = useCallback((): void => { + contextDisconnectWallet(); + }, [contextDisconnectWallet]); + + const openModal = useCallback(async (): Promise => { + if (modal) { + await modal.open(); + } + }, []); + + const closeModal = useCallback((): void => { + if (modal) { + modal.close(); + } + }, []); + + return { + // Wallet state + isConnected: isAuthenticated, + address: currentUser?.address, + walletType: currentUser?.walletType, + verificationStatus, + currentUser, + + // Wallet actions + connect, + disconnect, + openModal, + closeModal, + }; +}; diff --git a/src/lib/services/CryptoService.ts b/src/lib/delegation/index.ts similarity index 63% rename from src/lib/services/CryptoService.ts rename to src/lib/delegation/index.ts index c7cbaf0..7a471ec 100644 --- a/src/lib/services/CryptoService.ts +++ b/src/lib/delegation/index.ts @@ -1,67 +1,15 @@ -/** - * CryptoService - Unified cryptographic operations - * - * Combines key delegation and message signing functionality into a single, - * cohesive service focused on all cryptographic operations. - */ - import * as ed from '@noble/ed25519'; import { sha512 } from '@noble/hashes/sha512'; import { bytesToHex, hexToBytes } from '@/lib/utils'; -import { LOCAL_STORAGE_KEYS } from '@/lib/waku/constants'; import { OpchanMessage } from '@/types/forum'; import { UnsignedMessage } from '@/types/waku'; +import { DelegationDuration, DelegationInfo, DelegationStatus } from './types'; +import { DelegationStorage } from './storage'; -export interface DelegationSignature { - signature: string; // Signature from wallet - expiryTimestamp: number; // When this delegation expires - browserPublicKey: string; // Browser-generated public key that was delegated to - walletAddress: string; // Wallet address that signed the delegation - walletType: 'bitcoin' | 'ethereum'; // Type of wallet that created the delegation -} - -export interface DelegationInfo extends DelegationSignature { - browserPrivateKey: string; -} - +// Set up ed25519 with sha512 ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m)); -export type DelegationDuration = '7days' | '30days'; - -export interface CryptoServiceInterface { - // Delegation management - createDelegation( - walletAddress: string, - signature: string, - browserPublicKey: string, - browserPrivateKey: string, - duration: DelegationDuration, - walletType: 'bitcoin' | 'ethereum' - ): void; - isDelegationValid( - currentAddress?: string, - currentWalletType?: 'bitcoin' | 'ethereum' - ): boolean; - getDelegationTimeRemaining(): number; - getBrowserPublicKey(): string | null; - clearDelegation(): void; - - // Keypair generation - generateKeypair(): { publicKey: string; privateKey: string }; - createDelegationMessage( - browserPublicKey: string, - walletAddress: string, - expiryTimestamp: number - ): string; - - // Message operations - signMessage(message: UnsignedMessage): OpchanMessage | null; - verifyMessage(message: OpchanMessage): boolean; -} - -export class CryptoService implements CryptoServiceInterface { - private static readonly STORAGE_KEY = LOCAL_STORAGE_KEYS.KEY_DELEGATION; - +export class DelegationManager { // Duration options in hours private static readonly DURATION_HOURS = { '7days': 24 * 7, // 168 hours @@ -72,7 +20,7 @@ export class CryptoService implements CryptoServiceInterface { * Get the number of hours for a given duration */ static getDurationHours(duration: DelegationDuration): number { - return CryptoService.DURATION_HOURS[duration]; + return DelegationManager.DURATION_HOURS[duration]; } // ============================================================================ @@ -80,7 +28,7 @@ export class CryptoService implements CryptoServiceInterface { // ============================================================================ /** - * Generates a new browser-based keypair for signing messages + * Generate a new browser-based keypair for signing messages */ generateKeypair(): { publicKey: string; privateKey: string } { const privateKey = ed.utils.randomPrivateKey(); @@ -96,7 +44,7 @@ export class CryptoService implements CryptoServiceInterface { } /** - * Creates a delegation message to be signed by the wallet + * Create a delegation message to be signed by the wallet */ createDelegationMessage( browserPublicKey: string, @@ -107,11 +55,11 @@ export class CryptoService implements CryptoServiceInterface { } // ============================================================================ - // DELEGATION MANAGEMENT + // DELEGATION LIFECYCLE // ============================================================================ /** - * Creates and stores a delegation + * Create and store a delegation */ createDelegation( walletAddress: string, @@ -121,7 +69,7 @@ export class CryptoService implements CryptoServiceInterface { duration: DelegationDuration = '7days', walletType: 'bitcoin' | 'ethereum' ): void { - const expiryHours = CryptoService.getDurationHours(duration); + const expiryHours = DelegationManager.getDurationHours(duration); const expiryTimestamp = Date.now() + expiryHours * 60 * 60 * 1000; const delegationInfo: DelegationInfo = { @@ -133,35 +81,17 @@ export class CryptoService implements CryptoServiceInterface { walletType, }; - localStorage.setItem( - CryptoService.STORAGE_KEY, - JSON.stringify(delegationInfo) - ); + DelegationStorage.store(delegationInfo); } /** - * Retrieves delegation information from local storage - */ - private retrieveDelegation(): DelegationInfo | null { - const delegationJson = localStorage.getItem(CryptoService.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 + * Check if a delegation is valid */ isDelegationValid( currentAddress?: string, currentWalletType?: 'bitcoin' | 'ethereum' ): boolean { - const delegation = this.retrieveDelegation(); + const delegation = DelegationStorage.retrieve(); if (!delegation) return false; // Check if delegation has expired @@ -182,10 +112,10 @@ export class CryptoService implements CryptoServiceInterface { } /** - * Gets the time remaining on the current delegation + * Get the time remaining on the current delegation in milliseconds */ getDelegationTimeRemaining(): number { - const delegation = this.retrieveDelegation(); + const delegation = DelegationStorage.retrieve(); if (!delegation) return 0; const now = Date.now(); @@ -193,19 +123,37 @@ export class CryptoService implements CryptoServiceInterface { } /** - * Gets the browser public key from the current delegation + * Get the browser public key from the current delegation */ getBrowserPublicKey(): string | null { - const delegation = this.retrieveDelegation(); + const delegation = DelegationStorage.retrieve(); if (!delegation) return null; return delegation.browserPublicKey; } /** - * Clears the stored delegation + * Get delegation status + */ + getDelegationStatus( + currentAddress?: string, + currentWalletType?: 'bitcoin' | 'ethereum' + ): DelegationStatus { + const hasDelegation = this.getBrowserPublicKey() !== null; + const isValid = this.isDelegationValid(currentAddress, currentWalletType); + const timeRemaining = this.getDelegationTimeRemaining(); + + return { + hasDelegation, + isValid, + timeRemaining: timeRemaining > 0 ? timeRemaining : undefined, + }; + } + + /** + * Clear the stored delegation */ clearDelegation(): void { - localStorage.removeItem(CryptoService.STORAGE_KEY); + DelegationStorage.clear(); } // ============================================================================ @@ -213,10 +161,10 @@ export class CryptoService implements CryptoServiceInterface { // ============================================================================ /** - * Signs a raw string message using the browser-generated private key + * Sign a raw string message using the browser-generated private key */ signRawMessage(message: string): string | null { - const delegation = this.retrieveDelegation(); + const delegation = DelegationStorage.retrieve(); if (!delegation || !this.isDelegationValid()) return null; try { @@ -232,15 +180,15 @@ export class CryptoService implements CryptoServiceInterface { } /** - * Signs an unsigned message with the delegated browser key + * Sign an unsigned message with the delegated browser key */ - signMessage(message: UnsignedMessage): OpchanMessage | null { + signMessageWithDelegatedKey(message: UnsignedMessage): OpchanMessage | null { if (!this.isDelegationValid()) { console.error('No valid key delegation found. Cannot sign message.'); return null; } - const delegation = this.retrieveDelegation(); + const delegation = DelegationStorage.retrieve(); if (!delegation) return null; // Create the message content to sign (without signature fields) @@ -261,7 +209,7 @@ export class CryptoService implements CryptoServiceInterface { } /** - * Verifies an OpchanMessage signature + * Verify an OpchanMessage signature */ verifyMessage(message: OpchanMessage): boolean { // Check for required signature fields @@ -294,7 +242,7 @@ export class CryptoService implements CryptoServiceInterface { } /** - * Verifies a signature made with the browser key + * Verify a signature made with the browser key */ private verifyRawSignature( message: string, @@ -313,3 +261,8 @@ export class CryptoService implements CryptoServiceInterface { } } } + +// Export singleton instance +export const delegationManager = new DelegationManager(); +export * from './types'; +export { DelegationStorage }; diff --git a/src/lib/delegation/storage.ts b/src/lib/delegation/storage.ts new file mode 100644 index 0000000..c15a929 --- /dev/null +++ b/src/lib/delegation/storage.ts @@ -0,0 +1,38 @@ +import { LOCAL_STORAGE_KEYS } from '@/lib/waku/constants'; +import { DelegationInfo } from './types'; + +export class DelegationStorage { + private static readonly STORAGE_KEY = LOCAL_STORAGE_KEYS.KEY_DELEGATION; + + /** + * Store delegation information in localStorage + */ + static store(delegation: DelegationInfo): void { + localStorage.setItem( + DelegationStorage.STORAGE_KEY, + JSON.stringify(delegation) + ); + } + + /** + * Retrieve delegation information from localStorage + */ + static retrieve(): DelegationInfo | null { + const delegationJson = localStorage.getItem(DelegationStorage.STORAGE_KEY); + if (!delegationJson) return null; + + try { + return JSON.parse(delegationJson); + } catch (e) { + console.error('Failed to parse delegation information', e); + return null; + } + } + + /** + * Clear stored delegation information + */ + static clear(): void { + localStorage.removeItem(DelegationStorage.STORAGE_KEY); + } +} diff --git a/src/lib/delegation/types.ts b/src/lib/delegation/types.ts new file mode 100644 index 0000000..4012c0f --- /dev/null +++ b/src/lib/delegation/types.ts @@ -0,0 +1,19 @@ +export type DelegationDuration = '7days' | '30days'; + +export interface DelegationSignature { + signature: string; // Signature from wallet + expiryTimestamp: number; // When this delegation expires + browserPublicKey: string; // Browser-generated public key that was delegated to + walletAddress: string; // Wallet address that signed the delegation + walletType: 'bitcoin' | 'ethereum'; // Type of wallet that created the delegation +} + +export interface DelegationInfo extends DelegationSignature { + browserPrivateKey: string; +} + +export interface DelegationStatus { + hasDelegation: boolean; + isValid: boolean; + timeRemaining?: number; +} diff --git a/src/lib/forum/ForumActions.ts b/src/lib/forum/ForumActions.ts index 902ef42..ec6ab55 100644 --- a/src/lib/forum/ForumActions.ts +++ b/src/lib/forum/ForumActions.ts @@ -13,7 +13,8 @@ import { import { Cell, Comment, Post } from '@/types/forum'; import { EVerificationStatus, User } from '@/types/identity'; import { transformCell, transformComment, transformPost } from './transformers'; -import { MessageService, CryptoService } from '@/lib/services'; +import { MessageService } from '@/lib/services'; +import { DelegationManager } from '@/lib/delegation'; type ActionResult = { success: boolean; @@ -22,12 +23,12 @@ type ActionResult = { }; export class ForumActions { - private cryptoService: CryptoService; + private delegationManager: DelegationManager; private messageService: MessageService; - constructor(cryptoService?: CryptoService) { - this.cryptoService = cryptoService || new CryptoService(); - this.messageService = new MessageService(this.cryptoService); + constructor(delegationManager?: DelegationManager) { + this.delegationManager = delegationManager || new DelegationManager(); + this.messageService = new MessageService(this.delegationManager); } /* ------------------------------------------------------------------ diff --git a/src/lib/services/AuthService.ts b/src/lib/services/AuthService.ts deleted file mode 100644 index 60a5018..0000000 --- a/src/lib/services/AuthService.ts +++ /dev/null @@ -1,358 +0,0 @@ -import walletService from './WalletService'; -import { UseAppKitAccountReturn } from '@reown/appkit/react'; -import { AppKit } from '@reown/appkit'; -import { CryptoService, DelegationDuration } from './CryptoService'; -import { EVerificationStatus, User, DisplayPreference } from '@/types/identity'; -import { WalletInfo } from './WalletService'; - -export interface AuthResult { - success: boolean; - user?: User; - error?: string; -} - -export interface AuthServiceInterface { - // Wallet operations - setAccounts( - bitcoinAccount: UseAppKitAccountReturn, - ethereumAccount: UseAppKitAccountReturn - ): void; - setAppKit(appKit: AppKit): void; - connectWallet(): Promise; - disconnectWallet(): Promise; - - // Verification - verifyOwnership(user: User): Promise; - - // Delegation setup - delegateKey(user: User, duration?: DelegationDuration): Promise; - - // User persistence - loadStoredUser(): User | null; - saveUser(user: User): void; - clearStoredUser(): void; - - // Wallet info - getWalletInfo(): Promise; -} - -export class AuthService implements AuthServiceInterface { - private walletService: typeof walletService; - private cryptoService: CryptoService; - - constructor(cryptoService: CryptoService) { - this.walletService = walletService; - this.cryptoService = cryptoService; - } - - /** - * Set AppKit accounts for wallet service - */ - setAccounts( - bitcoinAccount: UseAppKitAccountReturn, - ethereumAccount: UseAppKitAccountReturn - ) { - this.walletService.setAccounts(bitcoinAccount, ethereumAccount); - } - - /** - * Set AppKit instance for wallet service - */ - setAppKit(appKit: AppKit) { - this.walletService.setAppKit(appKit); - } - - /** - * Get the active wallet address - */ - private getActiveAddress(): string | null { - const isBitcoinConnected = this.walletService.isWalletAvailable('bitcoin'); - const isEthereumConnected = - this.walletService.isWalletAvailable('ethereum'); - - if (isBitcoinConnected) { - return this.walletService.getActiveAddress('bitcoin') || null; - } else if (isEthereumConnected) { - return this.walletService.getActiveAddress('ethereum') || null; - } - - return null; - } - - /** - * Connect to wallet and create user - */ - async connectWallet(): Promise { - try { - // Check which wallet is connected - const isBitcoinConnected = - this.walletService.isWalletAvailable('bitcoin'); - const isEthereumConnected = - this.walletService.isWalletAvailable('ethereum'); - - if (!isBitcoinConnected && !isEthereumConnected) { - return { - success: false, - error: 'No wallet connected', - }; - } - - // Determine which wallet is active - const walletType = isBitcoinConnected ? 'bitcoin' : 'ethereum'; - const address = this.getActiveAddress(); - - if (!address) { - return { - success: false, - error: 'No wallet address available', - }; - } - - const user: User = { - address: address, - walletType: walletType, - verificationStatus: EVerificationStatus.UNVERIFIED, - displayPreference: DisplayPreference.WALLET_ADDRESS, - lastChecked: Date.now(), - }; - - // Add ENS info for Ethereum wallets (if available) - if (walletType === 'ethereum') { - try { - const walletInfo = await this.walletService.getWalletInfo(); - if (walletInfo?.ensName) { - user.ensDetails = { - ensName: walletInfo.ensName, - }; - } - } catch (error) { - console.warn( - 'Failed to resolve ENS during wallet connection:', - error - ); - user.ensDetails = undefined; - } - } - - return { - success: true, - user, - }; - } catch (error) { - return { - success: false, - error: - error instanceof Error ? error.message : 'Failed to connect wallet', - }; - } - } - - /** - * Disconnect wallet and clear stored data - */ - async disconnectWallet(): Promise { - // Clear any existing delegations when disconnecting - this.cryptoService.clearDelegation(); - - // Clear stored user data - this.clearStoredUser(); - } - - /** - * Verify ordinal ownership for Bitcoin users or ENS ownership for Ethereum users - */ - async verifyOwnership(user: User): Promise { - try { - if (user.walletType === 'bitcoin') { - return await this.verifyBitcoinOrdinal(user); - } else if (user.walletType === 'ethereum') { - return await this.verifyEthereumENS(user); - } else { - return { - success: false, - error: 'Unknown wallet type', - }; - } - } catch (error) { - return { - success: false, - error: - error instanceof Error ? error.message : 'Failed to verify ownership', - }; - } - } - - /** - * Verify Bitcoin Ordinal ownership - */ - private async verifyBitcoinOrdinal(user: User): Promise { - // TODO: revert when the API is ready - // const response = await this.ordinalApi.getOperatorDetails(user.address); - // const hasOperators = response.has_operators; - const hasOperators = true; - - const updatedUser = { - ...user, - ordinalOwnership: hasOperators, - verificationStatus: hasOperators - ? EVerificationStatus.VERIFIED_OWNER - : EVerificationStatus.VERIFIED_BASIC, - lastChecked: Date.now(), - }; - - return { - success: true, - user: updatedUser, - }; - } - - /** - * Verify Ethereum ENS ownership - */ - private async verifyEthereumENS(user: User): Promise { - try { - // Get wallet info with ENS resolution - const walletInfo = await this.walletService.getWalletInfo(); - - const hasENS = !!walletInfo?.ensName; - const ensName = walletInfo?.ensName; - - const updatedUser = { - ...user, - ensOwnership: hasENS, - ensName: ensName, - verificationStatus: hasENS - ? EVerificationStatus.VERIFIED_OWNER - : EVerificationStatus.VERIFIED_BASIC, - lastChecked: Date.now(), - }; - - return { - success: true, - user: updatedUser, - }; - } catch (error) { - console.error('Error verifying ENS ownership:', error); - - // Fall back to basic verification on error - const updatedUser = { - ...user, - ensOwnership: false, - ensName: undefined, - verificationStatus: EVerificationStatus.VERIFIED_BASIC, - lastChecked: Date.now(), - }; - - return { - success: true, - user: updatedUser, - }; - } - } - - /** - * Set up key delegation for the user - */ - async delegateKey( - user: User, - duration: DelegationDuration = '7days' - ): Promise { - try { - const walletType = user.walletType; - const isAvailable = this.walletService.isWalletAvailable(walletType); - - if (!isAvailable) { - return { - success: false, - error: `${walletType} wallet is not available or connected. Please ensure it is connected.`, - }; - } - - const success = await this.walletService.createKeyDelegation( - walletType, - duration - ); - - if (!success) { - return { - success: false, - error: 'Failed to create key delegation', - }; - } - - // Get delegation status to update user - const delegationStatus = - this.walletService.getDelegationStatus(walletType); - - // Get the actual browser public key from the delegation - const browserPublicKey = this.cryptoService.getBrowserPublicKey(); - - const updatedUser = { - ...user, - browserPubKey: browserPublicKey || undefined, - delegationSignature: delegationStatus.isValid ? 'valid' : undefined, - delegationExpiry: delegationStatus.timeRemaining - ? Date.now() + delegationStatus.timeRemaining - : undefined, - }; - - return { - success: true, - user: updatedUser, - }; - } catch (error) { - return { - success: false, - error: - error instanceof Error ? error.message : 'Failed to delegate key', - }; - } - } - - /** - * Get current wallet info - */ - async getWalletInfo(): Promise { - // Use the wallet service to get detailed wallet info including ENS - return await this.walletService.getWalletInfo(); - } - - /** - * Load user from localStorage - */ - loadStoredUser(): User | null { - const storedUser = localStorage.getItem('opchan-user'); - if (!storedUser) return null; - - try { - const user = JSON.parse(storedUser); - const lastChecked = user.lastChecked || 0; - const expiryTime = 24 * 60 * 60 * 1000; - - if (Date.now() - lastChecked < expiryTime) { - return user; - } else { - localStorage.removeItem('opchan-user'); - return null; - } - } catch (e) { - console.error('Failed to parse stored user data', e); - localStorage.removeItem('opchan-user'); - return null; - } - } - - /** - * Save user to localStorage - */ - saveUser(user: User): void { - localStorage.setItem('opchan-user', JSON.stringify(user)); - } - - /** - * Clear stored user data - */ - clearStoredUser(): void { - localStorage.removeItem('opchan-user'); - } -} diff --git a/src/lib/services/MessageService.ts b/src/lib/services/MessageService.ts index c5328fe..19a5240 100644 --- a/src/lib/services/MessageService.ts +++ b/src/lib/services/MessageService.ts @@ -1,6 +1,6 @@ import { OpchanMessage } from '@/types/forum'; import { UnsignedMessage } from '@/types/waku'; -import { CryptoService } from './CryptoService'; +import { DelegationManager } from '@/lib/delegation'; import messageManager from '@/lib/waku'; export interface MessageResult { @@ -15,10 +15,10 @@ export interface MessageServiceInterface { } export class MessageService implements MessageServiceInterface { - private cryptoService: CryptoService; + private delegationManager: DelegationManager; - constructor(cryptoService: CryptoService) { - this.cryptoService = cryptoService; + constructor(delegationManager: DelegationManager) { + this.delegationManager = delegationManager; } /** @@ -26,12 +26,13 @@ export class MessageService implements MessageServiceInterface { */ async sendMessage(message: UnsignedMessage): Promise { try { - const signedMessage = this.cryptoService.signMessage(message); + const signedMessage = + this.delegationManager.signMessageWithDelegatedKey(message); if (!signedMessage) { // Check if delegation exists but is expired const isDelegationExpired = - this.cryptoService.isDelegationValid() === false; + this.delegationManager.isDelegationValid() === false; return { success: false, @@ -81,6 +82,6 @@ export class MessageService implements MessageServiceInterface { * Verify a message signature */ verifyMessage(message: OpchanMessage): boolean { - return this.cryptoService.verifyMessage(message); + return this.delegationManager.verifyMessage(message); } } diff --git a/src/lib/services/WalletService/index.ts b/src/lib/services/WalletService/index.ts deleted file mode 100644 index b9fcf13..0000000 --- a/src/lib/services/WalletService/index.ts +++ /dev/null @@ -1,252 +0,0 @@ -import { UseAppKitAccountReturn } from '@reown/appkit/react'; -import { - CryptoService, - DelegationDuration, -} from '../CryptoService'; -import { AppKit } from '@reown/appkit'; -import { getEnsName } from '@wagmi/core'; -import { ChainNamespace } from '@reown/appkit-common'; -import { config } from './config'; -import { Provider } from '@reown/appkit-controllers'; - -export interface WalletInfo { - address: string; - walletType: 'bitcoin' | 'ethereum'; - ensName?: string; - isConnected: boolean; -} - -class WalletService { - private cryptoService: CryptoService; - private bitcoinAccount?: UseAppKitAccountReturn; - private ethereumAccount?: UseAppKitAccountReturn; - private appKit?: AppKit; - - constructor() { - this.cryptoService = new CryptoService(); - } - - /** - * Set account references from AppKit hooks - */ - setAccounts( - bitcoinAccount: UseAppKitAccountReturn, - ethereumAccount: UseAppKitAccountReturn - ) { - this.bitcoinAccount = bitcoinAccount; - this.ethereumAccount = ethereumAccount; - } - - /** - * Set the AppKit instance for accessing adapters - */ - setAppKit(appKit: AppKit) { - this.appKit = appKit; - } - - /** - * Check if a wallet type is available and connected - */ - isWalletAvailable(walletType: 'bitcoin' | 'ethereum'): boolean { - if (walletType === 'bitcoin') { - return this.bitcoinAccount?.isConnected ?? false; - } else { - return this.ethereumAccount?.isConnected ?? false; - } - } - - /** - * Get the active account based on wallet type - */ - private getActiveAccount( - walletType: 'bitcoin' | 'ethereum' - ): UseAppKitAccountReturn | undefined { - return walletType === 'bitcoin' - ? this.bitcoinAccount - : this.ethereumAccount; - } - - /** - * Get the active address for a given wallet type - */ - getActiveAddress(walletType: 'bitcoin' | 'ethereum'): string | undefined { - const account = this.getActiveAccount(walletType); - return account?.address; - } - - /** - * Get the appropriate namespace for the wallet type - */ - private getNamespace(walletType: 'bitcoin' | 'ethereum'): ChainNamespace { - return walletType === 'bitcoin' ? 'bip122' : 'eip155'; - } - - /** - * Sign a message using the appropriate adapter - */ - async signMessage( - messageBytes: Uint8Array, - walletType: 'bitcoin' | 'ethereum' - ): Promise { - if (!this.appKit) { - throw new Error('AppKit instance not set. Call setAppKit() first.'); - } - - const account = this.getActiveAccount(walletType); - if (!account?.address) { - throw new Error(`No ${walletType} wallet connected`); - } - - const namespace = this.getNamespace(walletType); - - // Convert message bytes to string for signing - const messageString = new TextDecoder().decode(messageBytes); - - try { - // Access the adapter through the appKit instance - // The adapter is available through the appKit's chainAdapters property - const adapter = this.appKit.chainAdapters?.[namespace]; - - if (!adapter) { - throw new Error(`No adapter found for namespace: ${namespace}`); - } - - // Get the provider for the current connection - const provider = this.appKit.getProvider(namespace); - - if (!provider) { - throw new Error(`No provider found for namespace: ${namespace}`); - } - - // Call the adapter's signMessage method - const result = await adapter.signMessage({ - message: messageString, - address: account.address, - provider: provider as Provider, - }); - - return result.signature; - } catch (error) { - console.error(`Error signing message with ${walletType} wallet:`, error); - throw new Error( - `Failed to sign message with ${walletType} wallet: ${error instanceof Error ? error.message : 'Unknown error'}` - ); - } - } - - /** - * Create a key delegation for the connected wallet - */ - async createKeyDelegation( - walletType: 'bitcoin' | 'ethereum', - duration: DelegationDuration = '7days' - ): Promise { - try { - const account = this.getActiveAccount(walletType); - if (!account?.address) { - throw new Error(`No ${walletType} wallet connected`); - } - - // Generate a new browser keypair - const keypair = this.cryptoService.generateKeypair(); - - // Create delegation message with expiry - const expiryHours = CryptoService.getDurationHours(duration); - const expiryTimestamp = Date.now() + expiryHours * 60 * 60 * 1000; - const delegationMessage = this.cryptoService.createDelegationMessage( - keypair.publicKey, - account.address, - expiryTimestamp - ); - - const messageBytes = new TextEncoder().encode(delegationMessage); - - // Sign the delegation message - const signature = await this.signMessage(messageBytes, walletType); - - // Create and store the delegation - this.cryptoService.createDelegation( - account.address, - signature, - keypair.publicKey, - keypair.privateKey, - duration, - walletType - ); - - return true; - } catch (error) { - console.error(`Error creating key delegation for ${walletType}:`, error); - return false; - } - } - - /** - * Get delegation status for the connected wallet - */ - getDelegationStatus(walletType: 'bitcoin' | 'ethereum'): { - hasDelegation: boolean; - isValid: boolean; - timeRemaining?: number; - } { - const account = this.getActiveAccount(walletType); - const currentAddress = account?.address; - - const hasDelegation = this.cryptoService.getBrowserPublicKey() !== null; - const isValid = this.cryptoService.isDelegationValid( - currentAddress, - walletType - ); - const timeRemaining = this.cryptoService.getDelegationTimeRemaining(); - - return { - hasDelegation, - isValid, - timeRemaining: timeRemaining > 0 ? timeRemaining : undefined, - }; - } - - /** - * Clear delegation for the connected wallet - */ - clearDelegation(): void { - this.cryptoService.clearDelegation(); - } - - /** - * Get wallet connection info with ENS resolution for Ethereum - */ - async getWalletInfo(): Promise { - if (this.bitcoinAccount?.isConnected) { - return { - address: this.bitcoinAccount.address as string, - walletType: 'bitcoin', - isConnected: true, - }; - } else if (this.ethereumAccount?.isConnected) { - // Use Wagmi to resolve ENS name - let ensName: string | undefined; - try { - const resolvedName = await getEnsName(config, { - address: this.ethereumAccount.address as `0x${string}`, - }); - ensName = resolvedName || undefined; - } catch (error) { - console.warn('Failed to resolve ENS name:', error); - // Continue without ENS name - } - - return { - address: this.ethereumAccount.address as string, - walletType: 'ethereum', - ensName, - isConnected: true, - }; - } - - return null; - } -} - -const walletService = new WalletService(); -export default walletService; diff --git a/src/lib/services/index.ts b/src/lib/services/index.ts index 2c81577..26d26f2 100644 --- a/src/lib/services/index.ts +++ b/src/lib/services/index.ts @@ -1,11 +1 @@ -export { - AuthService, - type AuthServiceInterface, - type AuthResult, -} from './AuthService'; export { MessageService, type MessageServiceInterface } from './MessageService'; -export { - CryptoService, - type CryptoServiceInterface, - type DelegationDuration, -} from './CryptoService'; diff --git a/src/lib/utils/MessageValidator.ts b/src/lib/utils/MessageValidator.ts index 9f75073..96a866d 100644 --- a/src/lib/utils/MessageValidator.ts +++ b/src/lib/utils/MessageValidator.ts @@ -1,5 +1,5 @@ import { OpchanMessage } from '@/types/forum'; -import { CryptoService } from '@/lib/services/CryptoService'; +import { DelegationManager } from '@/lib/delegation'; /** * Type for potential message objects with partial structure @@ -18,10 +18,10 @@ interface PartialMessage { * Ensures all messages have valid signatures and browserPubKey */ export class MessageValidator { - private cryptoService: CryptoService; + private delegationManager: DelegationManager; - constructor(cryptoService?: CryptoService) { - this.cryptoService = cryptoService || new CryptoService(); + constructor(delegationManager?: DelegationManager) { + this.delegationManager = delegationManager || new DelegationManager(); } /** @@ -34,7 +34,7 @@ export class MessageValidator { } // Verify signature - we know it's safe to cast here since hasRequiredFields passed - return this.cryptoService.verifyMessage(message as OpchanMessage); + return this.delegationManager.verifyMessage(message as OpchanMessage); } /** @@ -123,7 +123,7 @@ export class MessageValidator { continue; } - if (!this.cryptoService.verifyMessage(message as OpchanMessage)) { + if (!this.delegationManager.verifyMessage(message as OpchanMessage)) { invalidCount.invalidSignature++; continue; } @@ -166,7 +166,7 @@ export class MessageValidator { ); } - if (!this.cryptoService.verifyMessage(message as OpchanMessage)) { + if (!this.delegationManager.verifyMessage(message as OpchanMessage)) { const partialMsg = message as PartialMessage; throw new Error( `Message validation failed: Invalid signature (messageId: ${partialMsg?.id})` @@ -242,7 +242,7 @@ export class MessageValidator { } if (hasRequiredFields) { - hasValidSignature = this.cryptoService.verifyMessage( + hasValidSignature = this.delegationManager.verifyMessage( message as OpchanMessage ); if (!hasValidSignature) { diff --git a/src/lib/services/WalletService/config.ts b/src/lib/wallet/config.ts similarity index 100% rename from src/lib/services/WalletService/config.ts rename to src/lib/wallet/config.ts diff --git a/src/lib/wallet/index.ts b/src/lib/wallet/index.ts new file mode 100644 index 0000000..aa95c23 --- /dev/null +++ b/src/lib/wallet/index.ts @@ -0,0 +1,196 @@ +import { UseAppKitAccountReturn } from '@reown/appkit/react'; +import { AppKit } from '@reown/appkit'; +import { getEnsName } from '@wagmi/core'; +import { ChainNamespace } from '@reown/appkit-common'; +import { config } from './config'; +import { Provider } from '@reown/appkit-controllers'; +import { WalletInfo, ActiveWallet } from './types'; + +export class WalletManager { + private appKit?: AppKit; + private bitcoinAccount?: UseAppKitAccountReturn; + private ethereumAccount?: UseAppKitAccountReturn; + + /** + * Set the AppKit instance for accessing adapters + */ + setAppKit(appKit: AppKit): void { + this.appKit = appKit; + } + + /** + * Set account references from AppKit hooks + */ + setAccounts( + bitcoinAccount: UseAppKitAccountReturn, + ethereumAccount: UseAppKitAccountReturn + ): void { + this.bitcoinAccount = bitcoinAccount; + this.ethereumAccount = ethereumAccount; + } + + /** + * Get the currently active wallet (Bitcoin takes priority) + */ + getActiveWallet(): ActiveWallet | null { + if (this.bitcoinAccount?.isConnected && this.bitcoinAccount.address) { + return { + type: 'bitcoin', + address: this.bitcoinAccount.address, + isConnected: true, + }; + } + + if (this.ethereumAccount?.isConnected && this.ethereumAccount.address) { + return { + type: 'ethereum', + address: this.ethereumAccount.address, + isConnected: true, + }; + } + + return null; + } + + /** + * Check if any wallet is connected + */ + isConnected(): boolean { + return ( + (this.bitcoinAccount?.isConnected ?? false) || + (this.ethereumAccount?.isConnected ?? false) + ); + } + + /** + * Check if a specific wallet type is connected + */ + isWalletConnected(walletType: 'bitcoin' | 'ethereum'): boolean { + const account = + walletType === 'bitcoin' ? this.bitcoinAccount : this.ethereumAccount; + return account?.isConnected ?? false; + } + + /** + * Get address for a specific wallet type + */ + getAddress(walletType: 'bitcoin' | 'ethereum'): string | undefined { + const account = + walletType === 'bitcoin' ? this.bitcoinAccount : this.ethereumAccount; + return account?.address; + } + + /** + * Get the appropriate namespace for the wallet type + */ + private getNamespace(walletType: 'bitcoin' | 'ethereum'): ChainNamespace { + return walletType === 'bitcoin' ? 'bip122' : 'eip155'; + } + + /** + * Sign a message using the appropriate wallet adapter + */ + async signMessage( + message: string, + walletType: 'bitcoin' | 'ethereum' + ): Promise { + if (!this.appKit) { + throw new Error('AppKit instance not set. Call setAppKit() first.'); + } + + const account = + walletType === 'bitcoin' ? this.bitcoinAccount : this.ethereumAccount; + if (!account?.address) { + throw new Error(`No ${walletType} wallet connected`); + } + + const namespace = this.getNamespace(walletType); + + try { + // Access the adapter through the appKit instance + const adapter = this.appKit.chainAdapters?.[namespace]; + + if (!adapter) { + throw new Error(`No adapter found for namespace: ${namespace}`); + } + + // Get the provider for the current connection + const provider = this.appKit.getProvider(namespace); + + if (!provider) { + throw new Error(`No provider found for namespace: ${namespace}`); + } + + // Call the adapter's signMessage method + const result = await adapter.signMessage({ + message, + address: account.address, + provider: provider as Provider, + }); + + return result.signature; + } catch (error) { + console.error(`Error signing message with ${walletType} wallet:`, error); + throw new Error( + `Failed to sign message with ${walletType} wallet: ${error instanceof Error ? error.message : 'Unknown error'}` + ); + } + } + + /** + * Get comprehensive wallet info including ENS resolution for Ethereum + */ + async getWalletInfo(): Promise { + if (this.bitcoinAccount?.isConnected) { + return { + address: this.bitcoinAccount.address as string, + walletType: 'bitcoin', + isConnected: true, + }; + } + + if (this.ethereumAccount?.isConnected) { + const address = this.ethereumAccount.address as string; + + // Try to resolve ENS name + let ensName: string | undefined; + try { + const resolvedName = await getEnsName(config, { + address: address as `0x${string}`, + }); + ensName = resolvedName || undefined; + } catch (error) { + console.warn('Failed to resolve ENS name:', error); + } + + return { + address, + walletType: 'ethereum', + ensName, + isConnected: true, + }; + } + + return null; + } + + /** + * Resolve ENS name for an Ethereum address + */ + async resolveENS(address: string): Promise { + try { + const ensName = await getEnsName(config, { + address: address as `0x${string}`, + }); + return ensName || null; + } catch (error) { + console.warn('Failed to resolve ENS name:', error); + return null; + } + } +} + +// Export singleton instance +export const walletManager = new WalletManager(); +export * from './types'; +export * from './config'; diff --git a/src/lib/wallet/types.ts b/src/lib/wallet/types.ts new file mode 100644 index 0000000..b77f34f --- /dev/null +++ b/src/lib/wallet/types.ts @@ -0,0 +1,12 @@ +export interface WalletInfo { + address: string; + walletType: 'bitcoin' | 'ethereum'; + ensName?: string; + isConnected: boolean; +} + +export interface ActiveWallet { + type: 'bitcoin' | 'ethereum'; + address: string; + isConnected: boolean; +}