2025-04-24 16:30:50 +05:30
|
|
|
import React, { createContext, useContext, useState, useEffect, useRef } from 'react';
|
2025-04-15 16:28:03 +05:30
|
|
|
import { useToast } from '@/components/ui/use-toast';
|
2025-04-16 14:45:27 +05:30
|
|
|
import { User } from '@/types';
|
2025-07-30 13:22:06 +05:30
|
|
|
import { AuthService, AuthResult } from '@/lib/identity/services/AuthService';
|
|
|
|
|
import { OpchanMessage } from '@/types';
|
2025-08-05 09:56:32 +05:30
|
|
|
import { useAppKitAccount, useDisconnect } from '@reown/appkit/react';
|
2025-04-15 16:28:03 +05:30
|
|
|
|
2025-04-24 14:31:00 +05:30
|
|
|
export type VerificationStatus = 'unverified' | 'verified-none' | 'verified-owner' | 'verifying';
|
|
|
|
|
|
2025-04-15 16:28:03 +05:30
|
|
|
interface AuthContextType {
|
|
|
|
|
currentUser: User | null;
|
|
|
|
|
isAuthenticated: boolean;
|
|
|
|
|
isAuthenticating: boolean;
|
2025-04-24 14:31:00 +05:30
|
|
|
verificationStatus: VerificationStatus;
|
2025-08-05 10:10:08 +05:30
|
|
|
verifyOwnership: () => Promise<boolean>;
|
2025-04-24 16:30:50 +05:30
|
|
|
delegateKey: () => Promise<boolean>;
|
|
|
|
|
isDelegationValid: () => boolean;
|
|
|
|
|
delegationTimeRemaining: () => number;
|
2025-07-30 15:55:13 +05:30
|
|
|
isWalletAvailable: () => boolean;
|
2025-07-30 13:22:06 +05:30
|
|
|
messageSigning: {
|
|
|
|
|
signMessage: (message: OpchanMessage) => Promise<OpchanMessage | null>;
|
|
|
|
|
verifyMessage: (message: OpchanMessage) => boolean;
|
|
|
|
|
};
|
2025-04-15 16:28:03 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|
|
|
|
|
2025-07-30 13:22:06 +05:30
|
|
|
export { AuthContext };
|
|
|
|
|
|
2025-04-15 16:28:03 +05:30
|
|
|
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|
|
|
|
const [currentUser, setCurrentUser] = useState<User | null>(null);
|
|
|
|
|
const [isAuthenticating, setIsAuthenticating] = useState(false);
|
2025-04-24 14:31:00 +05:30
|
|
|
const [verificationStatus, setVerificationStatus] = useState<VerificationStatus>('unverified');
|
2025-04-15 16:28:03 +05:30
|
|
|
const { toast } = useToast();
|
|
|
|
|
|
2025-08-05 09:56:32 +05:30
|
|
|
// 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;
|
|
|
|
|
|
2025-07-30 13:22:06 +05:30
|
|
|
// Create ref for AuthService so it persists between renders
|
|
|
|
|
const authServiceRef = useRef(new AuthService());
|
2025-04-24 16:30:50 +05:30
|
|
|
|
2025-08-05 10:10:08 +05:30
|
|
|
// Set AppKit accounts in AuthService
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
authServiceRef.current.setAccounts(bitcoinAccount, ethereumAccount);
|
|
|
|
|
}, [bitcoinAccount, ethereumAccount]);
|
|
|
|
|
|
2025-08-05 09:56:32 +05:30
|
|
|
// Sync with AppKit wallet state
|
2025-04-15 16:28:03 +05:30
|
|
|
useEffect(() => {
|
2025-08-05 09:56:32 +05:30
|
|
|
if (isConnected && address) {
|
|
|
|
|
// Check if we have a stored user for this address
|
|
|
|
|
const storedUser = authServiceRef.current.loadStoredUser();
|
2025-07-30 12:27:23 +05:30
|
|
|
|
2025-08-05 09:56:32 +05:30
|
|
|
if (storedUser && storedUser.address === address) {
|
|
|
|
|
// Use stored user data
|
|
|
|
|
setCurrentUser(storedUser);
|
2025-08-05 10:10:08 +05:30
|
|
|
setVerificationStatus(getVerificationStatus(storedUser));
|
2025-07-30 12:27:23 +05:30
|
|
|
} else {
|
2025-08-05 09:56:32 +05:30
|
|
|
// Create new user from AppKit wallet
|
|
|
|
|
const newUser: User = {
|
|
|
|
|
address,
|
|
|
|
|
walletType: isBitcoinConnected ? 'bitcoin' : 'ethereum',
|
|
|
|
|
verificationStatus: 'unverified',
|
2025-08-05 10:10:08 +05:30
|
|
|
lastChecked: Date.now(),
|
2025-08-05 09:56:32 +05:30
|
|
|
};
|
2025-08-05 10:10:08 +05:30
|
|
|
|
2025-08-05 09:56:32 +05:30
|
|
|
setCurrentUser(newUser);
|
2025-07-30 12:27:23 +05:30
|
|
|
setVerificationStatus('unverified');
|
2025-08-05 09:56:32 +05:30
|
|
|
authServiceRef.current.saveUser(newUser);
|
|
|
|
|
|
|
|
|
|
const chainName = isBitcoinConnected ? 'Bitcoin' : 'Ethereum';
|
2025-08-05 10:10:08 +05:30
|
|
|
const displayName = `${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
|
|
|
|
2025-04-24 16:30:50 +05:30
|
|
|
toast({
|
2025-08-05 09:56:32 +05:30
|
|
|
title: "Wallet Connected",
|
2025-08-05 10:10:08 +05:30
|
|
|
description: `Connected to ${chainName} with ${displayName}`,
|
2025-08-05 09:56:32 +05:30
|
|
|
});
|
|
|
|
|
|
2025-08-05 10:10:08 +05:30
|
|
|
const verificationType = isBitcoinConnected ? 'Ordinal ownership' : 'ENS ownership';
|
2025-08-05 09:56:32 +05:30
|
|
|
toast({
|
|
|
|
|
title: "Action Required",
|
2025-08-05 10:10:08 +05:30
|
|
|
description: `Please verify your ${verificationType} and delegate a signing key for better UX.`,
|
2025-04-24 16:30:50 +05:30
|
|
|
});
|
|
|
|
|
}
|
2025-08-05 09:56:32 +05:30
|
|
|
} else {
|
|
|
|
|
// Wallet disconnected
|
|
|
|
|
setCurrentUser(null);
|
2025-04-24 14:31:00 +05:30
|
|
|
setVerificationStatus('unverified');
|
2025-04-15 16:28:03 +05:30
|
|
|
}
|
2025-08-05 10:10:08 +05:30
|
|
|
}, [isConnected, address, isBitcoinConnected, isEthereumConnected, toast]);
|
2025-04-15 16:28:03 +05:30
|
|
|
|
2025-08-05 10:10:08 +05:30
|
|
|
const getVerificationStatus = (user: User): VerificationStatus => {
|
|
|
|
|
if (user.walletType === 'bitcoin') {
|
|
|
|
|
return user.ordinalOwnership ? 'verified-owner' : 'verified-none';
|
|
|
|
|
} else if (user.walletType === 'ethereum') {
|
|
|
|
|
return user.ensOwnership ? 'verified-owner' : 'verified-none';
|
|
|
|
|
}
|
|
|
|
|
return 'unverified';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const verifyOwnership = async (): Promise<boolean> => {
|
2025-04-23 08:22:50 +05:30
|
|
|
if (!currentUser || !currentUser.address) {
|
2025-04-15 16:28:03 +05:30
|
|
|
toast({
|
|
|
|
|
title: "Not Connected",
|
|
|
|
|
description: "Please connect your wallet first.",
|
|
|
|
|
variant: "destructive",
|
|
|
|
|
});
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setIsAuthenticating(true);
|
2025-04-24 14:31:00 +05:30
|
|
|
setVerificationStatus('verifying');
|
|
|
|
|
|
2025-04-15 16:28:03 +05:30
|
|
|
try {
|
2025-08-05 10:10:08 +05:30
|
|
|
const verificationType = currentUser.walletType === 'bitcoin' ? 'Ordinal' : 'ENS';
|
2025-04-24 14:31:00 +05:30
|
|
|
toast({
|
2025-08-05 10:10:08 +05:30
|
|
|
title: `Verifying ${verificationType}`,
|
|
|
|
|
description: `Checking your wallet for ${verificationType} ownership...`
|
2025-04-24 14:31:00 +05:30
|
|
|
});
|
2025-04-23 08:22:50 +05:30
|
|
|
|
2025-08-05 10:10:08 +05:30
|
|
|
const result: AuthResult = await authServiceRef.current.verifyOwnership(currentUser);
|
2025-04-15 16:28:03 +05:30
|
|
|
|
2025-07-30 13:22:06 +05:30
|
|
|
if (!result.success) {
|
|
|
|
|
throw new Error(result.error);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const updatedUser = result.user!;
|
2025-04-15 16:28:03 +05:30
|
|
|
setCurrentUser(updatedUser);
|
2025-07-30 13:22:06 +05:30
|
|
|
authServiceRef.current.saveUser(updatedUser);
|
2025-04-15 16:28:03 +05:30
|
|
|
|
2025-04-24 14:31:00 +05:30
|
|
|
// Update verification status
|
2025-08-05 10:10:08 +05:30
|
|
|
setVerificationStatus(getVerificationStatus(updatedUser));
|
2025-04-24 14:31:00 +05:30
|
|
|
|
2025-08-05 10:10:08 +05:30
|
|
|
if (updatedUser.walletType === 'bitcoin' && updatedUser.ordinalOwnership) {
|
2025-04-23 08:22:50 +05:30
|
|
|
toast({
|
|
|
|
|
title: "Ordinal Verified",
|
2025-04-24 16:30:50 +05:30
|
|
|
description: "You now have full access. We recommend delegating a key for better UX.",
|
2025-04-23 08:22:50 +05:30
|
|
|
});
|
2025-08-05 10:10:08 +05:30
|
|
|
} else if (updatedUser.walletType === 'ethereum' && updatedUser.ensOwnership) {
|
|
|
|
|
toast({
|
|
|
|
|
title: "ENS Verified",
|
|
|
|
|
description: "You now have full access. We recommend delegating a key for better UX.",
|
|
|
|
|
});
|
2025-04-23 08:22:50 +05:30
|
|
|
} else {
|
2025-08-05 10:10:08 +05:30
|
|
|
const verificationType = updatedUser.walletType === 'bitcoin' ? 'Ordinal Operators' : 'ENS domain';
|
2025-04-23 08:22:50 +05:30
|
|
|
toast({
|
2025-04-24 14:31:00 +05:30
|
|
|
title: "Read-Only Access",
|
2025-08-05 10:10:08 +05:30
|
|
|
description: `No ${verificationType} found. You have read-only access.`,
|
2025-04-24 14:31:00 +05:30
|
|
|
variant: "default",
|
2025-04-23 08:22:50 +05:30
|
|
|
});
|
|
|
|
|
}
|
2025-04-15 16:28:03 +05:30
|
|
|
|
2025-08-05 10:10:08 +05:30
|
|
|
return Boolean(
|
|
|
|
|
(updatedUser.walletType === 'bitcoin' && updatedUser.ordinalOwnership) ||
|
|
|
|
|
(updatedUser.walletType === 'ethereum' && updatedUser.ensOwnership)
|
|
|
|
|
);
|
2025-04-15 16:28:03 +05:30
|
|
|
} catch (error) {
|
2025-08-05 10:10:08 +05:30
|
|
|
console.error("Error verifying ownership:", error);
|
2025-04-24 14:31:00 +05:30
|
|
|
setVerificationStatus('unverified');
|
|
|
|
|
|
2025-08-05 10:10:08 +05:30
|
|
|
let errorMessage = "Failed to verify ownership. Please try again.";
|
2025-04-23 08:22:50 +05:30
|
|
|
if (error instanceof Error) {
|
|
|
|
|
errorMessage = error.message;
|
|
|
|
|
}
|
2025-04-24 14:31:00 +05:30
|
|
|
|
2025-04-15 16:28:03 +05:30
|
|
|
toast({
|
2025-04-23 08:22:50 +05:30
|
|
|
title: "Verification Error",
|
|
|
|
|
description: errorMessage,
|
2025-04-15 16:28:03 +05:30
|
|
|
variant: "destructive",
|
|
|
|
|
});
|
2025-04-24 14:31:00 +05:30
|
|
|
|
2025-04-15 16:28:03 +05:30
|
|
|
return false;
|
|
|
|
|
} finally {
|
|
|
|
|
setIsAuthenticating(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-04-24 16:30:50 +05:30
|
|
|
const delegateKey = async (): Promise<boolean> => {
|
|
|
|
|
if (!currentUser || !currentUser.address) {
|
|
|
|
|
toast({
|
|
|
|
|
title: "Not Connected",
|
|
|
|
|
description: "Please connect your wallet first.",
|
|
|
|
|
variant: "destructive",
|
|
|
|
|
});
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setIsAuthenticating(true);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
toast({
|
2025-04-27 15:54:24 +05:30
|
|
|
title: "Starting Key Delegation",
|
|
|
|
|
description: "This will let you post, comment, and vote without approving each action for 24 hours.",
|
2025-04-24 16:30:50 +05:30
|
|
|
});
|
|
|
|
|
|
2025-07-30 13:22:06 +05:30
|
|
|
const result: AuthResult = await authServiceRef.current.delegateKey(currentUser);
|
2025-04-24 16:30:50 +05:30
|
|
|
|
2025-07-30 13:22:06 +05:30
|
|
|
if (!result.success) {
|
|
|
|
|
throw new Error(result.error);
|
|
|
|
|
}
|
2025-04-24 16:30:50 +05:30
|
|
|
|
2025-07-30 13:22:06 +05:30
|
|
|
const updatedUser = result.user!;
|
|
|
|
|
setCurrentUser(updatedUser);
|
|
|
|
|
authServiceRef.current.saveUser(updatedUser);
|
2025-04-24 16:30:50 +05:30
|
|
|
|
2025-04-27 15:54:24 +05:30
|
|
|
// Format date for user-friendly display
|
2025-07-30 13:22:06 +05:30
|
|
|
const expiryDate = new Date(updatedUser.delegationExpiry!);
|
2025-04-27 15:54:24 +05:30
|
|
|
const formattedExpiry = expiryDate.toLocaleString();
|
|
|
|
|
|
2025-04-24 16:30:50 +05:30
|
|
|
toast({
|
2025-04-27 15:54:24 +05:30
|
|
|
title: "Key Delegation Successful",
|
|
|
|
|
description: `You can now interact with the forum without additional wallet approvals until ${formattedExpiry}.`,
|
2025-04-24 16:30:50 +05:30
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Error delegating key:", error);
|
|
|
|
|
|
|
|
|
|
let errorMessage = "Failed to delegate key. Please try again.";
|
|
|
|
|
if (error instanceof Error) {
|
2025-08-05 10:10:08 +05:30
|
|
|
errorMessage = error.message;
|
2025-04-24 16:30:50 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
toast({
|
2025-08-05 10:10:08 +05:30
|
|
|
title: "Delegation Error",
|
2025-04-24 16:30:50 +05:30
|
|
|
description: errorMessage,
|
|
|
|
|
variant: "destructive",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
} finally {
|
|
|
|
|
setIsAuthenticating(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-08-05 10:10:08 +05:30
|
|
|
|
2025-04-24 16:30:50 +05:30
|
|
|
const isDelegationValid = (): boolean => {
|
2025-07-30 13:22:06 +05:30
|
|
|
return authServiceRef.current.isDelegationValid();
|
2025-04-24 16:30:50 +05:30
|
|
|
};
|
2025-08-05 10:10:08 +05:30
|
|
|
|
2025-04-24 16:30:50 +05:30
|
|
|
const delegationTimeRemaining = (): number => {
|
2025-07-30 13:22:06 +05:30
|
|
|
return authServiceRef.current.getDelegationTimeRemaining();
|
2025-04-24 16:30:50 +05:30
|
|
|
};
|
|
|
|
|
|
2025-07-30 15:55:13 +05:30
|
|
|
const isWalletAvailable = (): boolean => {
|
2025-08-05 10:10:08 +05:30
|
|
|
return isConnected;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const messageSigning = {
|
|
|
|
|
signMessage: async (message: OpchanMessage): Promise<OpchanMessage | null> => {
|
|
|
|
|
return authServiceRef.current.signMessage(message);
|
|
|
|
|
},
|
|
|
|
|
verifyMessage: (message: OpchanMessage): boolean => {
|
|
|
|
|
return authServiceRef.current.verifyMessage(message);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const value: AuthContextType = {
|
|
|
|
|
currentUser,
|
|
|
|
|
isAuthenticated: Boolean(currentUser && isConnected),
|
|
|
|
|
isAuthenticating,
|
|
|
|
|
verificationStatus,
|
|
|
|
|
verifyOwnership,
|
|
|
|
|
delegateKey,
|
|
|
|
|
isDelegationValid,
|
|
|
|
|
delegationTimeRemaining,
|
|
|
|
|
isWalletAvailable,
|
|
|
|
|
messageSigning
|
2025-07-30 15:55:13 +05:30
|
|
|
};
|
|
|
|
|
|
2025-04-15 16:28:03 +05:30
|
|
|
return (
|
2025-08-05 10:10:08 +05:30
|
|
|
<AuthContext.Provider value={value}>
|
2025-04-15 16:28:03 +05:30
|
|
|
{children}
|
|
|
|
|
</AuthContext.Provider>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-30 13:22:06 +05:30
|
|
|
|