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-04-23 08:22:50 +05:30
|
|
|
import { OrdinalAPI } from '@/lib/identity/ordinal';
|
2025-04-24 16:30:50 +05:30
|
|
|
import { KeyDelegation } from '@/lib/identity/signatures/key-delegation';
|
|
|
|
|
import { PhantomWalletAdapter } from '@/lib/identity/wallets/phantom';
|
|
|
|
|
import { MessageSigning } from '@/lib/identity/signatures/message-signing';
|
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-04-15 16:28:03 +05:30
|
|
|
connectWallet: () => Promise<void>;
|
|
|
|
|
disconnectWallet: () => void;
|
|
|
|
|
verifyOrdinal: () => Promise<boolean>;
|
2025-04-24 16:30:50 +05:30
|
|
|
delegateKey: () => Promise<boolean>;
|
|
|
|
|
isDelegationValid: () => boolean;
|
|
|
|
|
delegationTimeRemaining: () => number;
|
|
|
|
|
messageSigning: MessageSigning;
|
2025-04-15 16:28:03 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|
|
|
|
|
|
|
|
|
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-04-23 08:22:50 +05:30
|
|
|
const ordinalApi = new OrdinalAPI();
|
2025-04-15 16:28:03 +05:30
|
|
|
|
2025-04-24 16:30:50 +05:30
|
|
|
// Create refs for our services so they persist between renders
|
|
|
|
|
const phantomWalletRef = useRef(new PhantomWalletAdapter());
|
|
|
|
|
const keyDelegationRef = useRef(new KeyDelegation());
|
|
|
|
|
const messageSigningRef = useRef(new MessageSigning(keyDelegationRef.current));
|
|
|
|
|
|
2025-04-15 16:28:03 +05:30
|
|
|
useEffect(() => {
|
|
|
|
|
const storedUser = localStorage.getItem('opchan-user');
|
|
|
|
|
if (storedUser) {
|
|
|
|
|
try {
|
|
|
|
|
const user = JSON.parse(storedUser);
|
|
|
|
|
const lastChecked = user.lastChecked || 0;
|
2025-04-24 14:31:00 +05:30
|
|
|
const expiryTime = 24 * 60 * 60 * 1000;
|
2025-04-15 16:28:03 +05:30
|
|
|
|
|
|
|
|
if (Date.now() - lastChecked < expiryTime) {
|
|
|
|
|
setCurrentUser(user);
|
2025-04-24 14:31:00 +05:30
|
|
|
|
|
|
|
|
if ('ordinalOwnership' in user) {
|
|
|
|
|
setVerificationStatus(user.ordinalOwnership ? 'verified-owner' : 'verified-none');
|
|
|
|
|
} else {
|
|
|
|
|
setVerificationStatus('unverified');
|
|
|
|
|
}
|
2025-04-15 16:28:03 +05:30
|
|
|
} else {
|
|
|
|
|
localStorage.removeItem('opchan-user');
|
2025-04-24 14:31:00 +05:30
|
|
|
setVerificationStatus('unverified');
|
2025-04-15 16:28:03 +05:30
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error("Failed to parse stored user data", e);
|
|
|
|
|
localStorage.removeItem('opchan-user');
|
2025-04-24 14:31:00 +05:30
|
|
|
setVerificationStatus('unverified');
|
2025-04-15 16:28:03 +05:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const connectWallet = async () => {
|
|
|
|
|
setIsAuthenticating(true);
|
|
|
|
|
try {
|
2025-04-24 16:30:50 +05:30
|
|
|
// Check if Phantom wallet is installed
|
|
|
|
|
if (!phantomWalletRef.current.isInstalled()) {
|
|
|
|
|
toast({
|
|
|
|
|
title: "Wallet Not Found",
|
|
|
|
|
description: "Please install Phantom wallet to continue.",
|
|
|
|
|
variant: "destructive",
|
|
|
|
|
});
|
|
|
|
|
throw new Error("Phantom wallet not installed");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Connect to wallet
|
|
|
|
|
const address = await phantomWalletRef.current.connect();
|
2025-04-15 16:28:03 +05:30
|
|
|
|
|
|
|
|
// Create a new user object
|
|
|
|
|
const newUser: User = {
|
2025-04-24 16:30:50 +05:30
|
|
|
address,
|
2025-04-15 16:28:03 +05:30
|
|
|
lastChecked: Date.now(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Store user data
|
|
|
|
|
setCurrentUser(newUser);
|
|
|
|
|
localStorage.setItem('opchan-user', JSON.stringify(newUser));
|
2025-04-24 14:31:00 +05:30
|
|
|
setVerificationStatus('unverified');
|
2025-04-15 16:28:03 +05:30
|
|
|
|
|
|
|
|
toast({
|
|
|
|
|
title: "Wallet Connected",
|
2025-04-24 16:30:50 +05:30
|
|
|
description: `Connected with address ${address.slice(0, 6)}...${address.slice(-4)}`,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Prompt the user to verify ordinal ownership and delegate key
|
|
|
|
|
toast({
|
|
|
|
|
title: "Action Required",
|
|
|
|
|
description: "Please verify your Ordinal ownership and delegate a signing key for better UX.",
|
2025-04-15 16:28:03 +05:30
|
|
|
});
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Error connecting wallet:", error);
|
|
|
|
|
toast({
|
|
|
|
|
title: "Connection Failed",
|
|
|
|
|
description: "Failed to connect to wallet. Please try again.",
|
|
|
|
|
variant: "destructive",
|
|
|
|
|
});
|
|
|
|
|
throw error;
|
|
|
|
|
} finally {
|
|
|
|
|
setIsAuthenticating(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const disconnectWallet = () => {
|
2025-04-24 16:30:50 +05:30
|
|
|
// Disconnect from Phantom wallet
|
|
|
|
|
phantomWalletRef.current.disconnect();
|
|
|
|
|
|
|
|
|
|
// Clear user data and delegation
|
2025-04-15 16:28:03 +05:30
|
|
|
setCurrentUser(null);
|
|
|
|
|
localStorage.removeItem('opchan-user');
|
2025-04-24 16:30:50 +05:30
|
|
|
keyDelegationRef.current.clearDelegation();
|
2025-04-24 14:31:00 +05:30
|
|
|
setVerificationStatus('unverified');
|
2025-04-24 16:30:50 +05:30
|
|
|
|
2025-04-15 16:28:03 +05:30
|
|
|
toast({
|
|
|
|
|
title: "Disconnected",
|
|
|
|
|
description: "Your wallet has been disconnected.",
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const verifyOrdinal = async () => {
|
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-04-24 14:31:00 +05:30
|
|
|
toast({
|
|
|
|
|
title: "Verifying Ordinal",
|
|
|
|
|
description: "Checking your wallet for Ordinal Operators..."
|
|
|
|
|
});
|
2025-04-23 08:22:50 +05:30
|
|
|
|
2025-07-16 16:32:11 +05:30
|
|
|
//TODO: revert when the API is ready
|
|
|
|
|
// const response = await ordinalApi.getOperatorDetails(currentUser.address);
|
|
|
|
|
// const hasOperators = response.has_operators;
|
|
|
|
|
const hasOperators = true;
|
2025-04-23 08:22:50 +05:30
|
|
|
|
2025-04-15 16:28:03 +05:30
|
|
|
const updatedUser = {
|
|
|
|
|
...currentUser,
|
2025-04-23 08:22:50 +05:30
|
|
|
ordinalOwnership: hasOperators,
|
2025-04-15 16:28:03 +05:30
|
|
|
lastChecked: Date.now(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
setCurrentUser(updatedUser);
|
|
|
|
|
localStorage.setItem('opchan-user', JSON.stringify(updatedUser));
|
|
|
|
|
|
2025-04-24 14:31:00 +05:30
|
|
|
// Update verification status
|
|
|
|
|
setVerificationStatus(hasOperators ? 'verified-owner' : 'verified-none');
|
|
|
|
|
|
2025-04-23 08:22:50 +05:30
|
|
|
if (hasOperators) {
|
|
|
|
|
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
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
toast({
|
2025-04-24 14:31:00 +05:30
|
|
|
title: "Read-Only Access",
|
|
|
|
|
description: "No Ordinal Operators found. You have read-only access.",
|
|
|
|
|
variant: "default",
|
2025-04-23 08:22:50 +05:30
|
|
|
});
|
|
|
|
|
}
|
2025-04-15 16:28:03 +05:30
|
|
|
|
2025-04-23 08:22:50 +05:30
|
|
|
return hasOperators;
|
2025-04-15 16:28:03 +05:30
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Error verifying Ordinal:", error);
|
2025-04-24 14:31:00 +05:30
|
|
|
setVerificationStatus('unverified');
|
|
|
|
|
|
2025-04-23 08:22:50 +05:30
|
|
|
let errorMessage = "Failed to verify Ordinal ownership. Please try again.";
|
|
|
|
|
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
|
|
|
/**
|
|
|
|
|
* Creates a key delegation by generating a browser keypair, having the
|
|
|
|
|
* wallet sign a delegation message, and storing the delegation
|
|
|
|
|
*/
|
|
|
|
|
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
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Generate a browser keypair
|
|
|
|
|
const keypair = await keyDelegationRef.current.generateKeypair();
|
|
|
|
|
|
|
|
|
|
// Calculate expiry time (24 hours from now)
|
|
|
|
|
const expiryHours = 24;
|
|
|
|
|
const expiryTimestamp = Date.now() + (expiryHours * 60 * 60 * 1000);
|
|
|
|
|
|
|
|
|
|
// Create delegation message
|
|
|
|
|
const delegationMessage = keyDelegationRef.current.createDelegationMessage(
|
|
|
|
|
keypair.publicKey,
|
|
|
|
|
currentUser.address,
|
|
|
|
|
expiryTimestamp
|
|
|
|
|
);
|
|
|
|
|
|
2025-04-27 15:54:24 +05:30
|
|
|
// Format date for user-friendly display
|
|
|
|
|
const expiryDate = new Date(expiryTimestamp);
|
|
|
|
|
const formattedExpiry = expiryDate.toLocaleString();
|
|
|
|
|
|
2025-04-24 16:30:50 +05:30
|
|
|
toast({
|
2025-04-27 15:54:24 +05:30
|
|
|
title: "Wallet Signature Required",
|
|
|
|
|
description: `Please sign with your wallet to authorize a temporary key valid until ${formattedExpiry}. This improves UX by reducing wallet prompts.`,
|
2025-04-24 16:30:50 +05:30
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const signature = await phantomWalletRef.current.signMessage(delegationMessage);
|
|
|
|
|
|
|
|
|
|
const delegationInfo = keyDelegationRef.current.createDelegation(
|
|
|
|
|
currentUser.address,
|
|
|
|
|
signature,
|
|
|
|
|
keypair.publicKey,
|
|
|
|
|
keypair.privateKey,
|
|
|
|
|
expiryHours
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
keyDelegationRef.current.storeDelegation(delegationInfo);
|
|
|
|
|
|
|
|
|
|
const updatedUser = {
|
|
|
|
|
...currentUser,
|
|
|
|
|
browserPubKey: keypair.publicKey,
|
|
|
|
|
delegationSignature: signature,
|
|
|
|
|
delegationExpiry: expiryTimestamp,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
setCurrentUser(updatedUser);
|
|
|
|
|
localStorage.setItem('opchan-user', JSON.stringify(updatedUser));
|
|
|
|
|
|
|
|
|
|
toast({
|
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.";
|
2025-04-27 15:54:24 +05:30
|
|
|
|
2025-04-24 16:30:50 +05:30
|
|
|
if (error instanceof Error) {
|
2025-04-27 15:54:24 +05:30
|
|
|
// Provide specific guidance based on error type
|
|
|
|
|
if (error.message.includes("rejected") || error.message.includes("declined") || error.message.includes("denied")) {
|
|
|
|
|
errorMessage = "You declined the signature request. Key delegation is optional but improves your experience.";
|
|
|
|
|
} else if (error.message.includes("timeout")) {
|
|
|
|
|
errorMessage = "Wallet request timed out. Please try again and approve the signature promptly.";
|
|
|
|
|
} else {
|
|
|
|
|
errorMessage = error.message;
|
|
|
|
|
}
|
2025-04-24 16:30:50 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
toast({
|
2025-04-27 15:54:24 +05:30
|
|
|
title: "Delegation Failed",
|
2025-04-24 16:30:50 +05:30
|
|
|
description: errorMessage,
|
|
|
|
|
variant: "destructive",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
} finally {
|
|
|
|
|
setIsAuthenticating(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Checks if the current delegation is valid
|
|
|
|
|
*/
|
|
|
|
|
const isDelegationValid = (): boolean => {
|
|
|
|
|
return keyDelegationRef.current.isDelegationValid();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns the time remaining on the current delegation in milliseconds
|
|
|
|
|
*/
|
|
|
|
|
const delegationTimeRemaining = (): number => {
|
|
|
|
|
return keyDelegationRef.current.getDelegationTimeRemaining();
|
|
|
|
|
};
|
|
|
|
|
|
2025-04-15 16:28:03 +05:30
|
|
|
return (
|
|
|
|
|
<AuthContext.Provider
|
|
|
|
|
value={{
|
|
|
|
|
currentUser,
|
|
|
|
|
isAuthenticated: !!currentUser?.ordinalOwnership,
|
|
|
|
|
isAuthenticating,
|
2025-04-24 14:31:00 +05:30
|
|
|
verificationStatus,
|
2025-04-15 16:28:03 +05:30
|
|
|
connectWallet,
|
|
|
|
|
disconnectWallet,
|
|
|
|
|
verifyOrdinal,
|
2025-04-24 16:30:50 +05:30
|
|
|
delegateKey,
|
|
|
|
|
isDelegationValid,
|
|
|
|
|
delegationTimeRemaining,
|
|
|
|
|
messageSigning: messageSigningRef.current,
|
2025-04-15 16:28:03 +05:30
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{children}
|
|
|
|
|
</AuthContext.Provider>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const useAuth = () => {
|
|
|
|
|
const context = useContext(AuthContext);
|
|
|
|
|
if (context === undefined) {
|
|
|
|
|
throw new Error("useAuth must be used within an AuthProvider");
|
|
|
|
|
}
|
|
|
|
|
return context;
|
|
|
|
|
};
|