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 { WalletManager } from '@/lib/wallet'; import { DelegationManager, DelegationDuration } from '@/lib/delegation'; import { useAppKitAccount, useDisconnect, modal } from '@reown/appkit/react'; export type VerificationStatus = | 'unverified' | 'verified-none' | 'verified-basic' | 'verified-owner' | 'verifying'; interface AuthContextType { currentUser: User | null; isAuthenticating: boolean; isAuthenticated: boolean; verificationStatus: VerificationStatus; connectWallet: () => Promise; disconnectWallet: () => void; verifyOwnership: () => Promise; delegateKey: (duration?: DelegationDuration) => Promise; isDelegationValid: () => boolean; delegationTimeRemaining: () => number; clearDelegation: () => void; signMessage: (message: OpchanMessage) => Promise; verifyMessage: (message: OpchanMessage) => boolean; } const AuthContext = createContext(undefined); export { AuthContext }; export function AuthProvider({ children }: { children: React.ReactNode }) { const [currentUser, setCurrentUser] = useState(null); const [isAuthenticating, setIsAuthenticating] = useState(false); const [verificationStatus, setVerificationStatus] = useState('unverified'); const { toast } = useToast(); // Use AppKit hooks for multi-chain support const bitcoinAccount = useAppKitAccount({ namespace: 'bip122' }); const ethereumAccount = useAppKitAccount({ namespace: 'eip155' }); // Determine which account is connected const isBitcoinConnected = bitcoinAccount.isConnected; const isEthereumConnected = ethereumAccount.isConnected; const isConnected = isBitcoinConnected || isEthereumConnected; // Get the active account info const activeAccount = isBitcoinConnected ? bitcoinAccount : ethereumAccount; const address = activeAccount.address; // Create manager instances that persist between renders const walletManager = useMemo(() => new WalletManager(), []); const delegationManager = useMemo(() => new DelegationManager(), []); // Set AppKit instance and accounts in WalletManager useEffect(() => { if (modal) { walletManager.setAppKit(modal); } }, [walletManager]); useEffect(() => { 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 = loadStoredUser(); if (storedUser && storedUser.address === address) { // Use stored user data setCurrentUser(storedUser); setVerificationStatus(getVerificationStatus(storedUser)); } else { // Create new user from AppKit wallet const newUser: User = { address, walletType: isBitcoinConnected ? 'bitcoin' : 'ethereum', verificationStatus: EVerificationStatus.VERIFIED_BASIC, // Connected wallets get basic verification by default displayPreference: DisplayPreference.WALLET_ADDRESS, lastChecked: Date.now(), }; // For Ethereum wallets, try to check ENS ownership immediately if (isEthereumConnected) { walletManager .getWalletInfo() .then(walletInfo => { if (walletInfo?.ensName) { const updatedUser = { ...newUser, ensDetails: { ensName: walletInfo.ensName }, verificationStatus: EVerificationStatus.VERIFIED_OWNER, }; setCurrentUser(updatedUser); setVerificationStatus('verified-owner'); saveUser(updatedUser); } else { setCurrentUser(newUser); setVerificationStatus('verified-basic'); saveUser(newUser); } }) .catch(() => { // Fallback to basic verification if ENS check fails setCurrentUser(newUser); setVerificationStatus('verified-basic'); saveUser(newUser); }); } else { setCurrentUser(newUser); setVerificationStatus('verified-basic'); saveUser(newUser); } const chainName = isBitcoinConnected ? 'Bitcoin' : 'Ethereum'; const displayName = `${address.slice(0, 6)}...${address.slice(-4)}`; toast({ title: 'Wallet Connected', description: `Connected to ${chainName} with ${displayName}`, }); const verificationType = isBitcoinConnected ? 'Ordinal ownership' : 'ENS ownership'; toast({ title: 'Action Required', description: `You can participate in the forum now! Verify your ${verificationType} for premium features and delegate a signing key for better UX.`, }); } } else { // Wallet disconnected setCurrentUser(null); setVerificationStatus('unverified'); } }, [ isConnected, address, isBitcoinConnected, isEthereumConnected, toast, walletManager, ]); const { disconnect } = useDisconnect(); const connectWallet = async (): Promise => { try { if (modal) { await modal.open(); return true; } return false; } catch (error) { console.error('Error connecting wallet:', error); return false; } }; const disconnectWallet = (): void => { disconnect(); }; const getVerificationStatus = (user: User): VerificationStatus => { if (user.walletType === 'bitcoin') { return user.ordinalDetails ? 'verified-owner' : 'verified-basic'; } else if (user.walletType === 'ethereum') { return user.ensDetails ? 'verified-owner' : 'verified-basic'; } return 'unverified'; }; const verifyOwnership = async (): Promise => { if (!currentUser || !currentUser.address) { toast({ title: 'Not Connected', description: 'Please connect your wallet first.', variant: 'destructive', }); return false; } setIsAuthenticating(true); setVerificationStatus('verifying'); try { const verificationType = currentUser.walletType === 'bitcoin' ? 'Ordinal' : 'ENS'; toast({ title: `Verifying ${verificationType}`, description: `Checking your wallet for ${verificationType} ownership...`, }); const updatedUser = await verifyUserOwnership(currentUser); setCurrentUser(updatedUser); saveUser(updatedUser); // Update verification status setVerificationStatus(getVerificationStatus(updatedUser)); if (updatedUser.walletType === 'bitcoin' && updatedUser.ordinalDetails) { toast({ title: 'Ordinal Verified', description: 'You now have premium access with higher relevance bonuses. We recommend delegating a key for better UX.', }); } else if ( updatedUser.walletType === 'ethereum' && updatedUser.ensDetails ) { toast({ title: 'ENS Verified', description: 'You now have premium access with higher relevance bonuses. We recommend delegating a key for better UX.', }); } else { const verificationType = updatedUser.walletType === 'bitcoin' ? 'Ordinal Operators' : 'ENS domain'; toast({ title: 'Basic Access Granted', description: `No ${verificationType} found, but you can still participate in the forum with your connected wallet.`, variant: 'default', }); } return Boolean( (updatedUser.walletType === 'bitcoin' && updatedUser.ordinalDetails) || (updatedUser.walletType === 'ethereum' && updatedUser.ensDetails) ); } catch (error) { console.error('Error verifying ownership:', error); setVerificationStatus('unverified'); let errorMessage = 'Failed to verify ownership. Please try again.'; if (error instanceof Error) { errorMessage = error.message; } toast({ title: 'Verification Error', description: errorMessage, variant: 'destructive', }); return false; } finally { setIsAuthenticating(false); } }; const delegateKey = async ( duration: DelegationDuration = '7days' ): Promise => { if (!currentUser) { toast({ title: 'No User Found', description: 'Please connect your wallet first.', variant: 'destructive', }); return false; } setIsAuthenticating(true); try { const durationText = duration === '7days' ? '1 week' : '30 days'; toast({ title: 'Starting Key Delegation', description: `This will let you post, comment, and vote without approving each action for ${durationText}.`, }); const success = await createUserDelegation(currentUser, duration); if (!success) { throw new Error('Failed to create key delegation'); } // 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); saveUser(updatedUser); // Format date for user-friendly display const expiryDate = new Date(updatedUser.delegationExpiry!); const formattedExpiry = expiryDate.toLocaleString(); toast({ title: 'Key Delegation Successful', description: `You can now interact with the forum without additional wallet approvals until ${formattedExpiry}.`, }); 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); } }; const isDelegationValid = (): boolean => { return delegationManager.isDelegationValid(); }; const delegationTimeRemaining = (): number => { return delegationManager.getDelegationTimeRemaining(); }; const clearDelegation = (): void => { delegationManager.clearDelegation(); // Update the current user to remove delegation info if (currentUser) { const updatedUser = { ...currentUser, delegationExpiry: undefined, browserPublicKey: undefined, }; setCurrentUser(updatedUser); saveUser(updatedUser); } toast({ title: 'Delegation Cleared', description: "Your delegated signing key has been removed. You'll need to delegate a new key to continue posting and voting.", }); }; const messageSigning = { signMessage: async ( message: OpchanMessage ): Promise => { return delegationManager.signMessageWithDelegatedKey(message); }, verifyMessage: (message: OpchanMessage): boolean => { return delegationManager.verifyMessage(message); }, }; const value: AuthContextType = { currentUser, isAuthenticating, isAuthenticated: Boolean(currentUser && isConnected), verificationStatus, connectWallet, disconnectWallet, verifyOwnership, delegateKey, isDelegationValid, delegationTimeRemaining, clearDelegation, signMessage: messageSigning.signMessage, verifyMessage: messageSigning.verifyMessage, }; return {children}; }