chore: move all storages to indexedDB

This commit is contained in:
Danish Arora 2025-09-05 14:03:29 +05:30
parent 9e6be6156f
commit aa17bda249
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
17 changed files with 463 additions and 221 deletions

View File

@ -4,6 +4,8 @@ import { useAuth, useNetworkStatus } from '@/hooks';
import { useAuth as useAuthContext } from '@/contexts/useAuth'; import { useAuth as useAuthContext } from '@/contexts/useAuth';
import { EVerificationStatus } from '@/types/identity'; import { EVerificationStatus } from '@/types/identity';
import { useForum } from '@/contexts/useForum'; import { useForum } from '@/contexts/useForum';
import { localDatabase } from '@/lib/database/LocalDatabase';
import { DelegationFullStatus } from '@/lib/delegation';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import {
@ -31,7 +33,8 @@ import { useUserDisplay } from '@/hooks';
const Header = () => { const Header = () => {
const { verificationStatus } = useAuth(); const { verificationStatus } = useAuth();
const { getDelegationStatus } = useAuthContext(); const { getDelegationStatus } = useAuthContext();
const delegationInfo = getDelegationStatus(); const [delegationInfo, setDelegationInfo] =
useState<DelegationFullStatus | null>(null);
const networkStatus = useNetworkStatus(); const networkStatus = useNetworkStatus();
const location = useLocation(); const location = useLocation();
const { toast } = useToast(); const { toast } = useToast();
@ -57,28 +60,38 @@ const Header = () => {
// ✅ Get display name from enhanced hook // ✅ Get display name from enhanced hook
const { displayName } = useUserDisplay(address || ''); const { displayName } = useUserDisplay(address || '');
// Use sessionStorage to persist wizard state across navigation // Load delegation status
const getHasShownWizard = () => { React.useEffect(() => {
getDelegationStatus().then(setDelegationInfo).catch(console.error);
}, [getDelegationStatus]);
// Use LocalDatabase to persist wizard state across navigation
const getHasShownWizard = async (): Promise<boolean> => {
try { try {
return sessionStorage.getItem('hasShownWalletWizard') === 'true'; const value = await localDatabase.loadUIState('hasShownWalletWizard');
return value === true;
} catch { } catch {
return false; return false;
} }
}; };
const setHasShownWizard = (value: boolean) => { const setHasShownWizard = async (value: boolean): Promise<void> => {
try { try {
sessionStorage.setItem('hasShownWalletWizard', value.toString()); await localDatabase.storeUIState('hasShownWalletWizard', value);
} catch { } catch (e) {
// Fallback if sessionStorage is not available console.error('Failed to store wizard state', e);
} }
}; };
// Auto-open wizard when wallet connects for the first time // Auto-open wizard when wallet connects for the first time
React.useEffect(() => { React.useEffect(() => {
if (isConnected && !getHasShownWizard()) { if (isConnected) {
setWalletWizardOpen(true); getHasShownWizard().then(hasShown => {
setHasShownWizard(true); if (!hasShown) {
setWalletWizardOpen(true);
setHasShownWizard(true).catch(console.error);
}
});
} }
}, [isConnected]); }, [isConnected]);
@ -88,7 +101,7 @@ const Header = () => {
const handleDisconnect = async () => { const handleDisconnect = async () => {
await disconnect(); await disconnect();
setHasShownWizard(false); // Reset so wizard can show again on next connection await setHasShownWizard(false); // Reset so wizard can show again on next connection
toast({ toast({
title: 'Wallet Disconnected', title: 'Wallet Disconnected',
description: 'Your wallet has been disconnected successfully.', description: 'Your wallet has been disconnected successfully.',
@ -99,7 +112,7 @@ const Header = () => {
if (!isConnected) return 'Connect Wallet'; if (!isConnected) return 'Connect Wallet';
if (verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED) { if (verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED) {
return delegationInfo.isValid ? 'Ready to Post' : 'Delegation Expired'; return delegationInfo?.isValid ? 'Ready to Post' : 'Delegation Expired';
} else if (verificationStatus === EVerificationStatus.WALLET_CONNECTED) { } else if (verificationStatus === EVerificationStatus.WALLET_CONNECTED) {
return 'Verified (Read-only)'; return 'Verified (Read-only)';
} else { } else {
@ -112,7 +125,7 @@ const Header = () => {
if ( if (
verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED && verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED &&
delegationInfo.isValid delegationInfo?.isValid
) { ) {
return 'text-green-400'; return 'text-green-400';
} else if (verificationStatus === EVerificationStatus.WALLET_CONNECTED) { } else if (verificationStatus === EVerificationStatus.WALLET_CONNECTED) {
@ -131,7 +144,7 @@ const Header = () => {
if ( if (
verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED && verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED &&
delegationInfo.isValid delegationInfo?.isValid
) { ) {
return <CheckCircle className="w-4 h-4" />; return <CheckCircle className="w-4 h-4" />;
} else if (verificationStatus === EVerificationStatus.WALLET_CONNECTED) { } else if (verificationStatus === EVerificationStatus.WALLET_CONNECTED) {
@ -244,7 +257,7 @@ const Header = () => {
<div className="text-xs"> <div className="text-xs">
<div>Address: {address?.slice(0, 8)}...</div> <div>Address: {address?.slice(0, 8)}...</div>
<div>Status: {getAccountStatusText()}</div> <div>Status: {getAccountStatusText()}</div>
{delegationInfo.timeRemaining && ( {delegationInfo?.timeRemaining && (
<div> <div>
Delegation: {delegationInfo.timeRemaining} remaining Delegation: {delegationInfo.timeRemaining} remaining
</div> </div>

View File

@ -1,3 +1,4 @@
import React, { useState, useEffect } from 'react';
import { import {
useForumData, useForumData,
useAuth, useAuth,
@ -10,6 +11,7 @@ import {
useForumSelectors, useForumSelectors,
} from '@/hooks'; } from '@/hooks';
import { useAuth as useAuthContext } from '@/contexts/useAuth'; import { useAuth as useAuthContext } from '@/contexts/useAuth';
import { DelegationFullStatus } from '@/lib/delegation';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
@ -24,6 +26,12 @@ export function HookDemoComponent() {
const forumData = useForumData(); const forumData = useForumData();
const auth = useAuth(); const auth = useAuth();
const { getDelegationStatus } = useAuthContext(); const { getDelegationStatus } = useAuthContext();
const [delegationStatus, setDelegationStatus] = useState<DelegationFullStatus | null>(null);
// Load delegation status
useEffect(() => {
getDelegationStatus().then(setDelegationStatus).catch(console.error);
}, [getDelegationStatus]);
// Derived hooks for specific data // Derived hooks for specific data
const userVotes = useUserVotes(); const userVotes = useUserVotes();
@ -137,7 +145,7 @@ export function HookDemoComponent() {
</div> </div>
<div> <div>
<strong>Delegation Active:</strong>{' '} <strong>Delegation Active:</strong>{' '}
{getDelegationStatus().isValid ? 'Yes' : 'No'} {delegationStatus?.isValid ? 'Yes' : 'No'}
</div> </div>
</div> </div>

View File

@ -1,9 +1,9 @@
import React from 'react'; import React, { useState, useEffect } from 'react';
import { Button } from './button'; import { Button } from './button';
import { useAuth, useAuthActions } from '@/hooks'; import { useAuth, useAuthActions } from '@/hooks';
import { useAuth as useAuthContext } from '@/contexts/useAuth'; import { useAuth as useAuthContext } from '@/contexts/useAuth';
import { CheckCircle, AlertCircle, Trash2 } from 'lucide-react'; import { CheckCircle, AlertCircle, Trash2 } from 'lucide-react';
import { DelegationDuration } from '@/lib/delegation'; import { DelegationDuration, DelegationFullStatus } from '@/lib/delegation';
interface DelegationStepProps { interface DelegationStepProps {
onComplete: () => void; onComplete: () => void;
@ -20,9 +20,15 @@ export function DelegationStep({
}: DelegationStepProps) { }: DelegationStepProps) {
const { currentUser, isAuthenticating } = useAuth(); const { currentUser, isAuthenticating } = useAuth();
const { getDelegationStatus } = useAuthContext(); const { getDelegationStatus } = useAuthContext();
const delegationInfo = getDelegationStatus(); const [delegationInfo, setDelegationInfo] =
useState<DelegationFullStatus | null>(null);
const { delegateKey, clearDelegation } = useAuthActions(); const { delegateKey, clearDelegation } = useAuthActions();
// Load delegation status
useEffect(() => {
getDelegationStatus().then(setDelegationInfo).catch(console.error);
}, [getDelegationStatus]);
const [selectedDuration, setSelectedDuration] = const [selectedDuration, setSelectedDuration] =
React.useState<DelegationDuration>('7days'); React.useState<DelegationDuration>('7days');
const [delegationResult, setDelegationResult] = React.useState<{ const [delegationResult, setDelegationResult] = React.useState<{
@ -128,19 +134,19 @@ export function DelegationStep({
<div className="space-y-3"> <div className="space-y-3">
{/* Status */} {/* Status */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{delegationInfo.isValid ? ( {delegationInfo?.isValid ? (
<CheckCircle className="h-4 w-4 text-green-500" /> <CheckCircle className="h-4 w-4 text-green-500" />
) : ( ) : (
<AlertCircle className="h-4 w-4 text-yellow-500" /> <AlertCircle className="h-4 w-4 text-yellow-500" />
)} )}
<span <span
className={`text-sm font-medium ${ className={`text-sm font-medium ${
delegationInfo.isValid ? 'text-green-400' : 'text-yellow-400' delegationInfo?.isValid ? 'text-green-400' : 'text-yellow-400'
}`} }`}
> >
{delegationInfo.isValid ? 'Delegated' : 'Required'} {delegationInfo?.isValid ? 'Delegated' : 'Required'}
</span> </span>
{delegationInfo.isValid && delegationInfo.timeRemaining && ( {delegationInfo?.isValid && delegationInfo?.timeRemaining && (
<span className="text-xs text-neutral-400"> <span className="text-xs text-neutral-400">
{delegationInfo.timeRemaining} remaining {delegationInfo.timeRemaining} remaining
</span> </span>
@ -148,7 +154,7 @@ export function DelegationStep({
</div> </div>
{/* Duration Selection */} {/* Duration Selection */}
{!delegationInfo.isValid && ( {!delegationInfo?.isValid && (
<div className="space-y-3"> <div className="space-y-3">
<label className="text-sm font-medium text-neutral-300"> <label className="text-sm font-medium text-neutral-300">
Delegation Duration: Delegation Duration:
@ -187,7 +193,7 @@ export function DelegationStep({
)} )}
{/* Delegated Browser Public Key */} {/* Delegated Browser Public Key */}
{delegationInfo.isValid && currentUser?.browserPubKey && ( {delegationInfo?.isValid && currentUser?.browserPubKey && (
<div className="text-xs text-neutral-400"> <div className="text-xs text-neutral-400">
<div className="font-mono break-all bg-neutral-800 p-2 rounded"> <div className="font-mono break-all bg-neutral-800 p-2 rounded">
{currentUser.browserPubKey} {currentUser.browserPubKey}
@ -203,7 +209,7 @@ export function DelegationStep({
)} )}
{/* Delete Button for Active Delegations */} {/* Delete Button for Active Delegations */}
{delegationInfo.isValid && ( {delegationInfo?.isValid && (
<div className="flex justify-end"> <div className="flex justify-end">
<Button <Button
onClick={clearDelegation} onClick={clearDelegation}
@ -221,7 +227,7 @@ export function DelegationStep({
{/* Action Buttons */} {/* Action Buttons */}
<div className="mt-auto space-y-2"> <div className="mt-auto space-y-2">
{delegationInfo.isValid ? ( {delegationInfo?.isValid ? (
<Button <Button
onClick={handleComplete} onClick={handleComplete}
className="w-full bg-green-600 hover:bg-green-700 text-white" className="w-full bg-green-600 hover:bg-green-700 text-white"

View File

@ -11,6 +11,7 @@ import { CheckCircle, Circle, Loader2 } from 'lucide-react';
import { useAuth } from '@/hooks'; import { useAuth } from '@/hooks';
import { useAuth as useAuthContext } from '@/contexts/useAuth'; import { useAuth as useAuthContext } from '@/contexts/useAuth';
import { EVerificationStatus } from '@/types/identity'; import { EVerificationStatus } from '@/types/identity';
import { DelegationFullStatus } from '@/lib/delegation';
import { WalletConnectionStep } from './wallet-connection-step'; import { WalletConnectionStep } from './wallet-connection-step';
import { VerificationStep } from './verification-step'; import { VerificationStep } from './verification-step';
import { DelegationStep } from './delegation-step'; import { DelegationStep } from './delegation-step';
@ -32,9 +33,15 @@ export function WalletWizard({
const [isLoading, setIsLoading] = React.useState(false); const [isLoading, setIsLoading] = React.useState(false);
const { isAuthenticated, verificationStatus } = useAuth(); const { isAuthenticated, verificationStatus } = useAuth();
const { getDelegationStatus } = useAuthContext(); const { getDelegationStatus } = useAuthContext();
const delegationInfo = getDelegationStatus(); const [delegationInfo, setDelegationInfo] =
React.useState<DelegationFullStatus | null>(null);
const hasInitialized = React.useRef(false); const hasInitialized = React.useRef(false);
// Load delegation status
React.useEffect(() => {
getDelegationStatus().then(setDelegationInfo).catch(console.error);
}, [getDelegationStatus]);
// Reset wizard when opened and determine starting step // Reset wizard when opened and determine starting step
React.useEffect(() => { React.useEffect(() => {
if (open && !hasInitialized.current) { if (open && !hasInitialized.current) {
@ -50,7 +57,7 @@ export function WalletWizard({
isAuthenticated && isAuthenticated &&
(verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED || (verificationStatus === EVerificationStatus.ENS_ORDINAL_VERIFIED ||
verificationStatus === EVerificationStatus.WALLET_CONNECTED) && verificationStatus === EVerificationStatus.WALLET_CONNECTED) &&
!delegationInfo.isValid !delegationInfo?.isValid
) { ) {
setCurrentStep(3); // Start at delegation step if verified but no valid delegation setCurrentStep(3); // Start at delegation step if verified but no valid delegation
} else { } else {
@ -92,7 +99,7 @@ export function WalletWizard({
) { ) {
return 'disabled'; return 'disabled';
} }
return delegationInfo.isValid ? 'complete' : 'current'; return delegationInfo?.isValid ? 'complete' : 'current';
} }
return 'disabled'; return 'disabled';
}; };

View File

@ -12,6 +12,7 @@ import {
DelegationDuration, DelegationDuration,
DelegationFullStatus, DelegationFullStatus,
} from '@/lib/delegation'; } from '@/lib/delegation';
import { localDatabase } from '@/lib/database/LocalDatabase';
import { useAppKitAccount, useDisconnect, modal } from '@reown/appkit/react'; import { useAppKitAccount, useDisconnect, modal } from '@reown/appkit/react';
// Removed VerificationStatus type - using EVerificationStatus enum directly // Removed VerificationStatus type - using EVerificationStatus enum directly
@ -25,8 +26,8 @@ interface AuthContextType {
disconnectWallet: () => void; disconnectWallet: () => void;
verifyOwnership: () => Promise<boolean>; verifyOwnership: () => Promise<boolean>;
delegateKey: (duration?: DelegationDuration) => Promise<boolean>; delegateKey: (duration?: DelegationDuration) => Promise<boolean>;
getDelegationStatus: () => DelegationFullStatus; getDelegationStatus: () => Promise<DelegationFullStatus>;
clearDelegation: () => void; clearDelegation: () => Promise<void>;
signMessage: (message: OpchanMessage) => Promise<OpchanMessage | null>; signMessage: (message: OpchanMessage) => Promise<OpchanMessage | null>;
verifyMessage: (message: OpchanMessage) => Promise<boolean>; verifyMessage: (message: OpchanMessage) => Promise<boolean>;
} }
@ -73,30 +74,21 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
}, [bitcoinAccount, ethereumAccount]); }, [bitcoinAccount, ethereumAccount]);
// Helper functions for user persistence // Helper functions for user persistence
const loadStoredUser = (): User | null => { const loadStoredUser = async (): Promise<User | null> => {
const storedUser = localStorage.getItem('opchan-user');
if (!storedUser) return null;
try { try {
const user = JSON.parse(storedUser); return await localDatabase.loadUser();
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) { } catch (e) {
console.error('Failed to parse stored user data', e); console.error('Failed to load stored user data', e);
localStorage.removeItem('opchan-user');
return null; return null;
} }
}; };
const saveUser = (user: User): void => { const saveUser = async (user: User): Promise<void> => {
localStorage.setItem('opchan-user', JSON.stringify(user)); try {
await localDatabase.storeUser(user);
} catch (e) {
console.error('Failed to save user data', e);
}
}; };
// Helper function for ownership verification // Helper function for ownership verification
@ -188,83 +180,83 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
useEffect(() => { useEffect(() => {
if (isConnected && address) { if (isConnected && address) {
// Check if we have a stored user for this address // Check if we have a stored user for this address
const storedUser = loadStoredUser(); loadStoredUser().then(async storedUser => {
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.WALLET_CONNECTED, // Connected wallets get basic verification by default
displayPreference: EDisplayPreference.WALLET_ADDRESS,
lastChecked: Date.now(),
};
if (storedUser && storedUser.address === address) { // For Ethereum wallets, try to check ENS ownership immediately
// Use stored user data if (isEthereumConnected) {
setCurrentUser(storedUser); try {
setVerificationStatus(getVerificationStatus(storedUser)); const walletManager = WalletManager.getInstance();
} else { walletManager
// Create new user from AppKit wallet .getWalletInfo()
const newUser: User = { .then(async walletInfo => {
address, if (walletInfo?.ensName) {
walletType: isBitcoinConnected ? 'bitcoin' : 'ethereum', const updatedUser = {
verificationStatus: EVerificationStatus.WALLET_CONNECTED, // Connected wallets get basic verification by default ...newUser,
displayPreference: EDisplayPreference.WALLET_ADDRESS, ensDetails: { ensName: walletInfo.ensName },
lastChecked: Date.now(), verificationStatus:
}; EVerificationStatus.ENS_ORDINAL_VERIFIED,
};
// For Ethereum wallets, try to check ENS ownership immediately setCurrentUser(updatedUser);
if (isEthereumConnected) { setVerificationStatus(
try { EVerificationStatus.ENS_ORDINAL_VERIFIED
const walletManager = WalletManager.getInstance(); );
walletManager await saveUser(updatedUser);
.getWalletInfo() } else {
.then(walletInfo => { setCurrentUser(newUser);
if (walletInfo?.ensName) { setVerificationStatus(EVerificationStatus.WALLET_CONNECTED);
const updatedUser = { await saveUser(newUser);
...newUser, }
ensDetails: { ensName: walletInfo.ensName }, })
verificationStatus: .catch(async () => {
EVerificationStatus.ENS_ORDINAL_VERIFIED, // Fallback to basic verification if ENS check fails
};
setCurrentUser(updatedUser);
setVerificationStatus(
EVerificationStatus.ENS_ORDINAL_VERIFIED
);
saveUser(updatedUser);
} else {
setCurrentUser(newUser); setCurrentUser(newUser);
setVerificationStatus(EVerificationStatus.WALLET_CONNECTED); setVerificationStatus(EVerificationStatus.WALLET_CONNECTED);
saveUser(newUser); await saveUser(newUser);
} });
}) } catch {
.catch(() => { // WalletManager not ready, fallback to basic verification
// Fallback to basic verification if ENS check fails setCurrentUser(newUser);
setCurrentUser(newUser); setVerificationStatus(EVerificationStatus.WALLET_CONNECTED);
setVerificationStatus(EVerificationStatus.WALLET_CONNECTED); await saveUser(newUser);
saveUser(newUser); }
}); } else {
} catch {
// WalletManager not ready, fallback to basic verification
setCurrentUser(newUser); setCurrentUser(newUser);
setVerificationStatus(EVerificationStatus.WALLET_CONNECTED); setVerificationStatus(EVerificationStatus.WALLET_CONNECTED);
saveUser(newUser); await saveUser(newUser);
} }
} else {
setCurrentUser(newUser); const chainName = isBitcoinConnected ? 'Bitcoin' : 'Ethereum';
setVerificationStatus(EVerificationStatus.WALLET_CONNECTED); // Note: We can't use useUserDisplay hook here since this is not a React component
saveUser(newUser); // This is just for toast messages, so simple truncation is acceptable
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.`,
});
} }
});
const chainName = isBitcoinConnected ? 'Bitcoin' : 'Ethereum';
// Note: We can't use useUserDisplay hook here since this is not a React component
// This is just for toast messages, so simple truncation is acceptable
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 { } else {
// Wallet disconnected // Wallet disconnected
setCurrentUser(null); setCurrentUser(null);
@ -327,7 +319,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const updatedUser = await verifyUserOwnership(currentUser); const updatedUser = await verifyUserOwnership(currentUser);
setCurrentUser(updatedUser); setCurrentUser(updatedUser);
saveUser(updatedUser); await saveUser(updatedUser);
// Update verification status // Update verification status
setVerificationStatus(getVerificationStatus(updatedUser)); setVerificationStatus(getVerificationStatus(updatedUser));
@ -411,7 +403,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
} }
// Update user with delegation info // Update user with delegation info
const delegationStatus = delegationManager.getStatus( const delegationStatus = await delegationManager.getStatus(
currentUser.address, currentUser.address,
currentUser.walletType currentUser.walletType
); );
@ -426,7 +418,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
}; };
setCurrentUser(updatedUser); setCurrentUser(updatedUser);
saveUser(updatedUser); await saveUser(updatedUser);
// Format date for user-friendly display // Format date for user-friendly display
const expiryDate = new Date(updatedUser.delegationExpiry!); const expiryDate = new Date(updatedUser.delegationExpiry!);
@ -458,15 +450,15 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
} }
}; };
const getDelegationStatus = (): DelegationFullStatus => { const getDelegationStatus = async (): Promise<DelegationFullStatus> => {
return delegationManager.getStatus( return await delegationManager.getStatus(
currentUser?.address, currentUser?.address,
currentUser?.walletType currentUser?.walletType
); );
}; };
const clearDelegation = (): void => { const clearDelegation = async (): Promise<void> => {
delegationManager.clear(); await delegationManager.clear();
// Update the current user to remove delegation info // Update the current user to remove delegation info
if (currentUser) { if (currentUser) {
@ -476,7 +468,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
browserPublicKey: undefined, browserPublicKey: undefined,
}; };
setCurrentUser(updatedUser); setCurrentUser(updatedUser);
saveUser(updatedUser); await saveUser(updatedUser);
} }
toast({ toast({
@ -490,7 +482,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
signMessage: async ( signMessage: async (
message: OpchanMessage message: OpchanMessage
): Promise<OpchanMessage | null> => { ): Promise<OpchanMessage | null> => {
return delegationManager.signMessage(message); return await delegationManager.signMessage(message);
}, },
verifyMessage: async (message: OpchanMessage): Promise<boolean> => { verifyMessage: async (message: OpchanMessage): Promise<boolean> => {
return await delegationManager.verify(message); return await delegationManager.verify(message);

View File

@ -245,7 +245,7 @@ export function useAuthActions(): AuthActions {
// Clear delegation // Clear delegation
const clearDelegation = useCallback(async (): Promise<boolean> => { const clearDelegation = useCallback(async (): Promise<boolean> => {
const delegationInfo = getDelegationStatus(); const delegationInfo = await getDelegationStatus();
if (!delegationInfo.isValid) { if (!delegationInfo.isValid) {
toast({ toast({
title: 'No Active Delegation', title: 'No Active Delegation',

View File

@ -1,4 +1,4 @@
import { useCallback, useContext, useMemo } from 'react'; import { useCallback, useContext, useState, useEffect } from 'react';
import { AuthContext } from '@/contexts/AuthContext'; import { AuthContext } from '@/contexts/AuthContext';
import { DelegationDuration } from '@/lib/delegation'; import { DelegationDuration } from '@/lib/delegation';
@ -27,20 +27,36 @@ export const useDelegation = () => {
contextClearDelegation(); contextClearDelegation();
}, [contextClearDelegation]); }, [contextClearDelegation]);
const delegationStatus = useMemo(() => { const [delegationStatus, setDelegationStatus] = useState<{
const status = contextGetDelegationStatus(); hasDelegation: boolean;
isValid: boolean;
timeRemaining?: number;
expiresAt?: Date;
publicKey?: string;
address?: string;
walletType?: 'bitcoin' | 'ethereum';
}>({
hasDelegation: false,
isValid: false,
});
return { // Load delegation status
hasDelegation: status.hasDelegation, useEffect(() => {
isValid: status.isValid, contextGetDelegationStatus()
timeRemaining: status.timeRemaining, .then(status => {
expiresAt: status.timeRemaining setDelegationStatus({
? new Date(Date.now() + status.timeRemaining) hasDelegation: status.hasDelegation,
: undefined, isValid: status.isValid,
publicKey: status.publicKey, timeRemaining: status.timeRemaining,
address: status.address, expiresAt: status.timeRemaining
walletType: status.walletType, ? new Date(Date.now() + status.timeRemaining)
}; : undefined,
publicKey: status.publicKey,
address: status.address,
walletType: status.walletType,
});
})
.catch(console.error);
}, [contextGetDelegationStatus]); }, [contextGetDelegationStatus]);
const formatTimeRemaining = useCallback((timeMs: number): string => { const formatTimeRemaining = useCallback((timeMs: number): string => {

View File

@ -18,7 +18,8 @@ export const useMessageSigning = () => {
const signMessage = useCallback( const signMessage = useCallback(
async (message: OpchanMessage): Promise<OpchanMessage | null> => { async (message: OpchanMessage): Promise<OpchanMessage | null> => {
// Check if we have a valid delegation before attempting to sign // Check if we have a valid delegation before attempting to sign
if (!getDelegationStatus().isValid) { const delegationStatus = await getDelegationStatus();
if (!delegationStatus.isValid) {
console.warn('No valid delegation found. Cannot sign message.'); console.warn('No valid delegation found. Cannot sign message.');
return null; return null;
} }
@ -35,8 +36,9 @@ export const useMessageSigning = () => {
[contextVerifyMessage] [contextVerifyMessage]
); );
const canSignMessages = useCallback((): boolean => { const canSignMessages = useCallback(async (): Promise<boolean> => {
return getDelegationStatus().isValid; const delegationStatus = await getDelegationStatus();
return delegationStatus.isValid;
}, [getDelegationStatus]); }, [getDelegationStatus]);
return { return {

View File

@ -1,7 +1,8 @@
import { useMemo } from 'react'; import { useMemo, useState, useEffect } from 'react';
import { useForum } from '@/contexts/useForum'; import { useForum } from '@/contexts/useForum';
import { useAuth } from '@/hooks/core/useAuth'; import { useAuth } from '@/hooks/core/useAuth';
import { useAuth as useAuthContext } from '@/contexts/useAuth'; import { useAuth as useAuthContext } from '@/contexts/useAuth';
import { DelegationFullStatus } from '@/lib/delegation';
export interface NetworkHealth { export interface NetworkHealth {
isConnected: boolean; isConnected: boolean;
@ -64,7 +65,13 @@ export function useNetworkStatus(): NetworkStatusData {
const { isAuthenticated, currentUser } = useAuth(); const { isAuthenticated, currentUser } = useAuth();
const { getDelegationStatus } = useAuthContext(); const { getDelegationStatus } = useAuthContext();
const delegationInfo = getDelegationStatus(); const [delegationInfo, setDelegationInfo] =
useState<DelegationFullStatus | null>(null);
// Load delegation status
useEffect(() => {
getDelegationStatus().then(setDelegationInfo).catch(console.error);
}, [getDelegationStatus]);
// Network health assessment // Network health assessment
const health = useMemo((): NetworkHealth => { const health = useMemo((): NetworkHealth => {
@ -78,7 +85,7 @@ export function useNetworkStatus(): NetworkStatusData {
issues.push(`Forum error: ${error}`); issues.push(`Forum error: ${error}`);
} }
if (isAuthenticated && !delegationInfo.isValid) { if (isAuthenticated && !delegationInfo?.isValid) {
issues.push('Key delegation expired'); issues.push('Key delegation expired');
} }
@ -93,7 +100,7 @@ export function useNetworkStatus(): NetworkStatusData {
syncAge, syncAge,
issues, issues,
}; };
}, [isNetworkConnected, error, isAuthenticated, delegationInfo.isValid]); }, [isNetworkConnected, error, isAuthenticated, delegationInfo?.isValid]);
// Sync status // Sync status
const sync = useMemo((): SyncStatus => { const sync = useMemo((): SyncStatus => {
@ -124,9 +131,9 @@ export function useNetworkStatus(): NetworkStatusData {
status: isAuthenticated ? 'connected' : 'disconnected', status: isAuthenticated ? 'connected' : 'disconnected',
}, },
delegation: { delegation: {
active: delegationInfo.isValid, active: delegationInfo?.isValid || false,
expires: delegationInfo.timeRemaining || null, expires: delegationInfo?.timeRemaining || null,
status: delegationInfo.isValid ? 'active' : 'expired', status: delegationInfo?.isValid ? 'active' : 'expired',
}, },
}; };
}, [isNetworkConnected, isAuthenticated, currentUser, delegationInfo]); }, [isNetworkConnected, isAuthenticated, currentUser, delegationInfo]);
@ -134,7 +141,7 @@ export function useNetworkStatus(): NetworkStatusData {
// Status assessment // Status assessment
const canRefresh = !isRefreshing && !isInitialLoading; const canRefresh = !isRefreshing && !isInitialLoading;
const canSync = isNetworkConnected && !isRefreshing; const canSync = isNetworkConnected && !isRefreshing;
const needsAttention = !health.isHealthy || !delegationInfo.isValid; const needsAttention = !health.isHealthy || !delegationInfo?.isValid;
// Helper methods // Helper methods
const getStatusMessage = useMemo(() => { const getStatusMessage = useMemo(() => {
@ -157,10 +164,15 @@ export function useNetworkStatus(): NetworkStatusData {
const getHealthColor = useMemo(() => { const getHealthColor = useMemo(() => {
return (): 'green' | 'yellow' | 'red' => { return (): 'green' | 'yellow' | 'red' => {
if (!isNetworkConnected || error) return 'red'; if (!isNetworkConnected || error) return 'red';
if (health.issues.length > 0 || !delegationInfo.isValid) return 'yellow'; if (health.issues.length > 0 || !delegationInfo?.isValid) return 'yellow';
return 'green'; return 'green';
}; };
}, [isNetworkConnected, error, health.issues.length, delegationInfo.isValid]); }, [
isNetworkConnected,
error,
health.issues.length,
delegationInfo?.isValid,
]);
const getRecommendedActions = useMemo(() => { const getRecommendedActions = useMemo(() => {
return (): string[] => { return (): string[] => {
@ -175,13 +187,13 @@ export function useNetworkStatus(): NetworkStatusData {
actions.push('Connect your wallet'); actions.push('Connect your wallet');
} }
if (!delegationInfo.isValid) { if (!delegationInfo?.isValid) {
actions.push('Renew key delegation'); actions.push('Renew key delegation');
} }
if ( if (
delegationInfo.isValid && delegationInfo?.isValid &&
delegationInfo.timeRemaining && delegationInfo?.timeRemaining &&
delegationInfo.timeRemaining < 3600 delegationInfo.timeRemaining < 3600
) { ) {
actions.push('Consider renewing key delegation soon'); actions.push('Consider renewing key delegation soon');

View File

@ -14,7 +14,8 @@ import {
} from '@/types/waku'; } from '@/types/waku';
import { OpchanMessage } from '@/types/forum'; import { OpchanMessage } from '@/types/forum';
import { MessageValidator } from '@/lib/utils/MessageValidator'; import { MessageValidator } from '@/lib/utils/MessageValidator';
import { EVerificationStatus } from '@/types/identity'; import { EVerificationStatus, User } from '@/types/identity';
import { DelegationInfo } from '@/lib/delegation/types';
import { openLocalDB, STORE, StoreName } from '@/lib/database/schema'; import { openLocalDB, STORE, StoreName } from '@/lib/database/schema';
export interface LocalDatabaseCache { export interface LocalDatabaseCache {
@ -277,6 +278,9 @@ export class LocalDatabase {
| ModerateMessage | ModerateMessage
| ({ address: string } & UserIdentityCache[string]) | ({ address: string } & UserIdentityCache[string])
| { key: string; value: unknown } | { key: string; value: unknown }
| { key: string; value: User; timestamp: number }
| { key: string; value: DelegationInfo; timestamp: number }
| { key: string; value: unknown; timestamp: number }
): void { ): void {
if (!this.db) return; if (!this.db) return;
const tx = this.db.transaction(storeName, 'readwrite'); const tx = this.db.transaction(storeName, 'readwrite');
@ -327,6 +331,159 @@ export class LocalDatabase {
this.pendingListeners.add(listener); this.pendingListeners.add(listener);
return () => this.pendingListeners.delete(listener); return () => this.pendingListeners.delete(listener);
} }
// ===== User Authentication Storage =====
/**
* Store user authentication data
*/
public async storeUser(user: User): Promise<void> {
const userData = {
key: 'current',
value: user,
timestamp: Date.now(),
};
this.put(STORE.USER_AUTH, userData);
}
/**
* Load user authentication data
*/
public async loadUser(): Promise<User | null> {
if (!this.db) return null;
return new Promise((resolve, reject) => {
const tx = this.db!.transaction(STORE.USER_AUTH, 'readonly');
const store = tx.objectStore(STORE.USER_AUTH);
const request = store.get('current');
request.onerror = () => reject(request.error);
request.onsuccess = () => {
const result = request.result as
| { key: string; value: User; timestamp: number }
| undefined;
if (!result) {
resolve(null);
return;
}
const user = result.value;
const lastChecked = user.lastChecked || 0;
const expiryTime = 24 * 60 * 60 * 1000; // 24 hours
if (Date.now() - lastChecked < expiryTime) {
resolve(user);
} else {
// User data expired, clear it
this.clearUser();
resolve(null);
}
};
});
}
/**
* Clear user authentication data
*/
public async clearUser(): Promise<void> {
if (!this.db) return;
const tx = this.db.transaction(STORE.USER_AUTH, 'readwrite');
const store = tx.objectStore(STORE.USER_AUTH);
store.delete('current');
}
// ===== Delegation Storage =====
/**
* Store delegation information
*/
public async storeDelegation(delegation: DelegationInfo): Promise<void> {
const delegationData = {
key: 'current',
value: delegation,
timestamp: Date.now(),
};
this.put(STORE.DELEGATION, delegationData);
}
/**
* Load delegation information
*/
public async loadDelegation(): Promise<DelegationInfo | null> {
if (!this.db) return null;
return new Promise((resolve, reject) => {
const tx = this.db!.transaction(STORE.DELEGATION, 'readonly');
const store = tx.objectStore(STORE.DELEGATION);
const request = store.get('current');
request.onerror = () => reject(request.error);
request.onsuccess = () => {
const result = request.result as
| { key: string; value: DelegationInfo; timestamp: number }
| undefined;
resolve(result?.value || null);
};
});
}
/**
* Clear delegation information
*/
public async clearDelegation(): Promise<void> {
if (!this.db) return;
const tx = this.db.transaction(STORE.DELEGATION, 'readwrite');
const store = tx.objectStore(STORE.DELEGATION);
store.delete('current');
}
// ===== UI State Storage =====
/**
* Store UI state value
*/
public async storeUIState(key: string, value: unknown): Promise<void> {
const stateData = {
key,
value,
timestamp: Date.now(),
};
this.put(STORE.UI_STATE, stateData);
}
/**
* Load UI state value
*/
public async loadUIState(key: string): Promise<unknown> {
if (!this.db) return null;
return new Promise((resolve, reject) => {
const tx = this.db!.transaction(STORE.UI_STATE, 'readonly');
const store = tx.objectStore(STORE.UI_STATE);
const request = store.get(key);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
const result = request.result as
| { key: string; value: unknown; timestamp: number }
| undefined;
resolve(result?.value || null);
};
});
}
/**
* Clear UI state value
*/
public async clearUIState(key: string): Promise<void> {
if (!this.db) return;
const tx = this.db.transaction(STORE.UI_STATE, 'readwrite');
const store = tx.objectStore(STORE.UI_STATE);
store.delete(key);
}
} }
export const localDatabase = new LocalDatabase(); export const localDatabase = new LocalDatabase();

View File

@ -1,5 +1,5 @@
export const DB_NAME = 'opchan-local'; export const DB_NAME = 'opchan-local';
export const DB_VERSION = 1; export const DB_VERSION = 2;
export const STORE = { export const STORE = {
CELLS: 'cells', CELLS: 'cells',
@ -8,6 +8,9 @@ export const STORE = {
VOTES: 'votes', VOTES: 'votes',
MODERATIONS: 'moderations', MODERATIONS: 'moderations',
USER_IDENTITIES: 'userIdentities', USER_IDENTITIES: 'userIdentities',
USER_AUTH: 'userAuth',
DELEGATION: 'delegation',
UI_STATE: 'uiState',
META: 'meta', META: 'meta',
} as const; } as const;
@ -53,6 +56,18 @@ export function openLocalDB(): Promise<IDBDatabase> {
// User identities keyed by address // User identities keyed by address
db.createObjectStore(STORE.USER_IDENTITIES, { keyPath: 'address' }); db.createObjectStore(STORE.USER_IDENTITIES, { keyPath: 'address' });
} }
if (!db.objectStoreNames.contains(STORE.USER_AUTH)) {
// User authentication data with single key 'current'
db.createObjectStore(STORE.USER_AUTH, { keyPath: 'key' });
}
if (!db.objectStoreNames.contains(STORE.DELEGATION)) {
// Key delegation information with single key 'current'
db.createObjectStore(STORE.DELEGATION, { keyPath: 'key' });
}
if (!db.objectStoreNames.contains(STORE.UI_STATE)) {
// UI state like wizard flags, preferences
db.createObjectStore(STORE.UI_STATE, { keyPath: 'key' });
}
if (!db.objectStoreNames.contains(STORE.META)) { if (!db.objectStoreNames.contains(STORE.META)) {
// Misc metadata like lastSync timestamps // Misc metadata like lastSync timestamps
db.createObjectStore(STORE.META, { keyPath: 'key' }); db.createObjectStore(STORE.META, { keyPath: 'key' });

View File

@ -72,7 +72,7 @@ export class DelegationManager {
nonce, nonce,
}; };
DelegationStorage.store(delegationInfo); await DelegationStorage.store(delegationInfo);
return true; return true;
} catch (error) { } catch (error) {
console.error('Error creating delegation:', error); console.error('Error creating delegation:', error);
@ -83,13 +83,13 @@ export class DelegationManager {
/** /**
* Sign a message with delegated key * Sign a message with delegated key
*/ */
signMessage(message: UnsignedMessage): OpchanMessage | null { async signMessage(message: UnsignedMessage): Promise<OpchanMessage | null> {
const now = Date.now(); const now = Date.now();
if ( if (
!this.cachedDelegation || !this.cachedDelegation ||
now - this.cachedAt > DelegationManager.CACHE_TTL_MS now - this.cachedAt > DelegationManager.CACHE_TTL_MS
) { ) {
this.cachedDelegation = DelegationStorage.retrieve(); this.cachedDelegation = await DelegationStorage.retrieve();
this.cachedAt = now; this.cachedAt = now;
} }
const delegation = this.cachedDelegation; const delegation = this.cachedDelegation;
@ -162,16 +162,16 @@ export class DelegationManager {
/** /**
* Get delegation status * Get delegation status
*/ */
getStatus( async getStatus(
currentAddress?: string, currentAddress?: string,
currentWalletType?: 'bitcoin' | 'ethereum' currentWalletType?: 'bitcoin' | 'ethereum'
): DelegationFullStatus { ): Promise<DelegationFullStatus> {
const now = Date.now(); const now = Date.now();
if ( if (
!this.cachedDelegation || !this.cachedDelegation ||
now - this.cachedAt > DelegationManager.CACHE_TTL_MS now - this.cachedAt > DelegationManager.CACHE_TTL_MS
) { ) {
this.cachedDelegation = DelegationStorage.retrieve(); this.cachedDelegation = await DelegationStorage.retrieve();
this.cachedAt = now; this.cachedAt = now;
} }
const delegation = this.cachedDelegation; const delegation = this.cachedDelegation;
@ -202,8 +202,8 @@ export class DelegationManager {
/** /**
* Clear stored delegation * Clear stored delegation
*/ */
clear(): void { async clear(): Promise<void> {
DelegationStorage.clear(); await DelegationStorage.clear();
} }
// ============================================================================ // ============================================================================

View File

@ -1,39 +1,35 @@
import { LOCAL_STORAGE_KEYS } from '@/lib/waku/constants'; import { localDatabase } from '@/lib/database/LocalDatabase';
import { DelegationInfo } from './types'; import { DelegationInfo } from './types';
export class DelegationStorage { export class DelegationStorage {
private static readonly STORAGE_KEY = LOCAL_STORAGE_KEYS.KEY_DELEGATION;
/** /**
* Store delegation information in localStorage * Store delegation information in IndexedDB
*/ */
static store(delegation: DelegationInfo): void { static async store(delegation: DelegationInfo): Promise<void> {
// Reduce verbose logging in production; keep minimal signal // Reduce verbose logging in production; keep minimal signal
if (import.meta.env?.MODE !== 'production') { if (import.meta.env?.MODE !== 'production') {
console.log('DelegationStorage.store'); console.log('DelegationStorage.store');
} }
localStorage.setItem( try {
DelegationStorage.STORAGE_KEY, await localDatabase.storeDelegation(delegation);
JSON.stringify(delegation) } catch (e) {
); console.error('Failed to store delegation information', e);
}
} }
/** /**
* Retrieve delegation information from localStorage * Retrieve delegation information from IndexedDB
*/ */
static retrieve(): DelegationInfo | null { static async retrieve(): Promise<DelegationInfo | null> {
const delegationJson = localStorage.getItem(DelegationStorage.STORAGE_KEY);
if (!delegationJson) return null;
try { try {
const delegation = JSON.parse(delegationJson); const delegation = await localDatabase.loadDelegation();
if (import.meta.env?.MODE !== 'production') { if (import.meta.env?.MODE !== 'production') {
console.log('DelegationStorage.retrieve'); console.log('DelegationStorage.retrieve');
} }
return delegation; return delegation;
} catch (e) { } catch (e) {
console.error('Failed to parse delegation information', e); console.error('Failed to retrieve delegation information', e);
return null; return null;
} }
} }
@ -41,7 +37,11 @@ export class DelegationStorage {
/** /**
* Clear stored delegation information * Clear stored delegation information
*/ */
static clear(): void { static async clear(): Promise<void> {
localStorage.removeItem(DelegationStorage.STORAGE_KEY); try {
await localDatabase.clearDelegation();
} catch (e) {
console.error('Failed to clear delegation information', e);
}
} }
} }

View File

@ -100,9 +100,9 @@ export class ForumActions {
author: currentUser!.address, // Safe after validation author: currentUser!.address, // Safe after validation
}; };
const signed = this.delegationManager.signMessage(unsignedPost); const signed = await this.delegationManager.signMessage(unsignedPost);
if (!signed) { if (!signed) {
const status = this.delegationManager.getStatus( const status = await this.delegationManager.getStatus(
currentUser!.address, currentUser!.address,
currentUser!.walletType currentUser!.walletType
); );
@ -169,9 +169,9 @@ export class ForumActions {
}; };
// Optimistic path: sign locally, write to cache, mark pending, render immediately // Optimistic path: sign locally, write to cache, mark pending, render immediately
const signed = this.delegationManager.signMessage(unsignedComment); const signed = await this.delegationManager.signMessage(unsignedComment);
if (!signed) { if (!signed) {
const status = this.delegationManager.getStatus( const status = await this.delegationManager.getStatus(
currentUser!.address, currentUser!.address,
currentUser!.walletType currentUser!.walletType
); );
@ -241,9 +241,9 @@ export class ForumActions {
author: currentUser!.address, author: currentUser!.address,
}; };
const signed = this.delegationManager.signMessage(unsignedCell); const signed = await this.delegationManager.signMessage(unsignedCell);
if (!signed) { if (!signed) {
const status = this.delegationManager.getStatus( const status = await this.delegationManager.getStatus(
currentUser!.address, currentUser!.address,
currentUser!.walletType currentUser!.walletType
); );
@ -313,9 +313,9 @@ export class ForumActions {
author: currentUser!.address, author: currentUser!.address,
}; };
const signed = this.delegationManager.signMessage(unsignedVote); const signed = await this.delegationManager.signMessage(unsignedVote);
if (!signed) { if (!signed) {
const status = this.delegationManager.getStatus( const status = await this.delegationManager.getStatus(
currentUser!.address, currentUser!.address,
currentUser!.walletType currentUser!.walletType
); );
@ -384,9 +384,9 @@ export class ForumActions {
author: currentUser!.address, author: currentUser!.address,
}; };
const signed = this.delegationManager.signMessage(unsignedMod); const signed = await this.delegationManager.signMessage(unsignedMod);
if (!signed) { if (!signed) {
const status = this.delegationManager.getStatus( const status = await this.delegationManager.getStatus(
currentUser!.address, currentUser!.address,
currentUser!.walletType currentUser!.walletType
); );
@ -457,9 +457,9 @@ export class ForumActions {
author: currentUser!.address, author: currentUser!.address,
}; };
const signed = this.delegationManager.signMessage(unsignedMod); const signed = await this.delegationManager.signMessage(unsignedMod);
if (!signed) { if (!signed) {
const status = this.delegationManager.getStatus( const status = await this.delegationManager.getStatus(
currentUser!.address, currentUser!.address,
currentUser!.walletType currentUser!.walletType
); );
@ -530,9 +530,9 @@ export class ForumActions {
timestamp: Date.now(), timestamp: Date.now(),
}; };
const signed = this.delegationManager.signMessage(unsignedMod); const signed = await this.delegationManager.signMessage(unsignedMod);
if (!signed) { if (!signed) {
const status = this.delegationManager.getStatus( const status = await this.delegationManager.getStatus(
currentUser!.address, currentUser!.address,
currentUser!.walletType currentUser!.walletType
); );

View File

@ -29,11 +29,11 @@ export class MessageService implements MessageServiceInterface {
*/ */
async sendMessage(message: UnsignedMessage): Promise<MessageResult> { async sendMessage(message: UnsignedMessage): Promise<MessageResult> {
try { try {
const signedMessage = this.delegationManager.signMessage(message); const signedMessage = await this.delegationManager.signMessage(message);
if (!signedMessage) { if (!signedMessage) {
// Check if delegation exists but is expired // Check if delegation exists but is expired
const delegationStatus = this.delegationManager.getStatus(); const delegationStatus = await this.delegationManager.getStatus();
const isDelegationExpired = !delegationStatus.isValid; const isDelegationExpired = !delegationStatus.isValid;
return { return {
@ -87,7 +87,7 @@ export class MessageService implements MessageServiceInterface {
message: UnsignedMessage message: UnsignedMessage
): Promise<OpchanMessage | null> { ): Promise<OpchanMessage | null> {
try { try {
const signedMessage = this.delegationManager.signMessage(message); const signedMessage = await this.delegationManager.signMessage(message);
if (!signedMessage) { if (!signedMessage) {
console.error('Failed to sign message'); console.error('Failed to sign message');
return null; return null;

View File

@ -9,10 +9,17 @@ interface ValidationReport {
} }
export class MessageValidator { export class MessageValidator {
private delegationManager: DelegationManager; private delegationManager?: DelegationManager;
constructor(delegationManager?: DelegationManager) { constructor(delegationManager?: DelegationManager) {
this.delegationManager = delegationManager || new DelegationManager(); this.delegationManager = delegationManager;
}
private getDelegationManager(): DelegationManager {
if (!this.delegationManager) {
this.delegationManager = new DelegationManager();
}
return this.delegationManager;
} }
/** /**
@ -26,7 +33,7 @@ export class MessageValidator {
// Verify signature and delegation proof - we know it's safe to cast here since hasRequiredFields passed // Verify signature and delegation proof - we know it's safe to cast here since hasRequiredFields passed
try { try {
return await this.delegationManager.verify( return await this.getDelegationManager().verify(
message as unknown as OpchanMessage message as unknown as OpchanMessage
); );
} catch { } catch {
@ -80,7 +87,7 @@ export class MessageValidator {
// Verify signature // Verify signature
try { try {
const isValid = await this.delegationManager.verify( const isValid = await this.getDelegationManager().verify(
message as unknown as OpchanMessage message as unknown as OpchanMessage
); );
if (!isValid) { if (!isValid) {
@ -120,7 +127,7 @@ export class MessageValidator {
// Verify signature and delegation proof // Verify signature and delegation proof
try { try {
const isValid = await this.delegationManager.verify( const isValid = await this.getDelegationManager().verify(
message as unknown as OpchanMessage message as unknown as OpchanMessage
); );
if (!isValid) { if (!isValid) {

View File

@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
import { useAuth, useUserActions, useForumActions } from '@/hooks'; import { useAuth, useUserActions, useForumActions } from '@/hooks';
import { useAuth as useAuthContext } from '@/contexts/useAuth'; import { useAuth as useAuthContext } from '@/contexts/useAuth';
import { useUserDisplay } from '@/hooks'; import { useUserDisplay } from '@/hooks';
import { DelegationFullStatus } from '@/lib/delegation';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
@ -37,9 +38,15 @@ export default function ProfilePage() {
// Get current user from auth context for the address // Get current user from auth context for the address
const { currentUser } = useAuth(); const { currentUser } = useAuth();
const { getDelegationStatus } = useAuthContext(); const { getDelegationStatus } = useAuthContext();
const delegationInfo = getDelegationStatus(); const [delegationInfo, setDelegationInfo] =
useState<DelegationFullStatus | null>(null);
const address = currentUser?.address; const address = currentUser?.address;
// Load delegation status
useEffect(() => {
getDelegationStatus().then(setDelegationInfo).catch(console.error);
}, [getDelegationStatus]);
// Get comprehensive user information from the unified hook // Get comprehensive user information from the unified hook
const userInfo = useUserDisplay(address || ''); const userInfo = useUserDisplay(address || '');
@ -341,21 +348,21 @@ export default function ProfilePage() {
{/* Delegation Status */} {/* Delegation Status */}
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Badge <Badge
variant={delegationInfo.isValid ? 'default' : 'secondary'} variant={delegationInfo?.isValid ? 'default' : 'secondary'}
className={ className={
delegationInfo.isValid delegationInfo?.isValid
? 'bg-green-600 hover:bg-green-700' ? 'bg-green-600 hover:bg-green-700'
: '' : ''
} }
> >
{delegationInfo.isValid ? 'Active' : 'Inactive'} {delegationInfo?.isValid ? 'Active' : 'Inactive'}
</Badge> </Badge>
{delegationInfo.isValid && delegationInfo.timeRemaining && ( {delegationInfo?.isValid && delegationInfo?.timeRemaining && (
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
{delegationInfo.timeRemaining} remaining {delegationInfo.timeRemaining} remaining
</span> </span>
)} )}
{!delegationInfo.isValid && ( {!delegationInfo?.isValid && (
<Badge <Badge
variant="outline" variant="outline"
className="text-yellow-600 border-yellow-600" className="text-yellow-600 border-yellow-600"
@ -363,7 +370,7 @@ export default function ProfilePage() {
Renewal Recommended Renewal Recommended
</Badge> </Badge>
)} )}
{!delegationInfo.isValid && ( {!delegationInfo?.isValid && (
<Badge variant="destructive">Expired</Badge> <Badge variant="destructive">Expired</Badge>
)} )}
</div> </div>
@ -426,7 +433,7 @@ export default function ProfilePage() {
Can Delegate Can Delegate
</Label> </Label>
<div className="mt-1 text-sm"> <div className="mt-1 text-sm">
{delegationInfo.hasDelegation ? ( {delegationInfo?.hasDelegation ? (
<Badge <Badge
variant="outline" variant="outline"
className="text-green-600 border-green-600" className="text-green-600 border-green-600"
@ -446,14 +453,14 @@ export default function ProfilePage() {
</div> </div>
{/* Delegation Actions */} {/* Delegation Actions */}
{delegationInfo.hasDelegation && ( {delegationInfo?.hasDelegation && (
<div className="pt-2"> <div className="pt-2">
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => setWalletWizardOpen(true)} onClick={() => setWalletWizardOpen(true)}
> >
{delegationInfo.isValid {delegationInfo?.isValid
? 'Renew Delegation' ? 'Renew Delegation'
: 'Delegate Key'} : 'Delegate Key'}
</Button> </Button>