feat: anonymous interactions

This commit is contained in:
Danish Arora 2025-08-18 14:07:01 +05:30
parent dd13ef6b2e
commit 88012154d3
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
14 changed files with 239 additions and 82 deletions

View File

@ -44,8 +44,10 @@ const FeedSidebar: React.FC = () => {
// Ethereum wallet with ENS // Ethereum wallet with ENS
if (currentUser.walletType === 'ethereum') { if (currentUser.walletType === 'ethereum') {
if (currentUser.ensName && verificationStatus === 'verified-owner') { if (currentUser.ensName && (verificationStatus === 'verified-owner' || currentUser.ensOwnership)) {
return <Badge className="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"> Owns ENS: {currentUser.ensName}</Badge>; return <Badge className="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"> Owns ENS: {currentUser.ensName}</Badge>;
} else if (verificationStatus === 'verified-basic') {
return <Badge className="bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200"> Connected Wallet</Badge>;
} else { } else {
return <Badge variant="outline">Read-only (No ENS detected)</Badge>; return <Badge variant="outline">Read-only (No ENS detected)</Badge>;
} }
@ -53,8 +55,10 @@ const FeedSidebar: React.FC = () => {
// Bitcoin wallet with Ordinal // Bitcoin wallet with Ordinal
if (currentUser.walletType === 'bitcoin') { if (currentUser.walletType === 'bitcoin') {
if (verificationStatus === 'verified-owner') { if (verificationStatus === 'verified-owner' || currentUser.ordinalOwnership) {
return <Badge className="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"> Owns Ordinal</Badge>; return <Badge className="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"> Owns Ordinal</Badge>;
} else if (verificationStatus === 'verified-basic') {
return <Badge className="bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200"> Connected Wallet</Badge>;
} else { } else {
return <Badge variant="outline">Read-only (No Ordinal detected)</Badge>; return <Badge variant="outline">Read-only (No Ordinal detected)</Badge>;
} }
@ -62,6 +66,8 @@ const FeedSidebar: React.FC = () => {
// Fallback cases // Fallback cases
switch (verificationStatus) { switch (verificationStatus) {
case 'verified-basic':
return <Badge className="bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200"> Connected Wallet</Badge>;
case 'verified-none': case 'verified-none':
return <Badge variant="outline">Read Only</Badge>; return <Badge variant="outline">Read Only</Badge>;
case 'verifying': case 'verifying':

View File

@ -18,8 +18,7 @@ const Header = () => {
verifyOwnership, verifyOwnership,
delegateKey, delegateKey,
isDelegationValid, isDelegationValid,
delegationTimeRemaining, delegationTimeRemaining
isWalletAvailable
} = useAuth(); } = useAuth();
const { isNetworkConnected, isRefreshing } = useForum(); const { isNetworkConnected, isRefreshing } = useForum();
const location = useLocation(); const location = useLocation();
@ -85,8 +84,10 @@ const Header = () => {
return 'Verifying...'; return 'Verifying...';
case 'verified-none': case 'verified-none':
return 'Read-Only Access'; return 'Read-Only Access';
case 'verified-owner': case 'verified-basic':
return isDelegationValid() ? 'Full Access' : 'Setup Key'; return isDelegationValid() ? 'Full Access' : 'Setup Key';
case 'verified-owner':
return isDelegationValid() ? 'Premium Access' : 'Setup Key';
default: default:
return 'Setup Account'; return 'Setup Account';
} }
@ -100,6 +101,8 @@ const Header = () => {
return <RefreshCw className="w-3 h-3 animate-spin" />; return <RefreshCw className="w-3 h-3 animate-spin" />;
case 'verified-none': case 'verified-none':
return <CircleSlash className="w-3 h-3" />; return <CircleSlash className="w-3 h-3" />;
case 'verified-basic':
return isDelegationValid() ? <CheckCircle className="w-3 h-3" /> : <Key className="w-3 h-3" />;
case 'verified-owner': case 'verified-owner':
return isDelegationValid() ? <CheckCircle className="w-3 h-3" /> : <Key className="w-3 h-3" />; return isDelegationValid() ? <CheckCircle className="w-3 h-3" /> : <Key className="w-3 h-3" />;
default: default:
@ -115,6 +118,8 @@ const Header = () => {
return 'outline'; return 'outline';
case 'verified-none': case 'verified-none':
return 'secondary'; return 'secondary';
case 'verified-basic':
return isDelegationValid() ? 'default' : 'outline';
case 'verified-owner': case 'verified-owner':
return isDelegationValid() ? 'default' : 'outline'; return isDelegationValid() ? 'default' : 'outline';
default: default:

View File

@ -84,12 +84,12 @@ const PostDetail = () => {
}; };
const handleVotePost = async (isUpvote: boolean) => { const handleVotePost = async (isUpvote: boolean) => {
if (verificationStatus !== 'verified-owner') return; if (verificationStatus !== 'verified-owner' && verificationStatus !== 'verified-basic' && !currentUser?.ensOwnership && !currentUser?.ordinalOwnership) return;
await votePost(post.id, isUpvote); await votePost(post.id, isUpvote);
}; };
const handleVoteComment = async (commentId: string, isUpvote: boolean) => { const handleVoteComment = async (commentId: string, isUpvote: boolean) => {
if (verificationStatus !== 'verified-owner') return; if (verificationStatus !== 'verified-owner' && verificationStatus !== 'verified-basic' && !currentUser?.ensOwnership && !currentUser?.ordinalOwnership) return;
await voteComment(commentId, isUpvote); await voteComment(commentId, isUpvote);
}; };
@ -185,7 +185,7 @@ const PostDetail = () => {
</div> </div>
</div> </div>
{verificationStatus === 'verified-owner' ? ( {(verificationStatus === 'verified-owner' || verificationStatus === 'verified-basic' || currentUser?.ensOwnership || currentUser?.ordinalOwnership) ? (
<div className="mb-8"> <div className="mb-8">
<form onSubmit={handleCreateComment}> <form onSubmit={handleCreateComment}>
<div className="flex gap-2"> <div className="flex gap-2">
@ -219,7 +219,7 @@ const PostDetail = () => {
</div> </div>
) : ( ) : (
<div className="mb-8 p-3 border border-muted rounded-sm bg-secondary/30 text-center"> <div className="mb-8 p-3 border border-muted rounded-sm bg-secondary/30 text-center">
<p className="text-sm mb-2">Connect wallet and verify Ordinal ownership to comment</p> <p className="text-sm mb-2">Connect wallet and verify ownership to comment</p>
<Button asChild size="sm"> <Button asChild size="sm">
<Link to="/">Go to Home</Link> <Link to="/">Go to Home</Link>
</Button> </Button>

View File

@ -4,6 +4,7 @@ import { Shield, Crown } from 'lucide-react';
import { UserVerificationStatus } from '@/lib/forum/types'; import { UserVerificationStatus } from '@/lib/forum/types';
import { getEnsName } from '@wagmi/core'; import { getEnsName } from '@wagmi/core';
import { config } from '@/lib/identity/wallets/appkit'; import { config } from '@/lib/identity/wallets/appkit';
import { OrdinalAPI } from '@/lib/identity/ordinal';
interface AuthorDisplayProps { interface AuthorDisplayProps {
address: string; address: string;
@ -20,20 +21,52 @@ export function AuthorDisplay({
}: AuthorDisplayProps) { }: AuthorDisplayProps) {
const userStatus = userVerificationStatus?.[address]; const userStatus = userVerificationStatus?.[address];
const [resolvedEns, setResolvedEns] = React.useState<string | undefined>(undefined); const [resolvedEns, setResolvedEns] = React.useState<string | undefined>(undefined);
const [resolvedOrdinal, setResolvedOrdinal] = React.useState<boolean | undefined>(undefined);
// Heuristics for address types
const isEthereumAddress = address.startsWith('0x') && address.length === 42;
const isBitcoinAddress = !isEthereumAddress; // simple heuristic for our context
// Lazily resolve ENS name for Ethereum addresses if not provided // Lazily resolve ENS name for Ethereum addresses if not provided
React.useEffect(() => { React.useEffect(() => {
const isEthereumAddress = address.startsWith('0x') && address.length === 42; let cancelled = false;
if (!userStatus?.ensName && isEthereumAddress) { if (!userStatus?.ensName && isEthereumAddress) {
getEnsName(config, { address: address as `0x${string}` }) getEnsName(config, { address: address as `0x${string}` })
.then((name) => setResolvedEns(name || undefined)) .then((name) => { if (!cancelled) setResolvedEns(name || undefined); })
.catch(() => setResolvedEns(undefined)); .catch(() => { if (!cancelled) setResolvedEns(undefined); });
} else {
setResolvedEns(userStatus?.ensName);
} }
return () => { cancelled = true; };
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [address]); }, [address, isEthereumAddress, userStatus?.ensName]);
const hasENS = userStatus?.hasENS || Boolean(resolvedEns) || Boolean(userStatus?.ensName); // Lazily check Ordinal ownership for Bitcoin addresses if not provided
const hasOrdinal = userStatus?.hasOrdinal || false; React.useEffect(() => {
let cancelled = false;
const run = async () => {
console.log({
isBitcoinAddress, userStatus
})
if (isBitcoinAddress) {
try {
const api = new OrdinalAPI();
const res = await api.getOperatorDetails(address);
if (!cancelled) setResolvedOrdinal(Boolean(res?.has_operators));
} catch {
if (!cancelled) setResolvedOrdinal(undefined);
}
} else {
setResolvedOrdinal(userStatus?.hasOrdinal);
}
};
run();
return () => { cancelled = true; };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [address, isBitcoinAddress, userStatus?.hasOrdinal]);
const hasENS = Boolean(userStatus?.hasENS) || Boolean(resolvedEns) || Boolean(userStatus?.ensName);
const hasOrdinal = Boolean(userStatus?.hasOrdinal) || Boolean(resolvedOrdinal);
// Only show a badge if the author has ENS or Ordinal ownership (not for basic verification) // Only show a badge if the author has ENS or Ordinal ownership (not for basic verification)
const shouldShowBadge = showBadge && (hasENS || hasOrdinal); const shouldShowBadge = showBadge && (hasENS || hasOrdinal);

View File

@ -36,7 +36,7 @@ export function VerificationStep({
const [verificationResult, setVerificationResult] = React.useState<{ const [verificationResult, setVerificationResult] = React.useState<{
success: boolean; success: boolean;
message: string; message: string;
details?: any; details?: { ensName?: string; ensAvatar?: string } | boolean | { id: string; details: string };
} | null>(null); } | null>(null);
const handleVerify = async () => { const handleVerify = async () => {
@ -62,8 +62,8 @@ export function VerificationStep({
setVerificationResult({ setVerificationResult({
success: false, success: false,
message: walletType === 'bitcoin' message: walletType === 'bitcoin'
? "No Ordinal ownership found. You'll have read-only access." ? "No Ordinal ownership found. You can still participate in the forum with your connected wallet!"
: "No ENS ownership found. You'll have read-only access." : "No ENS ownership found. You can still participate in the forum with your connected wallet!"
}); });
} }
} catch (error) { } catch (error) {
@ -81,7 +81,7 @@ export function VerificationStep({
}; };
const getVerificationType = () => { const getVerificationType = () => {
return walletType === 'bitcoin' ? 'Bitcoin Ordinal' : 'ENS Domain'; return walletType === 'bitcoin' ? 'Bitcoin Ordinal' : 'Ethereum ENS';
}; };
const getVerificationIcon = () => { const getVerificationIcon = () => {
@ -94,9 +94,9 @@ export function VerificationStep({
const getVerificationDescription = () => { const getVerificationDescription = () => {
if (walletType === 'bitcoin') { if (walletType === 'bitcoin') {
return "Verify that you own Bitcoin Ordinals to get full posting and voting access."; return "Verify your Bitcoin Ordinal ownership to unlock premium features. If you don't own any Ordinals, you can still participate in the forum with your connected wallet.";
} else { } else {
return "Verify that you own an ENS domain to get full posting and voting access."; return "Verify your Ethereum ENS ownership to unlock premium features. If you don't own any ENS, you can still participate in the forum with your connected wallet.";
} }
}; };
@ -128,9 +128,9 @@ export function VerificationStep({
{verificationResult.details && ( {verificationResult.details && (
<div className="text-xs text-neutral-400"> <div className="text-xs text-neutral-400">
{walletType === 'bitcoin' ? ( {walletType === 'bitcoin' ? (
<p>Ordinal ID: {verificationResult.details.id}</p> <p>Ordinal ID: {typeof verificationResult.details === 'object' && 'id' in verificationResult.details ? verificationResult.details.id : 'Verified'}</p>
) : ( ) : (
<p>ENS Name: {verificationResult.details.ensName}</p> <p>ENS Name: {typeof verificationResult.details === 'object' && 'ensName' in verificationResult.details ? verificationResult.details.ensName : 'Verified'}</p>
)} )}
</div> </div>
)} )}

View File

@ -39,7 +39,7 @@ export function WalletWizard({
setCurrentStep(1); // Start at connection step if not authenticated setCurrentStep(1); // Start at connection step if not authenticated
} else if (isAuthenticated && (verificationStatus === 'unverified' || verificationStatus === 'verifying')) { } else if (isAuthenticated && (verificationStatus === 'unverified' || verificationStatus === 'verifying')) {
setCurrentStep(2); // Start at verification step if authenticated but not verified setCurrentStep(2); // Start at verification step if authenticated but not verified
} else if (isAuthenticated && (verificationStatus === 'verified-owner' || verificationStatus === 'verified-none') && !isDelegationValid()) { } else if (isAuthenticated && (verificationStatus === 'verified-owner' || verificationStatus === 'verified-basic' || verificationStatus === 'verified-none') && !isDelegationValid()) {
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 {
setCurrentStep(3); // Default to step 3 if everything is complete setCurrentStep(3); // Default to step 3 if everything is complete
@ -66,34 +66,25 @@ export function WalletWizard({
}; };
const getStepStatus = (step: WizardStep) => { const getStepStatus = (step: WizardStep) => {
// Step 1: Wallet connection - completed when authenticated
if (step === 1) { if (step === 1) {
if (isAuthenticated) return "completed"; return isAuthenticated ? 'complete' : 'current';
if (currentStep === step) return "current"; } else if (step === 2) {
return "pending"; if (!isAuthenticated) return 'disabled';
if (verificationStatus === 'unverified' || verificationStatus === 'verifying') return 'current';
if (verificationStatus === 'verified-owner' || verificationStatus === 'verified-basic' || verificationStatus === 'verified-none') return 'complete';
return 'disabled';
} else if (step === 3) {
if (!isAuthenticated || (verificationStatus !== 'verified-owner' && verificationStatus !== 'verified-basic' && verificationStatus !== 'verified-none')) return 'disabled';
if (isDelegationValid()) return 'complete';
return 'current';
} }
return 'disabled';
// Step 2: Verification - completed when verified (either owner or none)
if (step === 2) {
if (verificationStatus === 'verified-owner' || verificationStatus === 'verified-none') return "completed";
if (currentStep === step) return "current";
return "pending";
}
// Step 3: Key delegation - completed when delegation is valid AND authenticated
if (step === 3) {
if (isAuthenticated && isDelegationValid()) return "completed";
if (currentStep === step) return "current";
return "pending";
}
return "pending";
}; };
const renderStepIcon = (step: WizardStep) => { const renderStepIcon = (step: WizardStep) => {
const status = getStepStatus(step); const status = getStepStatus(step);
if (status === "completed") { if (status === "complete") {
return <CheckCircle className="h-5 w-5 text-green-500" />; return <CheckCircle className="h-5 w-5 text-green-500" />;
} else if (status === "current") { } else if (status === "current") {
return <Loader2 className="h-5 w-5 text-blue-500 animate-spin" />; return <Loader2 className="h-5 w-5 text-blue-500 animate-spin" />;
@ -130,7 +121,7 @@ export function WalletWizard({
<span className={`text-sm ${ <span className={`text-sm ${
getStepStatus(step as WizardStep) === "current" getStepStatus(step as WizardStep) === "current"
? "text-blue-500 font-medium" ? "text-blue-500 font-medium"
: getStepStatus(step as WizardStep) === "completed" : getStepStatus(step as WizardStep) === "complete"
? "text-green-500" ? "text-green-500"
: "text-gray-400" : "text-gray-400"
}`}> }`}>
@ -139,7 +130,7 @@ export function WalletWizard({
</div> </div>
{step < 3 && ( {step < 3 && (
<div className={`w-8 h-px mx-2 ${ <div className={`w-8 h-px mx-2 ${
getStepStatus(step as WizardStep) === "completed" getStepStatus(step as WizardStep) === "complete"
? "bg-green-500" ? "bg-green-500"
: "bg-gray-600" : "bg-gray-600"
}`} /> }`} />

View File

@ -6,7 +6,7 @@ import { OpchanMessage } from '@/types';
import { useAppKitAccount, useDisconnect, modal } from '@reown/appkit/react'; import { useAppKitAccount, useDisconnect, modal } from '@reown/appkit/react';
import { DelegationDuration } from '@/lib/identity/signatures/key-delegation'; import { DelegationDuration } from '@/lib/identity/signatures/key-delegation';
export type VerificationStatus = 'unverified' | 'verified-none' | 'verified-owner' | 'verifying'; export type VerificationStatus = 'unverified' | 'verified-none' | 'verified-basic' | 'verified-owner' | 'verifying';
interface AuthContextType { interface AuthContextType {
currentUser: User | null; currentUser: User | null;
@ -76,13 +76,39 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const newUser: User = { const newUser: User = {
address, address,
walletType: isBitcoinConnected ? 'bitcoin' : 'ethereum', walletType: isBitcoinConnected ? 'bitcoin' : 'ethereum',
verificationStatus: 'unverified', verificationStatus: 'verified-basic', // Connected wallets get basic verification by default
lastChecked: Date.now(), lastChecked: Date.now(),
}; };
setCurrentUser(newUser); // For Ethereum wallets, try to check ENS ownership immediately
setVerificationStatus('unverified'); if (isEthereumConnected) {
authServiceRef.current.saveUser(newUser); authServiceRef.current.getWalletInfo().then((walletInfo) => {
if (walletInfo?.ensName) {
const updatedUser = {
...newUser,
ensOwnership: true,
ensName: walletInfo.ensName,
verificationStatus: 'verified-owner' as const,
};
setCurrentUser(updatedUser);
setVerificationStatus('verified-owner');
authServiceRef.current.saveUser(updatedUser);
} else {
setCurrentUser(newUser);
setVerificationStatus('verified-basic');
authServiceRef.current.saveUser(newUser);
}
}).catch(() => {
// Fallback to basic verification if ENS check fails
setCurrentUser(newUser);
setVerificationStatus('verified-basic');
authServiceRef.current.saveUser(newUser);
});
} else {
setCurrentUser(newUser);
setVerificationStatus('verified-basic');
authServiceRef.current.saveUser(newUser);
}
const chainName = isBitcoinConnected ? 'Bitcoin' : 'Ethereum'; const chainName = isBitcoinConnected ? 'Bitcoin' : 'Ethereum';
const displayName = `${address.slice(0, 6)}...${address.slice(-4)}`; const displayName = `${address.slice(0, 6)}...${address.slice(-4)}`;
@ -95,7 +121,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const verificationType = isBitcoinConnected ? 'Ordinal ownership' : 'ENS ownership'; const verificationType = isBitcoinConnected ? 'Ordinal ownership' : 'ENS ownership';
toast({ toast({
title: "Action Required", title: "Action Required",
description: `Please verify your ${verificationType} and delegate a signing key for better UX.`, description: `You can participate in the forum now! Verify your ${verificationType} for premium features and delegate a signing key for better UX.`,
}); });
} }
} else { } else {
@ -126,9 +152,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const getVerificationStatus = (user: User): VerificationStatus => { const getVerificationStatus = (user: User): VerificationStatus => {
if (user.walletType === 'bitcoin') { if (user.walletType === 'bitcoin') {
return user.ordinalOwnership ? 'verified-owner' : 'verified-none'; return user.ordinalOwnership ? 'verified-owner' : 'verified-basic';
} else if (user.walletType === 'ethereum') { } else if (user.walletType === 'ethereum') {
return user.ensOwnership ? 'verified-owner' : 'verified-none'; return user.ensOwnership ? 'verified-owner' : 'verified-basic';
} }
return 'unverified'; return 'unverified';
}; };
@ -169,18 +195,18 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
if (updatedUser.walletType === 'bitcoin' && updatedUser.ordinalOwnership) { if (updatedUser.walletType === 'bitcoin' && updatedUser.ordinalOwnership) {
toast({ toast({
title: "Ordinal Verified", title: "Ordinal Verified",
description: "You now have full access. We recommend delegating a key for better UX.", description: "You now have premium access with higher relevance bonuses. We recommend delegating a key for better UX.",
}); });
} else if (updatedUser.walletType === 'ethereum' && updatedUser.ensOwnership) { } else if (updatedUser.walletType === 'ethereum' && updatedUser.ensOwnership) {
toast({ toast({
title: "ENS Verified", title: "ENS Verified",
description: "You now have full access. We recommend delegating a key for better UX.", description: "You now have premium access with higher relevance bonuses. We recommend delegating a key for better UX.",
}); });
} else { } else {
const verificationType = updatedUser.walletType === 'bitcoin' ? 'Ordinal Operators' : 'ENS domain'; const verificationType = updatedUser.walletType === 'bitcoin' ? 'Ordinal Operators' : 'ENS domain';
toast({ toast({
title: "Read-Only Access", title: "Basic Access Granted",
description: `No ${verificationType} found. You have read-only access.`, description: `No ${verificationType} found, but you can still participate in the forum with your connected wallet.`,
variant: "default", variant: "default",
}); });
} }

View File

@ -21,6 +21,8 @@ import { getDataFromCache } from '@/lib/forum/transformers';
import { RelevanceCalculator } from '@/lib/forum/relevance'; import { RelevanceCalculator } from '@/lib/forum/relevance';
import { UserVerificationStatus } from '@/lib/forum/types'; import { UserVerificationStatus } from '@/lib/forum/types';
import { AuthService } from '@/lib/identity/services/AuthService'; import { AuthService } from '@/lib/identity/services/AuthService';
import { getEnsName } from '@wagmi/core';
import { config } from '@/lib/identity/wallets/appkit';
interface ForumContextType { interface ForumContextType {
cells: Cell[]; cells: Cell[];
@ -138,21 +140,55 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
// Create generic user object for other addresses // Create generic user object for other addresses
allUsers.push({ allUsers.push({
address, address,
walletType: 'bitcoin', // Default, will be updated if we have more info walletType: address.startsWith('0x') ? 'ethereum' : 'bitcoin',
verificationStatus: 'unverified' verificationStatus: 'unverified'
}); });
} }
}); });
const userVerificationStatus = relevanceCalculator.buildUserVerificationStatus(allUsers); const initialStatus = relevanceCalculator.buildUserVerificationStatus(allUsers);
// Transform data with relevance calculation // Transform data with relevance calculation (initial pass)
const { cells, posts, comments } = getDataFromCache(verifyFn, userVerificationStatus); const { cells, posts, comments } = getDataFromCache(verifyFn, initialStatus);
setCells(cells); setCells(cells);
setPosts(posts); setPosts(posts);
setComments(comments); setComments(comments);
setUserVerificationStatus(userVerificationStatus); setUserVerificationStatus(initialStatus);
// Enrich: resolve ENS for ethereum addresses asynchronously and update
(async () => {
const targets = allUsers.filter(u => u.walletType === 'ethereum' && !u.ensOwnership);
if (targets.length === 0) return;
const lookups = await Promise.all(targets.map(async (u) => {
try {
const name = await getEnsName(config, { address: u.address as `0x${string}` });
return { address: u.address, ensName: name || undefined };
} catch {
return { address: u.address, ensName: undefined };
}
}));
const ensByAddress = new Map<string, string | undefined>(lookups.map(l => [l.address, l.ensName]));
const enrichedUsers: User[] = allUsers.map(u => {
const ensName = ensByAddress.get(u.address);
if (ensName) {
return {
...u,
walletType: 'ethereum',
ensOwnership: true,
ensName,
verificationStatus: 'verified-owner'
} as User;
}
return u;
});
const enrichedStatus = relevanceCalculator.buildUserVerificationStatus(enrichedUsers);
const transformed = getDataFromCache(verifyFn, enrichedStatus);
setCells(transformed.cells);
setPosts(transformed.posts);
setComments(transformed.comments);
setUserVerificationStatus(enrichedStatus);
})();
}, [authService, isAuthenticated, currentUser]); }, [authService, isAuthenticated, currentUser]);
const handleRefreshData = async () => { const handleRefreshData = async () => {

View File

@ -35,7 +35,22 @@ export const createPost = async (
if (!isAuthenticated || !currentUser) { if (!isAuthenticated || !currentUser) {
toast({ toast({
title: 'Authentication Required', title: 'Authentication Required',
description: 'You need to verify Ordinal ownership to post.', description: 'You need to connect your wallet to post.',
variant: 'destructive',
});
return null;
}
// Check if user has basic verification or better, or owns ENS/Ordinal
const hasENSOrOrdinal = !!(currentUser.ensOwnership || currentUser.ordinalOwnership);
const isVerified = currentUser.verificationStatus === 'verified-owner' ||
currentUser.verificationStatus === 'verified-basic' ||
hasENSOrOrdinal;
if (!isVerified && (currentUser.verificationStatus === 'unverified' || currentUser.verificationStatus === 'verifying')) {
toast({
title: 'Verification Required',
description: 'Please complete wallet verification to post.',
variant: 'destructive', variant: 'destructive',
}); });
return null; return null;
@ -92,7 +107,22 @@ export const createComment = async (
if (!isAuthenticated || !currentUser) { if (!isAuthenticated || !currentUser) {
toast({ toast({
title: 'Authentication Required', title: 'Authentication Required',
description: 'You need to verify Ordinal ownership to comment.', description: 'You need to connect your wallet to comment.',
variant: 'destructive',
});
return null;
}
// Check if user has basic verification or better, or owns ENS/Ordinal
const hasENSOrOrdinal = !!(currentUser.ensOwnership || currentUser.ordinalOwnership);
const isVerified = currentUser.verificationStatus === 'verified-owner' ||
currentUser.verificationStatus === 'verified-basic' ||
hasENSOrOrdinal;
if (!isVerified && (currentUser.verificationStatus === 'unverified' || currentUser.verificationStatus === 'verifying')) {
toast({
title: 'Verification Required',
description: 'Please complete wallet verification to comment.',
variant: 'destructive', variant: 'destructive',
}); });
return null; return null;
@ -210,7 +240,22 @@ export const vote = async (
if (!isAuthenticated || !currentUser) { if (!isAuthenticated || !currentUser) {
toast({ toast({
title: 'Authentication Required', title: 'Authentication Required',
description: 'You need to verify Ordinal ownership to vote.', description: 'You need to connect your wallet to vote.',
variant: 'destructive',
});
return false;
}
// Check if user has basic verification or better, or owns ENS/Ordinal
const hasENSOrOrdinal = !!(currentUser.ensOwnership || currentUser.ordinalOwnership);
const isVerified = currentUser.verificationStatus === 'verified-owner' ||
currentUser.verificationStatus === 'verified-basic' ||
hasENSOrOrdinal;
if (!isVerified && (currentUser.verificationStatus === 'unverified' || currentUser.verificationStatus === 'verifying')) {
toast({
title: 'Verification Required',
description: 'Please complete wallet verification to vote.',
variant: 'destructive', variant: 'destructive',
}); });
return false; return false;

View File

@ -14,7 +14,8 @@ export class RelevanceCalculator {
COMMENT: 0.5 COMMENT: 0.5
}; };
private static readonly VERIFICATION_BONUS = 1.25; // 25% increase private static readonly VERIFICATION_BONUS = 1.25; // 25% increase for ENS/Ordinal owners
private static readonly BASIC_VERIFICATION_BONUS = 1.1; // 10% increase for basic verified users
private static readonly VERIFIED_UPVOTE_BONUS = 0.1; private static readonly VERIFIED_UPVOTE_BONUS = 0.1;
private static readonly VERIFIED_COMMENTER_BONUS = 0.05; private static readonly VERIFIED_COMMENTER_BONUS = 0.05;
@ -201,10 +202,10 @@ export class RelevanceCalculator {
/** /**
* Check if a user is verified (has ENS or ordinal ownership) * Check if a user is verified (has ENS or ordinal ownership, or basic verification)
*/ */
isUserVerified(user: User): boolean { isUserVerified(user: User): boolean {
return !!(user.ensOwnership || user.ordinalOwnership); return !!(user.ensOwnership || user.ordinalOwnership || user.verificationStatus === 'verified-basic');
} }
/** /**
@ -218,7 +219,8 @@ export class RelevanceCalculator {
isVerified: this.isUserVerified(user), isVerified: this.isUserVerified(user),
hasENS: !!user.ensOwnership, hasENS: !!user.ensOwnership,
hasOrdinal: !!user.ordinalOwnership, hasOrdinal: !!user.ordinalOwnership,
ensName: user.ensName ensName: user.ensName,
verificationStatus: user.verificationStatus
}; };
}); });
@ -245,22 +247,31 @@ export class RelevanceCalculator {
} }
/** /**
* Apply author verification bonus * Apply verification bonus for verified authors
*/ */
private applyAuthorVerificationBonus( private applyAuthorVerificationBonus(
score: number, score: number,
authorAddress: string, authorAddress: string,
userVerificationStatus: UserVerificationStatus userVerificationStatus: UserVerificationStatus
): { bonus: number; isVerified: boolean } { ): { bonus: number; isVerified: boolean } {
const authorStatus = userVerificationStatus[authorAddress]; const authorStatus = userVerificationStatus[authorAddress];
const isVerified = authorStatus?.isVerified || false; const isVerified = authorStatus?.isVerified || false;
if (isVerified) { if (!isVerified) {
const bonus = score * (RelevanceCalculator.VERIFICATION_BONUS - 1); return { bonus: 0, isVerified: false };
return { bonus, isVerified };
} }
return { bonus: 0, isVerified }; // Apply different bonuses based on verification level
let bonus = 0;
if (authorStatus?.verificationStatus === 'verified-owner') {
// Full bonus for ENS/Ordinal owners
bonus = score * (RelevanceCalculator.VERIFICATION_BONUS - 1);
} else if (authorStatus?.verificationStatus === 'verified-basic') {
// Lower bonus for basic verified users
bonus = score * (RelevanceCalculator.BASIC_VERIFICATION_BONUS - 1);
}
return { bonus, isVerified: true };
} }
/** /**

View File

@ -22,5 +22,6 @@ export interface RelevanceScoreDetails {
hasENS: boolean; hasENS: boolean;
hasOrdinal: boolean; hasOrdinal: boolean;
ensName?: string; ensName?: string;
verificationStatus?: 'unverified' | 'verified-none' | 'verified-basic' | 'verified-owner' | 'verifying';
}; };
} }

View File

@ -9,7 +9,7 @@ export class OrdinalAPI {
* @returns A promise that resolves with the API response. * @returns A promise that resolves with the API response.
*/ */
async getOperatorDetails(address: string): Promise<OrdinalApiResponse> { async getOperatorDetails(address: string): Promise<OrdinalApiResponse> {
if (import.meta.env.VITE_OPCHAN_MOCK_ORDINAL_CHECK === 'true') { if (import.meta.env.VITE_OPCHAN_MOCK_ORDINAL_CHECK === 'true') {
console.log(`[DEV] Bypassing ordinal verification for address: ${address}`); console.log(`[DEV] Bypassing ordinal verification for address: ${address}`);
return { return {

View File

@ -182,6 +182,7 @@ export class AuthService {
const updatedUser = { const updatedUser = {
...user, ...user,
ordinalOwnership: hasOperators, ordinalOwnership: hasOperators,
verificationStatus: hasOperators ? 'verified-owner' : 'verified-basic',
lastChecked: Date.now(), lastChecked: Date.now(),
}; };
@ -206,6 +207,7 @@ export class AuthService {
...user, ...user,
ensOwnership: hasENS, ensOwnership: hasENS,
ensName: ensName, ensName: ensName,
verificationStatus: hasENS ? 'verified-owner' : 'verified-basic',
lastChecked: Date.now(), lastChecked: Date.now(),
}; };
@ -216,11 +218,12 @@ export class AuthService {
} catch (error) { } catch (error) {
console.error('Error verifying ENS ownership:', error); console.error('Error verifying ENS ownership:', error);
// Fall back to no ENS ownership on error // Fall back to basic verification on error
const updatedUser = { const updatedUser = {
...user, ...user,
ensOwnership: false, ensOwnership: false,
ensName: undefined, ensName: undefined,
verificationStatus: 'verified-basic',
lastChecked: Date.now(), lastChecked: Date.now(),
}; };

View File

@ -15,7 +15,7 @@ export interface User {
ensAvatar?: string; ensAvatar?: string;
ensOwnership?: boolean; ensOwnership?: boolean;
verificationStatus: 'unverified' | 'verified-none' | 'verified-owner' | 'verifying'; verificationStatus: 'unverified' | 'verified-none' | 'verified-basic' | 'verified-owner' | 'verifying';
signature?: string; signature?: string;
lastChecked?: number; lastChecked?: number;