import { useQuery } from '@tanstack/react-query'; import { useNostr } from '@nostrify/react'; import { useCurrentUser } from './useCurrentUser'; import { onlyVerified } from '@/lib/verifyEvents'; /** * Hook to calculate trust score for a Nostr pubkey using Web of Trust (WoT). * * Trust scores are calculated using social graph analysis: * - 100: Self (viewing your own content) * - 90: Direct follow (you follow them) * - 60: Friend-of-friend (you follow someone who follows them) * - 30: Has NIP-05 verification * - 0: No connection or verification * * @param targetPubkey - The pubkey to calculate trust score for * @returns Trust score (0-100) or undefined if not available */ export function useTrustScore(targetPubkey: string | undefined) { const { nostr } = useNostr(); const { user } = useCurrentUser(); return useQuery({ queryKey: ['trust-score', targetPubkey, user?.pubkey], queryFn: async (c) => { if (!targetPubkey) return undefined; const signal = AbortSignal.any([c.signal, AbortSignal.timeout(5000)]); try { // Self score = 100 if (user?.pubkey && targetPubkey === user.pubkey) { return 100; } // Get user's follow list (kind 3) const sourcePubkey = user?.pubkey || '20e7c953439e66f3882ab96fe0faa8ba246d6fb12c03b0cb4e2dc4623b8bcb21'; // Signature-verify follow lists before deriving any trust from them — // an unverified kind-3 from a malicious relay could inflate a score. const followListEvents = onlyVerified(await nostr.query( [{ kinds: [3], authors: [sourcePubkey], limit: 1 }], { signal } )); const followList = followListEvents[0]; if (!followList) { return 0; } // Extract followed pubkeys from tags const following = new Set( followList.tags .filter(([tag]) => tag === 'p') .map(([, pubkey]) => pubkey) ); // Direct follow = 90 if (following.has(targetPubkey)) { return 90; } // Check friend-of-friend (2 hops) const friendsFollowLists = onlyVerified(await nostr.query( [{ kinds: [3], authors: Array.from(following), limit: 100 }], { signal } )); for (const friendList of friendsFollowLists) { const friendFollowing = friendList.tags .filter(([tag]) => tag === 'p') .map(([, pubkey]) => pubkey); if (friendFollowing.includes(targetPubkey)) { return 60; } } // Check if they have NIP-05 verification (basic credibility) const profileEvents = onlyVerified(await nostr.query( [{ kinds: [0], authors: [targetPubkey], limit: 1 }], { signal } )); if (profileEvents[0]) { try { const metadata = JSON.parse(profileEvents[0].content); if (metadata.nip05) { return 30; } } catch { // Ignore JSON parse errors } } // No trust signals found return 0; } catch (error) { console.warn('[useTrustScore] Error calculating trust score:', error); return undefined; } }, enabled: !!targetPubkey, staleTime: 3600000, // Cache for 1 hour (trust scores change slowly) gcTime: 86400000, // Keep in cache for 24 hours retry: 1, // Only retry once on failure }); } /** * Get trust badge configuration based on trust score */ export function getTrustBadgeConfig(score: number | undefined) { if (score === undefined || score < 50) { return { show: false, variant: 'outline' as const, className: 'border-gray-500 text-gray-500', label: 'UNVERIFIED', icon: null, }; } if (score < 80) { return { show: true, variant: 'outline' as const, className: 'border-yellow-500 text-yellow-500', label: 'TRUSTED', icon: 'shield', }; } return { show: true, variant: 'default' as const, className: 'bg-green-500 text-white', label: 'VERIFIED', icon: 'check', }; }